본문 바로가기
프로그래밍

[코딩] Riot API에서 정보 가져오기 3

by 기이한날개 2022. 2. 1.

시야 점수와 와드 관련 데이터를 막대그래프로 시각화하는 코드를 작성했다.

 

우선 Riot API에서 불러온 json 데이터를 우리가 사용하기 쉬운 pandas dataframe으로 바꿔주어야 한다.

create_dataframe 함수를 정의해 유저 한 명에 대한 여러 경기 데이터 json에서 유용한 정보들을 테이블 형식으로 바꾸어 주도록 했고, 입력값으로 index_list를 받아 변환된 dataframe 중 필요한 열만 가져올 수 있게 만들었다.

 

import sqlite3
from start import *
from tqdm import tqdm
import json
import pandas as pd
import matplotlib.pyplot as plt
import matplotlib
import numpy as np

conn = sqlite3.connect("gamedata.db") 
cur = conn.cursor()

def create_dataframe(nickname, index_list=None):
    cur.execute('''SELECT idx, puuid FROM user WHERE nickname_nocase = REPLACE( (?) , ' ','')''', (nickname, ))
    row = cur.fetchone()
    user_idx = row[0]
    puuid = row[1]

    # game_played 에서 puuid에 맞는 match_id 검색
    cur.execute("SELECT match_id FROM game_played WHERE user_idx = ( ? )", ( user_idx, ))
    match_list = cur.fetchall()

    # match_id로 match data 중 필요한 부분 추출
    column_index = ['match_id', 'game_creation_time', 'game_duration', 'game_mode', 'game_name', 'game_type', 
                    'champion_name', 'champion_id', 'kill', 'death', 'assist', 'win',
                    'neutral_minions_killed', 'total_minions_killed', 'gold_earned', 'gold_spent',
                    'total_damage_dealt', 'total_damage_dealt_to_champions', 'total_damage_taken', 
                    'physical_damage_dealt', 'physical_damage_dealt_to_champions', 'physical_damage_taken',
                    'magic_damage_dealt', 'magic_damage_dealt_to_champions', 'magic_damage_taken',
                    'true_damage_dealt', 'true_damage_dealt_to_champions', 'true_damage_taken',
                    'longest_time_spent_living', 'total_time_spent_dead',
                    'lane', 'vision_score', 'ward_bought', 'ward_placed', 'detector_ward_placed', 'ward_killed']
 
    df = pd.DataFrame(columns=column_index)
    for idx, match in tqdm(enumerate(match_list), desc='fetching match data'):
        match = match[0]
        cur.execute('SELECT data FROM match WHERE match_id = ( ? )', (str(match), ))
        data = cur.fetchone()[0]
        data = json.loads(data)

        if 'status' in data.keys():
            print(match, 'error in data file')
            print(data)
            continue

        game_creation_time = data['info']['gameCreation']
        game_duration = data['info']['gameDuration']
        game_mode = data['info']['gameMode']
        game_name = data['info']['gameName']
        game_type = data['info']['gameType']
        # game_end_time = data['info']['gameEndTimestamp']

        for player in data['info']['participants']:
            if player['puuid'] == puuid:
                # champ, kda
                champion_name = player['championName']
                champion_id = player['championId']
                kill = player['kills']
                death = player['deaths']
                assist = player['assists']
                win = player['win'] # True(Win) / False(Loss)
                # gold
                neutral_minions_killed = player['neutralMinionsKilled']
                total_minions_killed = player['totalMinionsKilled']
                gold_earned = player['goldEarned']
                gold_spent = player['goldSpent']
                # damage
                total_damage_dealt = player['totalDamageDealt']
                total_damage_dealt_to_champions = player['totalDamageDealtToChampions']
                total_damage_taken = player['totalDamageTaken']
                physical_damage_dealt = player['physicalDamageDealt']
                physical_damage_dealt_to_champions = player['physicalDamageDealtToChampions']
                physical_damage_taken = player['physicalDamageTaken']
                magic_damage_dealt = player['magicDamageDealt']
                magic_damage_dealt_to_champions = player['magicDamageDealtToChampions']
                magic_damage_taken = player['magicDamageTaken']
                true_damage_dealt = player['trueDamageDealt']
                true_damage_dealt_to_champions = player['trueDamageDealtToChampions']
                true_damage_taken = player['trueDamageTaken']
                # time dead
                longest_time_spent_living = player['longestTimeSpentLiving']
                total_time_spent_dead = player['totalTimeSpentDead']
                # vision, ward
                lane = player['teamPosition']
                vision_score = player['visionScore']
                ward_bought = player['visionWardsBoughtInGame']
                ward_placed = player['wardsPlaced']
                detector_ward_placed = player['detectorWardsPlaced']
                ward_killed = player['wardsKilled']
                
                df.loc[idx] = [str(match), game_creation_time, game_duration, game_mode, game_name, game_type, 
                                champion_name, champion_id, kill, death, assist, win, 
                                neutral_minions_killed, total_minions_killed, gold_earned, gold_spent, 
                                total_damage_dealt, total_damage_dealt_to_champions, total_damage_taken, 
                                physical_damage_dealt, physical_damage_dealt_to_champions, physical_damage_taken, 
                                magic_damage_dealt, magic_damage_dealt_to_champions, magic_damage_taken, 
                                true_damage_dealt, true_damage_dealt_to_champions, true_damage_taken, 
                                longest_time_spent_living, total_time_spent_dead, 
                                lane, vision_score, ward_bought, ward_placed, detector_ward_placed, ward_killed]
                break     

    if index_list is None:
        return df
    elif type(index_list) is list and all(i in column_index for i in index_list):
        return df[index_list]
    elif type(index_list) is list:
        print("Wrong Index List: Check for accurate index")
        print(column_index)
        return None
    else:
        print("Wrong Index List input(Must be a list of indices)")
        return None

최대한 많은 데이터를 뽑아내려고 하다 보니 코드가 길어졌지만, 결국은 json 데이터를 깔끔하게 정리하는 작업이다.

생각보다 흥미로운 데이터가 많아서 시각화도 해보고 분석도 해볼 수 있어서 유용했다.

이번 글에서는 이 중에서 시야 점수와 와드 관련 값만 사용해서 시각화해보았다.

 

dataframe에 결측치가 있는지도 확인해준다. 해당 함수는 DACON의 여러 Basic 대회에서 코드 Baseline으로 제공되는 코드를 그대로 가져와 사용했다.

https://dacon.io/competitions/official/235869/codeshare/4253?page=1&dtype=recent 

 

def check_missing_col(dataframe):
    counted_missing_col = 0
    for i, col in enumerate(dataframe.columns):
        missing_values = sum(dataframe[col].isna())
        is_missing = True if missing_values >= 1 else False
        if is_missing:
            counted_missing_col += 1
            print(f'결측치가 있는 컬럼은: {col}입니다')
            print(f'총 {missing_values}개의 결측치가 존재합니다.')

        if i == len(dataframe.columns) - 1 and counted_missing_col == 0:
            print('결측치가 존재하지 않습니다')

 

dataframe을 matplotlib 라이브러리를 이용해 시야 점수와 와드 관련 값을 막대그래프 형식으로 그려주는 함수를 정의해준다. 관심 있는 항목은 라인, 시야 점수, 구매한 와드 수, 박은 와드 수, 지운 와드 수 이렇게 5가지 여서 함수 내에서 해당 열만 골라내서 처리했다. 

 

def visualize_vision(df):
    matplotlib.rcParams['font.family'] = 'NanumBarunGothic'
    matplotlib.rcParams['font.size'] = 15
    matplotlib.rcParams['axes.unicode_minus'] = False 

    # 칼바람나락(ARAM), URF 제거
    print(len(df[df.game_mode != 'CLASSIC']))
    df.drop(df[df.game_mode != 'CLASSIC'].index, inplace=True)
    print(len(df), 'ARAM', 'URF')

    # 순서 변경
    df.sort_values(by='game_creation_time', axis=0, ascending=False, inplace=True)
        
    # 원하는 column만 설정
    index_list = ['lane', 'vision_score', 'ward_bought', 'ward_placed', 'ward_killed']
    df = df[index_list]

    # reset index
    df.reset_index(drop=True, inplace=True)
    
    #group by lane
    df_mean = df.groupby('lane').mean().reset_index()

    # order as ['TOP', 'JUNGLE', 'MIDDLE', 'BOTTOM', 'UTILITY']
    lane_list = ['TOP', 'JUNGLE', 'MIDDLE', 'BOTTOM', 'UTILITY']
    mapping = {day:i for i, day in enumerate(lane_list)}
    key = df_mean['lane'].map(mapping)
    df_mean = df_mean.iloc[key.argsort()]
    df_mean.reset_index(inplace=True, drop=True)

    index = np.arange(len(df_mean))
    w = 0.1
    plt.figure(figsize=(10,6))
    bar = plt.bar(index-3*w, df_mean['vision_score'], width=w, label='시야점수')
    plt.bar(index-w, df_mean['ward_placed'], width=w, label='박은 와드 수')
    plt.bar(index+w, df_mean['ward_bought'], width=w, label='구매 와드 수')
    plt.bar(index+3*w, df_mean['ward_killed'], width=w, label='지운 와드 수')
    for idx, rect in enumerate(bar):
        font = {'color':'black', 'size':12}
        plt.text(idx-0.3, rect.get_height() + 0.5, round(df_mean['vision_score'][idx],1), ha='center', fontdict=font)
    plt.legend(fontsize=14, loc=2)
    
    plt.xticks(index, ['탑', '정글', '미드', '원딜', '서폿'])
    plt.xlabel('Lane')
    plt.show()

하다 보니 게임 자체에 대한 이해와 같이 도메인 지식의 필요성을 느낄 수 있었다.

1. 장신구 와드나 서폿 아이템들을 통해 설치한 와드는 박은 와드 수에는 포함되지만, 구매한 것으로는 인정되지 않는다. 결국 구매한 와드 수는 제어 와드 수와 일치한다. 

2. 라이엇이 의도한 것인지는 모르겠지만, Riot Games Third Party Developer Community 디스코드 채널에서 얻은 정보에 따르면 티모 버섯, 니달리 덫, 요릭 구울 등의 스킬이 박은 와드 수에 포함이 된다고 한다. 따라서 해당 챔피언을 많이 플레이하는 사람일 경우 특정 라인의 ward_placed 데이터가 왜곡될 수 있다. 나는 그런 챔피언을 하지 않아서 상관이 없지만 다른 유저들의 데이터를 분석할 때는 주의가 필요하다. 

3. 칼바람 나락의 경우 시야 점수와 와드 관련 값이 모두 0이고, URF 등 특별 모드는 너무 높게 나왔기 때문에 이를 제외하고 게임 모드가 Classic 인 경기 데이터만을 사용해야 했다.

4. 역할군 별로 시야 점수가 상이할 수 있기 때문에 라인별로 따로 통계를 내기로 했다. 롤을 오래 해 온 사람으로서 서폿의 시야와 와드 관련 점수가 다른 라인에 비해 훨씬 높을 것이라고 예측할 수도 있었다. 

 

 

마지막으로 내 닉네임을 넣어서 모두 실행시켜주면 다음과 같이 그래프가 그려진다.

nickname = 닉네임
df = create_dataframe(nickname)
df.describe()
check_missing_col(df)
visualize_vision(df)

내 일반 게임 292경기에 대한 데이터 평균을 볼 수 있다. 

1. 예상대로 나는 서폿을 할 때 가장 높은 시야 점수를 얻었고 와드를 많이 박았으며 제어 와드도 많이 샀다. 

2. 원딜을 할 때 제어 와드도 사지 않고 장신구도 렌즈를 절대 사지 않는데 의외로 지운 와드 수가 많아서 원인을 생각해보니 서폿과 같이 와드를 지우는 경우가 많아서 높게 나온 것 같다. (찾아보니 와드를 여러 명이 나눠 때리면 막타가 아니더라도 같이 지운 것으로 간주함)

3. 미드를 할 때 구매하는 제어 와드 수가 0.5개 수준으로 너무나 적다. 나의 나쁜 습관 중 하나가 아닐까 싶다.

 

 

절대적으로 값이 높은지 낮은지에 대한 평가는 같은 티어에 속해 있는 다른 유저들의 평균과도 비교해보아야 알 수 있을 것 같다. 게임 시간이 길어질수록 전반적인 수치가 높아지기 마련이기 때문에 게임 시간에 따른 분석도 유의미할 것으로 생각된다.

 

 

Riot API 관련된 것은 이 정도로 마무리하려고 한다. 

 

공부 기간: 2개월

공부 시간: 50시간 내외

 

<성과>

1. 다소 낯설었던 API 사용법을 익힐 수 있었다.

2. Sqlite를 이용한 DB구성, Matplotlib를 통한 그래프 그리기 등을 복습할 수 있었고, 이렇게 따로따로 배운 내용들을 하나의 프로젝트에서 사용해본 것이 신기한 경험이었다.

3. 정말 많은 시행착오를 겪으면서 문제해결력을 기를 수 있었다. 수많은 에러를 끝없는 구글링으로 해결 방법을 하나하나 찾으면서 고쳐나갔고, 각 단계에서 기획한 것에 근본적인 문제가 생기면 계속해서 고민하고 다시 기획을 해야만 했다. 이렇게 고민하고 실패해본 경험이 그 순간에는 정말 절망스럽고 힘들지만 계속해서 극복해나간 것이 내가 한 층 더 성장할 수 있는 계기가 된 것 같다.

 

<아쉬운 점>

1. 생각보다 많은 분석을 하기가 힘들었다. 머신러닝이나 통계적 분석 관련 지식이 너무 부족해서 스스로의 힘으로 깊이 있는 분석이 불가능했다. 관련 분야를 더 공부해서 실력이 늘면 그때 이 프로젝트를 이어가면서 승리 예측, 문제점 분석 등을 해보는 것도 좋을 것 같다. 

2. 기획을 조금 더 치밀하게 하고 목적을 뚜렷하게 정하고 프로젝트를 시작해야 한다는 것을 느꼈다. 단순히 제공되는 데이터에서 무언가 가치를 창출하려고 막연히 시작한 감이 없지 않아 있었는데, 그러다 보니 방향성이 모호해지고 무엇을 해야 할지 정확하지 않아 시간이 많이 낭비되었던 것 같다.

 

끝.