플레이어가 뽑은 카드와 대진표 정보를 매칭하여 어떤 플레이어가 예선을 통과하여 본선 경기로 진출하는지 알아보자.
두 데이터프레임을 매칭하여 상위 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
과정 먼저 각 카드 점수에 따른 순위를 만들어놓자. 트럼프 카드의 숫자 카드는 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 분석에 활용할 수도 있겠다.