Py) 전처리 - 카드 게임의 본선 진출자는 누구?

Py) 전처리 - 카드 게임의 본선 진출자는 누구?

플레이어가 뽑은 카드와 대진표 정보를 매칭하여 어떤 플레이어가 예선을 통과하여 본선 경기로 진출하는지 알아보자.

두 데이터프레임을 매칭하여 상위 N 번째 값을 가지는 원소를 추출하는 예제를 카드게임의 예선전에 대입하여 알아보자.
※ 카드는 트럼프 카드로 가정하며 숫자가 A > K > Q > J 순이며 숫자의 경우 높을 수록 좋다.
트럼프 카드

문제 상황

예선 참가자는 추첨을 통해 이루어지고 각 대회마다 참가하는 선수 명단은 다음과 같다. 예를 들어 첫 번째 대회는 “A”, “C”, “D”, “F” 선수가 참가하였다고 가정한다.

1
2
3
df_p = pd.DataFrame([[1, "A,C,D,F"], [2, "B,F,D"], [3, "C,A,B"]],
columns = ["round_id", "match"])
df_p
round_id match
0 0 A,C,D,F
1 1 B,F,D
2 2 C,A,B

숫자 카드만 사용했다고 가정하고 각 대회 참가자가 뽑은 카드의 번호는 다음과 같다.

1
2
3
df_card = pd.DataFrame([["A", 10], ["B", 5],  ["C", 8], ["D", 2], ["F", 3]],
columns = ["player", "card"])
df_card
player card
0 A 10
1 B 5
2 C 8
3 D 2
4 F 3

상기 정보를 토대로 각 대회의 결선 진출자를 추려낸 결과는 다음과 같아야 하겠다.

1
2
3
df_result = pd.DataFrame([["C,A"], ["F,B"], ["C,A"]],
columns = ["Winner"])
df_result
Winner
0 C,A
1 F,B
2 C,A

과정

먼저 각 카드 점수에 따른 순위를 만들어놓자. 트럼프 카드의 숫자 카드는 2부터 10까지 있고 숫자가 클수록 좋기 때문에 .rank() 메서드를 사용할 때 “ascending” 인자에 False를 할당하여 큰 숫자의 순위가 더 높게 매겨지도록 한다.
※ 같은 숫자가 뽑히거나 알파벳 카드까지 사용되거나 매 예선 대회마다 다른 카드를 뽑는다면 코드는 훨씬 길어진다.

1
2
df_card["rank"] = df_card["card"].rank(ascending = False)
df_card
player card rank
0 A 10 1.0
1 B 5 3.0
2 C 8 2.0
3 D 2 5.0
4 F 3 4.0

각 예선 경기에 따른 선수들 매칭표를 .str 접근자(accessor)의 .split() 메서드로 분리해보자. 여기서 데이터프레임 객체를 얻기 위해 “expand” 인자에 True를 할당하였다.

1
2
df_r_split = df_r["match"].str.split(",", expand = True)
df_r_split
0 1 2 3
0 A C D F
1 B F D None
2 C A B None

상기 결과에서 최대 길이에 미치지 못하는 원소의 경우 최대 길이에서 모자란 만큼 결측치가 생기며 여기서는 NaN이 아닌 None으로 표기되며 결측치 관련 메서드를 사용할 수 있다. 다음의 결과를 보도록 하자.

1
2
df_r_split = df_r_split.reset_index().melt(id_vars = "index").dropna()
df_r_split
index variable value
0 0 0 A
1 1 0 B
2 2 0 C
3 0 1 C
4 1 1 F
5 2 1 A
6 0 2 D
7 1 2 D
8 2 2 B
9 0 3 F

각 선수와 카드 점수, 순위 정보를 기존 데이터와 엮어내고 추가로 정렬을 실시(반드시 할 필요는 없음)하여 그 결과를 확인해보자.

1
2
3
df_join = df_r_split.merge(df_card, left_on = "value", right_on = "player")
df_join = df_join.sort_values(["index", "rank"]).reset_index(drop = True)
df_join
index variable value player card rank
0 0 0 A A 10 1.0
1 0 1 C C 8 2.0
2 0 3 F F 3 4.0
3 0 2 D D 2 5.0
4 1 0 B B 5 3.0
5 1 1 F F 3 4.0
6 1 2 D D 2 5.0
7 2 1 A A 10 1.0
8 2 0 C C 8 2.0
9 2 2 B B 5 3.0

정렬을 실시했기 때문에 사용할 수 있는 코드는 다음과 같다. 변수 “index”에는 각 대회 회차정보가 있고 해당 변수를 기준으로 첫 두 개 행을 .head() 메서드를 사용해서 뽑을 수 있다. 단, 이 경우는 동점이 발생한 경우는 제대로 대응이 되지 않을 수 있다.

1
2
df_join_top2 = df_join.groupby("index").head(2)
df_join_top2
index variable value player card rank
0 0 0 A A 10 1.0
1 0 1 C C 8 2.0
4 1 0 B B 5 3.0
5 1 1 F F 3 4.0
7 2 1 A A 10 1.0
8 2 0 C C 8 2.0

만약 .groupby().head() 메서드를 연쇄(chaining)로 사용하지 않으면 .transform() 메서드를 사용한 접근법도 있다.

1
2
3
4
5
def is_rank_top_n(x, rank_top_n = 2):
return (x.rank(ascending = False) <= rank_top_n) + 0

condi = df_join.groupby("index")["card"].transform(lambda x: is_rank_top_n(x) == 1)
df_join.loc[condi, ]
index variable value player card rank
0 0 0 A A 10 1.0
1 0 1 C C 8 2.0
4 1 0 B B 5 3.0
5 1 1 F F 3 4.0
7 2 1 A A 10 1.0
8 2 0 C C 8 2.0

최종적으로 다음과 같이 대회별로 점수 상위 2명의 정보를 요약해볼 수 있다.

1
2
3
ser_top = df_join_top2.groupby("index")["player"].agg(lambda x: x.str.cat(sep = ","))
df_top = ser_top.reset_index()
df_top
index player
0 0 A,C
1 1 B,F
2 2 A,C

결과

이 예제는 앞에서 언급한 것과 같이 여러가지 제약사항이 걸려있고 실제로 일반화 하려면 훨씬 복잡한 코드가 필요할 수 있다. 그래도 이 예제를 통해 .rank().transform() 메서드를 활용하고 단순 순위가 아니라 그룹별 순위 처리를 반복문 없이 처리해보는 실습을 할 수 있기에 그 의미가 있다고 할 수 있겠다.

추가로 이런 게임 관련 예제가 아니라 어떤 사람이 구매한 제품 중 가잔 비싼 두 개의 상품을 추출하는 예제에도 응용할 수 있고 특정 업무단위별 세부 작업 중 가장 소요일이 긴 작업 n개를 추출하여 Critical Path 분석에 활용할 수도 있겠다.

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×