아이템 획득과 관련된 사용자의 게임 로그 데이터를 정제하는 예시를 알아본다.
상황 설정
게임개발사 DD는 이번 시즌 업데이트를 하면서 새로운 던전을 공개하였다. 이 던전의 입장권을 얻기 위한 히든 퀘스트 발동은 몬스터 사냥으로부터 떨어지는 특정 아이템 3개를 습득하였을 때 사용자의 우편함에 도착하는 “수상한 편지”를 읽는 순간 시작된다.
DD사의 데이터 분석가 욱킴은 업데이트 직후 사용자 플레이 로그데이터를 확보하여 이를 추적 분석하려고 했다. 그런데 기획 담당자의 실수로 최초 “수상한 편지” 발송 시점 로그 설계를 빠뜨려 해당 로그가 기록되지 않는 바람에 어쩔 수 없이 사용자의 아이템 루팅(습득)이력을 기반으로 역추적 해야하는 어처구니 없는 일이 발생했다.
이를 해결하기 위해 분석 핵심 코드를 작성해야 하는 욱킴은 고민에 빠지게 되고….
데이터 준비
한 명의 사용자 로그데이터를 정제하는 것을 기준으로 다음과 같이 데이터를 준비해본다.
1 | import pandas as pd |
먼저 사용자가 습득가능한 아이템은 A부터 F까지 있으며 랜덤하게 20개의 습득 데이터가 있다고 가정한다. 그리고 해당 아이템의 시간 정보를 생성하고 이를 데이터프레임으로 합친다.
1 | np.random.seed(random_seed) |
time_stamp | root | |
---|---|---|
0 | 86 | E |
1 | 118 | E |
2 | 681 | E |
3 | 724 | E |
4 | 728 | D |
5 | 953 | B |
6 | 967 | C |
7 | 1031 | C |
8 | 1045 | A |
9 | 1292 | B |
10 | 1294 | E |
11 | 1308 | C |
12 | 1473 | B |
13 | 1904 | A |
14 | 2002 | A |
15 | 2004 | E |
16 | 2466 | C |
17 | 2760 | F |
18 | 2856 | A |
19 | 2924 | F |
그리고 퀘스트 발동 조건에 필요한 아이템 목록은 다음과 같다.
1 | target_items = ["A", "B", "C"] |
기초적인 접근
먼저 생각할 수 있는 접근은 반복문이다. 한 줄씩 읽어가며 각 원소의 고유값에 타겟 아이템이 몇 개나 매칭이 되는지 확인하는 코드는 다음과 같다.
1 | for n in range(len(df_log)): |
중간 즈음 부터 3이 등장하는 것을 알 수 있다. 그리고 이 코드에서는 .iloc[]
인덱서가 아닌 .loc[]
인덱서를 사용하였기 때문에 코드를 옮겨 적는 경우에주의하도록 하자.
상기 코드는 출력물이 많으니 조건문을 추가해보자.
1 | for n in range(len(df_log)): |
“cnt” 객체는 굳이 필요없으나 너무 옆으로 길어지면 보기 불편할까봐 일부러 추가하였다. 아무튼 반복문과 조건문을 활용하여 원하는 시간을 뽑기 위한 인덱스 번호를 얻을 수 있지만 반복문이 있기 때문에 영 보기에 좋지 않고 대용량 데이터 처리에서 연산시간이 꽤 소요될 수 있다.
개선된 접근
반복문 없이 처리할 수 있을까?
일단 아이템 하나를 대상으로 했을 때 어떤 결과가 나오는지 보자.
1 | df_log["root"] == target_items[0] |
해당 아이템과 매칭이 되는 원소의 위치에 True
가 적혀있는 것을 볼 수 있다. 그리고 True
는 숫자로 취급될 경우 1이기 때문에 이를 힌트로 하여 최대값이 위치한 최초의 인덱스값을 뽑는 .idxmax()
메서드를 활용할 수 있다.
1 | (df_log["root"] == target_items[0]).idxmax() |
이 접근을 기반으로 다른 아이템도 똑같이 코드를 작성하게 되고 모든 아이템이 들어온 시점은 각 타겟 아이템의 획득 순서의 최대값이 바로 이벤트 발생 조건에 해당하는 아이템이 인벤토리에 들어온 시점에 해당하는 인덱스 값이 된다고 할 수 있다.
다음의 코드를 보자.
1 | max((df_log["root"] == target_items[0]).idxmax(), |
아이템은 3개이고 각 “target_items” 객체의 인덱스를 바꿔가며 계산한 결과를 볼 수 있다. 하지만 0부터 시작하는 등차수열의 모양새에 굳이 중복되는 코드를 길게 적을 필요는 없다고 본다. 그래서 이를 list comprehension(리스트 내포)로 바꿔보면 다음과 같다.
1 | max([(df_log["root"] == target_items[n]).idxmax() for n in range(len(target_items))]) |
이렇게 한줄로 간결하게 결과를 뽑을 수 있고 이를 기반으로 해당 코드를 필터링에 활용하게 되면 다음과 같다.
1 | df_log.loc[[max([(df_log["root"] == target_items[n]).idxmax() for n in range(len(target_items))])], ] |
time_stamp | root | |
---|---|---|
8 | 1045 | A |
이제 한 명의 유저를 대상으로 값을 뽑았으니 핵심 코드의 작성이 끝났다고 할 수 있다.
참고로 R버전의 풀이는 링크를 클릭하면 된다.