euphonictechnologies’s diary

Haskell超初心者の日記です。OCamlが好きです。

follow us in feedly

Google Cloud Datalabを使って未経験からKaggleのTitanicサブミットまで - その2 サブミットにこぎ着ける

前回までで

Google Cloud Datalabのセットアップができました。楽ちんでしたね。

サブミットまで

今回は、サブミットまでたどり着きたいのですが、サブミットするだけならテストデータの要素数分ランダムにゼロイチ並べて出せばいいのですが、それだとあれなので普通に取り組んで答えを出してみます。

その前に

Datalab落ちちゃっている場合はVMのコンソール開いて

datalab connect --zone us-west2-a --port 8081 datascience-01a

名前やゾーンは適宜変えてくださいね。

Pandasを使う

テーブルっぽいデータ構造pandasをつかいます。この手のデータ構造はどんなプログラミング言語にもありますよね。それを使って前回ダウンロードした問題ファイルを読み込みます。

import pandas as pd

train_data = pd.read_csv('train.csv')
train_data.head(4)

f:id:euphonictechnologies:20180923111918p:plain

こんな感じ。中身が見えています。中身は問題ページに書いてありますし、大体から無名からわかりますよね。わかりにくいところだと

  • Pclass: チケットのクラスです
  • SibSp, Parch: 乗船してる兄弟とか配偶者(SibSp)と、両親か子供(Parch)の数。兄弟か、配偶者か、両親か子供かはこの数字からはわからない。
  • Embarked: 乗船した場所。C = Cherbourg, Q = Queenstown, S = Southamptonです。タイタニックはイギリスからアメリカに行く途中で沈没したのでこの地名は全部イギリスの地名ですね。

残りの説明は: Titanic: Machine Learning from Disaster | Kaggle

こんな感じ。

データのゴミ取り

データにはゴミがたくさん含まれておりまして、

train_data.info()

すると

<class 'pandas.core.frame.DataFrame'>
RangeIndex: 891 entries, 0 to 890
Data columns (total 12 columns):
PassengerId    891 non-null int64
Survived       891 non-null int64
Pclass         891 non-null int64
Name           891 non-null object
Sex            891 non-null object
Age            714 non-null float64
SibSp          891 non-null int64
Parch          891 non-null int64
Ticket         891 non-null object
Fare           891 non-null float64
Cabin          204 non-null object
Embarked       889 non-null object
dtypes: float64(2), int64(5), object(5)
memory usage: 83.6+ KB

こんな感じで見えます。これを見るとAgeはNaNが入っている可能性があります。Ageは整数であるべきですよね。

train_data.isnull().any()

これをやってみます。isnull()はそれぞれのテーブルセル一つ一つをNaNかどうかのブールに置き換えて、anyは列ごとにTrueが一つでもあればTrueになります。

PassengerId    False
Survived       False
Pclass         False
Name           False
Sex            False
Age             True
SibSp          False
Parch          False
Ticket         False
Fare           False
Cabin           True
Embarked        True
dtype: bool

というわけなので、あってたっぽいです。Age, CabinとEmbarkedがNaNがあります。Nameとかは大丈夫でした。ゴミ取りしましょう。

Embarkedのゴミ取り

a = train_data.copy()
a["Embarked"].fillna(value='', inplace=True)
a.Embarked.value_counts()

中身をとりあえず見てみましょう。train_dataはオリジナルデータをとっておきたいので、copy()しましょう。変数名はひどいです。aはやめましょう。NaNはfillnaで空白に置き換えます。参照透明性大好き人間としてはinplaceは嫌いです。

S    644
C    168
Q     77
       2
Name: Embarked, dtype: int64

すっとNaNが2個あることがわかりました。2個ならどっちでもええやろ、ってことで

a = train_data.copy()
# seems like Embaked NaN are only 2, so map it to S (maximum)
a["Embarked"].fillna(value='', inplace=True)
a = dummify(a, 'Embarked', {'S': 0, 'C': 1, 'Q': 2, '': 0}, 'int64')

として空白をSに割り当ててダミー変数化してしまいます。港をそれぞれ{0, 1, 2}に割り当てることにしました。dummify関数は私のオリジナルのやつで

def dummify(table, col_name, mapping, astype=None):
  ret = table.copy()
  ret[col_name] = ret[col_name].map(mapping)
  if astype:
    ret[col_name] = ret[col_name].astype(astype)
  return ret

こんな感じ!単にマッピング掛けるだけかーい。

Cabinのゴミ取り

CabinはCXXXとかAXXXみたいな形のようです。なので頭だけ取り出すことにします。多分キャビン番号使うと生存率予想に使えるはずですが、難しいので無視しましょう。

a = train_data.copy()
a["Cabin"].fillna(value='', inplace=True)
a.Cabin.value_counts()

とすると

               687
B96 B98          4
G6               4
C23 C25 C27      4
D                3
              ... 
B78              1
D56              1
C62 C64          1
B41              1
Name: Cabin, Length: 148, dtype: int64

こんな感じ。ほとんど空白やないか!と言うわけで、あるやつは頭のアルファベット、無いやつはUnknownのUと言うことにしましょう、とりあえず。

a["Cabin"].fillna(value='', inplace=True)  
a.Cabin = a.Cabin.map(lambda x : x[0] if len(x) > 0 else 'U')
a = dummify(a, 'Cabin', {'A': 8, 'B': 7, 'C': 6, 'D': 5, 'E': 4, 'F': 3, 'G': 2, 'T': 1, 'U': 0}, 'int64')

頭文字取り出して整数に割り当てます。一応連続性を考慮してAから数字が減っていって空白はゼロに割り当てました。どうなんでしょうねこれ。

Ageのゴミ取り

Ageはちょっと難しいです。適当に全体の平均値なり中央値で埋めてもいいんですが、それもちょっとまずそう。てことで何か使えないかというと、名前が使えそう。名前についている敬称は年齢をある程度反映してるかもしれないです。実際に見てみましょう。

まずは、敬称をとるために全部名前を小文字に変換して、その中身で"Sir"というカラム名をつけます。

a = train_data.copy()
a.Name = a.Name.map(lambda name : name.lower())
a.loc[:,'Sir'] = a.Name.map(lambda name : 1 if 'mrs' in name else (2 if 'mr' in name else (3 if 'miss' in name else 4 if 'master' in name else 0)))
a.head(10)

としてみると、Sirに敬称、"Mrs", "Mr", "Miss"そして"Master"に応じた数字が割り振られます。無いやつは0にマップしました。 a.loc[:,'Sir']は、あたらしカラムを作るためにそうしてます。他にやり方がわかりませんでした…。 んでもって中身を見てみると、

import matplotlib.pyplot as plt

plt.style.use('ggplot')
plt.scatter(a.Age,a.Sir,color="#cc6699",alpha=0.5)
plt.show()

f:id:euphonictechnologies:20180923120112p:plain

こんな感じで分布しています。横軸が年齢、縦軸はそれぞれの敬称。敬称によって年齢の分布が変わっていることはわかります。本当だったらその要素ごとに敬称と他のフィールド要素から最もらしい年齢を類推するのがよいのでしょうが面倒なので、適当にそれぞれの敬称グループごとの平均とって、それで。

# Check age and sir
a[['Age', 'Sir']].groupby('Sir').mean()

要素を並べたリストでaを引くと、そのテーブルだけselectできます。でもってgroupbyして、meanをとります。ここら辺SQLの勘が働く人は強いですね。

Age
Sir 
0   42.666667
1   35.689922
2   32.301158
3   21.805556
4   4.525000

いい感じに平均ばらけてるので、これでいきましょう。本当はMasterとか眉唾物ですね、

 = train_data.copy()
a.Name = a.Name.map(lambda name : name.lower())
a.loc[:,'Sir'] = a.Name.map(lambda name : 1 if 'mrs' in name else (2 if 'mr' in name else (3 if 'miss' in name else 4 if 'master' in name else 0)))
a.Age = a.Age.where(a.Age == a.Age, a.Sir.map({0: 43, 1: 36, 2: 32, 3: 22, 4: 5})).astype('int64')  
a.head(10)

このwhereがNaNでは失敗するので(NaN!=NaN)、そのマッチングが失敗してFalseになる部分ではマップを使ってSirによって当てはめる数字を決めて、入れ込みます。

あとはダミー変数化

あとは適当にダミー変数化していらないコラムをそぎ落とします。

まずは性別。

a = dummify(a, 'Sex', {'male': 1, 'female': 0}, 'int64')

次に、落とすカラム

a = a.drop('Ticket', axis=1).drop('Name', axis=1)

2つ落としちまいましょう。

SibSpとParch何かに使えない?

この二つは全部足すと家族の人数になります。詳しい分析は置いておいて、家族の人数ってことにしましょう。

a.loc[:, 'FamilySize'] = a.SibSp + a.Parch + 1

自分を含めた家族の人数です。多分ですが、家族が多いほど死にやすいはず。兄弟と両親のカラムはFamilySizeに置き換えたいので、FamilySizeを追加して、他二つは落としましょう。

a = a.drop('SibSp', axis=1).drop('Parch', axis=1)

テーブルのお掃除をまとめます

というわけで、ここまでの作業を全部一つの関数にまとめます。こんな感じ。

def dummify(table, col_name, mapping, astype=None):
  ret = table.copy()
  ret[col_name] = ret[col_name].map(mapping)
  if astype:
    ret[col_name] = ret[col_name].astype(astype)
  return ret

def get_normalized_dataset(train_data):
  a = train_data.copy()

  # seems like Embaked NaN are only 2, so map it to S (maximum)
  a["Embarked"].fillna(value='', inplace=True)
  a = dummify(a, 'Embarked', {'S': 0, 'C': 1, 'Q': 2, '': 0}, 'int64')

  a["Cabin"].fillna(value='', inplace=True)    
  a.Cabin = a.Cabin.map(lambda x : x[0] if len(x) > 0 else 'U')
  a = dummify(a, 'Cabin', {'A': 8, 'B': 7, 'C': 6, 'D': 5, 'E': 4, 'F': 3, 'G': 2, 'T': 1, 'U': 0}, 'int64')

  a.Name = a.Name.map(lambda name : name.lower())
  a.loc[:,'Sir'] = a.Name.map(lambda name : 1 if 'mrs' in name else (2 if 'mr' in name else (3 if 'miss' in name else 4 if 'master' in name else 0)))
  a.Age = a.Age.where(a.Age == a.Age, a.Sir.map({0: 43, 1: 36, 2: 32, 3: 22, 4: 5})).astype('int64')
    
  a = dummify(a, 'Sex', {'male': 1, 'female': 0}, 'int64')    

  a.loc[:, 'FamilySize'] = a.SibSp + a.Parch + 1
  
  a = a.drop('Ticket', axis=1).drop('Name', axis=1)
  a = a.drop('SibSp', axis=1).drop('Parch', axis=1)

  return a

この関数にpandasテーブルを渡すと結果が返ってきます。

train_data = pd.read_csv('train.csv')
train_data.head(4)
a = get_normalized_dataset(train_data)
a.head(10)

こんな感じ。

f:id:euphonictechnologies:20180923132429p:plain

大分減っちゃいましたね。ここで全部数値データになっていることを確認しておきます。

学習するんや

学習しましょう。こっから楽しいところですね。LightGBMを使います。決定木をたくさん集めるアンサンブル学習を勾配ブースティングを使ってうまいことやってくれるらしいです。中身はまあ、あれです。使ってみましょう。

とりあえず使う

from sklearn.model_selection import train_test_split

aa = pd.get_dummies(a)
x_train, x_test, y_train, y_test = train_test_split(
    aa.values[:, 2:], aa.values[:, 1], test_size=0.33, random_state=201612
)

とりあえずpandasのget_dummies関数をかましています。多分ちゃんときれいなテーブルになっていたらいらないやつです。ここでtrain_test_split関数を使ってtrainデータを訓練用と検証用のデータに分けます。レコード数を2:1に分割します。aa.values[:, 2:]は生存結果の右側、使うデータ。values[:, 1]は生存結果、つまり答え。

そうするとx/y_trainは訓練用のフィールドがたくさん並んだリスト、x/y_testは答えの入った単なるバイナリ値のリストです。名前はもうちょっと考えた方がいいですね。

import lightgbm as lgb

gbm = lgb.LGBMClassifier(objective='binary',
                        num_leaves=22,
                        learning_rate=0.1,
                        min_child_samples=10,
                        n_estimators=100)

gbm.fit(x_train, y_train,
        eval_set=[(x_test, y_test)],
        eval_metric='multi_logloss',
        early_stopping_rounds=10)

y_pred = gbm.predict(x_test, num_iteration=gbm.best_iteration_)

とりあえずこれでLightGBMが動きます。うれしいですね。

最後の行は検証用訓練データを使って検証用データへの答えを出力します。検証用データの実際の答えと比べてみます。

# eval
print('Accuracy is:', 1 - abs(y_test - y_pred).sum() / len(y_test))

これで、正解率を計算すると

Accuracy is: 0.840677966101695

84点ですね。まあまあでしょうか。とにもかくにも与えられた訓練用データ全体を訓練用と検証用に勝手に分けて点数をつけているだけの話です。早速サブミットしてkaggleに点数をつけてもらいましょう。

テストデータを評価する

test_data = pd.read_csv('test.csv')
test_data.head(4)
b = get_normalized_dataset(test_data)
b.head(10)

テストデータを読み込んでさっきのデータ調整関数に通して予測できる形にします。

pred_for_submit = gbm.predict(b.values[:, 1:], num_iteration=gbm.best_iteration_)

このgbmは訓練データを読み込ませてfitしたやつです。これで予測して、結果を出します。

array([0., 0., 0., 0., 1., 0., 0., 0., 1., 0., 0., 0., 1., 0., 1., 1., 0.,
       0., 1., 1., 0., 1., 1., 0., 1., 0., 1., 0., 0., 0., 0., 0., 1., 1.,
       0., 0., 0., 0., 0., 0., 0., 1., 0., 1., 1., 0., 1., 0., 1., 1., 1.,
       0., 1., 1., 0., 0., 0., 0., 0., 1., 0., 0., 0., 1., 0., 1., 1., 0.,
       0., 1., 1., 0., 0., 1., 1., 0., 0., 1., 0., 1., 1., 0., 0., 0., 0.,
       0., 1., 0., 0., 1., 1., 0., 1., 0., 0., 0., 1., 0., 1., 0., 1., 0.,
       0., 0., 1., 0., 0., 0., 0., 1., 0., 1., 1., 1., 1., 0., 0., 1., 0.,
       1., 1., 0., 1., 0., 0., 0., 0., 1., 0., 0., 0., 0., 0., 0., 0., 0.,
       0., 0., 0., 0., 0., 1., 0., 0., 1., 0., 0., 0., 1., 0., 1., 0., 0.,
       1., 0., 0., 1., 1., 1., 1., 1., 1., 1., 0., 0., 0., 1., 0., 1., 0.,
       0., 1., 0., 0., 0., 1., 1., 0., 1., 1., 0., 1., 1., 0., 1., 0., 1.,
       0., 0., 0., 0., 0., 1., 0., 1., 0., 1., 1., 0., 1., 1., 1., 0., 1.,
       0., 0., 0., 0., 1., 0., 0., 0., 0., 1., 0., 0., 1., 0., 1., 0., 1.,
       0., 1., 0., 1., 0., 0., 1., 0., 0., 0., 1., 0., 0., 0., 0., 0., 0.,
       1., 1., 1., 1., 0., 0., 1., 0., 1., 0., 1., 0., 1., 0., 1., 0., 0.,
       0., 0., 0., 1., 0., 0., 0., 1., 1., 0., 0., 0., 0., 0., 0., 0., 0.,
       1., 1., 0., 1., 0., 0., 0., 0., 0., 1., 1., 1., 1., 0., 0., 1., 0.,
       0., 0., 0., 0., 1., 0., 0., 1., 0., 0., 0., 0., 0., 0., 0., 1., 1.,
       0., 1., 1., 0., 0., 0., 0., 0., 1., 1., 1., 0., 0., 0., 0., 1., 1.,
       1., 1., 0., 1., 0., 0., 0., 1., 1., 0., 1., 0., 0., 0., 0., 0., 1.,
       0., 0., 0., 1., 1., 1., 0., 1., 0., 1., 1., 0., 0., 0., 1., 0., 1.,
       0., 0., 1., 0., 1., 1., 0., 1., 0., 0., 0., 1., 0., 0., 1., 0., 0.,
       1., 1., 0., 0., 0., 0., 0., 0., 1., 1., 0., 1., 0., 0., 0., 0., 1.,
       1., 1., 0., 0., 1., 0., 1., 0., 0., 1., 0., 1., 0., 0., 1., 0., 0.,
       1., 1., 1., 1., 0., 0., 1., 0., 0., 1.])

こんなのが得られます。確かに1か0に分類されていますね。

サブミットしましょう

答えがpred_for_submitに格納されています。というわけで、サクッとサブミットしてしまいましょう。まず提出用のcsvに書き出します。

import csv
with open("predict_result_data.csv", "w") as f:
    writer = csv.writer(f, lineterminator='\n')
    writer.writerow(["PassengerId", "Survived"])
    for pid, survived in zip(test_data.values[:,0].astype(int), pred_for_submit.astype(int)):
        writer.writerow([pid, survived])
    print('Written.\n')

でもって、これをkaggleのCIツールで提出します。

%%bash
kaggle competitions submit -f predict_result_data.csv -m 'ここに提出データの説明を割と詳しくつけましょう' -c titanic

f:id:euphonictechnologies:20180923140742p:plain

という感じで、サブミットした結果。どうなったかというと…

f:id:euphonictechnologies:20180923140952p:plain

うーん。あんまり点数高くないですね。

というわけで、サブミットまでできました。

リーダーボードのランキングを見てみると上の方は全部1.00です。つまり、全問正解というわけですね。ちょっと萎えますね。

これでひとまず全部一回通せたので他のコンペティションにも取り組んでみたいと思いました。おしまい。

全部のコード

%%bash
pip install kaggle lightgbm
%%bash
echo '{"username":"my_user_name","key":"key_key_key"}' > ~/.kaggle/kaggle.json
%%bash
chmod 600 ~/.kaggle/kaggle.json
%%bash
kaggle competitions download -c titanic
from google.datalab import Context
import google.datalab.bigquery as bq
import google.datalab.storage as storage
import pandas as pd
from io import BytesIO as StringIO
import matplotlib.pyplot as plt
import seaborn as sns
from sklearn.model_selection import train_test_split
from sklearn.ensemble import RandomForestClassifier
from sklearn.metrics import mean_squared_error
from sklearn.model_selection import GridSearchCV
import sklearn.preprocessing as sp

train_data = pd.read_csv('train.csv')
train_data.head(4)

def dummify(table, col_name, mapping, astype=None):
  ret = table.copy()
  ret[col_name] = ret[col_name].map(mapping)
  if astype:
    ret[col_name] = ret[col_name].astype(astype)
  return ret

def get_normalized_dataset(train_data):
  a = train_data.copy()

  # seems like Embaked NaN are only 2, so map it to S (maximum)
  a["Embarked"].fillna(value='', inplace=True)
  a = dummify(a, 'Embarked', {'S': 0, 'C': 1, 'Q': 2, '': 0}, 'int64')

  a["Cabin"].fillna(value='', inplace=True)    
  a.Cabin = a.Cabin.map(lambda x : x[0] if len(x) > 0 else 'U')
  a = dummify(a, 'Cabin', {'A': 8, 'B': 7, 'C': 6, 'D': 5, 'E': 4, 'F': 3, 'G': 2, 'T': 1, 'U': 0}, 'int64')

  a.Name = a.Name.map(lambda name : name.lower())
  a.loc[:,'Sir'] = a.Name.map(lambda name : 1 if 'mrs' in name else (2 if 'mr' in name else (3 if 'miss' in name else 4 if 'master' in name else 0)))
  a.Age = a.Age.where(a.Age == a.Age, a.Sir.map({0: 43, 1: 36, 2: 32, 3: 22, 4: 5})).astype('int64')
    
  a = dummify(a, 'Sex', {'male': 1, 'female': 0}, 'int64')    

  a.loc[:, 'FamilySize'] = a.SibSp + a.Parch + 1
  
  a = a.drop('Ticket', axis=1).drop('Name', axis=1)
  a = a.drop('SibSp', axis=1).drop('Parch', axis=1)

  return a

a = get_normalized_dataset(train_data)
a.head(10)

# Check age and sir
a[['Age', 'Sir']].groupby('Sir').mean()

plt.style.use('ggplot')
plt.scatter(a.Age,a.Sir,color="#cc6699",alpha=0.5)
plt.show()

a.Cabin.value_counts()

aa = pd.get_dummies(a)
x_train, x_test, y_train, y_test = train_test_split(
    aa.values[:, 2:], aa.values[:, 1], test_size=0.33, random_state=201612
)

gbm = lgb.LGBMClassifier(objective='binary',
                        num_leaves=22,
                        learning_rate=0.1,
                        min_child_samples=10,
                        n_estimators=30)

gbm.fit(x_train, y_train,
        eval_set=[(x_test, y_test)],
        eval_metric='multi_logloss',
        early_stopping_rounds=10)

y_pred = gbm.predict(x_test, num_iteration=gbm.best_iteration_)

# eval
print('Accuracy is:', 1 - abs(y_test - y_pred).sum() / len(y_test))

# feature importances
print('Feature importances:', list(gbm.feature_importances_))

test_data = pd.read_csv('test.csv')
test_data.head(4)
b = get_normalized_dataset(test_data)
b.head(10)

pred_for_submit = gbm.predict(b.values[:, 1:], num_iteration=gbm.best_iteration_)

import csv
with open("predict_result_data.csv", "w") as f:
    writer = csv.writer(f, lineterminator='\n')
    writer.writerow(["PassengerId", "Survived"])
    for pid, survived in zip(test_data.values[:,0].astype(int), pred_for_submit.astype(int)):
        writer.writerow([pid, survived])
    print('Written.\n')
%%bash
kaggle competitions submit -f predict_result_data.csv -m 'LightBGM, adjusted3' -c titanic