FE(ファイアーエムブレム)風花雪月が発売されたことを記念して、自分のブログに書いた記事のリメイクをします。FEというゲームのシリーズで、ユニットの成長率から性別を推定しようというものです。多変量解析とは何か、シリーズ間でどのような標準化を行ったか、という点は過去記事を見ていただくとして、今回は
- 風花雪月のユニットを追加
- 統計手法の追加
の2点によりリメイクを行いました。また、コードも載せるのでQiitaに媒体を移します。
風花雪月のユニットを追加
csvの形で全ユニットの名前、登場シリーズ、性別、成長率をまとめております。githubリポジトリはここ。何かミスがあるといけないので、随時プルリクお待ちしております。
風花雪月のユニットを加えた結果、計588ユニットとなりました。リメイクによるユニットの重複を避けるため、また成長率の種類がシリーズによって違うため、以下のような整理を行っております。
タイトル | HP | 力 | 魔力 | 技 | 速さ | 運 | 守備 | 魔防 |
---|---|---|---|---|---|---|---|---|
聖戦の系譜 | ○ | ○ | ○ | ○ | ○ | ○ | ○ | ○ |
トラキア776 | ○ | ○ | ○ | ○ | ○ | ○ | ○ | 魔力 |
封印の剣 | ○ | ○ | 力 | ○ | ○ | ○ | ○ | ○ |
烈火の剣 | ○ | ○ | 力 | ○ | ○ | ○ | ○ | ○ |
聖魔の光石 | ○ | ○ | 力 | ○ | ○ | ○ | ○ | ○ |
暁の女神 | ○ | ○ | ○ | ○ | ○ | ○ | ○ | ○ |
新・紋章の謎 | ○ | ○ | ○ | ○ | ○ | ○ | ○ | ○ |
覚醒 | ○ | ○ | ○ | ○ | ○ | ○ | ○ | ○ |
if | ○ | ○ | ○ | ○ | ○ | ○ | ○ | ○ |
エコーズ | ○ | ○ | 力 | ○ | ○ | ○ | ○ | △ |
風花雪月 | ○ | ○ | ○ | ○ | ○ | ○ | ○ | ○ |
この整理の詳しい説明については過去記事を参照ください。
データの可視化
まずはデータを可視化して整理します。
##プロッティング
成長率間の関係をみるためにプロッティングします。なお、この記事内の実装コードはすべて上のcsvを使用するので、実装したい方はリポジトリをクローンしてから実行してください。
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from pandas import plotting
#オリジナルデータを読み込む
df = pd.read_csv('growth_rate.csv')
#後で性別のカラーラベルに使用するためにbとrに置き換えたものを作成
col_sex = df['sex'].replace('Male','b').replace('Female','r')
plotting.scatter_matrix(df.iloc[:, 3:], figsize=(8, 8), c = col_sex, alpha=.2)
plt.show()
青が男性、赤が女性を示していますが、結構入り組んでおり、精度よく判別するのは難しそうです。POW-MAGのグラフで不自然な直線が見られるのは、シリーズ間の標準化によるものです(魔力の成長率が存在しないので、力の成長率と同じ値を補完)。
主成分分析
過去記事でも行った主成分分析を行います。複数のパラメータをまとめて主要な成分にまとめる作業です。グラフを見やすい「角度」を見つける方法とも言えます。データの標準化を行うかについては議論が分かれますが、結果的に判別率に大差がなかったこと、しない方がわずかにデータが見やすかったことから、行っておりません。
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import sklearn
from sklearn.decomposition import PCA
#オリジナルデータを読み込む
df = pd.read_csv('growth_rate.csv')
#後で性別のカラーラベルに使用するためにbとrに置き換えたものを作成
col_sex = df['sex'].replace('Male','b').replace('Female','r')
#ここで主成分分析
pca = PCA()
feature = pca.fit(df.iloc[:,3:])
feature = pca.transform(df.iloc[:,3:])
#寄与率
print(pca.explained_variance_ratio_)
#ユニットの名前を散布図にする
fig, ax = plt.subplots(figsize=(10,10), dpi=100)
ax.scatter(feature[:,0],feature[:,1],alpha=0) #空のマーカー
#下のイテレーションでテキストラベルを貼り付ける
for i,(name,PC1,PC2,col) in enumerate(zip(df['name'],feature[:,0],feature[:,1],col_sex)):
ax.annotate(name,(PC1,PC2),c = col)
plt.show()
以下のような図が描かれます。
横軸がPC1、縦軸がPC2です。右にいけばいくほど高成長ユニット(GBAシリーズのマムクートが筆頭)、上にいけばいくほど魔術系ユニット(サラやミカヤが筆頭)、という傾向が見て取れます。PC1が全体的な成長率の良さ、PC2が魔術系ユニットかそうでないかを示している、と言えそうです。また、PC2が高い方に女性が偏ってそうです。教師なし学習でラベルを与えていないにも関わらずこのように傾向が見てとれるのは面白いですね。主成分分析最高。
とはいっても、寄与率は以下の通りなので、あまりパラメータを集約できているとは言えません。(PC2まで5割弱)
[0.25157553 0.2315441 0.16288644 0.1174174 0.08079689 0.0610306
0.05210715 0.04264189]
まあ、元のデータにそれだけ偏りがないとも言えますが……。とりあえず色々な解析を試みていきます。
教師あり学習による判別
ユニットの性別がわかった状態で性別判定アルゴリズムを作っていきます。すべてのデータを使うと正解率が高くなって当たり前なので、未知のデータに対しどれだけの正答率を示すかによって性能を測っていきます。こういう場合、用意したデータを学習用とテスト用とに分けますが、以降の分析ではすべてテストデータの割合を0.4
としています。
線形判別分析
線形判別分析では主成分分析と同じようなパラメータの合成を行いますが、今度は正解として性別が与えられており、それをうまく二分(またはn分)するような面(または超平面)を決定します。もちろん現実のデータは面でキッパリ分かれている訳ではないので100%の精度は出ません。
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from sklearn.model_selection import train_test_split
from sklearn.discriminant_analysis import LinearDiscriminantAnalysis
#オリジナルデータを読み込む
df = pd.read_csv('growth_rate.csv')
#ここで線形判別分析
X = df.iloc[:,3:]
y = df['sex']
X_train, X_test, y_train, y_test = train_test_split(X,y,test_size=0.4,random_state = 0)
lda = LinearDiscriminantAnalysis()
lda.fit(X_train, y_train)
#判別のスコア
print(lda.score(X_train, y_train))
print(lda.score(X_test, y_test))
判別のスコアは以下の通り
0.7386363636363636
0.7161016949152542
上の数字がトレーニングデータに対する判別率、下の数字がテストデータに対する判別率になります。重要なのは下の数字です。$71.6$%と、あまり芳しい数字は出ません。これは手法がどうのこうのというよりも、元データが入り組みすぎてて男女をキッパリ分けるのはそもそも不可能である、ということでしょう。
判別関数がある程度「性差」を示す指標になります。せっかくなので先ほど得られた主成分分析のPC2と比較してみます。
#第二主成分をグラフ化
print(pca.components_[1])
plt.subplot(2,1,1)
plt.bar(range(1,9),pca.components_[1],tick_label = df.columns[3:])
plt.title('主成分分析における第2主成分')
#判別関数をグラフ化
print(lda.coef_[0])
plt.subplot(2,1,2)
plt.bar(range(1,9),-lda.coef_[0],tick_label = df.columns[3:])
plt.title('線形判別分析における判別関数')
plt.show()
似たような結果が得られました(スケールの違いはここでは問題ありません)。先ほどはそもそもデータが入り組んでいると言いましたが、教師なし学習と教師あり学習で似たような結果が得られることは面白いですね。
両者の成分に共通して言えることは、魔力・速さ・幸運・魔防の成長率が高く、これらの傾向がユニット全体の成長率の違いをうまく説明するとともに、性別ともある程度の相関を持つことがわかりました。
決定木分析
線形判別分析であまり良い精度が出なかったので、次は決定木分析をします。
決定木分析は、例えば「ある値がx以上か?」といった分岐を重ねることによりクラスを判定する手法です。決定木の階層が浅い場合は良いデータの整理になります。
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from sklearn.model_selection import train_test_split
from sklearn.tree import DecisionTreeClassifier
#オリジナルデータを読み込む
df = pd.read_csv('growth_rate.csv')
#ここで決定木分析
X = df.iloc[:,3:11]
y = df['sex']
X_train, X_test, y_train, y_test = train_test_split(X,y,test_size=0.4,random_state=0)
tree = DecisionTreeClassifier(random_state=0, max_depth=3)
tree.fit(X_train, y_train)
#判別のスコア
print(tree.score(X_train, y_train))
print(tree.score(X_test, y_test))
#dotデータをエクスポート
from sklearn.tree import export_graphviz
feature_label = df.columns[3:]
export_graphviz(tree, out_file='tree.dot', class_names=["Female","Male"], feature_names=feature_label, filled = True)
正答率は以下の通り。
0.7130681818181818
0.6864406779661016
良くならねえ。なお、決定木のアルゴリズムを図示すると以下のようになります。
- 速さが47.5より上か?
- 幸運が72.5より上か?
- 魔防が60.0より上か?
というようなフローチャートによって女性の割合が多くなっていくことがわかります。最初に主成分分析によって得られた表を見た時は魔術系ユニットが多いのが目立ちましたが、真に女性性を規定するのは速さと幸運の成長率なのかもしれません。この結果は線形判別分析の判別関数とも一致します。
ディープラーニング
今までの手法で精度があまり奮わなかったので、史上最強の解析法であるディープラーニングを活用するとしましょう。残念ながらその判別関数の意味は人間には解釈不可能ですが、きっと恐ろしい精度をたたき出してくれるはずです。
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from sklearn.model_selection import train_test_split
from sklearn.neural_network import MLPClassifier
#オリジナルデータを読み込む
df = pd.read_csv('growth_rate.csv')
#ここでMLP解析
X = df.iloc[:,3:]
y = df['sex']
X_train, X_test, y_train, y_test = train_test_split(X,y,test_size=0.4,random_state = 0)
mlp = MLPClassifier(hidden_layer_sizes=[10,10,10], random_state=0)
mlp.fit(X_train, y_train)
#判別のスコア
print(mlp.score(X_train, y_train))
print(mlp.score(X_test, y_test))
0.7698863636363636
0.7033898305084746
やっぱりよくならねえ! 隠れ層等のパラメータ調整が悪いのかと思っていろいろと変えてみましたが、このあたりが最高値でした。まあ元データにそもそも傾向の違いがなければ、どのような解析を用いても精度の良い判別は不可能ということですね(当たり前)。
一覧
今まで色々な手法を試してきましたが、結局どの手法がよいのか、またそれぞれの手法で性別の決定境界がどうなっているかを一覧したいです。幸い、今回使っているscikit-learnというパッケージにはそのようなサンプルコードがあります。データの部分だけオリジナルデータに差し替えて走らせてみましょう。PC1、PC2、PC3から2つを選んで解析していますが、まあどの組み合わせでもどの手法でも精度は約7割といったところです。
手法によって決定境界の形が違うのは面白いですが、やはりこれがデータの限界のようです。
有意差検定
最後は、男女によって成長率に統計的に優位な差があるかを調べてみます。
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from scipy import stats
#オリジナルデータを読み込む
df = pd.read_csv('growth_rate.csv')
males = df[df.sex=='Male'].sum(axis=1) #男性ユニットの合計成長率
females = df[df.sex=='Female'].sum(axis=1) #女性ユニットの合計成長率
print(males.mean()) #男性ユニットの合計成長率の平均
print(females.mean()) #女性ユニットの合計成長率の平均
#等分散も正規分布も仮定できないのでU検定
print(stats.mannwhitneyu(males, females, alternative='two-sided'))
284.1764705882353
303.28504672897196
MannwhitneyuResult(statistic=34691.0, pvalue=0.007175140720685967)
帰無仮説の可能性0.7%ということで、やはり**男女の成長率に有意な差(女性が上)**があるという結果になりました。まあこれは前回(if以前の結果)と同じですね。
結論
- FEユニットの成長率から性別を推定する場合、精度は約7割程度
- 解析手法によって精度はそれほど変わらない。恐らくデータの限界。 ←NEW!
- 女性かどうかを判別するには、速さと幸運が比較的重要なファクターとなる ←NEW!
- シリーズ全体としては、女性の成長率の方が優位に高い