Contents

자동 팀 배정 프로그램을 만들어 보았다

Pixabay로부터 입수된 Maike und Björn Bröskamp님의 이미지 입니다.


실생활에서 마주하는 문제를 코딩을 이용하면 꽤 효율적으로 해결할 수 있다. 특히 문서작업에서 자동화를 한다면 훨씬 효율적으로 일할 수 있다.

최근에 동아리의 팀 자동 배정 프로그램을 만들어 보았다. 만약 이 프로그램이 없다면 매우 힘들었을 것이다. 왜냐하면 많은수의 해당하는 사람들의 시간들을 직접 확인하면서 팀을 짜야하기 때문이다.

이는 많은 시행착오가 필요하지만 코드는 돌리기만 하면 자동으로 배정된다.

목적

코드의 목적은 많은수의 사람들의 팀을 자동 배정하는 것이다.

각 사람들은 여러 시간 중에서 자신이 원하는 선택지를 복수 선택한다. 각 인원을 여러 개의 팀으로 균등하게 분배해야 한다. 만약 사람들이 많아지면 직접 배정하기는 더욱 힘들어 질 것이다.

엑셀 파일을 입력으로 받아 자동으로 여러팀이 균등한 인원으로 배정된 후 엑셀 파일로 반환된다.

이 프로그램은 특정 엑셀 파일에만 맞추어진 것이기 때문에 참고만 할 것

처음에는 쉬울 것이라고 생각했다. 만약 알고리즘을 연습 했었더라면..

평소에 코테 실력을 키워놨으면 훨씬 간결하고 효율적인 코드를 짤 수 있을 것 같다. 하지만 모든 일에 ‘완벽’ 이란 없다.

따라서 내가 할 수 있는 최선의 코드를 짜고 꽤 유용한 도구를 만들어 내면 된다.

prerequisites

코드는 파이썬으로 작성했고 pandas, random, collection, copy 등의 라이브러리가 필요하다. 누구나 쉽게 세팅하고 코드를 돌릴 수 있는 Google collab.ipynb 파일을 기반으로 만들었다.

Google colab notebook구글 드라이브에서 무료로 만들 수 있다. 해당 파일에서 파이썬 코드를 cell 단위로 실행할 수 있다. 딥러닝 공부하고 산학 프로그램 만들 때 이용했던 것들인데 다시 보니 반가웠다.

파이썬으로 프로그램 만든 건 산학 프로그램이 있다. 그렇지만 딥러닝 이기 때문에 이런 프로그램과는 성격이 다르다. 번외로 매크로를 만들어 본 적이 있는데 이것과 유사 했던 것 같다. 매크로를 어디에 썼는지는 비밀이다. 궁금한 사람은 댓글로

전처리

pandas를 이용해서 엑셀파일을 가져왔다. 엑셀 파일로 받아 왔지만 값들이 완벽히 정제되어 있지는 않았다. 전처리의 주요 역할은 다음과 같다.

  • 동일 이름 한 개로 통일하기, 스페이스 없애기 , 오전 10시 vs 10시 등
  • , 를 구분으로 하여 가능한 시간들 가져오기
  • 랜덤 배정을 돌리기 위한 딕셔너리 데이터 만들고 반환
# 이름 통일 시킨다.
def change_A(time_list):
	result = []
	for time in time_list:
		if time == 'A 10시':
			result.append('A 새벽 10시')
		else:
			result.append(time)
	return result

# 파일 -> 결과정보, 시간정보, 사람정보, 평균인원수
def 파일가져오기(파일이름):
	df = pd.read_excel(파일이름)
	df = df[~df.duplicated(subset='이름', keep='last')]
	print("column_names: ",df.columns.tolist())
	df = df.rename(columns={'시간 ': '시간'})
	df['시간'] = df['시간'].str.split(', ')
	df['시간'] = df['시간'].apply(change_A)
	df_exploded = df.explode('시간')[['시간','이름']]
	
	unique_times = df_exploded['시간'].unique()
	df_result = pd.DataFrame()
	df_result['시간'] = unique_times
	df_result['이름'] = [[] for _ in range(len(df_result))]
	result_dict = df_result.set_index('시간')['이름'].to_dict()
	
	time_df = df_exploded.groupby('시간').agg(lambda x: x.unique().tolist() if x.dtype == 'O' else x.tolist()).reset_index()
	time_dict = time_df.set_index('시간')['이름'].to_dict()
	
	people_dict = df.set_index('이름')['시간'].to_dict()
	average_number = int(df.shape[0]/df_result.shape[0])
	
	return result_dict,time_dict,people_dict,average_number

dictionary vs pandas DataFrame

DataFrame 은 SQL 테이블, Excel 과 같은 데이터 구조를 갖고 있다.

The pandas DataFrame is a structure that contains two-dimensional data and its corresponding labels. DataFrames are widely used in data science, machine learning, scientific computing, and many other data-intensive fields.

파이썬에서 엑셀 데이터를 처리하기에는 pandas DataFrame 이 제일 좋다. 그럼에도 불구하고 왜 Dictionary 로 값들을 저장하였는가 에 대해서 이야기를 해볼까 한다.

  1. 개별적으로 수정하기 까다롭다.

    DataFrame 은 일괄 처리 하기에는 좋다. 그러나 개별적으로 관리하기에는 어려웠다. 프로그램을 만들 때 랜덤한 선택을 하고 다양한 기능들이 수행 되어야 한다.

     DataFrame 으로 저장하고 코드를 짜보았는데 이 부분이 어려웠다. 그러나 Dictionary 로 바꾸고 나서 매우 수월하게 코드를 짤 수 있었다. 특히 데이터를 개별적으로 없애거나 추가하기 좋다.

  2. 배정하는 과정에서 로그를 찍기 어렵다.

    함수를 실행 할때 Dictionary는 for loop 로 실행하며 진행 상황을 볼 수 있지만 DataFrame 은 불가능 하다. 각 iteration 마다 랜덤으로 배정하고 현황을 파악하기에는 Dictionary가 더 편했다.

주요 함수들

여기에는 모든 함수나 코드를 올리진 않았다. 따라서 전체 코드가 궁금하면 github 참고

pop

배정이 완료된 사람이나 시간들은 제거해 줘야 한다.

def pop_person(name):
	for key in time_dict:
	if name in time_dict[key]:
		time_dict[key].remove(name)
	if name in people_dict:
		people_dict.pop(name)

def pop_time(time):
	for key in people_dict:
		if time in people_dict[key]:
			people_dict[key].remove(time)
		if time in time_dict:
			time_dict.pop(time)

어쩔수없는 배정

각 사람은 복수로 자신이 가능한 시간들을 선택했다. 각 팀들에게 랜덤으로 사람들이 배정이 된다. 

팀 배정이 진행됨에 따라 ‘귀염둥이’라는 사람이 가능한 팀들 중에서 하나만 남을 수 있다. 이때 그 사람은 어쩔 수 없이 남은 시간에 배정돼야 한다.

시간도 마찬 가지이다. 자신에게 배정될 수 있는 사람들 pool 이 있다. 랜덤으로 배정되기 시작하면 그 사람들은 점점 떠나 간다. 그럼 남은 사람이라도 잡아야 한다. 따라서 남은 사람들은 강제로 그 시간대에 배정된다.

def 어쩔수없는사람배정():
	# 어쩔수 없는 사람배정
	배정완료된사람들 = []
	배정완료된시간들 = []
	for key in people_dict: 
		if(len(people_dict[key]) == 1):
			time = people_dict[key][0]
			result_dict[time].append(key)
			배정완료된사람들.append(key)
			if(len(result_dict[time]) >= average_number):
				배정완료된시간들.append(time)
	사람과시간제거하기(배정완료된사람들,배정완료된시간들)
def 어쩔수없는시간배정():
	# 어쩔수 없는 시간배정
	배정완료된사람들 = []
	배정완료된시간들 = []
	for key in time_dict:
		# 남아있는 카드를 모두 써야 할때
		if(len(time_dict[key]) <= (average_number - len(result_dict[key]))):
			print('어쩔수없는시간배정 실행', key,": ",time_dict[key])
			result_dict[key].extend(time_dict[key])
			배정완료된사람들.extend(time_dict[key])
			배정완료된시간들.append(key)
	
	# Use Counter to find duplicated values
	counter = Counter(배정완료된사람들)
	# Get duplicated values
	duplicates = [item for item, count in counter.items() if count > 1]
	if len(duplicates) > 0:
		print("중복배정된 사람들: ", duplicates)
	
	# Get unique values
	배정완료된사람들 = list(counter.keys())
	
	사람과시간제거하기(배정완료된사람들,배정완료된시간들)

랜덤배정 및 남은사람배정

랜덤 배정은 말 그대로 각 팀에게 랜덤으로 사람들을 배정하는 것이다.

한 가지 생각해야 하는 부분이 있는데 바로 남은 사람을 배정하는 것이다. 예를 들어서 각 팀에게 할당되어야 하는 최소 인원이 9 명이라고 한다면 모두 배정되고 나서 5명 정도가 남을 수 있다. 이때 5명을 다른 프로세스로 배정해 주는 함수가 남은사람배정()이다.

왜 이렇게 복잡하게 했냐고 물어볼 수도 있다. 어쩌다 보니까 그렇게 되었다. 코드를 짜다 보면 이런 상황이 생긴다. 따라서 우리는 생각의 힘을 길러야 한다.

def 랜덤시간배정(time):
	배정완료된사람들 = []
	배정완료된시간들 = []
	names = random.sample(time_dict[time], average_number - len(result_dict[time]))
	result_dict[time].extend(names)
	배정완료된시간들.append(time)
	배정완료된사람들.extend(names)
	사람과시간제거하기(배정완료된사람들,배정완료된시간들)

def 남은사람배정(data):
	print('---------------------------')
	print("남은사람들 배정: ",len(data),"명")
	already_assignmed_time = []
	for name in data:
		times = list(set(data[name])- set(already_assignmed_time))
		if(len(times) != 0):
			random_time = random.choice(times)
			already_assignmed_time.append(random_time)
		else:
			random_time = random.choice(data[name])
		print(name,': ',random_time)
		result_dict[random_time].append(name)
	print("-----------배정완료------------")

마무리 함수

앞에 나왔던 함수들을 최종적으로 통합하고 전체 프로세스를 책임지는 함수이다.

def 배정하기(file_name):
	deep_copy = copy.deepcopy(people_dict)
	print('---------- #0 -----------')
	어쩔수없는사람배정()
	어쩔수없는시간배정()
	총사람수(result_dict)
	for i in range(len(time_dict)):
		print('-----------#{}------------'.format(i+1))
		랜덤시간배정(sort_dict(time_dict)[0])
		총사람수(result_dict)
		if(len(time_dict) == 0):
			break
	
	remained_people_dict = {key: deep_copy[key] for key in list(people_dict.keys())}
	남은사람배정(remained_people_dict)
	
	for time in result_dict:
		print(time , ": ",len(result_dict[time]),'명 -',result_dict[time])
	
	pd.DataFrame.from_dict(result_dict, orient='index').to_excel(file_name)

 남은 사람 배정()에 함정이 있다. 이 함수는 균등하게 배정하지 않는다.   따라서 한 팀이 평균 인원보다 2명 이상 많다면 다시 배정하면 좋다.     밑에 두 개의 셀만 다시 실행 하면 된다. python 은 느리지만 사람보다는 빠르기 때문에 금방 된다.

  1. result_dict,time_dict,people_dict,average_number = 파일가져오기(input_filename)
  2. 배정하기(output_filename)

결과를 사진으로 보여주고 싶지만 개인 정보이기 때문에 생략하도록 하겠다.

결론

단순히 맞는 시간을 정하는 것이었다면 쉬웠을 것이다. 하지만 랜덤과 적절한 배분을 동시에 하는 게 까다로웠다. 짧은 시간 동안 조금은 서툴지만 흥미로운 코드를 짤 수 있어서 재밌었다. 

1년 만에 python 과 pandas 를 보니 추억이 되살아났고 이번에 나의 기술을 이용해 누군가를 도와줄 수 있었다. 따라서 뭐든지 헛된 공부는 없고 앞으로 무엇을 배우든지 즐거운 마음을 갖고 해야겠다.