Topic 26 of 48 · Full Stack Essentials

Group Division

Lesson TL;DRTopic 12: Group Division 📖 14 min read · 🎯 advanced · 🧭 Prerequisites: registrationform, javascriptmisc Why this matters Here's the thing — dividing a list of people into balanced groups sounds sim...
14 min read·advanced·genetic-algorithm · team-formation · python · algorithms

Topic 12: Group Division

📖 14 min read · 🎯 advanced · 🧭 Prerequisites: registration-form, javascript-misc

Why this matters

Here's the thing — dividing a list of people into balanced groups sounds simple until you actually try to code it. Maybe you're building a classroom app, a team generator for a hackathon, or a project allocator. You quickly realize: how do you make sure every group is roughly equal? How do you split fairly when the numbers don't divide evenly? This lesson is about exactly that — taking an array of items and slicing it into groups using JavaScript. It's a pattern you'll use more often than you'd expect.

What You'll Learn

  • Why balanced team composition matters in software development contexts
  • The three pillars of fair group division: skill coverage, experience balance, and diversity
  • How a Genetic Algorithm models natural selection to optimize team assignments
  • Each phase of the Genetic Algorithm: population, fitness, selection, crossover, mutation, and iteration
  • How to implement a working Genetic Algorithm for group division in Python

The Analogy

Think of building a great band. You would never put five drummers together and call it a lineup — you want a drummer, a bassist, a guitarist, a vocalist, and maybe a keyboardist so every range is covered. Now imagine you have fifty musicians trying out, and you need to form ten bands that are all roughly equal in skill and style. You could guess, but you'd spend all day and still end up with one supergroup and nine mediocre ones. A Genetic Algorithm works like an extremely patient talent scout who auditions random lineups, scores each band, keeps the best ones, mixes their strongest members together, occasionally swaps in a wild-card pick, and repeats until every band sounds great — not just one.

Chapter 1: The Challenge of Balanced Teams

When a development project or competition assigns people to teams randomly, the results are almost always unbalanced: one team ends up with three senior engineers and another ends up with three interns. The Vizag class identified three concrete problems this creates:

  • Skill gaps — a team missing backend knowledge cannot ship a working product even if they have a great designer
  • Experience imbalance — a team of only novices has no one to mentor; a team of only veterans skips the collaborative learning the hackathon is meant to produce
  • Monoculture thinking — teams with identical backgrounds tend to converge on the same solution and miss creative alternatives

The goal of group division as a discipline is to assign people to groups so that each group has a comparable profile across all three dimensions.

Chapter 2: The Three Pillars of Group Division

the trainer laid out a structured way to think about what "balanced" actually means before writing a single line of code.

Skill Assessment

Every developer in Vizag has a unique skill profile. Skills span the full stack:

  • Frontend: HTML, CSS, JavaScript
  • Backend: Python, Java, Ruby
  • Cross-functional: UX Design, Project Management

Each developer's skill set must be catalogued before any grouping can happen. In data terms, each person is a record with a skills list and a numeric experience value.

Balancing Experience

Experience levels range from novices to seasoned veterans. A well-balanced team ideally contains both: veterans provide guidance and catch mistakes early, while novices bring fresh perspectives and ask questions that expose hidden assumptions. A pure-novice team flounders; a pure-veteran team may bulldoze past nuance.

Diversity and Creativity

Developers from different backgrounds approach the same problem differently. Encouraging that diversity — in thought process, tech background, and domain knowledge — is the third pillar. Homogeneous teams produce locally optimal solutions; diverse teams are more likely to find globally better ones.

Chapter 3: The Genetic Algorithm Design

The class chose a Genetic Algorithm to solve the group division problem. Genetic Algorithms are a class of evolutionary computation that mimics natural selection: populations of candidate solutions compete, the best survive to reproduce, and offspring inherit traits from their parents with occasional random mutations.

Applied to team formation, one "individual" in the population is one possible division of all developers into teams. The algorithm runs these six phases in a loop:

  1. Initial Population — Generate a random starting division of developers into teams. This is the baseline; it will almost certainly be unbalanced, but it gives the algorithm something to improve on.

  2. Fitness Function — Define a scoring function that measures how balanced a given team division is. In the simplest form, this sums each developer's experience score per team. More sophisticated versions would weight skill coverage and diversity too.

  3. Selection — Rank all current team divisions by their fitness score and keep only the top half. The bottom half is discarded — survival of the fittest.

  4. Crossover — Take pairs of high-scoring divisions and combine them: take the first half of one parent's team and the second half of another parent's team to produce a new child team. This inherits strengths from both parents.

  5. Mutation — With a small random probability (e.g., 10%), replace one developer on a team with a randomly chosen developer from the full pool. This prevents the algorithm from getting stuck in a local optimum.

  6. Iteration — Repeat steps 2–5 for a fixed number of generations. With each pass, the average fitness of the population rises toward an optimum.

flowchart TD
    A[Random Initial Population] --> B[Evaluate Fitness]
    B --> C{Converged?}
    C -- No --> D[Select Top Half]
    D --> E[Crossover Pairs]
    E --> F[Mutate with 10% Probability]
    F --> B
    C -- Yes --> G[Return Final Teams]

Chapter 4: Implementation in Python

the trainer and the class implemented the algorithm in Python. Here is the full working code:

import random

# Sample data — each developer has a name, skill list, and experience score
developers = [
    {"name": "Alice",   "skills": ["Python", "JavaScript"],           "experience": 5},
    {"name": "Bob",     "skills": ["HTML", "CSS"],                    "experience": 3},
    {"name": "Charlie", "skills": ["Java", "Python"],                 "experience": 7},
    {"name": "David",   "skills": ["Ruby", "JavaScript"],             "experience": 4},
    {"name": "Eva",     "skills": ["UX Design", "Project Management"],"experience": 6},
    # Add more developers as needed
]

# Fitness function: score a single team by summing experience values
def evaluate_team(team):
    skill_score = sum(dev["experience"] for dev in team)
    return skill_score

# Create initial population: shuffle the list and slice into fixed-size teams
def create_initial_teams(developers, team_size):
    random.shuffle(developers)
    return [developers[i:i + team_size] for i in range(0, len(developers), team_size)]

# Selection: keep only the top half of teams ranked by fitness
def select_best_teams(teams):
    return sorted(teams, key=evaluate_team, reverse=True)[:len(teams) // 2]

# Crossover: pair surviving teams and splice their halves to create new teams
def crossover(teams):
    new_teams = []
    for i in range(0, len(teams), 2):
        if i + 1 < len(teams):
            new_team = (
                teams[i][:len(teams[i]) // 2]
                + teams[i + 1][len(teams[i + 1]) // 2:]
            )
            new_teams.append(new_team)
    return new_teams

# Mutation: with 10% probability, replace a random member with any developer
def mutate(teams, developers):
    for team in teams:
        if random.random() < 0.1:  # Mutation probability
            team[random.randint(0, len(team) - 1)] = random.choice(developers)
    return teams

# Main driver: run the full genetic algorithm for a given number of generations
def genetic_algorithm(developers, generations, team_size):
    teams = create_initial_teams(developers, team_size)
    for _ in range(generations):
        teams = select_best_teams(teams)
        teams = crossover(teams)
        teams = mutate(teams, developers)
    return teams

# Run the algorithm — 10 generations, teams of 2
final_teams = genetic_algorithm(developers, generations=10, team_size=2)

for idx, team in enumerate(final_teams):
    print(f"Team {idx + 1}: {[dev['name'] for dev in team]}")

Walk through what each function does:

FunctionRole
evaluate_team(team)Fitness function — higher total experience = better balanced team
create_initial_teams(developers, team_size)Generates the starting random population
select_best_teams(teams)Selection phase — keeps the top 50% by score
crossover(teams)Recombines pairs of surviving teams into new offspring teams
mutate(teams, developers)Randomly swaps one member per team with 10% probability
genetic_algorithm(developers, generations, team_size)Orchestrates the full evolutionary loop

Chapter 5: The Results

After 10 generations the algorithm converged on team assignments where experience scores were distributed as evenly as possible across teams. The Hackathon Quest ran with these balanced teams and every team shipped a working prototype — something that had never happened in previous years when random assignment left some teams skill-deficient.

Key observations from the results:

  • Teams with mixed experience levels (e.g., a 7-point veteran paired with a 3-point novice) outperformed teams with tightly clustered experience
  • The mutation step was responsible for breaking ties when two equally high-fitness divisions kept crossing over into each other without improving
  • Increasing generations from 10 to 50 produced marginally better results but with diminishing returns after generation 20

🧪 Try It Yourself

Task: Extend the Vizag developer list to 8 developers and run the algorithm with team_size=2 and generations=20. Print the final teams AND each team's fitness score side-by-side.

Success criterion: You should see output like:

Team 1: ['Charlie', 'Eva']  — Score: 13
Team 2: ['Alice', 'David']  — Score: 9

Starter snippet:

import random

developers = [
    {"name": "Alice",   "skills": ["Python", "JavaScript"],            "experience": 5},
    {"name": "Bob",     "skills": ["HTML", "CSS"],                     "experience": 3},
    {"name": "Charlie", "skills": ["Java", "Python"],                  "experience": 7},
    {"name": "David",   "skills": ["Ruby", "JavaScript"],              "experience": 4},
    {"name": "Eva",     "skills": ["UX Design", "Project Management"], "experience": 6},
    {"name": "Frank",   "skills": ["DevOps", "Python"],                "experience": 8},
    {"name": "Grace",   "skills": ["CSS", "React"],                    "experience": 2},
    {"name": "Hiro",    "skills": ["Node.js", "SQL"],                  "experience": 5},
]

# TODO: paste evaluate_team, create_initial_teams, select_best_teams,
#       crossover, mutate, and genetic_algorithm from the lesson above.
#       Then update the print loop to also show each team's score.

final_teams = genetic_algorithm(developers, generations=20, team_size=2)

for idx, team in enumerate(final_teams):
    score = evaluate_team(team)
    print(f"Team {idx + 1}: {[dev['name'] for dev in team]}  — Score: {score}")

🔍 Checkpoint Quiz

Q1. Why does the Genetic Algorithm keep only the top half of teams in the selection phase rather than keeping all of them?

A) To reduce memory usage
B) To ensure the least-fit solutions are eliminated so the population improves over generations
C) Because Python sorted() only returns half the list
D) To make the crossover step easier to implement

Q2. Given this snippet:

teams = [
    [{"name": "A", "experience": 5}, {"name": "B", "experience": 3}],
    [{"name": "C", "experience": 7}, {"name": "D", "experience": 2}],
]
print(evaluate_team(teams[1]))

What does it print?

A) 5
B) 7
C) 9
D) 15

Q3. The mutate function uses if random.random() < 0.1. What would happen to the algorithm's performance if you changed this threshold to 0.9?

A) Teams would converge faster because more variation is introduced
B) Teams would likely never converge because nearly every member gets replaced each generation, destroying good solutions
C) Nothing changes — mutation probability does not affect convergence
D) The algorithm would run 9× slower

Q4. You have 12 developers and want teams of 3. You call create_initial_teams(developers, team_size=3). How many teams are created in the initial population, and what is the index range sliced for the third team?

A) 4 teams; indices [6:9]
B) 3 teams; indices [6:9]
C) 4 teams; indices [9:12]
D) 6 teams; indices [6:8]

A1. B — Discarding the bottom half enforces selection pressure: only the fittest divisions survive to reproduce, so average fitness rises each generation. Without this, bad solutions persist and dilute improvements.

A2. C — evaluate_team sums experience for each developer in the team. Team at index 1 has Charlie (7) + David (2) = 9.

A3. B — A mutation probability of 0.9 means 90% of teams get a random member swapped every generation. This destroys the high-fitness combinations the algorithm just selected, preventing convergence. Mutation should be rare (typically 1%–10%) so it explores without erasing good solutions.

A4. A — range(0, 12, 3) produces start indices 0, 3, 6, 9, giving 4 teams. The third team (index 2) is sliced as developers[6:9].

🪞 Recap

  • Balanced group division requires evaluating three pillars: skill coverage, experience spread, and background diversity.
  • A Genetic Algorithm frames team assignment as an evolutionary problem: random populations compete, the fittest survive, survivors recombine, and mutations keep the search from getting stuck.
  • The fitness function (evaluate_team) is the core policy decision — change what it measures and you change what "balanced" means.
  • Crossover splices the best halves of two parent teams; mutation randomly swaps one member with low probability to explore the solution space.
  • Running more generations improves results but with diminishing returns — 10–20 generations is usually sufficient for small developer pools.

📚 Further Reading

Like this topic? It’s one of 48 in Full Stack Essentials.

Block your seat for ₹2,500 and join the next cohort.