Yugosoviet通信

公共性

アドベントカレンダーの記事を書くためにKaggleのTitanicコンペをやり直したら上位10%にも入れなかった話

この記事はICT Advent Calendar 2017 14日目の記事です.

 

こんばんは.駐日ユーゴスラビア臨時政府大使館( @ICT_yugosoviet )です.

今日はデイトン合意の日*1なので初投稿です.

 

いつの間にかアドベントカレンダーを書くことになっていたので書くネタどうしようかと迷っていたんですが,せっかく(?)なのでKaggleネタで行こうと思います.

卒業生のありがたいお言葉やアドバイスはたにし(@tanishi345)が書いてくれると思うので,私の記事では割愛させて頂きます.

 

 

そもそも誰?

アカウント名に大使館とありますが自然人です.

去年度に沖縄高専を卒業し,現在は長岡技術科学大学の学生をしています.

高専時代にはICT委員会に5年間所属し,プロコンに出たりビジコンに出たり米国に行ったりしていました.

 

 

ところでKaggleとは

データ分析のスキルを競うコンペティションサイトです.

Kaggle: Your Home for Data Science

Kaggleには様々な企業や団体,ときには米国国土安全保障省といった政府機関がデータセットおよびテーマを提供しており,参加者は提供されたデータセットをもとに機械学習などを用いて予測モデル構築し,その予測精度を競います.

大体のコンペでは成績優秀者に対して賞金が出る(たまに100万ドルoverの賞金が出たりする)のでとてもよい.

 

 

今回取り組むテーマ

Titanic: Machine Learning from Disaster | Kaggle

おそらくKaggleに登録したユーザが一番初めに挑戦するであろうテーマ(というかチュートリアル)です.

これはタイタニック号沈没事故の生存者を乗員乗客のデータから予測するというコンペです.

 

Kaggleに登録したての頃に少しだけ取り組んだ問題でもあるので,「まあ今からやっても良い結果出そうだしアドベントカレンダーにも間に合うだろう」という非常に戦略的な発想の元この問題を選択しました.

 

 

人生の9割は前処理

面倒なのでグラフの画像は載せません.

 

何はともあれライブラリ群のimportから.

import numpy as np
import pandas as pd
import lightgbm as lgbm
import seaborn as sns
from sklearn.model_selection import GridSearchCV

 

データを読み込んで眺める.

train_raw = pd.read_csv("data/train.csv")
test_raw = pd.read_csv("data/test.csv")

 

なるほどね.

f:id:ICT_yugosoviet:20171214184755p:plain

 

含まれているデータは以下の通り.

  • PassengerID:乗客のID
  • Survived:生存結果(0:死亡,1:生存)
  • Pclass:乗客の等級(1が一番上,3が一番下)
  • Name:なまえ
  • Sex:性別
  • Age:年齢
  • SibSp:兄弟,配偶者の数
  • Parch:両親,子供の数
  • Ticket:チケットの番号
  • Fare:運賃
  • Cabin:客室番号
  • Embarked:乗船港

 

文字列が含まれていると分析しづらいため,文字列データを数値に置き換えていきます.

まずは性別と乗船港のデータから.

train = train_raw.copy()
test = test_raw.copy()

train["Sex"] = train["Sex"].map({"male": 0, "female": 1})
test["Sex"] = test["Sex"].map({"male": 0, "female": 1})

train["Embarked"] = train["Embarked"].map({"C":0, "Q": 1, "S": 2})
test["Embarked"] = test["Embarked"].map({"C": 0, "Q": 1, "S": 2})

 

Cabin(客室番号)は欠損値がかなり多いので捨てようと思いましたが,客室の位置は生存を大きく左右するのではと考えたので今回は用いることにします.

正規表現を用いてどのブロックの客室なのかだけを抽出し,数値に置き換えます.

train["Cabin"] = train["Cabin"].str.extract('(^[A-Z])')
test["Cabin"] = test["Cabin"].str.extract('(^[A-Z])')

cabin_dict = {
    "A": 0,
    "B": 1,
    "C": 2,
    "E": 3,
    "F": 4,
    "G": 5,
    "T": 6
}

train["Cabin"] = train["Cabin"].map(cabin_dict)
test["Cabin"] = test["Cabin"].map(cabin_dict)

 

 

次に特徴量を良い感じに作っていきます.

 

タイタニック号沈没事故では大家族ほど死亡率が高かったことがデータで示されているので,家族の人数が良い指標となりそうです.

という訳で,SibSp(兄弟,配偶者の数)とParch(両親,子供の数)を足してFamilySizeという特徴量を追加します.

また,お一人様の死亡率も高いことから,一人であるかどうかを示す特徴量Aloneを追加します.

train["FamilySize"] = train["SibSp"] + train["Parch"] + 1
test["FamilySize"] = test["SibSp"] + test["Parch"] + 1

train["Alone"] = 0
train.loc[train.FamilySize == 1, "Alone"] = 1
test["Alone"] = 0
test.loc[test.FamilySize == 1, "Alone"] = 1 

 

 

さて,ここでデータをぼーっと眺めていると,姓が一緒である人はチケット番号が同じであることが多いということに気づきました.

このことから「チケット番号が同じである場合は同じ客室に泊まっているのでは?」と適当に予想したので,これも特徴量として追加することにします.*2

正規表現でチケット番号のみを抽出してグルーピングし,各チケットのグループの大きさを特徴量GroupSizeとして追加します.

こういった気付きが得られるので,元データをExcelか何かで眺めてみるのも良いと思います.

train["Ticket"] = train["Ticket"].str.extract('([0-9]+$)')
test["Ticket"] = test["Ticket"].str.extract('([0-9]+$)')

ticket_group_dict = pd.concat([train["Ticket"], test["Ticket"]], 
ignore_index=True).value_counts().to_dict() train["GroupSize"] = train["Ticket"].map(ticket_group_dict) test["GroupSize"] = test["Ticket"].map(ticket_group_dict) train.loc[train.GroupSize.isnull(), "GroupSize"] = train.GroupSize.median()

 

次に名前のデータに注目します.

Kaggleで他のユーザの分析手法を眺めていると,名前の敬称を特徴量として用いている人を散見しました.

というわけで,正規表現を用いて敬称を抽出してみます.

print(train["Name"].str.extract('([A-Za-z]+\.)').value_counts(), "\n")
print(test["Name"].str.extract('([A-Za-z]+\.)').value_counts(), "\n")
なるほどなるほど.

f:id:ICT_yugosoviet:20171214200129p:plain

 

最終的に以下のグループに分けてみました.

  • 一般男性: Mr.
  • 少年:Master.
  • 婚約済女性:Mrs. Mme. Ms.
  • 未婚女性:Miss. Mlle.
  • 士官とか:Dr. Rev. Capt. Col. Major.
  • 貴族かな:Don. Jonkheer. Sir. Dona. Countess. Lady.

 

そして特徴量追加.

honorific_dict = {
    "Mr.": 0,
    "Master.": 1,
    "Mrs.": 2,
    "Mme.": 2,
    "Ms.": 2,
    "Miss.": 3,
    "Mlle.": 3,
    "Dr.": 4,
    "Rev.": 4,
    "Capt.": 4,
    "Col.": 4,
    "Major.": 4,
    "Don.": 5,
    "Jonkheer.": 5,
    "Sir.": 5,
    "Dona.": 5,
    "Countess.": 5,
    "Lady.": 5
}

train["Honorific"] = train["Name"].str.extract('([A-Za-z]+\.)').map(honorific_dict)
test["Honorific"] = test["Name"].str.extract('([A-Za-z]+\.)').map(honorific_dict)

 

 

次は年齢データの欠損値を補完していきます.

年齢は名前の敬称からある程度予想できそうなので,各敬称の年齢の中央値をもとに補完していきます.

tmp_df = pd.concat([train[["Age", "Honorific", "Embarked", "Fare", "Pclass"]], test[["Age", "Honorific", "Embarked", "Fare", "Pclass"]]], ignore_index=True)
train.loc[train.Age.isnull(), "Age"] = train.loc[train.Age.isnull()].apply(lambda x: tmp_df.loc[tmp_df.Honorific == x["Honorific"], "Age"].median(), axis=1)
test.loc[test.Age.isnull(), "Age"] = test.loc[test.Age.isnull()].apply(lambda x: tmp_df.loc[tmp_df.Honorific == x["Honorific"], "Age"].median(), axis=1)

 

乗船港の欠損は乗船港の最頻値で,運賃の欠損はその乗客が属する等級の運賃の中央値で補完します.

train["Embarked"].fillna(tmp_df.loc[tmp_df.Embarked.notnull(), "Embarked"].mode()[0], inplace=True)
test.loc[test.Fare.isnull(), "Fare"] = test.loc[test.Fare.isnull()].apply(lambda x: tmp_df.loc[tmp_df.Pclass == x["Pclass"], "Fare"].median(), axis=1)

 

最後に不要な特徴量を消します. 

train = train.drop(["Name", "Ticket"], axis=1)
test = test.drop(["Name", "Ticket"], axis=1)

 

 

 

予測モデルの構築&パラメータチューニング

機械学習モデルには様々なものがありますが,今回はLightGBMというGradient Boosting系のライブラリを使用します.

github.com

 

Boostingについて知りたいのであれば『はじめてのパターン認識』の11章を読むと良いと思います.

はじめてのパターン認識

はじめてのパターン認識

 

 

クロスバリデーションでバリバリしつつグリッドサーチでグリグリする.

良さそうなパラメータがでてくるまでこれを繰り返します.かなり面倒ですがここは根性でカバー.

clf = GridSearchCV(lgbm.LGBMClassifier(categorical_feature=[0, 1, 6, 7, 10]),
                  params,
                  cv=5,
                  scoring='f1')

clf.fit(train.drop(["PassengerId", "Survived"], axis=1), train["Survived"])

  

良さそうなパラメータが出たら,テストデータを学習済モデルに与えて生存結果を予測します.

そして提出ファイルを作成してsubmit.

submit_df = pd.DataFrame()
submit_df["PassengerId"] = test["PassengerId"]
submit_df["Survived"] = clf.predict(test.drop(["PassengerId"], axis=1))
submit_df.to_csv("result/submit1.csv", index=False)

 

 

結果

はい.(Accuracy:0.79425)

 

f:id:ICT_yugosoviet:20171214221628p:plain

 

f:id:ICT_yugosoviet:20171214193837p:plain

 

 

まとめ

真面目にパラメータチューニングを行えば Accuracy 0.8 は超えて10 %圏内に入れそうな気がします.頑張りましょう. 

データ分析コンペはじっくり時間をかけて取り組むもうな👊👊👊

 

ICT委員会の方々も,データ分析や機械学習に興味がある人は是非Kaggleに参加してみましょう.ついでにICTslackのResearchチャンネルにもjoinしてくれると嬉しいです.

 

 

 

明日のアドベントカレンダーはICTのギルティことマテ茶先輩です.期待してます.

 

 

Конец.

*1:この和平合意によりボスニア・ヘルツェゴビナ紛争は終結した.ちなみに署名が行われたのはデイトンではなくパリです.

*2:後で検索したら同じことやっている人が居た(それはそう)