😃

Riot APIを使って戦績データを分析する全手順

2024/12/25に公開

概要

この記事では、Riot GamesのAPIを使用して、KRサーバーに属するチャレンジャーMIDレーナー5名の戦績データを取得し、それを基に統計分析を行った取り組みを紹介します。具体的には、以下の内容について説明します。

  • 対象読者:
    • Riot API初心者、Pythonを使ったデータ取得や分析に興味がある方
  • この記事で学べること:
    • Riot APIの基本的な使い方
    • Pythonを使ったデータ取得・保存の方法
    • ゲームデータの統計分析と可視化手法

今回の作業では、大量のリクエスト制限に対応する仕組みを作成し、各選手500試合分のデータを収集しました。このプロセスと結果について、詳細を解説します。

目標

  • 各選手の直近500試合分の戦績データを収集する
  • チャンピオンの使用傾向や勝率を統計的に分析し、グラフで視覚化する

対象選手

以下の5名の選手を対象とします。

左から順に

  • KDF BuLLDoG 選手
  • DK ShowMaker 選手
  • FPX Yondaime 選手
  • SHG FATE 選手
  • TWIS MG 選手

コードリポジトリ:
詳細なコードはGitHubリポジトリに公開しています。

環境

このプロジェクトは、以下の環境で開発を行いました。

  • Python 3.12: Pythonを勉強中なので使用しました。
  • RiotWatcher: Riot APIを簡単に利用できるPython用ライブラリで、ゲーム情報を取得するのに使用しました。
  • Visual Studio Code: コードエディタとしてVSCodeを使用しました。

Riot APIの取得

  1. Riot Developer Portalにアクセスします。

  2. 画面中央のSIGN UP NOWからRiotアカウントでログインを行う。

  3. DASHBOARDからREGISTER PRODUCTを押下。

  4. PERSONAL API KEYを選択

  5. PRODUCT INFOMATIONの項目を入力 Product Game Focusで「League of Legends」を選択SUBMITを押下し、RIOT APIを発行。以後DASHBOARDで確認、更新可能。APIは24時間有効

  6. DASHBOARDでAPIKEYを確認、更新可能。APIは24時間有効

取得手順

ライブラリのインポート

以下のライブラリを使用しています。最初にこれらをインポートすることで、APIリクエストや時間管理を行っています。

import time
import json
from riotwatcher import LolWatcher, ApiError, RiotWatcher

APIの設定

Riot APIにアクセスするために必要なAPIキーを設定します。

api_key = "Your_API_Key"
region = "asia" #調べたい地域がJP,KRでもアジア地域なら"asia"を設定

riotwatcher = RiotWatcher(api_key)
lolwatcher = LolWatcher(api_key)

リクエスト制限の管理

Riot APIには1秒あたり20リクエスト、2分あたり100リクエスト制限があり、過剰なリクエストを送るとエラーになります。そのため、リクエスト数を管理するための関数を定義しました。

request_count = 0  # 現在のリクエスト数
start_time = time.time()  # 開始時間

def rate_limit_request(request_func, *args, **kwargs):
    global request_count, start_time
    elapsed_time = time.time() - start_time  # 経過時間を計算

    # 1秒あたり20リクエスト制限
    if request_count >= 20 and elapsed_time < 1:
        time.sleep(1 - elapsed_time)  # 必要に応じて1秒まで待機
        start_time = time.time()  # 開始時間をリセット
        request_count = 0  # カウントをリセット

    # 2分あたり100リクエスト制限
    if request_count % 100 == 0 and request_count != 0 and elapsed_time < 120:
        time.sleep(120 - elapsed_time)  # 必要に応じて2分まで待機
        start_time = time.time()  # 開始時間をリセット

    request_count += 1  # リクエストカウントを増加
    return request_func(*args, **kwargs)  # リクエストを実行

プレイヤー情報の取得

PUUIDの取得

puuidはRiot APIで使用されるプレイヤーの一意の識別子です。
プレイヤーの名前とタグラインを使って、puuidを取得するためのコードを以下に示します。

players = [
    {"name": "민철이여친구함", "tagline": "0415"},
    {"name": "M G", "tagline": "2821"},
    {"name": "DK ShowMaker", "tagline": "KR1"},
    {"name": "Yondaime", "tagline": "Luo"},
    {"name": "FATE", "tagline": "KR2"},
]

puuids = {}  # プレイヤー名をキーにPUUIDを保存
for player in players:
    pu = riotwatcher.account.by_riot_id(region=region, game_name=player['name'], tag_line=player['tagline'])
    puuids[player['name']] = pu['puuid']
    print(f" {player['name']}: {pu['puuid']}")

出力結果がこちら

민철이여친구함: Tg6htH2UT_o9o11TbmE11RYbJ-o9Uk4iAWU1iaYOu13gc3V0nriz-vqdHfSrRJmhkBC-2ejHAjZ2fA
 M G: yBObD3pa4qWEzI5v_Uo-azpDPxKgX9c7KbgzeIBw6uQCQmLc3xsZVj8KsEI3JkESDPiYxvKv8MIuDA
 DK ShowMaker: RaFeXYSCommWUge7Uj_eVGna__co3FOav4nXhVOqjPq2dHGYxa-OlFwT7Om0L-w1c9ShIn9JaOt02Q
 Yondaime: _DJNQWYxK5yggUNX6lF40G9fibqJZuSOr-JFrE1sWyGON9afLLJKjtqL6TO4Oz0KloQ9rfLi3MB6_g
 FATE: dXbgFId6t4WGo8XttZ5bcuBq6r-Xycjas9WJNgOMcTR025YA7WM4PO1A3kT-fjYtkyw3v0gFiw8fcg

MatchIDの取得

指定したプレイヤーのpuuidを使って、直近の500試合のMatchIDを取得するものです。MatchIDは、プレイヤーの試合情報を取得するために必要です。

match_ids = {player['name']: [] for player in players}  # プレイヤー名をキーに試合IDリストを保存

for player in players:
    puuid = puuids.get(player['name'])
    for start in range(0, 500, 100):
        # PUUIDを使って試合リストを取得
        matches = lolwatcher.match.matchlist_by_puuid(region=region, puuid=puuid, start=start, count=100)
        match_ids[player['name']].extend(matches)

# 結果を表示
total_matches = sum(len(matches) for matches in match_ids.values())  # 合計試合数を計算

print(f"合計取得された試合ID数: {total_matches}")
for name, matches in match_ids.items():
    print(f"Player: {name}, Match IDの数: {len(matches)}")
    print(f"Match IDs: {matches}")

出力結果がこちら

合計取得された試合ID数: 2500
Player: 민철이여친구함, Match IDの数: 500
Match IDs: ['KR_7434772509', 'KR_7434736535', 'KR_7434708823', 'KR_7434679132',...
Player: M G, Match IDの数: 500
Match IDs: ['KR_7434288904', 'KR_7433746333', 'KR_7433699564', 'KR_7433667382',...
Player: DK ShowMaker, Match IDの数: 500
Match IDs: ['KR_7432998252', 'KR_7431107535', 'KR_7431057578', 'KR_7431010960',...
Player: Yondaime, Match IDの数: 500
Match IDs: ['KR_7434792599', 'KR_7434719479', 'KR_7434644351', 'KR_7434543486',...
Player: FATE, Match IDの数: 500
Match IDs: ['KR_7434391124', 'KR_7432866748', 'KR_7432736478', 'KR_7432665222', ...

全試合データをJSONファイルに保存

全てのMatchIDを使って試合データを取得し、それをmatch_data.jsonというJSONファイルに保存するものです。JSON形式で保存することで、後で解析や処理を行いやすくします。

# 出力先のJSONファイル
output_file = 'match_data.json'

# JSONファイルを初期化(空のリストを作成)
with open(output_file, 'w', encoding='utf-8') as file:
    file.write('[')  # JSON配列の開始

# 全ての試合IDをリストとして取得
all_match_ids = [match_id for match_list in match_ids.values() for match_id in match_list]

# 全試合データを取得して保存
for index, match_id in enumerate(all_match_ids):
    # 試合データを取得
    match_data = lolwatcher.match.by_id(region=region, match_id=match_id)

    # 試合データを保存
    with open(output_file, 'a', encoding='utf-8') as file:
        if index > 0:  # 最初の試合データ以外はカンマを追加
            file.write(',\n')
        json.dump(match_data, file, ensure_ascii=False, indent=4)

    print(f"Match ID {match_id} 保存完了 ({index + 1}/{len(all_match_ids)})")

# JSON配列の終了部分を追加
with open(output_file, 'a', encoding='utf-8') as file:
    file.write('\n]')  # JSON配列の終了

print("全ての試合データの保存完了")

ファイルのサイズが大きいのでmatch_data.jsonを確認して戦績を正しく取得できているか確認してください。

プレイヤーの試合データ(json)をCSV形式で出力

試合データを取得し、そのデータを整理してCSVファイルに保存する方法を紹介します。

  1. プレイヤー情報の読み込み
    プレイヤー名とpuuidを格納したリストを作成します。これにより、特定のプレイヤーの試合データを処理できます。
players = [
    {"name": "민철이여친구함", "puuid": "Tg6htH2UT_o9o11TbmE11RYbJ-o9Uk4iAWU1iaYOu13gc3V0nriz-vqdHfSrRJmhkBC-2ejHAjZ2fA"},
    {"name": "M G", "puuid": "yBObD3pa4qWEzI5v_Uo-azpDPxKgX9c7KbgzeIBw6uQCQmLc3xsZVj8KsEI3JkESDPiYxvKv8MIuDA"},
    {"name": "DK ShowMaker", "puuid": "RaFeXYSCommWUge7Uj_eVGna__co3FOav4nXhVOqjPq2dHGYxa-OlFwT7Om0L-w1c9ShIn9JaOt02Q"},
    {"name": "Yondaime", "puuid": "_DJNQWYxK5yggUNX6lF40G9fibqJZuSOr-JFrE1sWyGON9afLLJKjtqL6TO4Oz0KloQ9rfLi3MB6_g"},
    {"name": "FATE", "puuid": "dXbgFId6t4WGo8XttZ5bcuBq6r-Xycjas9WJNgOMcTR025YA7WM4PO1A3kT-fjYtkyw3v0gFiw8fcg"},
]
  1. 試合データの取得
    match_data.jsonファイルから試合データをロードし、各試合のデータを処理します。各試合の参加者の情報をチェックし、プレイヤーに関連するデータを抽出します。

取得したいフィールドは以下のURLから参照してください。:
https://developer.riotgames.com/apis#match-v5/GET_getMatch

# match_data.jsonからデータを取得
with open('match_data.json', 'r', encoding='utf-8') as file:
    matches = json.load(file)

# 結果を格納するリスト
filtered_data = []

# 重複試合を防ぐためのセット
used_match_ids = set()

# 各試合のデータを処理
for match in matches:
    if "info" in match and "participants" in match["info"]:
        match_id = match["metadata"]["matchId"]

        # 同じ試合を2回処理しないためのチェック
        if match_id in used_match_ids:
            continue
        used_match_ids.add(match_id)

        # 試合ごとのプレイヤーデータ
        for participant in match["info"]["participants"]:
            puuid = participant["puuid"]

            # プレイヤーのPUUIDがリストに存在するかチェック
            for player in players:
                if player["puuid"] == puuid:
                    # ゲーム時間(秒)の取得
                    game_length_seconds = participant["challenges"].get("gameLength", 0)

                    # MMSS形式に変換(整数にキャスト)
                    minutes = int(game_length_seconds // 60)
                    seconds = int(game_length_seconds % 60)
                    game_duration = f"{minutes:02d}:{seconds:02d}"

                    # 分間CSの計算
                    cs_per_minute = (participant["totalMinionsKilled"] + participant["neutralMinionsKilled"]) / (game_length_seconds / 60) if game_length_seconds > 0 else 0

                    # Pingsの合計
                    total_pings = sum([
                        participant["allInPings"], participant["assistMePings"], participant["commandPings"],
                        participant["enemyMissingPings"], participant["enemyVisionPings"], participant["holdPings"],
                        participant["needVisionPings"], participant["onMyWayPings"], participant["pushPings"],
                        participant["getBackPings"], participant["visionClearedPings"]
                    ])

                    # 取得するデータに追加
                    filtered_data.append({
                        # 基本情報
                        "試合ID": match["metadata"]["matchId"],
                        "サモナーネーム": player["name"],  # プレイヤー名(リストから取得)
                        "チャンピオン": participant["championName"],
                        "レーン": participant["lane"],
                        "勝敗": "win" if participant["win"] else "lose",
                        "試合時間": game_duration,

                        # パフォーマンス
                        "キル数": participant["kills"],
                        "デス数": participant["deaths"],
                        "アシスト数": participant["assists"],
                        "KDA": participant["challenges"].get("kda", None),
                        "キル関与率": participant["challenges"].get("killParticipation", None),

                        # ダメージ & ゴールド
                        "チャンピオンへの総ダメージ": participant["totalDamageDealtToChampions"],
                        "獲得ゴールド": participant["goldEarned"],
                        "分間ダメージ": participant["challenges"].get("damagePerMinute", None),
                        "分間ゴールド": participant["challenges"].get("goldPerMinute", None),

                        # ファーム & オブジェクト管理
                        "ミニオンキル数": participant["totalMinionsKilled"],
                        "分間CS": cs_per_minute,
                        "ソロキル数": participant["challenges"].get("soloKills", None),

                        # マップコントロール
                        "配置したワード数": participant["wardsPlaced"],
                        "分間ビジョンスコア": participant["challenges"].get("visionScorePerMinute", None),

                        # Pings
                        "総Ping数": total_pings,
                        "オールインPing数": participant["allInPings"],
                        "アシストミーPing数": participant["assistMePings"],
                        "コマンドPing数": participant["commandPings"],
                        "ミアPing数": participant["enemyMissingPings"],
                        "敵の視界Ping数": participant["enemyVisionPings"],
                        "ホールドPing数": participant["holdPings"],
                        "視界必要Ping数": participant["needVisionPings"],
                        "オンマイウェイPing数": participant["onMyWayPings"],
                        "プッシュPing数": participant["pushPings"],
                        "退却Ping数": participant["getBackPings"],
                        "視界クリアPing数": participant["visionClearedPings"],
                    })
  1. データの正規化と重複削除
    json_normalizeを使ってデータをフラットにし、サモナーネームで並べ替えた後、重複する行を削除します。
normalized_data = json_normalize(filtered_data)
sorted_df = normalized_data.sort_values(by="サモナーネーム")
sorted_df = sorted_df.drop_duplicates(subset=["サモナーネーム", "試合ID"], keep="first")
  1. CSVファイルへの出力
    最終的に、整理されたデータをCSVファイルに出力します。ファイル名はsenseki.csvとして保存されます。
sorted_df.to_csv('senseki.csv', index=False, encoding='utf-8-sig')

出力したCSVには、各試合ごとの基本情報(試合ID、サモナーネーム、チャンピオン名、レーンなど)とプレイヤーのパフォーマンス指標(KDA、ミニオンキル数、総ダメージ量など)が含まれています。

試合ID,サモナーネーム,チャンピオン,レーン,勝敗,試合時間,キル数,デス数,アシスト数,KDA,キル関与率,チャンピオンへの総ダメージ,分間ダメージ,分間ゴールド,ミニオンキル数,分間CS,ソロキル数,配置したワード数,分間ビジョンスコア,総Ping数,オールインPing数,アシストミーPing数,コマンドPing数,ミアPing数,敵の視界Ping数,ホールドPing数,視界Ping数,オンマイウェイPing数,プッシュPing数,退却Ping数,視界クリアPing数
KR_7268784396,DK ShowMaker,Smolder,MIDDLE,lose,32:52,5,5,16,4.2,0.875,44817,1363.3811021745132,488.6434556845219,295,9.36961774914985,0,14,0.9269843243737741,83,0,8,29,2,10,0,0,32,0,2,0
KR_7190928447,DK ShowMaker,Yasuo,TOP,win,23:09,9,3,7,5.333333333333333,0.43243243243243246,18788,811.5967918730694,607.238532344484,235,10.75591767097378,2,7,1.0405165456317185,46,0,3,12,0,6,0,0,19,0,6,0
...
  1. キャラクターごとのプレイ回数、戦績をcsv出力
# プレイヤーごとにデータをグループ化
grouped = sorted_df.groupby('サモナーネーム')

# 各プレイヤーごとにCSVファイルを保存
for player_name, player_data in grouped:
    # プレイヤー名をファイル名に使う
    player_file = f"{player_name}_game_data.csv"
    player_data.to_csv(player_file, index=False, encoding='utf-8-sig')
# 5人全員のチャンピオンごとのプレイ回数と勝利回数を集計
champion_stats = sorted_df.groupby("チャンピオン").agg(
    プレイ回数=('チャンピオン', 'size'),  # チャンピオンの使用回数
    勝利回数=('勝敗', lambda x: (x == 'win').sum())  # 勝利回数
).reset_index()

# 勝率の計算
champion_stats['勝率'] = champion_stats['勝利回数'] / champion_stats['プレイ回数'] * 100

# CSVに出力
champion_stats.to_csv("チャンピオン使用回数と勝率.csv", index=False, encoding="utf-8")

出力CSVには、チャンピオンごとのチャンピオン,プレイ回数,勝利回数,勝率が含まれています

チャンピオン,プレイ回数,勝利回数,勝率
Aatrox,3,2,66.66666666666666
Ahri,52,27,51.92307692307693
...

分析

試合データ各選手500試合ずつ、2500試合まとめた結果をいくつか紹介します。
詳しく見たい方はスプレッドシートからご確認ください。
https://docs.google.com/spreadsheets/d/1-72Jr-cx7ZqfqdKAUx9T6ZT3DzuMaF9ZNH1LeWaba9w/edit?usp=sharing

2500試合のチャンピオンプレイ回数と勝率


やはりサイラス、ヨネ、オーロラはチャンピオンパワーが強いことからよく使われているかつ、勝率が高いことがわかります。

勝率と各要因の相関

アシスト数が勝率に影響しない理由

アシスト数が多いからといって、必ずしもチームが勝利しているわけではありません。アシスト数はチーム全体の協力を示す指標ではあるものの、ゲームの結果を左右する直接的な要因ではないと考えられます。

ゴールドとKDAが勝率に影響する理由

一方で、ゴールド獲得量やKDA(キル/デス/アシストの比率)はチームの優位性を直接的に反映しています。特にゴールド獲得量が高いプレイヤーは装備が充実し、試合の流れを有利に進めることができるため、勝率との相関が高い結果となりました。

工夫した点

今回のプロジェクトで特に工夫した点や良かった点を以下にまとめます。

1. リクエスト制限の管理

  • Riot APIのリクエスト制限に対応するために、リクエスト数をカウントして適切に待機時間を挿入する仕組みを導入しました。エラーを回避しつつ、効率的にデータを取得できました。

2. データの整理と保存

  • 取得したデータをJSON形式で保存し、後から再利用しやすい形にしました。
  • 試合ごとのプレイヤーデータを抽出してCSV形式で保存し、分析や共有がしやすくなるよう工夫しました。

3. 分析の可視化

  • チャンピオンごとの使用回数や勝率を棒グラフで可視化し、データを直感的に理解できるようにしました。
  • 勝率と各要因の相関を散布図で表し、意外な発見(アシスト数が勝率に影響しない)を得られた点が興味深かったです。

感想

  1. 初めてRiot APIを使った際、リクエスト制限に悩まされましたが、リクエスト数を管理する関数を作ることでスムーズに解決できました。また、500試合分のデータを扱う際に、重複データの整理やJSON形式での保存方法を工夫する必要がありました。これらの経験を通じて、データ収集から分析までの一連の流れを深く理解できたと感じています。

  2. この分析を通じて、KRサーバーのチャレンジャー帯におけるチャンピオンの傾向や勝率を深く理解することができましたが、まだまだ探索の余地があります。例えば、特定の選手のプレイスタイルやメタとの関係をさらに掘り下げることも可能です。

  3. この記事が、同じようにデータ分析やRiot APIを活用してみたいと思う方の参考になれば幸いです。次回は、さらに高度な分析や予測モデルの作成に挑戦してみたいと考えています!

Discussion