粗大メモ置き場

個人用,たまーに来訪者を意識する雑記メモ

① ikawidget2のデータを使ってSplatoon2での編成を評価 ~1.データ取得と集計編~

Abstract

発売から2年半経っていますがSplatoon2を未だにやってます。ここまで長いことやったゲームは初めてでつまり神ゲーってことです。

今回はSplatoon2の編成強い弱い問題を最近遊んだ機械学習ライブラリで解き明かせないかという企画になります。(なお著者はCS専門ではないです。)

  • 自分の戦績データから,「勝敗に関連しやすそうな要素」を抽出する
  • 編成などから「勝敗予想」モデルを作成する

というのを目標に据えてやっていきます。

記事

①ではデータの読み出しとデータ構造の解析に焦点を当てます。とりあえずデータのとり方など技術的な話をするので興味ない方は飛ばして結構です。

Splatoon 2 (スプラトゥーン2) - Switch

Splatoon 2 (スプラトゥーン2) - Switch

  • 発売日: 2017/07/21
  • メディア: Video Game

背景(モチベーション)

Splatoon2では敵味方4対4に分かれて試合を行うのですが,その時のブキの編成が結果を非常に左右しやすいという印象を受けます。

例えば,チャージャー,ローラー,ブラスターなどを2枚引くと塗りや打開が厳しいというのは誰しもが思っていることでしょう。

感覚的に厳しい編成かどうかはわかるのですがうまいことこれをデータで示せないか,というのがモチベーションです。

自分の戦績はikawidget2というスマホアプリから取得したものを用います。

参考情報ですが,2020/4/28時のデータはこの様になってます。

  • ウデマエ:エリア・ホコ・ヤグラ XP2000~2200,アサリ S+ 2000前後
  • 持ちブキ:チャージャー・スピナー以外全般
  • 戦績データ数:エリア: 826戦ヤグラ:845戦ホコ:432戦アサリ:339戦

です。

ikawidget2の戦績データを抽出する

このブログを参考にまずはデータをPythonで読み出します

  1. バックアップを行う → 本体に ~~.ikaxという名前のファイルが保存されるのでそれをPC上にダウンロード。
  2. ~~.ikaxの拡張子を.zipに変更して中身を開く → stats.realmというファイルが出てくる。
  3. Realm StudioをDLしてstats.realmを開き,json形式で保存する。

json形式の戦績データを読む

以下のようにして読み出します。

import json

with open("../data/stats.json","r",encoding="utf-8") as f:
    result_json=json.load(f)

試合結果はresult_json["Results"]にリストの形式で入っています。

試合の情報全てがこのResultsに入っており,

  • ガチパワー,ステージ等の情報
  • プレーヤーのギア・ブキや戦績情報
  • 勝敗

が辞書型で記録されています。

試しにkeysを見てみると,

result_json.keys()
>> dict_keys(['Boss', 'BossCount', 'Brand', 'CoopResult', 'CoopSchedule', 'CoopStage', 'CoopWeapon', 'EventType', 'Fes', 'Game', 'Gear', 'Grade', 'Player', 'PlayerType', 'Result', 'Skill', 'SkillLog', 'Skills', 'Special', 'Stage', 'SubWeapon', 'WaterLevel', 'WaveDetail', 'Weapon', 'Worker'])

となります。主に使う戦績データはResultに入ってます。

ステージ毎の勝率計算

参考ブログでは,戦績からfor文を用いてガチエリアの戦績を集計しています。 試しに苦手なガチヤグラ'game': `を集計してみました。


補足:

  • ルール情報は'game'にある。ヤグラはtower_controlgachi,エリアはsplat_zonesgachietc...
  • ブキ・ギア・ステージ等は番号で管理されており,IDと名前の対応はWeapon・Gear・Stageのkeys下に保管されている
yagura_result={}

for item in result_json["Result"]:
    if item["udemae"]<10 and item["game"]!="tower_controlgachi": #S以上の戦績に限定&ガチヤグラ以外のルールを排除
        continue
    for i in result_json["Stage"]:
        if i["ID"]==item["stage"]:
            stage_name=i["name"]

    # 勝ちと負けをステージごとにカウント
    if not stage_name in yagura_result.keys():
        yagura_result[stage_name]={"win":0,"lose":0}
    if item["win"]:
        yagura_result[stage_name]["win"]+=1
    else:
        yagura_result[stage_name]["lose"]+=1

# 勝率高い順にソート        
for key in sorted(yagura_result.keys(),key = lambda x:yagura_result[x]["win"]/(yagura_result[x]["win"] +yagura_result[x]["lose"]),reverse=True):
    print(key,yagura_result[key]["win"]/(yagura_result[key]["win"] +yagura_result[key]["lose"]))

結果は

デボン海洋博物館 0.6153846153846154
(中略)
モズク農園 0.41346153846153844

となりました。この時点での情報はikawidgetでも見れますね。

プレーヤー情報の取得

ここから勝ち負けを評価するためにプレーヤーの情報を抽出します。

プレーヤー情報はResults以下の myMembers(味方),otherMember(敵),player(自分自身)の3つのkeyに保存されています。使いそうなのは

  • kii / allKill: キル数 / アシスト込のキル
  • assist: アシスト数
  • death: デス数
  • paintpoint: 塗りポイント
  • special: スペシャル回数
  • weapon: ブキ

くらいでしょうか。

偽相関が出そうですが,以下の情報も使えそうです。

  • elapsed time:試合時間
  • gachiEstimatePower / gachiEstimateXPower :ガチパワー / Xパワー (Player以下のudemaeIsXの変数フラグにより切り替え)

味方ブキ毎の勝率計算

例としてステージ毎の集計と似たような感じで味方のブキ毎の勝率を計算することができます。

#エリアで勝ったときの味方のブキを集める
win_weapon_ally = {}

# プレイヤー情報の入った辞書データから勝敗を抽出する関数
def addwin(win_weapon,result_json,item,win):
    w_name = "null"
    for i in result_json["Weapon"]:
        if i["ID"]==item:
            w_name=i["name"]
    if not w_name in win_weapon.keys():
        win_weapon[w_name]={"win":0,"lose":0}
    if win:
        win_weapon[w_name]["win"] +=1
    else:
        win_weapon[w_name]["lose"] +=1

# 試合結果から結果を抜き出すループ
for item in result_json["Result"]:
    #if item["udemae"]<10 and item["game"]!="tower_controlgachi": #S以上の戦績に限定&ガチヤグラ以外のルールを排除
    if item["udemae"]<10 and item["game"]!="splat_zonesgachi": #S以上の戦績に限定&ガチヤグラ以外のルールを排除
        continue
    for ally in item["myMembers"]:
        if item["win"]:
            addwin(win_weapon_ally,result_json,ally["weapon"],1)
        else:
            addwin(win_weapon_ally,result_json,ally["weapon"],0)

結果がこれ。(長文注意)

出会ったことの少ないマイナーなブキが上位に来てますが50戦以上データがあるなかで勝率が良かったのはなんとボールドマーカー7(約62%)でした。 最下位はソイチューバー2種。かわいそう...

H3リールガンチェリー 1.0 2
ヒーロースピナー レプリカ 0.8888888888888888 9
14式竹筒銃・丙 0.8181818181818182 11
スプラローラーコラボ 0.7619047619047619 21
スパイガジェット 0.7368421052631579 19
スパイガジェットベッチュー 0.7368421052631579 19
スクリュースロッシャーネオ 0.7 10
プロモデラーRG 0.6666666666666666 12
スプラスコープベッチュー 0.6666666666666666 21
ノーチラス79 0.6666666666666666 15
ノヴァブラスターネオ 0.6666666666666666 3
ロングブラスターカスタム 0.6666666666666666 3
スプラマニューバーベッチュー 0.6410256410256411 39
ホクサイ 0.6363636363636364 22
ホットブラスターカスタム 0.6285714285714286 35
L3リールガンベッチュー 0.625 8
リッター4Kカスタム 0.625 8
ジェットスイーパーカスタム 0.6222222222222222 45
ボールドマーカー7 0.6197183098591549 71
ホクサイベッチュー 0.6176470588235294 34
カーボンローラー 0.6153846153846154 13
プライムシューター 0.6111111111111112 36
スプラマニューバー 0.6086956521739131 23
プロモデラーPG 0.6071428571428571 28
.52ガロン 0.6 10
ヴァリアブルローラー 0.6 5
パーマネント・パブロ 0.6 10
クラッシュブラスターネオ 0.5897435897435898 78
ヒッセン・ヒュー 0.5882352941176471 51
ジェットスイーパー 0.5882352941176471 68
クーゲルシュライバー 0.5882352941176471 17
ケルビン525デコ 0.5869565217391305 46
ヒッセン 0.5853658536585366 41
ヒーローローラー レプリカ 0.5842696629213483 89
ヒーローチャージャー レプリカ 0.5833333333333334 60
ヒーローブラシ レプリカ 0.5833333333333334 12
バケットスロッシャーソーダ 0.5833333333333334 24
ダイナモローラーベッチュー 0.5789473684210527 133
ボールドマーカーネオ 0.5740740740740741 54
ノヴァブラスター 0.5737704918032787 61
スプラスコープコラボ 0.5714285714285714 28
Rブラスターエリートデコ 0.5714285714285714 7
デュアルスイーパー 0.5645161290322581 62
L3リールガン 0.5625 32
スパッタリークリア 0.5609756097560976 123
スプラローラー 0.5598290598290598 234
.96ガロンデコ 0.5555555555555556 36
.96ガロン 0.5555555555555556 27
バレルスピナーデコ 0.5555555555555556 27
ケルビン525ベッチュー 0.5555555555555556 9
ロングブラスター 0.55 60
ボトルガイザーフォイル 0.55 40
プライムシューターコラボ 0.5454545454545454 44
ヒーローマニューバー レプリカ 0.5454545454545454 11
ハイドラントカスタム 0.5443786982248521 169
オーバーフロッシャーデコ 0.5384615384615384 39
H3リールガンD 0.5384615384615384 26
スプラスピナーベッチュー 0.5384615384615384 13
リッター4K 0.5368421052631579 95
オクタシューター レプリカ 0.5344827586206896 58
もみじシューター 0.5333333333333333 75
デュアルスイーパーカスタム 0.5329512893982808 349
オーバーフロッシャー 0.5307692307692308 130
スプラスピナー 0.5277777777777778 36
パラシェルターソレーラ 0.5263157894736842 19
4Kスコープ 0.5263157894736842 95
14式竹筒銃・甲 0.5254237288135594 59
スクイックリンα 0.5217391304347826 23
バレルスピナーリミックス 0.5190839694656488 131
スプラシューターコラボ 0.5181818181818182 220
N-ZAP89 0.5163398692810458 153
プライムシューターベッチュー 0.5150684931506849 365
ボールドマーカー 0.5142857142857142 140
クアッドホッパーブラック 0.5135135135135135 74
ホクサイ・ヒュー 0.5128205128205128 39
N-ZAP85 0.5114285714285715 350
スプラシューターベッチュー 0.5032258064516129 155
わかばシューター 0.5023041474654378 217
ラピッドブラスターベッチュー 0.5 70
ノーチラス47 0.5 24
ハイドラント 0.5 20
スパイガジェットソレーラ 0.5 6
ヴァリアブルローラーフォイル 0.5 48
バケットスロッシャー 0.5 8
スクリュースロッシャー 0.5 6
ヒーロースロッシャー レプリカ 0.5 2
ラピッドブラスターデコ 0.5 14
ケルビン525 0.5 6
スプラスピナーコラボ 0.5 10
シャープマーカーネオ 0.49382716049382713 81
パブロ 0.49019607843137253 51
キャンピングシェルターカーモ 0.4864864864864865 37
L3リールガンD 0.4827586206896552 58
スプラチャージャー 0.4803921568627451 102
クーゲルシュライバー・ヒュー 0.47706422018348627 109
エクスプロッシャーカスタム 0.47297297297297297 74
スパッタリー・ヒュー 0.4722222222222222 72
スプラシューター 0.4714285714285714 70
ラピッドブラスター 0.46153846153846156 13
Rブラスターエリート 0.46153846153846156 13
スプラマニューバーコラボ 0.4578313253012048 83
スプラチャージャーコラボ 0.45454545454545453 44
クラッシュブラスター 0.45454545454545453 22
ヒーローブラスター レプリカ 0.45454545454545453 11
シャープマーカー 0.45454545454545453 33
スクリュースロッシャーベッチュー 0.45098039215686275 102
ロングブラスターネクロ 0.45098039215686275 51
.52ガロンベッチュー 0.4482758620689655 29
おちばシューター 0.4444444444444444 63
スプラローラーベッチュー 0.44 25
カーボンローラーデコ 0.44 50
ダイナモローラーテスラ 0.4318181818181818 44
.52ガロンデコ 0.42857142857142855 7
スクイックリンβ 0.42857142857142855 21
スプラチャージャーベッチュー 0.42857142857142855 35
クアッドホッパーホワイト 0.42857142857142855 7
ダイナモローラー 0.42857142857142855 14
パブロ・ヒュー 0.425 40
ホットブラスター 0.4166666666666667 24
スプラスコープ 0.4084507042253521 71
H3リールガン 0.4 5
4Kスコープカスタム 0.4 5
スパッタリー 0.39285714285714285 28
ヒーローシューター レプリカ 0.38461538461538464 13
プロモデラーMG 0.3793103448275862 29
パラシェルター 0.375 24
スクイックリンγ 0.375 16
N-ZAP83 0.36 25
ノヴァブラスターベッチュー 0.34782608695652173 46
エクスプロッシャー 0.3333333333333333 36
バケットスロッシャーデコ 0.3333333333333333 21
ボトルガイザー 0.3333333333333333 3
14式竹筒銃・乙 0.3333333333333333 9
バレルスピナー 0.29411764705882354 17
キャンピングシェルターソレーラ 0.2857142857142857 7
ヒーローシェルター レプリカ 0.2 10
キャンピングシェルター 0.14285714285714285 7
ソイチューバー 0.14285714285714285 7
ソイチューバーカスタム 0.0 2

この結果は私の持ちブキや立ち回りとの相性という話なので一般的ではないですが,傾向を見る上では参考になりそうです。

より大規模にはika.statsというサイトがあります。

結構ちゃんと分析されてるようなので私の出る幕があるかどうかは微妙です。まぁしばらくは車輪を再発明していきましょう。

github.com

解析に使うデータを整形する

今回の生データはjsonで記述されていますが,これは今後の解析を考える上ではあまり使いやすいものとは言えません。

Kaggleで遊んだときのようなcsvデータのような形式で試合結果を保存するのが良いでしょう。

ossyaritoori.hatenablog.com

行列形式で1行に試合結果を入れるとすると,ひとまず列に必要そうな要素としては

  • ステージ
  • 自分のブキ
  • 自分のキル・デス
  • 自チームの合計キル・デス , 相手チームの合計キル・デス
  • 自チームの合計SP回数, 相手チームの合計SP回数
  • 自チームの合計塗りポイント,相手チームの合計塗りポイント(以後”相手~”の部分省略)
  • 自チームの武器ごとの編成数, 相手~
  • 勝ち負け

あたりでしょうか。 Resultsからこれを抽出する関数を書きましょう。

使用するのはPandasのDataFrameとします。

結果からスコアを抜き出すテスト

試しにいくつかの要素をピックアップしてpandasのdataframeに変換してみましょう。ブキの分類はちょっと考察がいるので後回しにすることにします。

import pandas as pd
import numpy as np

# 一時保存用
dfarray = []

# For ループを回してデータをArray状にする
for item in result_json["Result"]:
    # 1. game information
    game = item["game"]
    # 2. gachi power if X or not
    gachipower = item["gachiEstimateXPower"] if item["udemaeIsX"] else item["gachiEstimatePower"] 
    # 3. extract elapsed time
    battletime = item["elapsedTime"]
    # 4. stage
    stage = item["stage"]
    # 5, Mypower
    mypower = item["xPower"]
    # 6. My data
    mykill = item["player"]["kill"]
    mydeath = item["player"]["death"]    
    mysp = item["player"]["special"]
    myweapon = item["player"]["weapon"]
    mypaintpt = item["player"]["paintPoint"]
    # 7. Our data
    ourkill = mykill
    ourdeath = mydeath
    oursp = mysp
    ourweapon = [myweapon]
    ourpaintpt = mypaintpt
    for ally in item["myMembers"]:
        ourkill += ally["kill"]
        ourdeath += ally["death"]    
        oursp += ally["special"]
        ourweapon.append(ally["weapon"])
        ourpaintpt += ally["paintPoint"]
    # 8. Enemy data
    theirkill = 0
    theirdeath = 0
    theirsp = 0
    theirweapon = []
    theirpaintpt = 0
    for ene in item["otherMembers"]:
        theirkill += ene["kill"]
        theirdeath += ene["death"]    
        theirsp += ene["special"]
        theirweapon.append(ene["weapon"])
        theirpaintpt += ene["paintPoint"]
    # 9. Results
    win = 1 if item["win"] else 0
    
    dfarray.append([game,gachipower,battletime,stage,mypower,mykill,mydeath,mysp,mypaintpt,ourkill,ourdeath,oursp,ourpaintpt,theirkill,theirdeath,theirsp,theirpaintpt,win])

column_name = ["game","gachipower","battletime","stage","mypower","mykill","mydeath","mysp","mypaintpt","ourkill","ourdeath","oursp","ourpaintpt","theirkill","theirdeath","theirsp","theirpaintpt","win"]

# テスト用のデータ格納
testdata=pd.DataFrame(data=dfarray,columns=column_name)

f:id:ossyaritoori:20200429233406p:plain
抜き出したのはこんな感じ。昔のデータがあるのでS+時代のデータから始まってるのはご愛嬌。

テスト解析:sklearnの決定木でガチエリアの集計

以前やった決定木から試していきましょう。(Localの環境構築を忘れていたのでsklearnから始めます。)

ガチエリアのX帯の戦績に着目してデータを抜き出します。

areadata = testdata[testdata["game"]=="splat_zonesgachi" ]
areaXdata = areadata[areadata["mypower"]>0]
areaXdata = areaXdata.drop(["game","stage","battletime"],axis=1)

f:id:ossyaritoori:20200429235659p:plain
整形後のガチエリアX帯のデータ

さて,以前のコードを流用して簡単に分析ごっこをしてみましょう。

KaggleのTitanicデータに対してsklearnの決定木を試してみる - 粗大メモ置き場

まずはモデルを作成。

import sklearn.tree as tree
from sklearn.model_selection import train_test_split
from sklearn.metrics import accuracy_score

clf = tree.DecisionTreeClassifier(max_depth=10)

そして交差検証で学習です。

# 訓練と結果に分ける
X_train = areaXdata.drop(["win"],axis=1)
y_train = areaXdata["win"]


# 5分割交差検証を指定し、インスタンス化
from sklearn.model_selection import KFold
kf = KFold(n_splits=5, shuffle=True, random_state=0)

# スコアとモデルを格納するリスト
score_list = []
models = []

# 各分割ごとに評価
for fold_, (train_index, valid_index) in enumerate(kf.split(X_train, y_train)):    
    print(f'fold{fold_ + 1} start')
    train_x = X_train.iloc[train_index]
    valid_x = X_train.iloc[valid_index]
    train_y = y_train.iloc[train_index]
    valid_y = y_train.iloc[valid_index]
    
    ## 分割データで学習・予測・評価
    clf = tree.DecisionTreeClassifier(max_depth=10)
    model = clf.fit(train_x, train_y)
    
    # データを用いて予測,記録
    predicted = model.predict(valid_x)
    score_list.append(accuracy_score(predicted,valid_y))
    models.append(model)
print(score_list, '平均score', round(np.mean(score_list), 3))

結果は以下の通りで,85%の精度で結果を推定できました。

[0.9099099099099099, 0.8018018018018018, 0.8198198198198198, 0.8918918918918919, 0.8090909090909091] 平均score 0.847

ここでこの木における重要度,すなわちどの要素が結果を大きく左右したかについて見てみます。

import matplotlib.pyplot as plt

importances = np.zeros(model.feature_importances_.size)

for model in models:
    importances += model.feature_importances_

fig, ax = plt.subplots()
plt.grid()
ax.bar(train_x.columns,importances)
fig.autofmt_xdate() # make space for and rotate the x-axis tick labels
plt.show()

f:id:ossyaritoori:20200430024808p:plain
エリアの勝敗への各変数の影響力:敵味方のキルデスの他に自身のデスなども比較的重要と出ました。


データ数と解析手法がおもちゃなのでこのまま信用することはできませんが,言われてみればちょっと納得できるような結果になったのではないでしょうか。

これを拡張して編成を評価できるようにしていきたいですね。

決定木による検証2:他のルールでのキル・デスの関係性

同様の解析をルール毎にかけてみました。

ここから,ホコ・ヤグラ・アサリはオブジェクトの動きに依存するためエリアのようにリザルトだけを見ても勝敗を予想しづらいという仮説が建てられます。

これらのルールそれぞれでの各変数の影響力を比較してみましょう。xticksという機能を使って折れ線グラフのx軸を各変数名に変えています。

fig, ax = plt.subplots()
plt.grid()
xrange = range(len(area_importances))
ax.plot(xrange,area_importances,marker = 'o',label="area")
ax.plot(xrange,yagura_importances, marker='x',label="yagura")
ax.plot(xrange,hoko_importances,marker = 's',label="hoko")
ax.plot(xrange,asari_importances, marker='*',label="asari")
plt.legend()
plt.xticks(xrange,train_x.columns)
fig.autofmt_xdate() # make space for and rotate the x-axis tick labels
plt.show()

f:id:ossyaritoori:20200430030646p:plain
各ルール毎の変数の重要度の比較:エリアでは味方のキルが重要であることがわかります。また,ヤグラでは敵のキルと試合結果に大きく相関があります。


深さ10,5回交差検定の決定木による解析結果まとめ

  • エリアでは味方のキル数が試合結果に大きく影響する
  • ヤグラでは敵のデス数が試合結果に大きく影響する
  • アサリ以外では味方のキル・デスと敵のキルデスの占める割合が大きい
  • アサリは他に比べて塗りポイントの重要性が高い?

アサリは特に複雑性の高いルールですのでちょっとなんとも言えないですが,言われてみればそこそこ的を射ているのではないでしょうか。

まとめ

長くなりましたが,第一回としては

  • ikawidget2のデータをrealm→json→pandas(csv)形式に変換
  • データ内容,解析の手法・方向性やテスト

について書きました。

  • 戦績のリザルトからある程度勝敗について予測できるのは直感通り
  • どのスコアがどの程度影響するか可視化できるのは面白い

戦績データをためてて解析してみてほしいという人は一報ください。

補足:LightGBMを用いた際のリザルト重要度比較

今度はKaggleでよく使われるLightGBMの決定木を用いて重要度を比較してみましょう。()内はsklearnからの増分です。

  • ガチエリア:平均86.8%の的中率(+2%)
  • ガチヤグラ:平均81.5%の的中率(+5%)
  • ガチホコ:平均75.9%の的中率(+5%)
  • ガチアサリ:平均70.7%の的中率(+7%)

このときの各変数の重要度の比較は,以下のようになりました。

f:id:ossyaritoori:20200501173809p:plain
LightGBMで学習したアンサンブル木での重要度:味方のキルデスの重要度が高いのはそのままですが,敵のキルデスよりもこちらの塗りや自分のデスなどに比重が多くなる場合もあります。

LightGBMの決定木による解析結果まとめ

  • 全般的に味方のキル・デス数が試合結果に大きく影響する
  • SPの回数はあまり勝敗を決しない
  • ヤグラ以外ではデスの方がキルよりも試合への影響が大きい
  • アサリは他に比べて塗りポイントの重要性が高い

TODO

ブキの分類

流石にブキの種類が多いため,そのままブキ名をデータに加えても有意義な解析ができないと思います。 したがって種類ごとに分類します。

  • 公式の分類通りに分類
  • 射程毎に分類
  • 塗りの強さ毎に分類

最も方針が立てやすいのは公式の分類でしょう。ika statsでも似たような表があります。

f:id:ossyaritoori:20200429230610p:plain
ika stats様から引用

塗りの評価はika statsさんが出してくれているのでそれを流用するのも良いかもしれません。