前回までで
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)
こんな感じ。中身が見えています。中身は問題ページに書いてありますし、大体から無名からわかりますよね。わかりにくいところだと
- 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()
こんな感じで分布しています。横軸が年齢、縦軸はそれぞれの敬称。敬称によって年齢の分布が変わっていることはわかります。本当だったらその要素ごとに敬称と他のフィールド要素から最もらしい年齢を類推するのがよいのでしょうが面倒なので、適当にそれぞれの敬称グループごとの平均とって、それで。
# 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)
こんな感じ。
大分減っちゃいましたね。ここで全部数値データになっていることを確認しておきます。
学習するんや
学習しましょう。こっから楽しいところですね。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
という感じで、サブミットした結果。どうなったかというと…
うーん。あんまり点数高くないですね。
というわけで、サブミットまでできました。
リーダーボードのランキングを見てみると上の方は全部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