Help us understand the problem. What is going on with this article?

[Kaggle初心者向け] 初心者がkaggleのtitanic challengeで意味のある学びをするにはを説明しまっせ

More than 1 year has passed since last update.

kaggleで遊ぼう

kaggleはみなさんご存知データサイエンティストが様々な課題でスコアを競うクールな遊びです。

このkaggleってデータサイエンスを知らない人でも結構興味を惹かれるものです。だってお金も稼げるし(100%無理な件は置いといて)

巷にはいくつもの初心者向け記事が溢れていますが、いまいち解き方とか、上位3%に入るにはみたいな記事はあっても、どう学んで次に繋げていけばいいか、みたいな記事がなかったので、書いてみようと思いました。
コードは切れ端しか載せてませんが、やることは書いてます。

1. Datasetを読み込もう

データセット読み込み
def load_dataset():
    location = '../csv_files/'
    train_df = pd.read_csv(location + 'train.csv', dtype={'Age': np.float64}, )
    test_df = pd.read_csv(location + 'test.csv', dtype={'Age': np.float64}, )
    labels = train_df.Survived

    return train_df, test_df, labels

Screen Shot 2018-06-25 at 0.12.09.png

2. 一旦featureをダミー変数化しよう

ダミー変数化
def make_dummies(df):
    sex_dum = pd.get_dummies(df['Sex'])
    df = pd.concat((df, sex_dum), axis=1)
    df = df.drop('Sex', axis=1)
    df = df.drop('female', axis=1)

    emb_dum = pd.get_dummies(df['Embarked'])
    df = pd.concat((df, emb_dum), axis=1)
    df = df.drop('Embarked', axis=1)

    return df

Screen Shot 2018-06-25 at 0.09.55.png

性別は'male', 'female'だったのが、maleかどうかで0,1で表すことができました。

3. 欠損値を確認しよう

欠損値を表示
print(train_df.isnull().sum())
print(test_df.isnull().sum())
train_df
PassengerId      0
Survived         0
Pclass           0
Name             0
Sex              0
Age            177
SibSp            0
Parch            0
Ticket           0
Fare             0
Cabin          687
Embarked         2
dtype: int64
test_df
PassengerId      0
Pclass           0
Name             0
Sex              0
Age             86
SibSp            0
Parch            0
Ticket           0
Fare             1
Cabin          327
Embarked         0
dtype: int64

以上の表から、train dataにはAgeが不明の人が177人、test dataには86人いることがわかります。

また、cabin(部屋番号)は欠損率がトレーニングデータで687/891 = 約77%ということで、使い物にならなそうですが、判断がつきません。

featureをなんでも学習させてしまうと、過学習と呼ばれる汎用性の無い学習モデルになったり、計算に時間がかかってしまいます。
どのfeatureを使うか、というのはdeep learningでない限りは基本的には人間が判断するしかありません。
しかし勘で選ぶのではなく、様々な指標や図示によってデータの特徴を掴んでいく、という流れになります。

4. feature(特徴量)がどれくらい大事か見てみよう

まずは相関を見てみましょう

相関を表示
train_df.corr()

Screen Shot 2018-06-25 at 0.08.28.png

相関は、-1から1の値を取ります。

1に近ければ近いほど、正の相関が強くなります。
すなわち、Aという要素が増加すると、Bという要素も増加する傾向があると言えます。

-1に近ければ近いほど、負の相関が強置くなります。
すなわち、Aという要素が増加すると、Bという要素は減少する傾向があります。

さて、この表から、Survivedの列を見てみましょう。
Pclass(チケットのクラス)に対して-0.338481という値が出ています。

公式サイトを見てみると

1 = 1st, 2 = 2nd, 3 = 3rd

となっているように、Pclassは値が小さければ小さいほどリッチであることを意味します。

弱い負の相関があることを考えると、

良いチケットを持っている人は若干生き残りやすい

と言えなくもないでしょう。

他にはどうでしょうか。

maleのところが-0.543351となっており、まずまずの負の相関になっています。
maleは

  • 0(女)
  • 1(男)

の2値を取りますので、1(男)であると生き残る確率が下がる、と言えます。
これは女性は優先的に助けられていたということでしょう。

一応図で見てみましょうか。

性別毎の生存率
male_survived_ratio = len(train_df[(train_df.Sex == 'male') & (train_df.Survived == 1)]) / len(train_df[train_df.Sex == 'male'])
female_survived_ratio = len(train_df[(train_df.Sex == 'female') & (train_df.Survived == 1)]) / len(train_df[train_df.Sex == 'female'])
sex_survived_ratio = [male_survived_ratio, female_survived_ratio]
plt.bar([0,1], sex_survived_ratio, tick_label=['male', 'female'], width=0.5)

sex_survived_ratio.png

女性が男性をなぎ倒しながらボートに進んでいった様が目に浮かびます(浮かばない)
女性は74%の生存確率ということが確認できました。

さて、本題の欠損値ですが、欠損がたくさんあるAgeに関してはそれほど相関は高くありませんね。

では、年代別に生き残る確率を見てみましょうか。

5. データを図示して解釈をする

性別・年代毎の生存率
male_df = train_df[train_df.Sex == 'male']
female_df = train_df[train_df.Sex == 'female']

male_age_survived_ratio_list = []
female_age_survived_ratio_list = []
for i in range(0, int(max(train_df.Age))+1, 10):
    male_df_of_age = male_df[(male_df.Age >= i) & (male_df.Age < i+9)]
    female_df_of_age = female_df[(female_df.Age >= i) & (female_df.Age < i+9)]

    male_s = len(male_df_of_age[male_df_of_age.Survived == 1])
    female_s = len(female_df_of_age[female_df_of_age.Survived == 1])

    male_total = len(male_df_of_age)
    female_total = len(female_df_of_age)

    if male_total  == 0:
        male_age_survived_ratio_list.append(0.5)
    else:
        male_age_survived_ratio_list.append(male_s/male_total)

    if female_total == 0:
        female_age_survived_ratio_list.append(0.5)
    else:
        female_age_survived_ratio_list.append(female_s/female_total)

print(male_age_survived_ratio_list, female_age_survived_ratio_list)

x_labels = []
for i in range(0, int(max(train_df.Age))+1, 10):
    x_labels.append(str(i) + '-' + str(i+9))

plt.figure(figsize=(16,8))
x1 = [i for i in range(0, int(max(train_df.Age))+ 1, 10)]
x2 = [i + 2 for i in range(0, int(max(train_df.Age))+ 1, 10)]
plt.bar(x, male_age_survived_ratio_list, tick_label=x_labels, width=3, color='blue')
plt.bar(x2,female_age_survived_ratio_list, tick_label=x_labels, width=3, color='red')
plt.tick_params(labelsize = 15)

age_sex_survived_ratio.png

80歳以上男性は1人しかいなくて生き残ったので、生存率100%になってます。
こうしてみると女性の圧倒的生存率がより見えますね。
0-9歳は性別関係なく生き残りやすい感じです。

となるとやはり年齢をしっかり推定したいところです。

6. 年齢を推定する方法を検討する。

さて、年齢を推定したいのですが、一つその前にデータを加工しましょう。
名前を見てみましょう。

train_df.Name.values.tolist()[:5]
['Braund, Mr. Owen Harris',
 'Cumings, Mrs. John Bradley (Florence Briggs Thayer)',
 'Heikkinen, Miss. Laina',
 'Futrelle, Mrs. Jacques Heath (Lily May Peel)',
 'Allen, Mr. William Henry']

さて、Mr, Mrs, Missなど、敬称が付いています。

敬称をダミー変数化してみましょう。

Screen Shot 2018-06-25 at 9.51.46.png

こんな感じで、mrなのかmrsなのかとかがわかるようになりました。
この状態で相関を見てみると、age-masterが-0.398とか、少しだけ相関がありそうです。
つまり、masterが付いていると年齢は低いはず、ということですね。

敬称毎の年齢の中央値を比べてみましょうか。

title_medians.png

明らかにmasterが付いている人は年齢が低いですね。
titleは一つの年齢推定の重要な要素になりそうですね。

さて、Fareはどうなんでしょうか。

Fareが0の人のデータ
train_name_df.Fare.min()
train_name_df[train_name_df.Fare == 0]

Screen Shot 2018-06-25 at 20.55.41.png

こうしてみると、無賃乗船している人は一人を除いて全員が死んでいます笑
そして全員Sから乗船しています。(Sから乗っている人は全体の中で一番多いですが)

また、彼らは全員がMrです。
そして、全員兄弟、夫婦、親、子供の一人も連れてきていません。

これから見るに、なんだかFareやParch, SibSpなども年齢の推定に入れたほうが良い感じがしますね。

Cabinはどうでしょうか。

実は、cabin(部屋)が判明していない人は、traing dataの861人中610人程度います。
部屋が分かっている人の生存率VS部屋が分かっていない人の生存率を見てみましょうか。
結果だけ載せます。

部屋が判明している人の生存率 0.6666666666666666

部屋が不明な人の生存率 0.29985443959243085

結構大きな差があることが見て取れます。
こんなに欠損しているので、適切かわかりませんが、cabinの有無を入れて考えることにします。

Ticketに関しては、Pclassが完璧であるので特に使う必要はないかと思います。
(ちなみにかなりの種類のticketがありました。)

ちなみに、pclss毎の生存率は以下の通りです。

1: 0.6296296296296297
2: 0.47282608695652173
3: 0.24236252545824846

つまり、ファーストクラスバンザイ。

家族人数による生存率を見てみると

1人: 0.30353817504655495
2人から3人: 0.5787671232876712
4人以上: 0.16129032258064516

ということで、関係がありそうです。

7.年齢を推定するモデルの選定

簡単に年齢はtitleの中央値をあてはめたりすればそんなに悪くないとは思いますが、せっかくなので年齢を機械学習で推定します。

年齢を推定する際に、年齢がfloatになっているとうまく学習ができなかったのでintに直しました。
しかし、年齢は0.5歳とかもあるので、年齢全部に100をかけました。(悪いことですが、とりあえず実行するために今は気にしないことにします。)

さて、

def evaluate_models(train_data, predictors):
    from sklearn.linear_model import LogisticRegression
    from sklearn.svm import SVC, LinearSVC
    from sklearn.neighbors import KNeighborsClassifier
    from sklearn.tree import DecisionTreeClassifier
    from sklearn.ensemble import RandomForestClassifier
    from sklearn.neural_network import MLPClassifier

    models = []

    models.append(("LogisticRegression", LogisticRegression()))
    models.append(("SVC", SVC()))
    models.append(("LinearSVC", LinearSVC()))
    models.append(("KNeighbors", KNeighborsClassifier()))
    models.append(("DecisionTree", DecisionTreeClassifier()))
    models.append(("RandomForest", RandomForestClassifier()))
    models.append(("MLPClassifier", MLPClassifier(solver='lbfgs', random_state=0)))

    from sklearn.metrics import mean_squared_error
    from sklearn.model_selection import train_test_split

    results = []
    names = []

    X_train, X_test, y_train, y_test = train_test_split(train_data[predictors], train_data.Age, test_size=0.25)

    print(X_train.dtypes)

    for name, model in models:
        model.fit(X_train, y_train)
        pred = model.predict(X_test)
        result = mean_squared_error(y_test, pred)

        names.append(name)
        results.append(result)

    return names, results

こちらのコードで平均二条誤差(連続値を推定する際、正解と推定がどれほど離れていたかを指標とする誤差関数)を計算してみましょう。

実行毎にデータ分割が変わるので結果が変わるので、for分で10回だけ回してみます。

names = ['LogisticRegression',
  'SVC',
  'LinearSVC',
  'KNeighbors',
  'DecisionTree',
  'RandomForest',
  'MLPClassifier']

result_list = []
for i in range(0,10):
    n, r = evaluate_models(family_size_df, age_predictors)
    for j, k in zip(n, r):
        result_list.append([j, k])

import statistics

for n in names:
    print(n)
    r = [i[1] for i in result_list if i[0] == n]
    print(sum(r)/len(r), statistics.median(r))

LogisticRegression
7347051.41076 7309180.44395
SVC
7655820.93857 7701665.30942
LinearSVC
6243641.12556 6280067.08744
KNeighbors
5544237.81614 5609264.98655
DecisionTree
4323462.24215 4244058.61435
RandomForest
4048898.83274 3868791.02018
MLPClassifier
6856468.92063 6816154.29821

結果的に、平均、中央値ともにRandomForestClassifierが優秀そうですので、こちらをトレーニングしていきたいと思います。(XGBoostも計測してみましたが奮わず。)
(10回回すのとか間違ってると思いますがとりあえずはこれでいきます。)

8. 最適なパラメーターの設定

RandomForestを使用することが決まったので、実際に学習をさせていきます。
RandomForesetのハイパーパラメーターを決定したいので、今回はgrid searchを使います。

def gscv_random_forest_for_age(train_data, predictors):
    from sklearn.ensemble import RandomForestClassifier

    parameters = {
    'n_estimators': [5,10,20,30,50,100,300],
    'max_depth': [3,5,10,15,20,25,30,40,50,100],
    'random_state': [0]
    }
    from sklearn.model_selection import GridSearchCV
    gsc = GridSearchCV(RandomForestClassifier(), parameters, cv=3)
    gsc.fit(train_data[predictors], train_data["Age"])

    return gsc
GridSearchCV(cv=3, error_score='raise',
       estimator=RandomForestClassifier(bootstrap=True, class_weight=None, criterion='gini',
            max_depth=None, max_features='auto', max_leaf_nodes=None,
            min_impurity_decrease=0.0, min_impurity_split=None,
            min_samples_leaf=1, min_samples_split=2,
            min_weight_fraction_leaf=0.0, n_estimators=10, n_jobs=1,
            oob_score=False, random_state=None, verbose=0,
            warm_start=False),
       fit_params=None, iid=True, n_jobs=1,
       param_grid={'n_estimators': [5, 10, 20, 30, 50, 100, 300], 'max_depth': [3, 5, 10, 15, 20, 25, 30, 40, 50, 100], 'random_state': [0]},
       pre_dispatch='2*n_jobs', refit=True, return_train_score=True,
       scoring=None, verbose=0)

意味不明ですね。
まあこういうのは追々理解すればいいですよ。。。

あとはpredictするだけですが、test_dataもtrain_dataと同じように前処理をしないと学習できません。

9. 予測する

基本的に予測は

model.predict(train_x, train_y)

でできます。

私の予測した結果

{200.0,
 600.0,
 900.0,
 1200.0,
 1400.0,
 1800.0,
 2100.0,
 2200.0,
 2400.0,
 2500.0,
 3000.0,
 3600.0,
 3900.0,
 4000.0}

以下の種類の年齢が予測の結果として出ました。
なんか若い気がしますが、一旦これを使ってみることにします。








また上で使用したコードで、生存したかどうかを予測するのにどのモデルが一番良さそうかを比べてみた結果、XGBoostとLogisticRegressionが同じぐらいのスコアでしたので、こちらを使って予測をしていきます。

def make_xgb_model3(X, Y):
    from xgboost.sklearn import XGBRegressor
    import scipy.stats as st

    one_to_left = st.beta(10, 1)
    from_zero_positive = st.expon(0, 50)

    params = {
        "n_estimators": st.randint(3, 40),
        "max_depth": st.randint(3, 40),
        "learning_rate": st.uniform(0.05, 0.4),
        "colsample_bytree": one_to_left,
        "subsample": one_to_left,
        "gamma": st.uniform(0, 10),
        'reg_alpha': from_zero_positive,
        "min_child_weight": from_zero_positive,
    }

    xgbreg = XGBRegressor(nthreads=-1)

    from sklearn.model_selection import RandomizedSearchCV

    gs = RandomizedSearchCV(xgbreg, params, n_jobs=1)
    gs.fit(X, Y)
    print(gs.best_params_)

    return gs.fit(X, Y)

こんな感じでRandomizedSearchCVで最適なパラメータを求め、XGBoostの予測モデルを得ます。

これにpredict(test_x)させれば、予測結果がもらえます。

結果はなんと

Screen Shot 2018-06-27 at 8.58.52.png

77.99%

kaggle_titanic_score_hist.png

全体としては超えられない80%の壁の少し前にいます。
上位30%のところにいます。

一応LogisticRegressionでも予測をしてみました。
0.77511でした。

さて、ここからが本題です。

これまでいろいろやってきましたが、まだまだ改善するべきところがあります。

kernels(参加者が自分の戦略を上げている場所)を見てみると、有効そうな作戦が見えてきます。

今回は機械学習エンジニアの方に質問して得られたアドバイスを少し実行してみます。

まずは、現在のdfの様子を確認しましょう。

Screen Shot 2018-06-27 at 20.47.40.png

ここから以下の改善をします。

  • 年齢をカテゴリ変数化
    • 5分割して、どの層にいるかをあらわす
    • 普通に正規化したらどうなるかもやってみたいところです。
  • 家族系をまとめてfam_sizeとしてSibSp + Parchで表す。
  • titleをまとめて1カラムにする

変数が無駄に多いのは良くないです。

さて、結果こうなりました

Screen Shot 2018-06-27 at 23.10.14.png

こちらのシンプルなデータで予測結果がどうなるか試してみましょう。

XGBでやってみると

Screen Shot 2018-06-27 at 23.24.02.png

79.425%!!
割といいのではないでしょうか!!

ハイパーパラメータをCVで最適化するとなんとなんと

Screen Shot 2018-06-27 at 23.44.15.png

キターーーーーーーーーー
念願の80%超え。

これで上位3.5%辺りですね。
0.035505080693365214

これをさらに改善するとしたら、
- 途中AgeをRandomForestで推定したところをもう少しうまくできるのでは
- 最後にAgeをカテゴリ化しましたが、正規化したらどうなるのか
- ハイパーパラメータの調整
- LightGBMなど他の分類モデルを使う

などが考えられるのではないでしょうか。

また学習の結果を判断するためにclassification_reportを使ったり、過学習の判定、より複雑な図示による考察など、このtitanicをやる中で自分が理解していないところがどんどん出てきますので、まずそれらを理解することが大きな学びになるのではないでしょうか。

とにかく、
1. データの前処理
2. そのための図示による解釈
3. 適切なモデルの選択
4. ハイパーパラメータの選択
5. 学習結果の評価

この大きな5パートの中のテクニックを1つずつ抑えていく感じが良いのではないでしょうか。

とはいえ、理論ばっかり学んでると疲れますよね
なので他のkaggleのtutorialに参加しつつ場数を踏んで、kernelsとかでよく出てくるテクニックなどについて学んでいけば良いのではないかと思います。

あとは問題のタイプもいろいろ経験を積んでみたいですね。
2値分類から多クラス分類、回帰や自然言語系、画像系など。

まあこんな感じでお粗末なアウトプットでしたが、参考になると嬉しいです。

Why do not you register as a user and use Qiita more conveniently?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away