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:
-
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.
-
Fitness Function — Define a scoring function that measures how balanced a given team division is. In the simplest form, this sums each developer's
experiencescore per team. More sophisticated versions would weight skill coverage and diversity too. -
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.
-
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.
-
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.
-
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:
| Function | Role |
|---|---|
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 a3-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
generationsfrom 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
- Python
randommodule docs — the source of truth onrandom.shuffle,random.random, andrandom.choiceused throughout this lesson - Introduction to Genetic Algorithms — Towards Data Science — accessible walkthrough of the full evolutionary computation paradigm
- Genetic Algorithms in Search, Optimization and Machine Learning — Goldberg — the canonical textbook if you want the mathematical foundations
- ⬅️ Previous: JavaScript Misc
- ➡️ Next: PHP with Select, Fetch