feat: 시니어 균등 분배 + 김태현 진행요원 제외
변경: - 김태현 PEOPLE에서 제거 (진행요원, 35→34명) - 팀 사이즈 [5,5,5,5,5,5,4] 가변 - 시니어 명단 10명 정의 (Platform/Data/HPC/System만) - 알고리즘 일반화: 가변 사이즈 + 부서 균등 + 시니어 균등 + 모든 팀 ≥1 시니어/EffTech - 시드 재시도로 모든 제약 만족 - 김재현(최주니어) 표시 결과: 시니어 [1,1,1,2,2,1,2] Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
142
assign_teams.py
142
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()
|
||||
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
Reference in New Issue
Block a user