CRUNKYおいしい

CRUNKY食べながらのんびり書いてる

Pokemon Challenge - ポケモンデータセットを使った機械学習トライ

Pokemon Challenge - ポケモンデータセットを使った機械学習トライ

前回(http://rautaku.hatenablog.com/entry/2017/12/15/211915)の続きです。 前回はポケモン対戦データの解析を行いましたので、今回はそのデータから勝者予測を行ってみたいです。 変数やデータフレーム等は前回を引き継ぎますので、必要な方は前回を見てください。

勝者予測を行うにはデータの抜けなどがあると困りますので、まずはデータ補完を行なっていきます。 前回のポケモン個体ごとの勝率データを見返します。

  

battle_win.info()
<class 'pandas.core.frame.DataFrame'>
Index: 782 entries, Abomasnow to Zygarde Half Forme
Data columns (total 3 columns):
battle    782 non-null int64
win       781 non-null float64
ratio     781 non-null float64
dtypes: float64(2), int64(1)
memory usage: 24.4+ KB

上記情報を見ると、782体分しか情報がありません。またその内1体は勝星数、勝率情報 がありません。予測する側のデータにこのポケモン達がが出てきてしまうとデータ不足で予測できませんので補完していきます。まずは

battle_win[battle_win["win"].isnull()]

Shuckleこいつです。

ツボツボ。前回の勝星数算出処理の際に戦闘はしてるにも関わらず1勝もしていないため勝者リストに無く、正しく処理されなかったみたいです。ツボツボ悲しい。。win=0, ratio=0を追加してあげます。

battle_win.ix["Shuckle", ["win", "ratio"]] = 0
battle_win[battle_win.index=="Shuckle"]

次に戦闘をしていないためデータ抜けがある18体の補完を行いたいところですが、勝率などを補完するためには種族値などの情報も必要だと思われるので、図鑑に勝率を追加することで各ポケモンデータと勝率を結び付けます。

id_dict = dict(zip(pokemon['Name'], pokemon['#']))
battle_win["Name"] = battle_win.index
battle_win["#"] = battle_win["Name"].replace(id_dict)
ratio_dict = dict(zip(battle_win['#'], battle_win['ratio']))
pokemon["ratio"] = pokemon["#"].replace(ratio_dict)
pokemon.head()

図鑑に勝率情報が追加されました。図鑑ナンバーから勝率変換を行いましたので、データ抜けがあるポケモンたちの勝率欄には図鑑ナンバーが入ってしまっていると思います。調べます。

nobattle_pokemon = pokemon[pokemon["ratio"]>1]
print ("There are {} pokemons have NaN ratio.".format(len(nobattle_pokemon.index)))
nobattle_pokemon[["#", "Name", "ratio"]]
There are 18 pokemons have NaN ratio.

はい予想通り18体が該当しました。眺めて見ると1体名前が入ってないポケモンがいますね。#=63の格闘ポケモンです。普通に検索で調べてみるとPrimeapeオコリザルみたいです。追加しといてあげましょう。

pokemon.loc[62, "Name"] = "Primeape"
pokemon[pokemon["Name"]=="Primeape"][["#", "Name", "ratio"]]

さて勝率の補完ですが、前回の解析から種族値が勝率と相関がありそうでした。図示して見ます。

battle_pokemon = pokemon[pokemon["ratio"] <= 1]
sns.lmplot(x="stats_sum", y="ratio", data=battle_pokemon)
<seaborn.axisgrid.FacetGrid at 0x10eeaf048>

ばらつきはあるものの比較的相関はありそうですので、この相関を用いて、戦闘履歴のないポケモンの勝率を種族値から補完してみたいと思います。線形回帰を使います。

from sklearn.linear_model import LinearRegression
linreg = LinearRegression()
linreg.fit(battle_pokemon["stats_sum"].values.reshape(-1, 1), battle_pokemon["ratio"].values.reshape(-1, 1))
nobattle_pokemon["ratio"] = linreg.predict(nobattle_pokemon["stats_sum"].values.reshape(-1, 1))
nobattle_pokemon[["#", "Name", "ratio"]]

はい補完できました。これで予測作業のための補完は終了です。

ここからは勝者を予測するためのデータ整理を行っていきます。勝利に関与する因子として前回は種族値とタイプ相性を考察しましたが、他の情報としては、攻撃力、防御力、素早さなどもあるのでこれらも今回は入れ込みたいと思います。

戦闘結果combats.csvに今回因子としたい各種族値、タイプ相性情報を追加します。 【追加項目】 先攻側各種族値、勝率、後攻側各種族値、勝率、タイプ相性、先行側勝利判定

combats_add_data = combats.copy()
type_dict = dict(zip(pokemon['#'], pokemon['Type']))
hp_dict = dict(zip(pokemon['#'], pokemon['HP']))
attack_dict = dict(zip(pokemon['#'], pokemon['Attack']))
defense_dict = dict(zip(pokemon['#'], pokemon['Defense']))
spattack_dict = dict(zip(pokemon['#'], pokemon['Sp. Atk']))
spdefense_dict = dict(zip(pokemon['#'], pokemon['Sp. Def']))
speed_dict = dict(zip(pokemon['#'], pokemon['Speed']))
stats_sum_dict = dict(zip(pokemon['#'], pokemon['stats_sum']))
ratio_dict = dict(zip(pokemon['#'], pokemon['ratio']))
combats_add_data["First_pokemon_type"] = combats_add_data["First_pokemon"].replace(type_dict)
combats_add_data["First_pokemon_hp"] = combats_add_data["First_pokemon"].replace(hp_dict)
combats_add_data["First_pokemon_attack"] = combats_add_data["First_pokemon"].replace(attack_dict)
combats_add_data["First_pokemon_defense"] = combats_add_data["First_pokemon"].replace(defense_dict)
combats_add_data["First_pokemon_spattack"] = combats_add_data["First_pokemon"].replace(spattack_dict)
combats_add_data["First_pokemon_spdefense"] = combats_add_data["First_pokemon"].replace(spdefense_dict)
combats_add_data["First_pokemon_speed"] = combats_add_data["First_pokemon"].replace(speed_dict)
combats_add_data["First_pokemon_stats"] = combats_add_data["First_pokemon"].replace(stats_sum_dict)
combats_add_data["First_pokemon_ratio"] = combats_add_data["First_pokemon"].replace(ratio_dict)
combats_add_data["Second_pokemon_type"] = combats_add_data["Second_pokemon"].replace(type_dict)
combats_add_data["Second_pokemon_hp"] = combats_add_data["Second_pokemon"].replace(hp_dict)
combats_add_data["Second_pokemon_attack"] = combats_add_data["Second_pokemon"].replace(attack_dict)
combats_add_data["Second_pokemon_defense"] = combats_add_data["Second_pokemon"].replace(defense_dict)
combats_add_data["Second_pokemon_spattack"] = combats_add_data["Second_pokemon"].replace(spattack_dict)
combats_add_data["Second_pokemon_spdefense"] = combats_add_data["Second_pokemon"].replace(spdefense_dict)
combats_add_data["Second_pokemon_speed"] = combats_add_data["Second_pokemon"].replace(speed_dict)
combats_add_data["Second_pokemon_stats"] = combats_add_data["Second_pokemon"].replace(stats_sum_dict)
combats_add_data["Second_pokemon_ratio"] = combats_add_data["Second_pokemon"].replace(ratio_dict)


def calcTypeRelation(combats_add_data):
    r0 = 1
    first_type1 = combats_add_data["First_pokemon_type"].split("/")[0]
    first_type2 = combats_add_data["First_pokemon_type"].split("/")[1]
    second_type1 = combats_add_data["Second_pokemon_type"].split("/")[0]
    second_type2 = combats_add_data["Second_pokemon_type"].split("/")[1]
    if first_type2 != "None" and second_type2 != "None":
        r1 = df_type_relation[first_type1][second_type1]
        r2 = df_type_relation[first_type1][second_type2]
        r3 = df_type_relation[first_type2][second_type1]
        r4 = df_type_relation[first_type2][second_type2]
        r = r0 * r1 * r2 * r3 * r4
    elif first_type2 != "None" and second_type2 == "None":
        r1 = df_type_relation[first_type1][second_type1]
        r3 = df_type_relation[first_type2][second_type1]
        r = r0 * r1 * r3
    elif first_type2 == "None" and second_type2 != "None":
        r1 = df_type_relation[first_type1][second_type1]
        r2 = df_type_relation[first_type1][second_type2]
        r = r0 * r1 * r2
    elif first_type2 == "None" and second_type2 == "None":
        r1 = df_type_relation[first_type1][second_type1]
        r = r0 * r1
    return r

combats_add_data["Relation"] = combats_add_data.apply(lambda x: calcTypeRelation(x), axis = 1)
combats_add_data["First_win"] = combats_add_data.apply(lambda x: 1 if x["First_pokemon"]==x["Winner"] else 0, axis=1)
noneed_cols = ["First_pokemon", "Second_pokemon", "Winner", "First_pokemon_type", "Second_pokemon_type"]
combats_add_data = combats_add_data.drop(noneed_cols, axis=1)
combats_add_data.head()

先攻後攻それぞれのポケモンの情報、お互いのタイプ相性係数、その戦闘結果として先攻ポケモンが勝利したかどうかという情報が揃いました。 これを使って予測のためのモデル作成をしたいと思います。 モデルを作成した後、予測精度検証を行いたいので現在のcombats_add_dataデータフレームを訓練データ(全体の80%)とテストデータ(全体の20%)に分割します。

from sklearn.model_selection import train_test_split

X = combats_add_data.drop("First_win", axis=1)
y = combats_add_data["First_win"]
X_train, X_test, y_train, y_test = train_test_split(X, y, train_size=0.8, random_state=1)
print("X_train.shape = " + str(X_train.shape))
print("X_test.shape = " + str(X_test.shape))
print("y_train.shape = " + str(y_train.shape))
print("y_test.shape = " + str(y_test.shape))
X_train.shape = (40000, 17)
X_test.shape = (10000, 17)
y_train.shape = (40000,)
y_test.shape = (10000,)

準備が全て整いましたので学習アルゴリズムを用いて予測していきます。今回は勉強のため数種類の教師あり学習器を用います。どの学習器が一番精度が良いか検証します。 比較するアルゴリズムは、ロジスティック回帰、k近傍法、ガウシアンナイーブベイズパーセプトロン、決定木、ランダムフォレストを使ってみます。

# Logistic Regression
from sklearn.linear_model import LogisticRegression
logreg = LogisticRegression()
logreg.fit(X_train, y_train)
acc_log = round(logreg.score(X_test, y_test)*100, 2)
acc_log
88.659999999999997
from sklearn.neighbors import KNeighborsClassifier
knn = KNeighborsClassifier(n_neighbors = 3)
knn.fit(X_train, y_train)
acc_knn = round(knn.score(X_test, y_test) * 100, 2)
acc_knn
85.659999999999997
# Gaussian Naive Bayes
from sklearn.naive_bayes import GaussianNB
gaussian = GaussianNB()
gaussian.fit(X_train, y_train)
acc_gaussian = round(gaussian.score(X_test, y_test) * 100, 2)
acc_gaussian
77.810000000000002
# Perceptron
from sklearn.linear_model import Perceptron
perceptron = Perceptron()
perceptron.fit(X_train, y_train)
acc_perceptron = round(perceptron.score(X_test, y_test) * 100, 2)
acc_perceptron
80.969999999999999
# Decision Tree
from sklearn.tree import DecisionTreeClassifier
decision_tree = DecisionTreeClassifier()
decision_tree.fit(X_train, y_train)
acc_decision_tree = round(decision_tree.score(X_test, y_test) * 100, 2)
acc_decision_tree
93.159999999999997
# Random Forest
from sklearn.ensemble import RandomForestClassifier
random_forest = RandomForestClassifier(n_estimators=100)
random_forest.fit(X_train, y_train)
acc_random_forest = round(random_forest.score(X_test, y_test) * 100, 2)
acc_random_forest
94.959999999999994

各モデルの精度順に並べてみますと以下の様になります。

models = pd.DataFrame({
    'Model': ['Logistic Regression', 'KNN', 
              'Naive Bayes', 'Perceptron', 'Decision Tree', 'Random Forest'],
    'Score': [acc_log, acc_knn, acc_gaussian, acc_perceptron, 
              acc_decision_tree, acc_random_forest]})
models.sort_values(by='Score', ascending=False)

ランダムフォレストが一番精度が良く95%という成果を出すことができました。結構いい水準だと思います。 ちなみに各因子の影響度はどの程度だったんでしょうか?

effective = pd.DataFrame()
effective["feature_name"] = X.columns.tolist()
effective["feature_importance"] = random_forest.feature_importances_
effective.sort_values("feature_importance",ascending=False)

意外な結果が出ました。一番影響度の高い因子は、種族値や勝率ではなく素早さみたいですね。 種族値やタイプ相性が多少不利な状況でも素早さが高ければ勝つ確率は高いという結果が得られました。 ちなみに素早さ10傑は以下だそうです!勝率10傑とは少しメンツが変わりましたね。

pokemon.sort_values("Speed",ascending=False).head(10)

今回作成したモデルをtests.csvに反映させれば予測完了です。モデル反映は非常に簡単な手順ですので今回は省略させてもらいます。 以上、ポケモンデータセットを使った機械学習のトライでした!