#はじめに
Dota2というMOBAゲームがあります。
5人ずつ2陣営に分かれて戦うというPvPゲームなのですが、試合後の結果のデータをAPIから取得することができます。
今回はそのゲームの試合データを使用して、Pythonでのデータ分析手法を学んでいこうと思います。
#どうやったか
###データ準備
こちらの手順に従い、ランクマッチ20万試合からheroのデータを200万レコード生成しました。
データはローカルのMySQLに格納されています。
###作業環境
- Python3.7
- Windows 10
###分析手法
- pandas,seabornによるデータ集計、可視化
- randomforestによる特徴量抽出
#データ集計、可視化
では実際に分析を行っていきます。
import pandas as pd
import mysql.connector
まずはpandas
とmysql.connector
をimportします。
pandas
によってDBから取得したデータをオブジェクト的に扱うことが可能になります。
DBとの接続は以下のように行います。
conn = mysql.connector.connect(user=USER, password=PASSWORD, host=HOST,database=DATABASE)
get_data_sql = ("SOME SQL TO GET DATA FROM DB")
df = pd.read_sql(get_data_sql,conn)
まずはDBへのコネクションと発行したいSQL文をそれぞれ用意しておきます。
その後、pandas.read_sql(SQL文,DBへのコネクション)
を使用することで、DBに接続してクエリを発行した結果をpandas
のデータフレームとして返すことができます。
今回は自前で用意した200万レコードほどのheroの試合後のデータを読み込みます。
#行数、列数の確認
df.shape
#columnsの確認
df.columns
#各columnの型を確認
df.dtypes
#データの最初の10行の確認
df.head(10)
上記の各メソッドを使用し、データフレームの中身の確認を行います。
出力として、以下のような結果が得られます。
>>> df.shape
(2324690, 14)
>>> df.columns
Index(['localized_name', 'kills', 'deaths', 'assists', 'last_hits', 'denies',
'gold_per_min', 'xp_per_min', 'hero_damage', 'hero_healing',
'tower_damage', 'level', 'role', 'win'],
dtype='object')
>>> df.index
Int64Index([ 0, 1, 2, 3, 4, 5, 6,
7, 8, 9,
...
2324680, 2324681, 2324682, 2324683, 2324684, 2324685, 2324686,
2324687, 2324688, 2324689],
dtype='int64', length=2324690)
>>> df.dtypes
localized_name object
kills int64
deaths int64
assists int64
last_hits int64
denies int64
gold_per_min int64
xp_per_min int64
hero_damage int64
hero_healing int64
tower_damage int64
level int64
role float64
win int64
dtype: object
>>> df.head(10)
localized_name kills deaths assists last_hits denies gold_per_min xp_per_min hero_damage hero_healing tower_damage level role win
0 Chaos Knight 2 7 15 148 14 365 525 19390 0 2496 22 2.0 0
1 Razor 3 12 19 154 8 399 597 31253 0 3280 25 4.0 1
2 Puck 14 4 11 117 12 648 629 20550 0 4926 17 1.0 1
3 Windranger 17 10 10 272 15 561 837 45332 0 2573 25 2.0 1
4 Bounty Hunter 9 15 14 79 4 376 389 19695 0 568 19 2.0 0
5 Medusa 2 8 7 342 5 466 554 18195 0 1221 23 1.0 0
6 Drow Ranger 4 11 11 264 12 463 612 14212 0 3907 24 3.0 1
7 Wraith King 0 10 5 110 8 278 341 8672 653 136 15 3.0 0
8 Viper 6 6 13 160 6 357 515 40166 0 207 22 3.0 0
9 Viper 9 9 18 321 14 544 765 37989 3344 3441 25 3.0 1
これでなんとなくデータフレームの中身が分かりました。
次はデータの集計を行っていきます。
>>> df[df['localized_name']=="Wraith King"].groupby('role').mean()
kills deaths assists last_hits denies gold_per_min xp_per_min hero_damage hero_healing tower_damage level win
role
1.0 8.148059 5.080573 14.404602 319.447215 9.224476 605.831597 689.485982 29056.807468 1943.241533 7506.423583 23.024315 0.581814
2.0 6.305348 6.056100 14.952829 260.457731 8.572455 521.927788 622.459381 23908.871882 2271.675143 5684.855673 22.174512 0.570805
3.0 5.043701 6.973615 15.006102 207.631926 7.273747 454.209433 561.282487 19626.316788 2461.276550 4234.463885 21.211082 0.532322
4.0 3.978129 7.995140 14.980964 166.241798 5.795869 383.629810 504.052653 16742.079789 2639.248279 3049.780478 20.347104 0.455650
5.0 2.725076 8.490433 13.041289 115.559919 4.201410 305.891239 400.220544 12426.337362 2446.492447 1887.459215 17.639476 0.373615
色々なメソッドを合わせて使用し、Wraith Kingのrole毎の各データの平均値を集計してみました。
gpmが高いほど勝率が高くなるcarryheroの典型ですね。しかも彼に関してはpos3となっていても53%の勝率を誇ります。
一方pos4,5に関しては一気に勝率が下がるので、support適正は低いと言えそうですね。
という感じでpandasを使用することで、データを集計して見える形で出力することができます。
では各手順について順番に解説していきます。
まずdf['column名']
は選択したcolumnのみのデータフレームを表します。
df[df['column名']=='抽出したいデータ']
であるcolumnについて、データを絞って抽出できます。今回はlocalized_name
がWraith Kingであるデータ群を抽出しています。
データフレームの持つメソッドgroupby('集計したいcolumn名')
を使用することで、あるcolumnについてデータを集計することができます。
集計後のオブジェクトに対してmean()
メソッドを使用することで、その平均値を集計したデータを出力することができます。
次にseaborn
を使用し、データの可視化を行います。
import seaborn as sns
import matplotlib.pyplot as plt
df = df.dropna(how='any')
df_viper = df.query("localized_name == 'Viper'").drop('localized_name', axis=1)
まずはライブラリのimportを行います。
そしてdf.dropna(how='any')
で欠損値の削除を行います。
引数how
にany
を指定することで、一つでも欠損値を含む行を削除することができます。
尚、デフォルトのhow
の引数はany
で設定されているため、dropna()
でも同じ処理が実行できます。
今回はViperというheroに関して見ていこうと思います。
なので、新たにデータフレームを作製します。
df.query()
は抽出を可能にするメソッドで、SQLに近い形で条件を指定することができます。
可視化する際にheroの名前を示すlocalized_name
は不要かつデータフレーム名で明言を行なっているため、drop
メソッドを使用することでcolumn
の削除を行います。
引数axis
に1を指定することで列の削除を行います。ちなみに0を指定すると行の削除が可能になります。
#####ヒストグラム
fig1, ax1 = plt.subplots(1,1)
sns.distplot(df_viper['gold_per_min'], kde=False, bins=100, ax=ax1)
ax1.set_xlim(0,1000)
まずはplt.subplots(行数,列数)
を使用しfigure,axes
のオブジェクトを作製します。
次にseaborn
のdistplot
メソッドを使用し、ヒストグラムを描画していきます、
引数としては、まず目的のデータを配列で渡します。
また、kde
はデフォルトでTrue
になっていて、その場合密度関数を表示してくれます。
今回はFalse
に設定したのでヒストグラムだけの表示で、y軸はx軸のvalue
のcount
を表します。
bins
はbinのサイズを指定します。今回は100で設定しています。
ax
には先ほど作製したaxes
オブジェクトを渡すことで、指定したaxes
オブジェクにグラフを描画することができます。
最後にaxes
オブジェクトのメソッドset_xlim
を使用することで、x軸の表示範囲を指定することができます。今回は0から1000の範囲を指定しています。
この結果として以下のようなグラフが得られます。
上記からgpm500あたりにピークが見られることがわかります。
全体としてgpm400-550あたりが割合の高い傾向が見られますね。
#####ヒートマップ
fig2, ax2 = plt.subplots(1,1)
sns.heatmap(df_viper.corr())
再び、figure,axes
オブジェクトを生成します。
df.corr()
はデータフレームの各列間の相関係数を返します。
sns.heatmap
はその名の通りヒートマップを描くことができます。
ヒートマップを描くことで、以下のように各列の相関が一目でわかるようになります。
上記の図のwinに注目すると、assists,gpm,towerdamageなどに正の相関が見られますね。
そんなの当たり前だろうという感じですが、、、
killsよりもassistsの方が正の相関が高いようなので、carryというよりはpos3くらいの立ち位置が勝利に貢献できるようですね。
#RandomForestによる特徴量抽出
次はメジャーな機械学習手法であるRandomForest
を使用し、特徴量抽出を行います。
RandomForest
では複数の決定木モデルを量産し、その多数決をとってアウトプットとします。
特徴として、変数、データセットから非復元抽出して各決定木を作製することで、バリアンスの低減が実現できます。
今回はWitch Doctorというheroのデータを使用します。
from sklearn.ensemble import RandomForestClassifier
from sklearn.model_selection import cross_validate
from sklearn.model_selection import GridSearchCV
まずはPythonの機械学習ライブラリのScikit-learn
からRandomForestClassifier,cross_validate,GridSearchCV
をimportします。
今回は、Cross Validationという手法でモデルの学習、評価を行います。
この手法ではデータセットをn分割し、n-1個のデータセットでモデル学習を行い、残り1つのデータセットで評価をする、ということをn回繰り返します。
こうすることでn個の別々のデータセットによる学習済みモデルを得ることで、汎化精度の向上が狙えます。
また、ハイパーパラメータの選定にGridSearchを使用します。
この手法では、パラメータのリストを用意し、それぞれに対して学習を行い、最適なパラメータを発見できます。
GridSearchCV
ではその評価にCross Validationを使用することでより精度の高い探索を行うことができます。
clf = RandomForestClassifier(n_jobs=-1)
params = {'n_estimators':[100,500,1000],'max_features':["auto","log2"]}
gs = GridSearchCV(estimator=clf,param_grid=params,cv=5,n_jobs=-1)
output = cross_validate(gs,df_wd.drop('win',axis=1),df_wd['win'],return_estimator =True,cv=5)
RandomForestClassifier()
でランダムフォレストのオブジェクトを生成します。
引数に用いたn_jobs
は並列処理のコア数を指定できます。-1を入力することで実行している環境の最大のコア数で処理をしてくれます。
次にパラメータのdict
を用意します。それぞれ試行したいパラメータをkey
とし、具体的な数値などをvalue
としてlist
で用意します。
そしてGridSearchCV()
でcross validationをしながらGrid Searchを行うオブジェクトを生成します。
estimator
に使用したいモデルのオブジェクト、param_grid
に評価したいパラメータのdict
、cv
に分割方法、n_jobs
に並列数を指定します。
最後に、cross_validate()
でcross validationを実行できます。
実行結果として、score
などの要素をkey
として持つdict
が返ってきます。
引数として、先ほどのGridSearchCV
のオブジェクト、説明変数、目的変数、return_estimator
、cv
を用います。
return_estimator
にTrueを与えると、返ってくるdict
の要素にestimator
を追加できます。
estimator
はvalue
として、cross valitationで学習したそれぞれのモデルのオブジェクトを持ちます。
f_impo_mean = np.array([np.array([model.best_estimator_.feature_importances_[x] for i, model in enumerate(output['estimator'])]).mean() for x in range(len(df_wd.drop('win',axis=1).columns))])
best_model = output['estimator'][0].best_estimator_
feature_importances = pd.DataFrame(f_impo_mean,
index = df_wd.drop('win',axis=1).columns,
columns=['importance']).sort_values('importance', ascending=False)
print(f"Scores:{output['test_score'].mean()}")
print(feature_importances)
すごい煩雑なコードを書いてしまいましたが、上記によって学習したモデルの平均スコアと平均の特徴量の重要度が得られます。
GridSearchCV().best_estimator_
によって、最適なパラメータで学習を行ったモデルが得られます。
そのモデル、つまりRandomForestClassifier
のオブジェクトは.feature_importances_
によって特徴量の重要度が配列として得られます。
今回は最適パラメータで学習したモデルの特徴量の重要度を全て抽出し、平均を出しています。
また、スコアについてはcross_valitade
が出力したdict
の要素としてそれぞれのモデルのスコアを配列として持っているので、その平均を取得します。
これにより今回得たモデルのスコア及び特徴量の重要度が以下のように得られます。
Scores:0.869820195786567
importance
tower_damage 0.318008
gold_per_min 0.124985
deaths 0.095011
assists 0.093349
hero_damage 0.071653
hero_healing 0.070373
last_hits 0.057846
xp_per_min 0.056232
role 0.029596
kills 0.028958
denies 0.027598
level 0.026391
ぼちぼちなスコアなのではないでしょうか。
目的変数を勝敗にしてあるので、特徴量の重要度としてtower_damageとgpmが来るのは当たり前といえば当たり前ですね、、、
assistsが高くkillが低めの数値であるので、集団戦には積極的に参加することが求められる一方、killは取らなくてもゲームの勝敗に関与はしなさそうですね。
また、hero_damageと同じくらいhero_healingの重要度が高いことがわかります。
Witch Doctorは回復スキルを持っているのですが、それを適切に使用することも勝敗には重要みたいですね。
まぁ勝ち試合であればずっと回復していられるからというのもあるかもしれませんが、、、
ともかく、以上のようにCross Validation, Grid Searchを使用したRandomForestによるモデルを利用して汎化精度が高い形で特徴量抽出が実現できます。
#終わりに
軽くですが、Pythonによる分析手法が学べたと思います。
今後として
-word2vecでのheroのベクトル化
-前処理、分析のフロー作製
-更なる手法の探索
などにトライしていこうと考えています。