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:
th-kim0823
2026-04-25 19:15:12 +09:00
parent 3453217e62
commit aa427d6670
2 changed files with 133 additions and 70 deletions

View File

@@ -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()

View File

@@ -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"
}