高卒技能員がタイタニック号生存予測に泥臭く挑んでみた話


-----
**今回やるにあたって参考にさせて頂いたサイト**
https://yolo-kiyoshi.com/2020/01/22/post-1588/
https://www.codexa.net/kaggle-titanic-beginner/
https://qiita.com/suzumi/items/8ce18bc90c942663d1e6

----
#考えたこと・バイアス
- 極寒の環境下で生き残るのは難しそう。。救命ボートにのれる女性や子供は優遇されたのでは？


- 社会的地位の高い人は優遇されたのでは？



#データの確認
info()でざっと確認します
![2020-04-17 (2).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/588211/0e70caf4-46a5-c80b-bc4f-2a73fdf925f4.png)
データはinfo()からAge,Cabinともに欠損していることが分かります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/588211/02b579aa-aaab-da90-7949-b4f6051b840b.png)

#欠損値をどうするか？
個人的にデータ分析をずっとやってた経験を纏めるとこんな感じです。皆さん如何ですかね？

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/588211/4f48998f-7f6a-340a-89c9-ff0ac6975ea1.png)

今回であれば、欠損中程度なAgeは平均か中央値、Cabinは欠損大なので使用しない。。
と言いたいところですが、それは他の方々が分かりやすくやってくださって居ますし、この部分を泥臭くやりましょう

#Ageの欠損

Ageの欠損に関して、重要なのはNameだと考えます。その中でも敬称と呼ばれる類のもの、男女、大人子供、女性であれば既婚・未婚、地位の高さなど雑駁とでもしれる貴重な情報です。

特に、平均的に30代ぐらいが乗りそうな船です。子供や、地位の高い年配の方の存在はAgeの精度を下げます



なので、まずは敬称を抜き出します。

```
in: #名前を表示
train_data['Name']

out:
0                                Braund, Mr. Owen Harris
1      Cumings, Mrs. John Bradley (Florence Briggs Th...
2                                 Heikkinen, Miss. Laina
3           Futrelle, Mrs. Jacques Heath (Lily May Peel)
4                               Allen, Mr. William Henry
                             ...                        
886                                Montvila, Rev. Juozas
887                         Graham, Miss. Margaret Edith
888             Johnston, Miss. Catherine Helen "Carrie"
889                                Behr, Mr. Karl Howell
890                                  Dooley, Mr. Patrick
Name: Name, Length: 891, dtype: object

```

データを俯瞰すると、「,」と「.」の間に敬称があるので抜き出します

```
in: #名前から敬称を取り出したい
#train testをマージ
train_data1 = train_data.copy()
test_data1 = test_data.copy()
train_data1['train_or_test'] = 'train' 
test_data1['train_or_test'] = 'test' 
test_data1['Survived'] = np.nan #テストにのSurvivedカラムをNaNにする
all_data = pd.concat(
    [
        train_data1,
        test_data1
    ],
    sort=False,
    axis=0 #列方向でtrain_data1、test_data1を結合
).reset_index(drop=True)
#all_dataから敬称を抜き出し+平均年齢年齢を算出
honorific=all_data['honorific'] = all_data['Name'].map(lambda x: x.split(', ')[1].split('. ')[0])
Average_age=all_data['Age'].groupby(all_data['honorific']).agg(['count','mean','median','std'])
Average_age

```
これをすると、敬称毎の平均年齢が割り出せます
![2020-04-19 (2).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/588211/4d11c93a-e4c7-bde0-b39e-0fcbae151012.png)


では、このデータを元に欠損しているデータの敬称から其々平均年齢を入れていきましょう

```
in:欠損値に敬称毎の平均年齢を当てはめる
f = lambda x: x.fillna(x.mean())
age_complement = all_data.groupby('honorific').transform(f)
age_complement.info()


out:
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 1309 entries, 0 to 1308
Data columns (total 7 columns):
PassengerId    1309 non-null int64
Survived       1308 non-null float64
Pclass         1309 non-null int64
Age            1309 non-null float64
SibSp          1309 non-null int64
Parch          1309 non-null int64
Fare           1309 non-null float64
dtypes: float64(3), int64(4)
memory usage: 71.7 KB
```
年齢の欠損はこれにて完了です。しかし、なぜかカラムが消えたので
元のall_dataのAgeに移し替えます

```
del(all_data['Age']) #all_dataのAgeを削除
all_data['Age']=age_complement['Age']　#Age列を作成し、そこに欠損地を補完した方のAgeデータを入れる
```
これにて、Ageデータの欠損値の処理は終わりです


#データを操作していく中で気になったFareのばらつき
データを眺めてると、Fareの価格のバラツキが気になったので、これは、一人当たりの料金ではなく、そのグループの合計金額が代入されているのでは、、？と考えました。
Fare/重複するチケットの数をして一人当たりの適正価格に戻してあげます


```
in:# 1.Tocketの重複の数を表す辞書型配列を作成
double_check_dict = all_data['Ticket'].value_counts().to_dict()

# 2.DataFrameに重複数の列を追加する
all_data['double_check'] = all_data['Ticket'].apply(lambda x: double_check_dict[x] if x in double_check_dict else 0)
all_data['Fare']=all_data['Fare']/all_data['double_check']
all_data
```
![2020-04-19 (5).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/588211/dedd032c-e695-9a4e-4850-de7da72a6b1a.png)


double_checkがTicketの重複する枚数ですが、やはり、Ticketの枚数が多い所はFareの価格が高騰してるので、Ticketの枚数分Fareを割ります

```
in:
all_data['Fare']=all_data['Fare']/all_data['double_check']
all_data
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/588211/a32de5d8-40b4-ab77-0098-7013196f2c80.png)

これで、価格のバラツキを抑えることが出来ました。

**これでCabinの部屋に、価格による層別されているのであれば、Fareから大体の部屋が求まるのでは？と考えます**

#Cabinについて考えてみる
Cabinデータが入ってあるデータを分解していきます。

- Cabinデータの頭文字を抽出し、A・B・C・D..に分解する
- 複数部屋を泊まるグループは「C12 C13 C14」と空白単位で区切っているので、「空白+1」で何部屋借りたのか算出します




```
in:
cabin_data=all_data.dropna() #Cabinデータがあるもののみ抽出
cabin_data['Cabin_id'] = cabin_data['Cabin'].map(lambda x:x[0]) #Cabin_idにCabinの頭文字を入れる
cabin_data['room']=cabin_data['Cabin'].map(lambda x:x.count(' '))+1
cabin_data.head(50)　
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/588211/f8446b7e-682b-fbee-ac6a-b90cb4da7458.png)

## Pclassごとに分けて部屋料金を算出できないか考える
各部屋のランクに価格差があるのか見るために全体のものを1つ・P_class 1 2 3で分けたものを作り、各部屋の料金を考えます

```
cabin_data_1=cabin_data.query('Pclass == "1"')
cabin_data_2=cabin_data.query('Pclass == "2"')
cabin_data_3=cabin_data.query('Pclass == "3"')

Average_ageC=cabin_data['Fare'].groupby(cabin_data['Cabin_id']).agg(['count','mean','median','std','max','min'])
Average_ageC
```
全体的な価格はこちら
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/588211/11670045-d9c9-b45d-4d5f-f29ed440ddda.png)


```
Average_ageC=cabin_data_1['Fare'].groupby(cabin_data_1['Cabin_id']).agg(['count','mean','median','std','max','min'])
Average_ageC
```
Pclass1
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/588211/437a800d-3064-14d3-5832-e4bf57094e4c.png)

```
Average_ageC=cabin_data_2['Fare'].groupby(cabin_data_2['Cabin_id']).agg(['count','mean','median','std','max','min'])
Average_ageC
```
Pclass2
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/588211/276eef11-c7f2-ad11-8c23-5d57a7f488fe.png)

```
Average_ageC=cabin_data_3['Fare'].groupby(cabin_data_3['Cabin_id']).agg(['count','mean','median','std','max','min'])
Average_ageC
```
Pclass3
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/588211/e0975d52-f7a2-22d9-a60e-d8e820bf8c8f.png)


ざっくり見るとこんな感じですが、例えば、同じクラス帯で部屋ごとに利用料金が異なればCabinの欠損値の推定は可能ですが、ほぼほぼ同じで厳密には推定できなさそうです。うーん、残念
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/588211/007f5da7-1fc3-b80f-c873-ecaa3fa0283a.png)




##　SibSpとParchから家族人数を出す
```
all_data['famiry_size']=all_data['SibSp']+all_data['Parch']+1
```

##　Fareを11分割する
11分割しましたがここはまだ深堀出来そうな気がします。

```
# Fareの分割
all_data['Fare_bin'] = pd.qcut(all_data.Fare, 11)#FareをFare_binとして、11分割する
```
##　Sexを分ける

```
sex_col = ['Sex']
le = LabelEncoder()
for col in sex_col:
    all_data[col] = le.fit_transform(all_data[col]) #Sexのmale famaleを0と1に分ける
```
##'Pclass','Embarked','honorific','Fare_bin','famiry_size',をカテゴリカル変数に置き換える

```
cat_col = ['Pclass','Embarked','honorific','Fare_bin','famiry_size',]
all_data = pd.get_dummies(all_data, drop_first=True, columns=cat_col)#'Pclass','Embarked','honorific','Fare_bin','famiry_size'を0と1でダミー変数化する

```

ここまでで、各カラムの分割は以上です。
## all_dataをtrainとtestデータに分ける

```
from sklearn.model_selection import train_test_split
 
train = all_data.query('train_or_test == "train"')#'train_or_testがtrainのものを抽出
test = all_data.query('train_or_test == "test"')
# ターゲット変数と、学習に不要なカラムを定義
target_col = 'Survived'
drop_col = ['PassengerId','Survived', 'Name', 'Fare', 'Ticket', 'Cabin', 'train_or_test','Parch','SibSp','honorific_Jonkheer','honorific_Mme','honorific_Dona','honorific_Lady','honorific_Ms',]
# 学習に必要な特徴量のみを保持
train_feature = train.drop(columns=drop_col)
test_feature = test.drop(columns=drop_col)
train_tagert = train[target_col]
# trainデータを分割
X_train, X_test, y_train, y_test = train_test_split(
    train_feature, train_tagert, test_size=0.3, random_state=0, stratify=train_tagert)

```

# 学習
ライブラリとして
RandomForestClassifier
SVC
LogisticRegression
CatBoostClassifier
を使います

```
from sklearn.ensemble import RandomForestClassifier
from sklearn.svm import SVC
from sklearn.linear_model import LogisticRegression
from catboost import CatBoostClassifier


rfc = RandomForestClassifier(random_state=0)
rfc.fit(X_train, y_train)
print('='*20)
print('RandomForestClassifier')
print(f'accuracy of train set: {rfc.score(X_train, y_train)}')
print(f'accuracy of test set: {rfc.score(X_test, y_test)}')


lr = LogisticRegression(random_state=0)
lr.fit(X_train, y_train)
print('='*20)
print('LogisticRegression')
print(f'accuracy of train set: {lr.score(X_train, y_train)}')
print(f'accuracy of train set: {lr.score(X_test, y_test)}')

svc = SVC(random_state=0)
svc.fit(X_train, y_train)
print('='*20)
print('SVC')
print(f'accuracy of train set: {svc.score(X_train, y_train)}')
print(f'accuracy of train set: {svc.score(X_test, y_test)}')


cat = CatBoostClassifier(random_state=0)
cat.fit(X_train, y_train)
print('='*20)
print('CAT')
print(f'accuracy of train set: {cat.score(X_train, y_train)}')
print(f'accuracy of train set: {cat.score(X_test, y_test)}')

```
out側として、

```
RandomForestClassifier
accuracy of train set: 0.9678972712680578
accuracy of test set: 0.8246268656716418
LogisticRegression
accuracy of train set: 0.8426966292134831
accuracy of train set: 0.832089552238806
SVC
accuracy of train set: 0.8330658105939005
accuracy of train set: 0.8134328358208955
CAT
accuracy of train set: 0.9085072231139647
accuracy of train set: 0.8507462686567164
```

#OputunaとKfold検定でハイパーパラメーターを調整する


RandomForestClassifier

```
from sklearn.model_selection import StratifiedKFold, cross_validate
import optuna

cv = 10
def objective(trial):
    
    param_grid_rfc = {
        "max_depth": trial.suggest_int("max_depth", 5, 15),
        "min_samples_leaf": trial.suggest_int("min_samples_leaf", 1, 5),
        'min_samples_split': trial.suggest_int("min_samples_split", 7, 15),
        "criterion": trial.suggest_categorical("criterion", ["gini", "entropy"]),
        'max_features': trial.suggest_int("max_features", 3, 10),
        "random_state": 0
    }
 
    model = RandomForestClassifier(**param_grid_rfc)
    
    # 5-Fold CV / Accuracy でモデルを評価する
    kf = StratifiedKFold(n_splits=5, shuffle=True, random_state=0)
    scores = cross_validate(model, X=X_train, y=y_train, cv=kf)
    # 最小化なので 1.0 からスコアを引く
    return scores['test_score'].mean()
 
study = optuna.create_study(direction='maximize')
study.optimize(objective, n_trials=100)
print(study.best_params)
print(study.best_value)
rfc_best_param = study.best_params


```


SVC

```
import warnings
warnings.filterwarnings('ignore')

def objective(trial):
    
    param_grid_lr = {
        'C' : trial.suggest_int("C", 1, 100),
        "random_state": 0
    }

    model = LogisticRegression(**param_grid_lr)
    
    # 5-Fold CV / Accuracy でモデルを評価する
    kf = StratifiedKFold(n_splits=5, shuffle=True, random_state=0)
    scores = cross_validate(model, X=X_train, y=y_train, cv=kf)
    # 最小化なので 1.0 からスコアを引く
    return scores['test_score'].mean()

study = optuna.create_study(direction='maximize')
study.optimize(objective, n_trials=100)
print(study.best_params)
print(study.best_value)
lr_best_param = study.best_params 
```
LogisticRegression

```
import warnings
warnings.filterwarnings('ignore')

def objective(trial):
    
    param_grid_svc = {
        'C' : trial.suggest_int("C", 50, 200),
        'gamma': trial.suggest_loguniform("gamma", 1e-4, 1.0),
        "random_state": 0,
        'kernel': 'rbf'
    }

    model = SVC(**param_grid_svc)
    
    # 5-Fold CV / Accuracy でモデルを評価する
    kf = StratifiedKFold(n_splits=5, shuffle=True, random_state=0)
    scores = cross_validate(model, X=X_train, y=y_train, cv=kf)
    # 最小化なので 1.0 からスコアを引く
    return scores['test_score'].mean()

study = optuna.create_study(direction='maximize')
study.optimize(objective, n_trials=100)
print(study.best_params)
print(study.best_value)
svc_best_param = study.best_params
```

CatBoostClassifier

```
from sklearn.model_selection import train_test_split
from catboost import Pool
import sklearn.metrics
X = train_feature
y = test_feature
categorical_features_indices = np.where(X.dtypes != np.float)[0]
def objective(trial):
    # トレーニングデータとテストデータを分割
    X_train, X_test, y_train, y_test = train_test_split(
    train_feature, train_tagert, test_size=0.35, random_state=0, stratify=train_tagert)
    train_pool = Pool(X_train, y_train, cat_features=categorical_features_indices)
    test_pool = Pool(X_test,y_test, cat_features=categorical_features_indices)

    # パラメータの指定
    params = {
        'iterations' : trial.suggest_int('iterations', 50, 300),                         
        'depth' : trial.suggest_int('depth', 4, 10),                                       
        'learning_rate' : trial.suggest_loguniform('learning_rate', 0.01, 0.3),               
        'random_strength' :trial.suggest_int('random_strength', 0, 100),                       
        'bagging_temperature' :trial.suggest_loguniform('bagging_temperature', 0.01, 100.00), 
        'od_type': trial.suggest_categorical('od_type', ['IncToDec', 'Iter']),
        'od_wait' :trial.suggest_int('od_wait', 10, 50)
    }

    # 学習
    model = CatBoostClassifier(**params)
    # 5-Fold CV / Accuracy でモデルを評価する
    kf = StratifiedKFold(n_splits=5, shuffle=True, random_state=0)
    scores = cross_validate(model, X=X_train, y=y_train, cv=kf)

    # 最小化なので 1.0 からスコアを引く
    return scores['test_score'].mean()

if __name__ == '__main__':
    study = optuna.create_study()
    study.optimize(objective, n_trials=5)
    cat_best_param = study.best_params
    print(study.best_value)
    print(cat_best_param)


```

##調整したパロメータで再度精度を検証する

```
# 5-Fold CV / Accuracy でモデルを評価する
kf = StratifiedKFold(n_splits=5, shuffle=True, random_state=0)

rfc_best = RandomForestClassifier(**rfc_best_param)
print('RandomForestClassifier')
scores = cross_validate(rfc_best, X=train_feature, y=train_tagert, cv=kf)
print(f'mean:{scores["test_score"].mean()}, std:{scores["test_score"].std()}')

lr_best = LogisticRegression(**lr_best_param)
print('LogisticRegression')
scores = cross_validate(lr_best, X=train_feature, y=train_tagert, cv=kf)
print(f'mean:{scores["test_score"].mean()}, std:{scores["test_score"].std()}')

svc_best = SVC(**svc_best_param)
print('SVC')
scores = cross_validate(svc_best, X=train_feature, y=train_tagert, cv=kf)
print(f'mean:{scores["test_score"].mean()}, std:{scores["test_score"].std()}')

cat_best =CatBoostClassifier
print('CAT')
scores = cross_validate(cat, X=train_feature, y=train_tagert, cv=kf)
print(f'mean:{scores["test_score"].mean()}, std:{scores["test_score"].std()}')
```
結果として

```
RandomForestClassifier
mean:0.827152263628423, std:0.029935476082608138
LogisticRegression
mean:0.8294309818436642, std:0.03568888547665349
SVC
mean:0.826034945192669, std:0.03392425879847107
CAT
mean:0.8249241166088076, std:0.030217830226771592
```
となりました。
LogisticRegressionが最も精度が高いですが、std値が高いのもきになります。

```
# RandomForest
rfc_best = RandomForestClassifier(**rfc_best_param)
rfc_best.fit(train_feature, train_tagert)

# LogisticRegression
lr_best = LogisticRegression(**lr_best_param)
lr_best.fit(train_feature, train_tagert)

# SVC
svc_best = SVC(**svc_best_param)
svc_best.fit(train_feature, train_tagert)

#CatBoostClassifier
cat_best = CatBoostClassifier(**cat_best_param)
cat_best.fit(train_feature, train_tagert)


# それぞれ予測する
pred = {
    'rfc': rfc_best.predict(test_feature).astype(int),
    'lr': lr_best.predict(test_feature).astype(int),
    'svc': svc_best.predict(test_feature).astype(int),
    'cat': cat_best.predict(test_feature).astype(int)
}


# ファイル出力
for key, value in pred.items():
    pd.concat(
        [
            pd.DataFrame(test.PassengerId, columns=['PassengerId']).reset_index(drop=True),
            pd.DataFrame(value, columns=['Survived'])
       ],
        axis=1
    ).to_csv(f'output_{key}.csv', index=False)
```

#結果

Kaggleに提出すると、SVCが最も高い結果となりました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/588211/aba84522-ef84-f991-65ab-229af03b8ece.png)

その後ももろもろカラムや、ハイパーパラメーターを調整しましたが、現状このスコアが最も高い結果でした。
前処理のかけ方次第で、変わってくると思いますので、今後もいい方法が思いつけば、実際に操作しながら検証していきます。