本記事以外のKaggle挑戦記事は下記となります。あわせてご覧くださいませ。
・Kaggleから学ぶ最新の機械学習実践Tips2018
・「KaggleのMercari Challengeでdeeplearningを駆使して上位10%(Bronze)入り」
・「KaggleのHousePrices問題を決定木系のアンサンブルで解く」
10月。それは日本企業において新人が現場配属されてから、ちょうど半年、新人に中だるみが起きる時期となります。
「階層ベイズモデル、ね。まあ、だいたい掴んだな。(「な、なにぃッ!俺が3ヶ月かけて習得した階層ベイズをわずか半日で!?」)」
「3,000、4,000、、、5,000人か(分析サンプルが)。5分、いや、3分で(分析を)片付けてやろう。(タイピングは)右手のみで十分だ」
といった新人の発言は渋谷、六本木、恵比寿、界隈のデータサイエンス企業ではよく聞かれることかと思います。
昔から、この手のニューカマーに改心いただくための物語の展開は決まっておりまして、
それは“トーナメント”となります。
昔から少年漫画はトーナメントに参加しがちです。※必ずしも改心しないですが、。
そこで、後輩と一緒にデータ分析会のトーナメント、Kaggleに参加しましたので、その過程をご報告したいと思います。
冒頭に申し上げますと、これは私の後輩が改心にいたる物語ではなく、
途中から私の方が熱中してしまいましたので、
私個人がKaggleのtitanicチャンレジで上位10%に入るまでの過程となります。
(であれば、冒頭の前振りは不要なのではという疑念はさておき)
Kaggleとは、世界中で30万人以上のデータサイエンティストが登録している世界最大のデータ解析コンペサイトとなります。
企業から様々なお題が出され、その優勝者(精度がもっとも高かった人orチーム)に、
優勝したプログラムコードを提供するかわりに、賞金が贈られる、というものです。
通常は、trainデータとtestデータがサイト上から渡され、trainデータをもとにモデリングし、testデータで予測し投稿する、という手順になります。
特徴は、
「オンラインで解析データを投稿すると即座に採点されて精度が出される」
「その精度に基づいて、参加者の順位が出る」
「議論が活発で様々な手法を公開しあっている」
という点にあります。
日本では、2015年にリクルートが日本企業で初めてKaggleでコンペ開催をしたことで話題となりました。
参照:
「賞金稼ぎから仕事探しまで、世界のデータサイエンティストが「Kaggle」に集まる理由」
「世界のデータサイエンティストが集う「Kaggle」とは?ビッグデータ分析を競い合え!」
Kaggleでは企業からの問題以外に、賞金なしのKaggleからのチュートリアルチャレンジもあります。
titanicチュートリアルは、その一つです。
1912年に起きた、かの有名なタイタニック号沈没事件を題材に、乗客の年齢、性別、社会階級ランク、などのデータから、
生死を予測する、というものです。
ちなみに、タイタニック号沈没事件は、
“若い乗客、女性の乗客から先に救命ボートに乗せた”、などの史実があり、
上記乗客のプロファイルデータからある程度生死を予測できることから、
よくデータ分析の題材に用いられますので、ご存知の方も多いかと思います。
※Rのデフォルトデータセットにも含まれております。
※KaggleのHouse Pricesチュートリアルについては別ポストにて記載しております。あわせてご覧くださいませ。
データセットは下記の通りです。
PassengerID | 乗客ID |
survival | 生死(0 = No; 1 = Yes)※trainデータのみに存在。testデータのこの値を予測する。 |
pclass | 乗客の社会階級(1 = 1st(High); 2 = 2nd(Middle); 3 = 3rd(Low)) |
name | Name |
sex | 性別 |
age | 年齢 |
sibsp | 乗船している夫婦、兄弟姉妹の数 |
parch | 乗船している親、子供の数 |
ticket | チケットNo |
fare | 乗船料金 |
cabin | 船室 |
embarked | 乗船場所(C = Cherbourg; Q = Queenstown; S = Southampton) |
ちなみにKaggleのtitanicチュートリアルチャレンジは
2016/10/21時点で、5,590人が参加しており、チュートリアルチャレンジではもっとも活況となります。
平均精度は、0.769、予測精度0.6〜0.8の間に参加者の80%がひしめき合う、という状況となります。
※Kaggle titanicチュートリアルチャレンジにおける予測精度ごとの人数分布(2016/10/21時点)
| (3) 分析環境
本稿における(というか、普段の)私のデータ分析環境は下記となります。
言語:Python3.0(anaconda)
環境:jupyter notebook
anacondaはpythonのデータ分析系のライブラリがセットとなっているパッケージとなります。
Pythonはライブラリのインストールでつまづくことも多いため、まずはanacondaをインストールし、
足りないライブラリを適宜pip installで追加することを推奨します。
jupyter notebookは、旧iPython notebookでして、
プログラムの記述、実行、可視化、メモ、が一元管理できる環境となります。
notebook形式のファイルを渡せば一通り、データ分析を再現できるため、
チームでのノウハウの共有、引き継ぎなどに適しております。githubでも最近notebookをあげる人が増えてきましたね。
※下記は今回作成したnotebookの一部(適当で恐縮ですが、。)
| (4) データ探索
では、titanicチュートリアルチャレンジを始めてみます。
まずは、データを読み込んで、形式を確認してみます。
1 2 3 |
import pandas as pd df_train = pd.read_csv("train.csv") df_train.head() |
まずはSex(性別)、Embarked(乗船場所)は、ダミー変数化しておいたほうがよさそうです。
1 2 3 4 5 6 7 8 9 |
sex_dum = pd.get_dummies(df_train['Sex']) df_train_proc = pd.concat((df_train,sex_dum),axis=1) df_train_proc = df_train_proc.drop('Sex',axis=1) df_train_proc = df_train_proc.drop('female',axis=1) emb_dum = pd.get_dummies(df_train['Embarked']) df_train_proc = pd.concat((df_train_proc,emb_dum),axis=1) df_train_proc = df_train_proc.drop('Embarked',axis=1) df_train_proc = df_train_proc.drop('S',axis=1) |
それ以外に、非構造化データであるName、Ticket、Cabinをどう扱うか、が肝になりそうです。
また、データの欠損についても確認しておきましょう。
1 |
df_train.isnull().sum() |
先ほど確認した非構造化データのうち、Cabinはそもそも欠損が多すぎるので、今回は除外してもよさそうです。
次いで、年齢データの欠損が多いようです。
ここで、一度欠損データを除外し、当てたいデータであるSurvivedともっとも相関が高い値を見てみましょう。
1 2 3 4 5 6 7 |
df_train_proc_dn = df_train_proc.dropna() df_train_proc_dn = df_train_proc_dn.drop('PassengerId',axis=1) df_train_proc_dn = df_train_proc_dn.drop('Name',axis=1) df_train_proc_dn = df_train_proc_dn.drop('Ticket',axis=1) df_train_proc_dn = df_train_proc_dn.drop('Cabin',axis=1) df_train_proc_dn.corr() |
生存については、性別がもっとも相関が高く、ついで年齢が高いようです。
よって、性別についで生存に関係する年齢をいかに推計するか、がポイントのようです。
ここで一度、性別と年齢ごとの生存を可視化してみましょう。
※x軸が年齢、y軸が性別(1=男性、0=女性)、赤色のプロットが生存、青色のプロットが死亡
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
import matplotlib.pyplot as plt import seaborn as sns plt.style.use('ggplot') df_train_sur = df_train_proc_dn[df_train_proc_dn.Survived==1] df_train_sur_age = df_train_sur.iloc[:,2] df_train_sur_s = df_train_sur.iloc[:,6] plt.scatter(df_train_sur_age,df_train_sur_s,color="#cc6699",alpha=0.5) df_train_sur = df_train_proc_dn[df_train_proc_dn.Survived==0] df_train_sur_age = df_train_sur.iloc[:,2] df_train_sur_s = df_train_sur.iloc[:,6] plt.scatter(df_train_sur_age,df_train_sur_s,color="#6699cc",alpha=0.5) plt.show() |
女性の方が優先的に救助されている(y=0:女性において、赤い点が多い)ことと、
男性の中(y=1)でも、若年層は多く生存していることが確認できました。
可視化することでわかることも多いので、できるだけデータ探索フェーズでは、可視化しましょう。
さて、年齢をどうやって補完するか、ですが、ここで非構造化データのうち、名前に注目してみます。
特に”敬称”、”苗字(家族名)”に注目します。
敬称は、”mr”、”master”、”miss”、”mrs”が主にありますが、
これらの敬称はそれぞれ年齢に関係ありそうです。実際に平均値を求めてみましょう。
敬称 | 備考 | 平均年齢 |
master | 主に男性の子供に使用される | 4.1歳 |
mr | 男性一般に利用される | 24.9歳 |
miss | 未婚女性(mrsよりも若い可能性) | 17.5歳 |
mrs | 既婚女性(missよりも年齢が高い可能性) | 31.1歳 |
上記のように敬称によって明確に平均年齢に違いがでるようですので(年齢の推計に敬称が利用できる)
“mr”、”master”、”miss”、”mrs”のフラグをデータに加えることにします。
また、苗字から、あるIDと別のIDが”家族”だと推測できますが、
家族の生存は本人の生存に関係がある可能性があります。
旅行時において家族は、近くで行動していたと思われますし、
特に非常事態では一緒に行動し、救命ボートもできるだけ家族一緒に乗りたいのではないでしょうか。
そこで裏付け検証をしてみます。
・自分と自分を除く家族の間での、生存の相関 : 0.41
・2人以上で乗船した家族において、”全員生存、あるいは死亡”した割合 : 58%
よって、家族の生存率(家族ごとの、家族人数における生存割合)は、パラメータとして有効活用できそうです。
| (5) 前処理
前述の考察から、下記3つの前処理をかけて、新たな変数として加えることにします。
・名前データから、”mr”,”master”,”mrs”,”miss”のフラグを抽出
・名前データから、家族名を抽出し、そこから家族の(自分以外の)生存率を計算
・敬称によって分けられたフラグ別に、k-近傍法で年齢を推計補完(近い属性の人の年齢を参考にして年齢を補完)
名前データからの敬称によるフラグ付けのコードは下記となります。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
def name_classifier(name_df): name_class_df = pd.DataFrame(columns={'miss','mrs','master','mr'}) for name in name_df: if 'Miss' in name: df = pd.DataFrame([[1,0,0,0]],columns={'miss','mrs','master','mr'}) elif 'Mrs' in name: df = pd.DataFrame([[0,1,0,0]],columns={'miss','mrs','master','mr'}) elif 'Master' in name: df = pd.DataFrame([[0,0,1,0]],columns={'miss','mrs','master','mr'}) elif 'Mr' in name: df = pd.DataFrame([[0,0,0,1]],columns={'miss','mrs','master','mr'}) else : df = pd.DataFrame([[0,0,0,0]],columns={'miss','mrs','master','mr'}) name_class_df = name_class_df.append(df,ignore_index=True) return name_class_df |
また、名前データから家族名を抽出し、そこから家族の生存率を計算するコードは下記となります。
※df_trainがトレーニングデータとして読み込んだものとなります。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 |
name_df_train = df_train.iloc[:,3] survive_array = df_train.iloc[:,1] name_df = name_df_train name_array = [] name_count = [] name_survive_count = [] name_survive_ratio = [] name_i = 0 for name in name_df: name_split = name.split(" ") name_split_first = name_split[0].replace(',','') if name_split_first in name_array: name_count[name_array.index(name_split_first)] = name_count[name_array.index(name_split_first)] + 1 name_survive_count[name_array.index(name_split_first)] = name_survive_count[name_array.index(name_split_first)] + survive_array[name_i] else: name_array.append(name_split_first) name_count.append(1) name_survive_count.append(survive_array[name_i]) name_i = name_i + 1 count = 0 for i in name_count: if name_count[count] == 1: name_survive_ratio.append(0) else: name_survive_ratio.append(name_survive_count[count]/(name_count[count])) count = count + 1 survive_ratio_array = [] for name in name_df: name_split = name.split(" ") name_split_first = name_split[0].replace(',','') survive_ratio_array.append(name_survive_ratio[name_array.index(name_split_first)]) survive_ratio_array |
ちなみに名前から推計した家族情報データ、に着目したアプローチは、Kaggle内でもいくつか散見されまして、
たとえば、“Large families not good for Survival”という投稿では、
いかに大家族が、沈没船からの生存に不利だったか、を鮮やかに分析しております。
今回の私の分析では、家族人数自体は考慮しておりませんでしたが、時間がある際に、是非取り入れてみたいアプローチです。
そして、最後に、敬称フラグごとに、k-近傍で属性の近い人の年齢を参考にして年齢推計します。
今回は属性が近い3人の人の平均年齢、で補完しました。
今回のケースでは、k-近傍による補完は平均値や線形回帰での補完よりも精度がよくなりました。
平均や線形回帰による補完は時に思わぬ偏りを生むケース、あてはまりがよくないケースがありますので注意です。
なお、欠損データの補完方法や考え方については、下記書籍がオススメです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 |
def estimate_age_by_distance(train_df): perfect_df = train_df.dropna() perfect_df_master = perfect_df[perfect_df.master==1] perfect_df_mr = perfect_df[perfect_df.mr==1] perfect_df_mrs = perfect_df[perfect_df.mrs==1] perfect_df_miss = perfect_df[perfect_df.miss==1] perfect_df_other = perfect_df.query("master==0 and mr==0 and mrs==0 and miss==0") est_age_df = pd.DataFrame(columns={'est_age'}) for row in range(0,len(train_df)): df = train_df.iloc[row] distance_list = [] if df.Age == 0: est_age = pd.DataFrame([[df.Age]],columns={'est_age'}) else: if df.master == 1: for p_row in range(0,len(perfect_df_master)): df2 = perfect_df_master.iloc[p_row] distance = distance_between_df_row(train_df,perfect_df_master,df,df2) distance_list.append([df2.Age,distance]) elif df.mr == 1: for p_row in range(0,len(perfect_df_mr)): df2 = perfect_df_mr.iloc[p_row] distance = distance_between_df_row(train_df,perfect_df_mr,df,df2) distance_list.append([df2.Age,distance]) elif df.mrs == 1: for p_row in range(0,len(perfect_df_mrs)): df2 = perfect_df_mrs.iloc[p_row] distance = distance_between_df_row(train_df,perfect_df_mrs,df,df2) distance_list.append([df2.Age,distance]) elif df.miss == 1: for p_row in range(0,len(perfect_df_miss)): df2 = perfect_df_miss.iloc[p_row] distance = distance_between_df_row(train_df,perfect_df_miss,df,df2) distance_list.append([df2.Age,distance]) else: for p_row in range(0,len(perfect_df_other)): df2 = perfect_df_other.iloc[p_row] distance = distance_between_df_row(train_df,perfect_df_other,df,df2) distance_list.append([df2.Age,distance]) distance_list.sort(key=lambda x: x[1]) sum_age = 0 avg_age = 0 for k in range(0,3): sum_age = sum_age + distance_list[k][0] avg_age = sum_age / 3 est_age = pd.DataFrame([[avg_age]],columns={'est_age'}) est_age_df = est_age_df.append(est_age,ignore_index=True) return est_age_df |
| (6) データ解析・モデリング
今回は、SVM(サポートベクターマシーン)を用いてモデリングすることにします。
(はしょりますが、ランダムフォレストより今回のケースの場合、精度がよかったので)
SVMについて簡単にご説明しますと、
・サポートベクトル
・カーネルトリック
という2つの特徴を使用した手法となります。
サポートベクトルとは、決定境界(今回では生存した人たちの境界、死亡した人たちの境界)にもっとも近いサンプルのこととなります。
SVMは、このサポートベクトル間のマージンを最大にするように、分離平面を選択する、手法です。
※ちなみに、この分離平面による誤分類のペナルティは変数としてチューニングすることが可能です。
(トレーニングデータセットでの精度の向上を目指すか、汎化性能を高めるか、のトレードオフ)
カーネルトリックとは、線形分離ができないデータに関しても高次元に射影することで、高次元空間で分離する手法です。
たとえば、左図の場合、線形分離が不可となりますが、中心点をx=a、y=bとすると、Z=(x-a)^2 + (y-b)^2なる新たな次元を設けると、
中心近くはzの値が小さくなり、中心から離れるほどzの値が大きくなりますので、あるzの閾値によって分離することが可能となります。(右図)
Pythonでのコードは下記となります。
※事前に、ランダムフォレストで0.7986の予測精度(トレーニングデータセットで)が出たので、それ以上だったときの予測精度のパラメータを探索しています。
※クロスバリデーションで、トレーニングデータセットを10分割したときの平均予測精度を出しています。
※SVMに、かける前にpipeline経由の処理でデータを正規化しています。
※X_train2_sur、y_train2_surが、前述の前処理後のデータ
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
from sklearn.svm import SVC best_score = 0.7986 for i in range(5,20): i = i / 100 for j in range(100,250): j = j / 100 clf_svm = SVC(kernel='rbf',random_state=0,gamma=i,C=j) pipe_svm = Pipeline([['sc', StandardScaler()],['clf', clf_svm]]) scores = cross_val_score(estimator=clf_svm, X=X_train2_sur, y=y_train2_sur, cv=10, scoring='roc_auc') if scores.mean() == best_score: print("gamma:",i,"C:",j,scores.mean(),"+-",scores.std()) |
| (7) 結果
予測精度:0.80861
順位:490位 / 5,590人
※2016/10/21時点
となり、無事上位10%以内(上位9%以内)にランクインできました。
このようにストレートに結果がでるため、後輩と一緒に参加している場合は、
負けると、悔しさのあまり嗚咽をもらしながら思わず後輩の椅子に画鋲を仕掛けることになります。
(後輩には、4%の精度の差を出して、勝ちましたので画鋲は仕掛けていません。)
ちなみに投稿は何度でも繰り返し行うことができ、
通常はコンペ期間内ギリギリまで精度の向上、ランキングの上昇を狙います。
※今回のtitanicはチュートリアルのため、期限は考慮せず。
ここまで記述した内容も実際には行ったりきたりしながら進めております。
| (8) 所感
さて、今回、Kaggleに(チュートリアルチャレンジですが)参加して感じた点を備忘録がてら下記に、整理してみます。
・データを解釈して、意味のあるフラグに分解する。
名前そのものでは、意味のないデータ(しかも非構造化データ)ですが、
“家族”、”敬称からのおおよその年齢”、”敬称からの身分”など、フラグ付けをすることで
意味のある”情報”となります。
データ分析プロセスでは、この手の”データの解釈”が非常に重要と感じます。
・予測精度を1%上げる意味
データ解析コンペでは、予測精度を1%どころか0.1%を争ったりするわけですが、
大手ECサイトですと、レコメンドの精度が1%異なることで、売上に多大な影響が出てしまいます。
一方、実環境では、導入コスト、計算コスト、さらにはデータ入手コストがあるわけでして、
トレードオフは常に気にする必要があります。
・精度をあげる過程で、事象に対する理解を深める。
1%精度を上げるためにデータを深堀する過程で、データの背景にある、
人間の行動、事象の理解が深まることがあります。
そうすると、与えられた問題以外の根源的な示唆が得られることがあります。
※今回の例では、タイタニック号の乗客の生死を予測する問題でしたが、
そもそも、救助活動はどのような手順にすることが有効か、などの問いへの示唆。
以上となります。
奢れるものは、沈みゆくタイタニック号と同じ運命をたどることになり、
そこからの救命ボートはjupyter notebookといえますので、本日も、notebookにしがみついております。
本記事以外のKaggle挑戦記事は下記となります。あわせてご覧くださいませ。
・Kaggleから学ぶ最新の機械学習実践Tips2018
・「KaggleのMercari Challengeでdeeplearningを駆使して上位10%(Bronze)入り」
・「KaggleのHousePrices問題を決定木系のアンサンブルで解く」