diff --git a/assign_teams.py b/assign_teams.py index 0f1924b..90db8b1 100644 --- a/assign_teams.py +++ b/assign_teams.py @@ -1,6 +1,10 @@ """ -참가자 35명 → 7팀 (팀당 5명) 배정. -같은 부서 2명 이하 제약. 랜덤 시드로 재현 가능. +참가자 34명 → 7팀 배정 (5명 6팀 + 4명 1팀). +- 부서별 균등 분배 (max - min ≤ 1) +- 시니어 균등 분배 (max - min ≤ 1, 모든 팀 ≥1) +- 김태현은 진행요원 (참가 X) +- AI Efficiency Tech는 시니어/주니어 정보 없음 → 균등 분배 대상 외 + 결과: participants.json (이름→팀 매핑) + 콘솔 출력. """ import json @@ -8,14 +12,22 @@ import random from collections import Counter, defaultdict from pathlib import Path +# 시니어 명단 (Platform/Data/HPC/System만 알고 있음) +SENIORS = { + "한지승", "손현준", "강승형", "변수민", # Platform/Data + "김정명", "김영관", "전효준", # HPC + "박영훈", "서희", "김병훈", # System +} + PEOPLE = [ + # (name, dept) ("한지승", "MLOps Platform"), ("변수민", "MLOps Data"), ("박재호", "MLOps Data"), - ("김태현", "MLOps Data"), ("강승형", "MLOps Data"), ("손현준", "MLOps Data"), ("김동국", "MLOps Data"), + # 김태현 진행요원 (제외) ("김재현", "MLOps HPC"), ("이준석", "MLOps HPC"), ("오근현", "MLOps HPC"), @@ -47,21 +59,24 @@ PEOPLE = [ ] NUM_TEAMS = 7 -TEAM_SIZE = 5 +# 34명 / 7팀 = 5명 6팀 + 4명 1팀 +TEAM_SIZES = [5] * 6 + [4] SEED = 20260428 # 행사일 시드 (재현 가능) -def assign(seed): +def assign_one(seed): """ - 부서별 균등 분배 + 팀 사이즈 5 보장. + 가변 팀 사이즈 균등 분배. - 각 부서 N명 → q명 모든 팀 + r명 추가 (q, r = divmod(N, NUM_TEAMS)). - 추가 r명 받을 팀은 "지금까지 추가 적게 받은 팀" 우선 → 모든 팀 정확히 같은 횟수만큼 ceil 받음. + 각 사람을 한 명씩 할당: + - 우선순위 1: 남은 슬롯 많은 팀 + - 우선순위 2: 이 부서 인원 적게 받은 팀 + - tie: random 효과: - - 부서별 분포: max - min ≤ 1 (균등) - - 모든 팀 정확히 5명 - - EffTech 12명: 5팀이 2명, 2팀이 1명 (모든 팀 ≥1) + - 팀 사이즈 정확히 TEAM_SIZES (가변) + - 부서별 max - min ≤ 1 (자연스럽게) + - 시니어 균등은 시드 재시도로 검증 """ rng = random.Random(seed) by_dept = defaultdict(list) @@ -71,41 +86,80 @@ def assign(seed): for d in by_dept: rng.shuffle(by_dept[d]) - depts_sorted = sorted(by_dept.keys(), key=lambda d: -len(by_dept[d])) teams = [[] for _ in range(NUM_TEAMS)] - ceil_count = [0] * NUM_TEAMS # 각 팀이 받은 ceil 횟수 + remaining = TEAM_SIZES[:] + + depts_sorted = sorted(by_dept.keys(), key=lambda d: -len(by_dept[d])) for dept in depts_sorted: - members = by_dept[dept] - n = len(members) - q, r = divmod(n, NUM_TEAMS) - - if r > 0: - # ceil_count 적은 팀 우선, tie는 random - order = sorted(range(NUM_TEAMS), key=lambda i: (ceil_count[i], rng.random())) - ceil_teams = set(order[:r]) - else: - ceil_teams = set() - - idx = 0 - for ti in range(NUM_TEAMS): - count = q + (1 if ti in ceil_teams else 0) - for _ in range(count): - teams[ti].append((members[idx], dept)) - idx += 1 - if ti in ceil_teams: - ceil_count[ti] += 1 + for person in by_dept[dept]: + dept_counts = [Counter(d for _, d in t).get(dept, 0) for t in teams] + priority = sorted( + range(NUM_TEAMS), + key=lambda i: ( + -remaining[i], + dept_counts[i], + rng.random(), + ), + ) + for ti in priority: + if remaining[ti] > 0: + teams[ti].append((person, dept)) + remaining[ti] -= 1 + break return teams +def validate(teams): + """모든 제약 검증.""" + sizes = [len(t) for t in teams] + if sorted(sizes, reverse=True) != sorted(TEAM_SIZES, reverse=True): + return False, "팀 사이즈 불일치" + + # 부서별 max - min ≤ 1 + all_depts = {d for _, d in PEOPLE} + for dept in all_depts: + per_team = [Counter(d for _, d in t).get(dept, 0) for t in teams] + if max(per_team) - min(per_team) > 1: + return False, f"부서 {dept} 분포 불균등: {per_team}" + + # EffTech 모든 팀 ≥1 + for ti, team in enumerate(teams): + eff = sum(1 for _, d in team if d == "AI Efficiency Tech") + if eff < 1: + return False, f"팀{ti+1} EffTech 0명" + + # 시니어 검증 + senior_per_team = [ + sum(1 for n, _ in t if n in SENIORS) for t in teams + ] + if min(senior_per_team) < 1: + return False, f"시니어 0명 팀 존재: {senior_per_team}" + if max(senior_per_team) - min(senior_per_team) > 1: + return False, f"시니어 분포 불균등: {senior_per_team}" + + return True, "OK" + + +def assign(base_seed): + """시드 재시도로 모든 제약 만족하는 배정 찾기.""" + for offset in range(10000): + seed = base_seed + offset + teams = assign_one(seed) + ok, msg = validate(teams) + if ok: + return teams, seed, offset + 1 + raise RuntimeError("10000회 시도에도 모든 제약 만족 실패") + + def main(): - assert len(PEOPLE) == NUM_TEAMS * TEAM_SIZE, ( - f"인원수 불일치: {len(PEOPLE)}명, 기대 {NUM_TEAMS * TEAM_SIZE}명" + assert len(PEOPLE) == sum(TEAM_SIZES), ( + f"인원수 불일치: {len(PEOPLE)}명, 기대 {sum(TEAM_SIZES)}명" ) - teams = assign(SEED) - print(f"# 시드 {SEED} (round-robin 결정적 배정)\n") + teams, seed_used, attempts = assign(SEED) + print(f"# 시드 {seed_used} (시도 {attempts}회), 팀 사이즈 {TEAM_SIZES}\n") # 부서별 분포 검증 dept_dist = defaultdict(list) @@ -117,16 +171,26 @@ def main(): print("# 부서별 팀 분배 (max - min ≤ 1 = 균등)") for dept, dist in sorted(dept_dist.items(), key=lambda x: -sum(x[1])): print(f" {dept}: {dist} (총 {sum(dist)}명)") + + senior_dist = [sum(1 for n, _ in t if n in SENIORS) for t in teams] + print(f"\n# 시니어 분배: {senior_dist} (총 {sum(senior_dist)}명, 모든 팀 ≥1)") print() mapping = {} for i, team in enumerate(teams, 1): team_name = f"팀{i}" dept_counts = Counter(d for _, d in team) - dept_summary = ", ".join(f"{d.replace('MLOps ', '').replace('AI Efficiency Tech', 'AI Eff')} {c}" for d, c in dept_counts.items()) - print(f"## {team_name} ({dept_summary})") + dept_summary = ", ".join( + f"{d.replace('MLOps ', '').replace('AI Efficiency Tech', 'AI Eff')} {c}" + for d, c in dept_counts.items() + ) + n_senior = sum(1 for n, _ in team if n in SENIORS) + size = len(team) + print(f"## {team_name} ({size}명, 시니어 {n_senior}, {dept_summary})") for name, dept in team: - print(f" - {name} ({dept})") + tag = " ⭐시니어" if name in SENIORS else "" + tag += " 🌱최주니어" if name == "김재현" else "" + print(f" - {name} ({dept}){tag}") mapping[name] = team_name print() diff --git a/participants.json b/participants.json index f4d82bf..730d121 100644 --- a/participants.json +++ b/participants.json @@ -1,37 +1,36 @@ { - "심성환": "팀1", - "오근현": "팀1", + "이성재": "팀1", + "김민섭": "팀1", + "이준석": "팀1", "박영훈": "팀1", - "이지환": "팀1", - "김동국": "팀1", - "이준형": "팀2", + "박재호": "팀1", "조민정": "팀2", - "최호진": "팀2", - "서희": "팀2", - "김태현": "팀2", - "정현준": "팀3", - "김민섭": "팀3", - "김영관": "팀3", - "김병훈": "팀3", - "변수민": "팀3", - "유준희": "팀4", - "유지원": "팀4", - "김재현": "팀4", + "이준형": "팀2", + "김재현": "팀2", + "길주현": "팀2", + "김병훈": "팀2", + "유준희": "팀3", + "유용혁": "팀3", + "오근현": "팀3", + "정채윤": "팀3", + "강승형": "팀3", + "이정태": "팀4", + "서한배": "팀4", + "김영관": "팀4", "장혁진": "팀4", - "박재호": "팀4", - "이재광": "팀5", - "서한배": "팀5", - "이준석": "팀5", - "장다현": "팀5", - "한지승": "팀5", - "이정태": "팀6", - "김호승": "팀6", - "유용혁": "팀6", - "길주현": "팀6", - "강승형": "팀6", - "이성재": "팀7", - "전효준": "팀7", + "손현준": "팀4", + "유지원": "팀5", + "정현준": "팀5", + "최호진": "팀5", + "서희": "팀5", + "변수민": "팀5", + "이재광": "팀6", + "심성환": "팀6", + "전효준": "팀6", + "장다현": "팀6", + "김동국": "팀6", + "김호승": "팀7", "김정명": "팀7", - "정채윤": "팀7", - "손현준": "팀7" + "이지환": "팀7", + "한지승": "팀7" } \ No newline at end of file