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명) 배정. 참가자 34명 → 7팀 배정 (5명 6팀 + 4명 1팀).
같은 부서 2명 이하 제약. 랜덤 시드로 재현 가능. - 부서별 균등 분배 (max - min ≤ 1)
- 시니어 균등 분배 (max - min ≤ 1, 모든 팀 ≥1)
- 김태현은 진행요원 (참가 X)
- AI Efficiency Tech는 시니어/주니어 정보 없음 → 균등 분배 대상 외
결과: participants.json (이름→팀 매핑) + 콘솔 출력. 결과: participants.json (이름→팀 매핑) + 콘솔 출력.
""" """
import json import json
@@ -8,14 +12,22 @@ import random
from collections import Counter, defaultdict from collections import Counter, defaultdict
from pathlib import Path from pathlib import Path
# 시니어 명단 (Platform/Data/HPC/System만 알고 있음)
SENIORS = {
"한지승", "손현준", "강승형", "변수민", # Platform/Data
"김정명", "김영관", "전효준", # HPC
"박영훈", "서희", "김병훈", # System
}
PEOPLE = [ PEOPLE = [
# (name, dept)
("한지승", "MLOps Platform"), ("한지승", "MLOps Platform"),
("변수민", "MLOps Data"), ("변수민", "MLOps Data"),
("박재호", "MLOps Data"), ("박재호", "MLOps Data"),
("김태현", "MLOps Data"),
("강승형", "MLOps Data"), ("강승형", "MLOps Data"),
("손현준", "MLOps Data"), ("손현준", "MLOps Data"),
("김동국", "MLOps Data"), ("김동국", "MLOps Data"),
# 김태현 진행요원 (제외)
("김재현", "MLOps HPC"), ("김재현", "MLOps HPC"),
("이준석", "MLOps HPC"), ("이준석", "MLOps HPC"),
("오근현", "MLOps HPC"), ("오근현", "MLOps HPC"),
@@ -47,21 +59,24 @@ PEOPLE = [
] ]
NUM_TEAMS = 7 NUM_TEAMS = 7
TEAM_SIZE = 5 # 34명 / 7팀 = 5명 6팀 + 4명 1팀
TEAM_SIZES = [5] * 6 + [4]
SEED = 20260428 # 행사일 시드 (재현 가능) 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 (균등) - 팀 사이즈 정확히 TEAM_SIZES (가변)
- 모든 팀 정확히 5명 - 부서별 max - min ≤ 1 (자연스럽게)
- EffTech 12명: 5팀이 2명, 2팀이 1명 (모든 팀 ≥1) - 시니어 균등은 시드 재시도로 검증
""" """
rng = random.Random(seed) rng = random.Random(seed)
by_dept = defaultdict(list) by_dept = defaultdict(list)
@@ -71,41 +86,80 @@ def assign(seed):
for d in by_dept: for d in by_dept:
rng.shuffle(by_dept[d]) rng.shuffle(by_dept[d])
depts_sorted = sorted(by_dept.keys(), key=lambda d: -len(by_dept[d]))
teams = [[] for _ in range(NUM_TEAMS)] 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: for dept in depts_sorted:
members = by_dept[dept] for person in by_dept[dept]:
n = len(members) dept_counts = [Counter(d for _, d in t).get(dept, 0) for t in teams]
q, r = divmod(n, NUM_TEAMS) priority = sorted(
range(NUM_TEAMS),
if r > 0: key=lambda i: (
# ceil_count 적은 팀 우선, tie는 random -remaining[i],
order = sorted(range(NUM_TEAMS), key=lambda i: (ceil_count[i], rng.random())) dept_counts[i],
ceil_teams = set(order[:r]) rng.random(),
else: ),
ceil_teams = set() )
for ti in priority:
idx = 0 if remaining[ti] > 0:
for ti in range(NUM_TEAMS): teams[ti].append((person, dept))
count = q + (1 if ti in ceil_teams else 0) remaining[ti] -= 1
for _ in range(count): break
teams[ti].append((members[idx], dept))
idx += 1
if ti in ceil_teams:
ceil_count[ti] += 1
return teams 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(): def main():
assert len(PEOPLE) == NUM_TEAMS * TEAM_SIZE, ( assert len(PEOPLE) == sum(TEAM_SIZES), (
f"인원수 불일치: {len(PEOPLE)}명, 기대 {NUM_TEAMS * TEAM_SIZE}" f"인원수 불일치: {len(PEOPLE)}명, 기대 {sum(TEAM_SIZES)}"
) )
teams = assign(SEED) teams, seed_used, attempts = assign(SEED)
print(f"# 시드 {SEED} (round-robin 결정적 배정)\n") print(f"# 시드 {seed_used} (시도 {attempts}회), 팀 사이즈 {TEAM_SIZES}\n")
# 부서별 분포 검증 # 부서별 분포 검증
dept_dist = defaultdict(list) dept_dist = defaultdict(list)
@@ -117,16 +171,26 @@ def main():
print("# 부서별 팀 분배 (max - min ≤ 1 = 균등)") print("# 부서별 팀 분배 (max - min ≤ 1 = 균등)")
for dept, dist in sorted(dept_dist.items(), key=lambda x: -sum(x[1])): for dept, dist in sorted(dept_dist.items(), key=lambda x: -sum(x[1])):
print(f" {dept}: {dist} (총 {sum(dist)}명)") 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() print()
mapping = {} mapping = {}
for i, team in enumerate(teams, 1): for i, team in enumerate(teams, 1):
team_name = f"{i}" team_name = f"{i}"
dept_counts = Counter(d for _, d in team) 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()) dept_summary = ", ".join(
print(f"## {team_name} ({dept_summary})") 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: 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 mapping[name] = team_name
print() print()

View File

@@ -1,37 +1,36 @@
{ {
"심성환": "팀1", "이성재": "팀1",
"오근현": "팀1", "김민섭": "팀1",
"이준석": "팀1",
"박영훈": "팀1", "박영훈": "팀1",
"이지환": "팀1", "박재호": "팀1",
"김동국": "팀1",
"이준형": "팀2",
"조민정": "팀2", "조민정": "팀2",
"최호진": "팀2", "이준형": "팀2",
"서희": "팀2", "김재현": "팀2",
"김태현": "팀2", "길주현": "팀2",
"정현준": "팀3", "김병훈": "팀2",
"김민섭": "팀3", "유준희": "팀3",
"김영관": "팀3", "유용혁": "팀3",
"김병훈": "팀3", "오근현": "팀3",
"변수민": "팀3", "정채윤": "팀3",
"유준희": "팀4", "강승형": "팀3",
"유지원": "팀4", "이정태": "팀4",
"김재현": "팀4", "서한배": "팀4",
"김영관": "팀4",
"장혁진": "팀4", "장혁진": "팀4",
"박재호": "팀4", "손현준": "팀4",
"이재광": "팀5", "유지원": "팀5",
"서한배": "팀5", "정현준": "팀5",
"이준석": "팀5", "최호진": "팀5",
"장다현": "팀5", "서희": "팀5",
"한지승": "팀5", "변수민": "팀5",
"이정태": "팀6", "이재광": "팀6",
"김호승": "팀6", "심성환": "팀6",
"유용혁": "팀6", "전효준": "팀6",
"길주현": "팀6", "장다현": "팀6",
"강승형": "팀6", "김동국": "팀6",
"이성재": "팀7", "김호승": "팀7",
"전효준": "팀7",
"김정명": "팀7", "김정명": "팀7",
"정채윤": "팀7", "이지환": "팀7",
"손현준": "팀7" "한지승": "팀7"
} }