kaggle2回目 タイタニック号の生存者予測

概要

kaggle2回目。タイタニック号の乗員乗客の生存者予測。 データを眺めて思いついたことをやってみると、ベースラインは超えられたけれど、今ひとつうまくいかなかった。 今後、続きをやるか、別の課題をやるか、全然違うことをやるかは考え中。

はじめに

kaggle初挑戦ということで、前回手書き文字認識をやった。 課題自体やり尽くされている感じであまりおもしろくないので、今回はタイタニック乗客の生存予測に挑戦することにする。 手法は諸々の事情からニューラルネットワークにした。

どういう課題か

タイタニック号の沈没事故によって、多くの人が亡くなっている。 今回の課題は、この沈没事故によって死亡した人と生き残った人とを分類する問題である。 kaggleから与えられる情報は以下。

  • PassengerId: kaggleが振ったただの連番。役に立たない気がする。
  • PassengerClass: 乗客の等級。1から3まで。金持ちが生き残りそう。
  • Name: 名前。"Mr."とか"Don."とか色々情報がある。うまくやれば家族の情報も抽出できるかも。
  • Sex: 性別。映画では女性、子どもが先に救命ボートに乗ったらしい。関係ありそう。
  • Age: 年齢。同上。
  • Sibsp: 海外に住んでいる兄弟、配偶者の数。なんやこれ
  • Parch: 海外に住んでいる両親、子どもの数。なんやこれ
  • Ticket: チケットの番号。"113803"とか"A/5 21171"とか規則がよくわからない。
  • Fare: 運賃。金持ちが生き残りそう。
  • Cabin: 部屋番号。"C85"みたいな感じ。ぱっと見た感じほとんど欠損している。
  • Embarked: 乗船した港。Cherbourg、Queenstown、Southamptonの3種類。

ぱっとみると、関係ありそうなデータからそうでもないデータまで色々ある。 課題用のものというより、事後処理のときの名簿か何かなのかもしれない。

kaggleから提示されるベースラインは4つ。

  1. Assume All Perished [0.62679] 全員死んだとしたときの精度。
  2. Gender Based Model [0.76555] 女性が全員助かって、男性が全員死んだとしたときの精度。
  3. My First Random Forest [0.77512] Name、Ticket、Cabinのデータ以外を使ってランダムフォレスト。コードも公開。
  4. Gender, Price and Class Based Model [0.77990] 性別、チケットのクラス、チケットの値段の割合に基づいて予測(?)

とりあえず、これら全てのベースラインを超えることを目標とする。

どういうデータか

とりあえず、生データを眺めて以下のことに気づいた。

  • 欠損値が多い。: Ageは結構抜けていて、Cabinにいたっては欠損している方が多い。
  • Fareが0の人がいる。: 欠損しているのか、来賓的なやつなのか。どう扱っていいものか迷う。
  • 同じ名字、部屋番号の人がそれなりにいる。: 家族なら生死を共にしている可能性が高い気がする。

欠損値の補完の方法によって精度が結構変わりそう。 kaggleのコードでは文字列的な情報は使っていないので、欠損値の補完は、単に中央値を用いていた。 補完せずにデータを捨てると、いくつぐらいになるのかを計算してみるとデータ数が891→183になった。 もともと多くはないデータ数をかなり削ることになってしまうので、この案はなし。

とりあえず、色々ヒストグラムを書きながら考えることにする。 まず、データを生存か死亡かで分割する。

import pandas as pd
import pylab as plt

data = pd.read_csv("train.csv").replace("male",0).replace("female",1)
data = data.replace("C",0).replace("Q",1).replace("S",2)
split_data = []
for did_survive in [0,1]:
    split_data.append(data[data.Survived==did_survive])

以下のヒストグラムは全て、青色が死亡した人、緑が生き残った人。 まずはAge。

temp = [i["Age"].dropna() for i in split_data]
plt.hist(temp, histtype="barstacked", bins=16)

思ったよりも5歳以下の子どもが助かる確率が高い。優先的に逃げれたのだろうか。 一方で、5歳から10歳になると突然低くなる。

60歳超えるとだんだん厳しくなると思いきや、75歳以上の人が1人助かっている。 少ない訓練データにこういうのがあると、すごい高齢の人は助かるみたいな謎の学習しそうで怖い。 なんにせよ年齢はかなり使えそう。

f:id:ksknw:20151129151059p:plain

次にPassenger Class。

temp = [i["Pclass"].dropna() for i in split_data]
plt.hist(temp, histtype="barstacked", bins=3)

やはり一等客の生存率が高い。お金の力か。 f:id:ksknw:20151129151101p:plain

続いて性別。

temp = [i["Sex"].dropna() for i in split_data]
plt.hist(temp, histtype="barstacked", bins=3)

男(左)はだいたい死んでいて、女性(右)はかなり助かっている。 救命ボートの数が足りなかったらしいタイタニックにおいて、レディファーストとかやってたんだろうか。 この値もかなり使えそう。 f:id:ksknw:20151129151103p:plain

次に運賃。

temp = [i["Fare"].dropna() for i in split_data]
plt.hist(temp, histtype="barstacked", bins=10)

これもある程度出していたほうが、生存率高くなりそう。 f:id:ksknw:20151129151105p:plain

ここから、役に立つかよくわからないデータ。 一つ目、国外に住む兄弟または配偶者の数

temp = [i["SibSp"].dropna() for i in split_data]
plt.hist(temp, histtype="barstacked", bins=8)

グラフを見る限り、0人よりも1人のほうが生存率高そうなんだけど、この値が何と関係しているのかわからない。 国外に家族がいたほうが、収入が高くなって、高い運賃を払いやすくなるんだろうか。 f:id:ksknw:20151129151107p:plain

次、国外に住む親または子どもの数 一方で、こちらはパッとしない。

temp = [i["Parch"].dropna() for i in split_data]
plt.hist(temp, histtype="barstacked", bins=8)

f:id:ksknw:20151129151109p:plain

乗船した港。 これも意外と役に立ちそうな値。 地域による所得差とか、売られていた部屋の分布が偏っているとかそういうのだろうか。

temp = [i["Embarked"].dropna() for i in split_data]
plt.hist(temp, histtype="barstacked", bins=3)

f:id:ksknw:20151129151112p:plain

My First Neural Network

自分的ベースラインとして、とりあえず、ニューラルネットに突っ込んでみる。 使った特徴量はPassengerClass,Sex,Age,Sibsp,Parch,Fare,Embarked。 欠損値はなんとなく平均値で補完した。 PassengerClassとEmbarkedは1ofKにした。 結果60%ぐらいで、これは先の男女だけで判別したモデルよりも低い。雑魚すぎる。 (実は正規化のところでやらかしていたので普通にミスだった)

欠損値の補完とはなにか

欠損していた場合、モデルにとってニュートラルな信号を入力すべきと思うんだけど、 例えば年齢を欠損していた時、これを平均値もしくは中央値で補完すると、死亡と判定する率が上がりそうな気がする。

調べてみると、欠損値を補完する手法が色々あるらしいが、面倒なので欠損の場所によってモデルを分けることにした。 上記の特徴量に関して、Testデータで欠損している部分を調べると、Ageが欠損しているパターンとFareが欠損しているパターンがあった。 よって3つのモデルを学習させることにする。

  1. Age、Fareが欠損していないデータを学習データにして、同様のデータを予測。
  2. Fareが欠損していないデータを学習データにして、Ageが欠損しているデータを予測。
  3. Ageが欠損していないデータを学習データにして、Fareが欠損しているデータを予測。

3つのネットワークを学習させて結果をsubmitすると、 0.77990でベースラインに並んだ。

f:id:ksknw:20151129150618p:plain

ここまで適当にしていたパラメータ調整をちゃんとやればベースラインを超えられる気がしたので、 パラメータ色々いじってsubmitしまくると、 0.78469になり、無事ベースラインを超えることができた。

f:id:ksknw:20151129150625p:plain

試行回数をふやす

何度かパラメータ調整をしてsubmitを繰り返していると、スコアが結構ブレることに気づく。 また、先ほどのモデルの1.に使う学習データは結構少ないので、精度が本当にでるのかやや不安でもある。 そこで、学習を複数回行い、また、1.のモデルで判別していたデータを2.のモデルでも判別した。 それらの結果の平均を最終的な判別結果とする。

submitすると、0.77990で、うーんという感じ。 結果の分散が減ったぶん、たまたまうまくいくのもなくなっているという印象。

別の特徴量を考える。

このままモデルをいじっていてもジリ貧な気がしたので、別な特徴量を考えることにした。

  • PassengerId: なんとなく削っていたけれども、もしかすると何かあるかもしれないので足す。
  • 欠損値の有無: そもそも欠損しているのはなぜか。死んだからではないのか。
  • Nameの敬称: Mr.とかMs.とかだけだと思っていたら、こちらによるともっと色々あるらしいので、突っ込んでみることにした。

以上を加えてモデルを学習させると 0.76077でいまいちだった。ぐぬぬ

ちなみに現在のコードのだいたい。

#-*- coding: utf-8 -*-
import pandas as pd
import numpy as np
from nn import Perceptron
from chainer import cuda
import sklearn.preprocessing


def to_feature(data):

    emberked = []
    for i in data["Embarked"]:
        temp = np.zeros(3)
        if not np.isnan(i):
            temp.put(i, 1)
        emberked.append(temp)
    feature = emberked

    pclass = []
    for i in data["Pclass"]:
        temp = np.zeros(3)
        if not np.isnan(i):
            temp.put(i - 1, 1)
        pclass.append(temp)
    feature = np.c_[feature, pclass]

    feature = np.c_[feature, data["Age_na"]]
    feature = np.c_[feature, data["Cabin_na"]]

    for keyword in ["Master.", "Col.", "Mrs.",
                    "Ms.", "Miss.", "Rev.",
                    "Mr.",  "Dr.", "Major."]:

        feature = np.c_[feature, data.Name.str.contains(keyword).astype(int)]

    try:
        feature = np.c_[feature, data["Age"]]
    except:
        pass
    try:
        feature = np.c_[feature, data["Fare"]]
    except:
        pass
    try:
        feature = np.c_[feature, data["SibSp"]]
    except:
        pass
    try:
        feature = np.c_[feature, data["Parch"]]
    except:
        pass
    feature = np.c_[feature, data["Sex"]]
    feature = np.c_[feature, data["PassengerId"]]

    return feature


def read_data(data_filename):
    data = pd.read_csv(data_filename).replace("male", 0).replace("female", 1)
    data = data.replace("C", 0).replace("Q", 1).replace("S", 2)
    return data


def add_is_na_columns(data):
    for label in ["Age",  "Cabin"]:
        temp = pd.DataFrame(pd.isnull(data[label]).astype(int))
        temp.columns = [label + "_na"]
        data = pd.concat([data, temp], axis=1)
    return data

if __name__ == '__main__':
    test_data = add_is_na_columns(read_data("test.csv"))
    train_data = add_is_na_columns(read_data("train.csv"))

    label_categories = [['PassengerId', 'Name', 'Pclass', 'Sex', 'Age', 'SibSp', 'Parch', 'Fare',  'Embarked', 'Cabin_na', 'Age_na'],
                        ['PassengerId', 'Name', 'Pclass', 'Sex', 'SibSp', 'Parch', 'Fare',  'Embarked', 'Cabin_na', 'Age_na']]

    results = []

    for i in range(1):
        print i
        for label in label_categories:
            temp_train_data = train_data.ix[:, train_data.columns.isin(label + ["Survived"])].dropna()
            temp_test_data = test_data.ix[:, test_data.columns.isin(label)].dropna()

            train_vec = to_feature(temp_train_data)
            test_vec = to_feature(temp_test_data)

            min_max_scaler = sklearn.preprocessing.MinMaxScaler()

            no_normalize_index = 8 + 9
            min_max_scaler.fit(train_vec[:, no_normalize_index:])

            test_vec[:, no_normalize_index:] = min_max_scaler.transform(test_vec[:, no_normalize_index:])
            train_vec[:, no_normalize_index:] = min_max_scaler.transform(train_vec[:, no_normalize_index:])

            nn = Perceptron(len(train_vec[0]), 2, 700, 45, 45)
            results.append([temp_test_data.PassengerId,
                            nn.train(train_vec,
                                     temp_train_data["Survived"],
                                     test_vec)])
            # from sklearn.ensemble import RandomForestClassifier
            # random_forest = RandomForestClassifier()
            # random_forest.fit(train_vec, temp_train_data["Survived"])
            # results.append([temp_test_data.PassengerId,
            #                 random_forest.predict(test_vec)])

    results.append([[1044], [1, 0]])  # Fareが欠損している一人は死んだことにする
    # results.append([[1044],  0])  # Fareが欠損している一人は死んだことにする

    print results
#    f = open("result_random_forest.csv", "w")
    f = open("result_nn_10.csv", "w")
    f.write("PassengerId,Survived\n")

    for result in results:
        for i in zip(*result):
            print i
            f.write("%d,%d\n" % (i[0], np.argmax(i[1])))
#            f.write("%d,%d\n" % (i[0], i[1]))
    f.close()

終わりに

なんか適当にパラメータいじっていたら0.78947になった。 たまたま感ある。 f:id:ksknw:20151129150629p:plain

色々やってみたけれども、なかなか難しい。 特徴量を増やしてみても、よくなるわけでもなく。 というかそもそも、どれぐらいいけるものなのかもわからない。 ランキングの一番上は1.000なので、萎える。

もうちょっとアイデイアがあるので、もしかすると続くかもしれないし、 飽きてきている感があるので、別の課題をやるかもしれない。

参考

kaggleで予測モデルを構築してみた (1) - kaggleって何? - About connecting the dots.