KaggleのTitanicデータに対してsklearnの決定木を試してみる
(注)コンペ目的でない素人の備忘録です。参考になるかは不明ですがコメントは歓迎します。
Kaggleとは
KaggleはHPにあるとおり機械学習/データサイエンスに携わる人や企業などのコミュニティです。
正直始めたてなのでざっくりとしかわからないですが,Competitionという、企業や政府などがコンペ形式で課題を提示し、賞金と引き換えに最も制度の高い分析モデルを買い取るといったシステムが有名です。
勉強用としてのKaggle(Titanic)
機械学習は教科書で深層学習やパーセプトロンなどをざっくり読んだだけなのでコンペに参加することはしばらくないでしょうが,勉強用にはいくつか使えそうな点があります。
例えば,練習用としてTitanicというタイタニック号事件のデータが提供されています。 構造もシンプルで初学者にとっても分かりやすいです。
また,人によってはnotebookをわかりやすくコメント付きで投稿してくれていたりします。 www.kaggle.com
他にも,ブラウザ上からアクセスするnotebook形式ではデフォルトでsklearnやlightBGMなどのライブラリがインストールされてる状態で始まるので環境構築の心配が現状なさそうです。
なお,真面目に入門するなら以下の解説が懇切丁寧でよかったです。(英語)
私個人の目的としてはスプラトゥーン2の編成格差問題を自前のデータから解析してみようかなと思っています。(別記事にて記載予定)
メモ:Notebookの作成
初めてKaggleに登録して開くと何をしていいのか本当によくわからなくなりますね。 コンペを探してそれに参加するというのが本来でしょうが,今回はKaggleの環境とデータで練習をするという目的でNotebookを作ります。なお,PythonやPandasなどの知識はどこか別のところで勉強してください。
コンペから探して作成
上のSearchでtitanic
を検索すればtitanicのページに移動します。
右上の「New Notebook」から始めます。
なお,notebookのタブには他の人のnotebookがありさんこうになります。
新規作成→Notebookにあとからデータを追加
sklearnの分類木ライブラリを用いた分類
決定木の説明は教科書等にに投げます。(Qiitaなら以下のリンクなんかが読みやすいのでは?)
[入門]初心者の初心者による初心者のための決定木分析 - Qiita
今回はsklearnというライブラリを用いて決定木のうち,分類木 (classification tree)を用いて学習します。
import pandas as pd import numpy as np import warnings warnings.filterwarnings('ignore') train = pd.read_csv('../input/titanic/train.csv') test = pd.read_csv('../input/titanic/test.csv') sample_submission = pd.read_csv('../input/titanic/gender_submission.csv')
trainは訓練用,testは評価用のデータです。
データの前処理
まずはデータの前処理をします。 機械学習のデータ前処理備忘録 - Qiita
主に,以下のようなことをします。
- 訓練に必要なデータの抜き出し,加工
- 空白データの補間,削除
データ形式の変換と不要なデータの削除
# データの変換 ##maleを0に、femaleを1に変換 train["Sex"] = train["Sex"].map({"male":0,"female":1}) test["Sex"] = test["Sex"].map({"male":0,"female":1}) # EmbarkedのOne-Hotエンコーディング train = pd.get_dummies(train, columns=['Embarked']) test = pd.get_dummies(test, columns=['Embarked']) ## 不要な列の削除 train.drop(['PassengerId', 'Name', 'Cabin', 'Ticket'], axis=1, inplace=True) test.drop(['PassengerId', 'Name', 'Cabin', 'Ticket'], axis=1, inplace=True)
- One-Hotエンコーディング:[A,B,C]のような3つ以上の値のある要素をAの有無としてBoolean値に変換する。
欠損データの確認と対処
ところで,データにはところどころ欠損(NaN)があり,学習に悪影響を及ぼすので取り除いたり穴埋めをしたいです。 pandasの機能でどの量に欠損があるのか簡単に割り出せます。 例えばNanを確認して取り除く場合は以下のように書くことができます。
# NaN の存在確認 と 除去 print(train.isnull().sum()) train2 = train.dropna() test2 = test.dropna()
一応結果を示すと「年齢」の項が抜けているのがわかりますね。
Survived 0 Pclass 0 Sex 0 Age 177 SibSp 0 Parch 0 Fare 0 Embarked_C 0 Embarked_Q 0 Embarked_S 0 dtype: int64
他にも中央値や平均値,ランダム値などで埋める手法があるようです。 参考:平均値でNanを埋めるタイプの方
学習
さて,いよいよ学習のフェーズです。まずは与えられたデータを入出力に分けます。ここでの出力(=予測したい値)は生存したかどうかなのでSurvivedの列を抽出します。
from sklearn.model_selection import train_test_split from sklearn.metrics import accuracy_score # devide train input and output X_train = train2.drop(['Survived'], axis=1) # X_trainはtrainのSurvived列以外 y_train = train2['Survived'] # y_trainはtrainのSurvived列 # X_trainとY_trainをtrainとvalidに分割 train_x, valid_x, train_y, valid_y = train_test_split(X_train, y_train, test_size=0.20, random_state=0)
最後の実行では訓練用のデータを更に分けて4:1に分けて訓練の評価をします。 訓練用データ内でのフィッティング結果と実際の評価用データ内でのフィッティング結果を分けたいがためです。(過学習とかで検索してください。)
訓練用データを2つに分けるのをホールドアウト法,3つ以上に分けるのを交差検証法と言います。(引用)
これで訓練用の入力と出力を分けられたのでsklearnの分類木ライブラリの学習で学習します。
import sklearn.tree as tree # 分類木だからClassifier,Regressorもある clf = tree.DecisionTreeClassifier(max_depth=4) # データを用いて学習 model = clf.fit(train_x, train_y) # データを用いて予測 predicted = model.predict(valid_x) print(accuracy_score(predicted,valid_y))
なんと,これだけです。
結果は0.7902097902097902
になりました。約8割程度の評価です。これは訓練用データに8割程度の正答率で合致しているという解釈になります。
補足:交差検証法による評価
先程の結果はデータのどの部分を訓練用に使うかにより左右されそうです。
したがってよりきちんとやる場合は,交差検証法で訓練データと評価用データをうまくローテーションして平均値などでモデルを評価します。
# 3分割交差検証を指定し、インスタンス化 from sklearn.model_selection import KFold kf = KFold(n_splits=3, 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=4) 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))
結果は以下の通り,データによってばらつきがあるということがわかりますね。
[0.8067226890756303, 0.7647058823529411, 0.819327731092437] 平均score 0.797
混合行列による予測結果の視覚化
予測結果の成否をよりわかりやすくするために混合行列を用いた評価ができます。
from sklearn.metrics import confusion_matrix #混同行列の作成 cmatrix = confusion_matrix(valid_y,predicted) #pandasで表の形に df = pd.DataFrame(cmatrix,index=["actual_died","actual_survived"],columns=["pred_died","pred_survived"]) print(df)
pred_died pred_survived actual_died 131 14 actual_survived 29 64
ざっくりというと,死んだという予測は18.1%
で間違っており,生きていたという予測は17.9%
で間違っていたということになります。
誤差のようなものですが,生きていたという予測のほうがかろうじて得意と言えそうです。
木構造の図示
学習した木を評価する手法にはいくつかあります。まずは木の構造を見たいと思うので木を図示します。
# 図示その1 import matplotlib.pyplot as plt fig, ax = plt.subplots(figsize=(30, 30)) # whatever size you want tree.plot_tree(model, ax=ax) plt.show()
ちょっと見づらい感じになります。
より良く見せたいならば,graphvizやdtreevizなどを使うと良いと思います。
木構造のかっこいい視覚化
Kernelに以下のモジュールをインストールします。
!pip install dtreeviz pydotplus
# graphvizによる視覚化 import pydotplus as pdp file_name = "./tree_visualization.png" dot_data = tree.export_graphviz(model, # 決定木オブジェクトを一つ指定する out_file=None, # ファイルは介さずにGraphvizにdot言語データを渡すのでNone filled=True, # Trueにすると、分岐の際にどちらのノードに多く分類されたのか色で示してくれる rounded=True, # Trueにすると、ノードの角を丸く描画する。 feature_names=train_x.columns, # これを指定しないとチャート上で特徴量の名前が表示されない class_names=['Dead','Sruvived'], # これを指定しないとチャート上で分類名が表示されない special_characters=True # 特殊文字を扱えるようにする ) graph = pdp.graph_from_dot_data(dot_data) graph.write_png(file_name) # dtreevizによる視覚化 from dtreeviz.trees import dtreeviz viz = dtreeviz( model, train_x, train_y, target_name='alive', feature_names=train_x.columns, class_names=['Dead','Sruvived'] ) #viz.view() # 外窓で画像を開くのでKaggleのKernelではエラーがでる? viz.save(”dtreeviz.svg”)
木構造の解析:変数の重要性
では,解析の結果どの変数が生存に大きな影響を及ぼしたのでしょうか。 関数を叩くと一発ですが,以下のワードくらいは抑えておくとします。
- ジニ不純度: ノードによる分類のバラケ方(≒エントロピーみたいな)を数値化したもの→半々で最大。「1 - 各分類木のノードの分類割合の2乗和 」の値。CARTで使われている。→ sklearnでのデフォルト。
- 変数重要度:”ある特徴量で分割することでどれくらいジニ不純度を下げられるか。”を表す量。分割後のジニ不純度のサンプル数の分の和が,分割前のジニ不純度のサンプル数和よりどの程度小さくなったかを表す。
詳細な説明はこちらの記事に投げます。
treeオブジェクトでは変数重要度は.feature_importances_
というメンバに入っています。
# 重要度を表示 print(dict(zip(train_x.columns, model.feature_importances_))) # bar plot fig, ax = plt.subplots() plt.grid() ax.bar(train_x.columns,model.feature_importances_) fig.autofmt_xdate() # make space for and rotate the x-axis tick labels plt.show()
確かタイタニックでは女性と子供が優先的に救命ボートに乗せられたはずなのでこの洞察は合っていると言えるでしょう。
ただし重要度は,如何にクラスを上手く抽出できたかを表す無方向性の値なので,年齢が幼い方が良いのか女性の方が良いのかはわかりません。どうすればよいかちょっと調べてみます。(TODO)
テスト提出
とりあえず提出用のデータを作ります。
# テスト これを提出 ## test の中身 print(test.isnull().sum()) ## 中央値で埋める test = test.fillna(test.median()) ## 予測結果 test_predicted = model.predict(test) ## 提出用データ sample_submission['Survived'] = test_predicted sample_submission.to_csv('submission.csv',index=False)
このcsvをダウンロードしてSubmit Predictionの該当箇所にUploadします。
本当はNotebookでSubmitしたいんですが,Submitできそうなボタンがないような?(@2020年4月20日)なんでやろ?
スコアボードを見ると全く同じスコアの人々がいるので多分テストコードを走らせただけの人々がそれなりにいるのでしょう。 なお,正答率100%の人々もかなりいます。だって答えわかってるもんね。
最後に一応My Notebookを載せます。大してきちんと書いてないですけどね。
補足・TODO
気分がアガり次第書きます。スプラトゥーンの記事は書いたまま塩漬けになってるので今度ちゃんと書き起こします…
TODOs:
今回は最も単純な分類木を用いましたが,
補足:今後の参考
素晴らしい記事をありがとうございます。後ほど参考にさせて頂く予定です。
ボツデータ:欠損値をどのように埋めるか問題
欠損値をどのように埋めるかですが,3番めに重要と思われるAgeが抜けておりました。
絶対適当に埋めるべきではない気がするのですが,中央値,平均値,それとも削除して無視するのどれが良いのか比較しようと試みました。
結論から言うと結果は全く変わらず。なんか違いが出ると思ったんですがね…
# 欠損値除去 VS 補填 (なお,比較のためここでやっているが,本来はデータの前処理の段階でやるべき) ## train_yのうちtrain_xがNanを含む行をdropする train_y_dropna = train_y.drop(train_x[train_x['Age'].isnull()].index.tolist()) train_x_dropna = train_x.dropna() ## 埋める方 shallow copy train_x_fillmed = train_x train_x_fill0 = train_x train_x_fillmed["Age"] = train_x_fillmed["Age"].fillna(train_x["Age"].median()) train_x_fill0["Age"] = train_x_fill0["Age"].fillna(0) # それぞれFit model_dropna = clf.fit(train_x_dropna, train_y_dropna) model_fillmed = clf.fit(train_x_fillmed, train_y) model_fill0 = clf.fit(train_x_fill0, train_y) # 予測 ## validの欠損値補間はMedianで valid_x["Age"] = valid_x["Age"].fillna(valid_x["Age"].median()) pred_dropna = model_dropna.predict(valid_x) pred_fillmed = model_fillmed.predict(valid_x) pred_fill0 = model_fill0.predict(valid_x) ## 評価 print(accuracy_score(pred_dropna,valid_y), accuracy_score(pred_fillmed,valid_y), accuracy_score(pred_fill0,valid_y))
追記:ちゃんと変化しました。Medianで適当に埋めるほうが少し精度が下がりそうです。
[0.8148148148148148, 0.7811447811447811, 0.7542087542087542] 平均score 0.783