개발/데이터 분석

배틀그라운드 유저 데이터 분석해보기 3. 데이터 가공(+팁)

잠수돌침대 2023. 1. 17. 18:00

완성된 프로젝트를 보고 계십니다. 분석 결과만을 보고 싶으시다면 아래의 URL에 접속해주세요.

https://songmin9813.tistory.com/51(Steam 버전 배틀그라운드유저 분석해보기)

 

이전 내용과 이어집니다.

https://songmin9813.tistory.com/44(2. 데이터 인사이트 및 추출)

 

사람마다 사용하거나 손에 익은 언어가 있다고는 하지만. 그리고 SQL을 능숙히 다루기 위해 시작한 프로젝트이기도 하다만. 결국 손에 맞는 Python을 먼저 찾게 되더라...ㅋㅋㅋ

 

지금 같은 경우에는 내가 원하는 데이터를 처음부터 다시 뽑고 나만의 데이터 마트를 만드는 것이 주된 목적이었기 때문에 SQL보다는 Python을 이용한 데이터 가공을 주된 목적으로 삼았다.

 

SQL을 따로 배워서 나쁠 건 없었다. 적어도 Join의 개념에 대해서는 확실하게 짚고 넘어간 건 이후 Tableau 사용에서도 많은 도움이 되었다.


데이터 가공 실패 - 나쁜 코드

 

여러 JSON->딕셔너리 형태로 존재하는 350MB 파일을 불러 읽고 이해하는 데에도 많은 시간을 잡아먹었다. 이 중에서 첫 번째로 가공하고 싶었던 항목은 다음 항목이었다.

 파일 내 included 이름 내 type이 participant(참여자)인 경우 그 정보만을 추출해 줘!

 

파일은 match_table이라는 변수를 만들어 이곳에 보관하는 과정을 포함하였는데, 처음에 짰던 코드는 아래 코드와 같다.

index=1 # 실패코드 속도가 너무 길어
for i1 in range(len(match_table)):
    for i2 in range(len(match_table[i1]['included'])):
        if i1==0 and i2==0:
            continue
        if match_table[i1]['included'][i2]['type']=='participant':
            temp=pd.concat([temp,pd.DataFrame(match_table[i1]['included'][i2]['attributes']['stats'],index=[index])])
            index+=1
temp.to_csv('./personal_attributes.csv')

 

기본적으로 최초 temp 변수에 1행 1열의 participant 정보를 담고, 그곳에 concat 함수를 이용한 적재 방식을 응용한 2중 for문으로 작성했다.

 

약 4만 판이었으니 아무리 많아야 100만 플레이어는 되겠지~ 생각하면서 여유롭게 프로그램을 돌려놓고 이런저런 게임을 하기 시작했다.

 

그렇게 시간이 지나기를 3시간... 코드는 끝날 기미를 보이지 않고, 단순한 2중 for문으로는 속도상의 한계가 뚜렷했음을 확신했다.

 

 언어가 다른 언어에 비해 상대적으로 느리긴 하지만, 대부분의 잘못은 개발자에게 있다. 좀 더 생각해라!  

 

개선된 코드 - map의 응용

 

위의 코드는 기본적으로 included 내 리스트 형식으로 존재할 수 있는 participant의 내용을 하나씩 concat 하는 과정을 담고 있는데, pandas를 배우면서 문득 '리스트 내 모든 값에 대한 똑같은 작업을 실시할 경우 map을 이용하면 훨씬 빠르게 진행이 가능하다.'라는 내용이 떠올랐다.

 

어캐어캐 map을 써볼 수 있나? 

 

map을 이용하면 속도상으로 n 제곱에 있는 복잡도를 n번의 수행으로 획기적으로 줄일 수 있고, 이는 약 4만 번의 빠른 시행으로 코드를 짤 수 있음을 의미했다.

 

이를 응용하여 스타(*) 키워드를 이용한 apply, map 응용 코드는 아래와 같다.

 

for i in range(1,len(match_table)):
    temp=pd.DataFrame(match_table[i]['included'])
    df=temp[temp['type']=='participant']['attributes'].apply(lambda x:x['stats'])
    first=pd.concat([first,pd.DataFrame([*df])])

 

똑같은 결과를 도출해 내지만 스타(*)의 사용과 람다식을 이용한 apply 함수를 작성하여 코드가 더 파이썬스러워진 건 정말 보기 좋은 것 같다. 코드도 짧아지면서 속도도 획기적으로 개선되니 개발의 세계는 넓고도 넓은 게 맞는 것 같다.


첫 번째 허들을 넘기고 - 새로운 정보 만들기

 

처음에는 플레이어 별로 죽은 시간을 초 단위로 그냥 표기를 하려고 했다. 그런데 이걸 아무리 타블로에 표현을 해보려 해도 정말 이상하게 표현되는 것을 알 수 있었다.

솔직히 초 단위로 그렇고 그렇게 직관적인 표기법이 아니다!

 

그렇다면 사용자 입장에서 다시 생각해 봤을 때 다음과 같은 질문을 던질 수 있었다.

 

 

생존 시간이 중요한 정보이기는 하는데, 조금 더 사용자 친화적인 표현은 없을까? 

 

이 생각에 대한 질문은 얼마 있지 않아 떠올릴 수 있었다.

 

 자기장 페이즈 별 생존 정보가 적당할 것 같은데?

 

실제로 필자가 배그를 플레이하면서 페이즈 별로 좁혀져 가는 자기장이 꽤나 무서우면서 강렬하게 다가왔고, 이를 데이터에 접목시켜 페이즈 별로 죽은 플레이어의 횟수로 변환을 시킨다면 더욱 시각적인 데이터가 될 것 같았다.

 

이를 위해 자기장이 좁혀지는 시간을 알려주는 정보 사이트를 여러 뒤져보았고, Battleground.party라는 사이트에서 대부분의 정보를 얻을 수 있었다.

https://battlegrounds.party/circle/(Circle Duration 정보)

 

아...진짜 감사합니다.

페이즈는 맵의 크기를 불문하고 9 페이즈까지 있는 것을 확인했지만, 비교적 최근에 나온 미라마, 태이고 맵에 대한 정보는 존재하지 않는 것을 확인했다. 이는 full video로 존재하는 몇몇 유튜버의 플레이 영상을 참고하면서 대충 비슷한 시간대를 추정하여 함수화 시키는 작업으로 만들었다.

 

한 가지 변수가 있다면 바로 가변 자기장 형식. 사녹과 같은 플레이 템포가 빠른 맵에 대해서는 특정 시간과 별개로 특정 인원 미만으로 해당 페이즈가 진행 중이라면 강제로 다음 페이즈로 넘어가는 기능을 추가한 것을 알 수 있었다.

 


Update #58 (Season 14 - Update 14.2)

  • The Dynamic Blue Zone function operates according to the number of survivors per phase in certain maps. A guiding message will be displayed so that players can be aware of it.
    • Maps: Sanhok, Karakin
    • Dynamic Bluezone Activation Conditions
      • 2nd Phase: Activated when there are less than 30 survivors
      • 3rd Phase: Activated when there are less than 18 survivors
      • 4th Phase: Activated when there are less than 10 survivors
      • 5th Phase: Activated when there are less than 3 survivors
      • 6th Phase: Activated when there are less than 3 survivors
  • The following message will be displayed
    • The Blue Zone delay has been reduced because of the decreased survivor count.

 

업데이트 #58(시즌 14 - 업데이트 14.2)

  • 가변 자기장 기능은 특정 지도에서 페이즈 별 생존자 수에 따라 작동합니다. 선수들이 알아차릴 수 있도록 안내 메시지가 표시됩니다.
    • 적용 맵 : 사녹, 카라킨
    • 가변 자기장 활성화 조건
      • 2 페이즈: 생존자가 30명 미만일 때 활성화됩니다
      • 3 페이즈: 생존자가 18명 미만일 때 활성화됩니다
      • 4 페이즈: 생존자가 10명 미만일 때 활성화됩니다
      • 5 페이즈: 생존자가 3명 미만일 때 활성화됩니다
      • 6 페이즈: 생존자가 3명 미만일 때 활성화됩니다
  • 다음과 같은 안내 메시지가 같이 표시됩니다.
    • 생존자 수가 감소하였기 때문에 자기장 시간이 감소했습니다.

 

출처 : Official PUBG Fandom Wiki(https://pubg.fandom.com/wiki/The_Playzone)


하지만 현재로서 이것까지 구현하는 데에는 데이터적 한계가 보였기에 이를 제하고 정적 자기장만을 계산하는 코드를 작성하여 함수화 시켰다.

 

아래는 초로 존재하는 생존 시간을 페이즈 정보로 바꾸어주는 코드이다.

# 파라모는 사녹, 태이고는 에란겔 자기장 정보 따라갔음
# 가변 자기장은 수식에서 제외(특정 인원 미만일 시 자기장 카운트 시작되는 기능)
def map_to_phase(mapName, duration):
    if mapName=='Savage_Main' or mapName=='Chimera_Main': # 사녹 파라모
        if 0<=duration<450:
            return 1
        elif duration<690:
            return 2
        elif duration<900:
            return 3
        elif duration<1080:
            return 4
        elif duration<1185:
            return 5
        elif duration<1290:
            return 6
        elif duration<1375:
            return 7
        elif duration<1460:
            return 8
        else:
            return 9
    elif mapName=='Desert_Main': # 미라마
        if 0<=duration<720:
            return 1
        elif duration<1060:
            return 2
        elif duration<1300:
            return 3
        elif duration<1480:
            return 4
        elif duration<1640:
            return 5
        elif duration<1760:
            return 6
        elif duration<1880:
            return 7
        elif duration<1970:
            return 8
        else:
            return 9
    elif mapName=='Baltic_Main' or 'Tiger_Main': # 에란겔 태이고
        if 0<=duration<690:
            return 1
        elif duration<990:
            return 2
        elif duration<1210:
            return 3
        elif duration<1390:
            return 4
        elif duration<1550:
            return 5
        elif duration<1670:
            return 6
        elif duration<1770:
            return 7
        elif duration<1860:
            return 8
        else:
            return 9
    else: # 비켄디
        if 0<=duration<630:
            return 1
        elif duration<870:
            return 2
        elif duration<1050:
            return 3
        elif duration<1220:
            return 4
        elif duration<1370:
            return 5
        elif duration<1490:
            return 6
        elif duration<1610:
            return 7
        elif duration<1730:
            return 8
        else:
            return 9

 

그리고 위 함수를 이용하여 한 줄의 파이썬 코드로 자기장 관련 열을 특정 데이터프레임에 추가시킬 수 있다.

df['phase']=list(map(lambda x,y:map_to_phase(x,y),df['mapName'],df['timeSurvived']))

파이썬은 개사기 언어가 맞다.

 

이걸 예쁘게 히스토그램화 시키면 아래와 같은 시각화 자료가 나온다.

전 그림보다 활용도가 높은 데이터이다.

시간이라는 연속 데이터를 범주화시켜 사용자에게 보여주는 것이 더 좋을 것이라는 판단하에 히스토그램화 시킨 결과라 해도 무방할 것이다.

데이터 가공은 결국 아티스트/장인의 영역이라는 것을 새삼 깨닫게 해주는 시간이었다...