[Kaggle] チュートリアル(Titanic)コンペで上位10％に入れた話(スコア：0.80861)

初投稿です。

タイトルの通り、データサイエンティストの登竜門であるKaggleのチュートリアルコンペ(タイタニック号の生存者予測)でAccuracy 0.8を超え、参加者上位10％に入ることが出来たのでやったことをつらつらと書いていきます。
自分用の備忘録として残すことが主目的なので、タイタニックコンペに参加している人、もしくは本コンペに関する基本知識(※)があること前提の雑な書き方をしますが、悪しからず。。
※ どんなデータがある？データ構造はどうなっている？というような知識

## やったこと概要
- 最初から用意されている特徴量だけを使って、いくつかのモデルでCVを行い最も良かったモデルで予測(モデル1)
- EDAを行い、そこで得られた知見をもとに一部のレコードをルールベースで予測し、残りは新たに特徴量をいくつか生成した上でrandomforestで予測(モデル2)
- モデル2のものにさらに追加で特徴量を生成し、xgboostを使って予測(モデル3)
- モデル1～3の結果を単純にvoting(多数決)したものを最終予測としてsubmit

## 結果
<img width="712" alt="titanic.JPG" src="https://qiita-image-store.s3.amazonaws.com/0/264219/4851405e-5779-60f8-604f-fa172ced4f2f.jpeg">

Accuracy 0.8を超え、参加者(約11500人)の上位10％に入ることが出来ました。

## 得られた知見
- **trainとtestのデータ傾向がそれなりに異なるらしく、アンサンブル学習が効く(モデル1～3のAccuracyは全て0.75前後)**

### モデル1(特徴量追加なし、randomforest)詳細
限りなくチュートリアル的なやり方でrandomforestをかけて予測してみただけです。単体スコアは0.76555。
「限りなく」、という書き方をしていますが、一つだけ工夫をしています。
trainもtestもAgeに多数の欠損がありますが、これについては値を一律に埋めるとかえってノイズになりそうだったので、Ageに値があるデータ、ないデータに分割して

- Ageに値があるtrainデータで学習し、Ageに値があるtestデータを(特徴量にAgeを含めて)予測
- Ageに値がないtrainデータで学習し、Ageに値がないtestデータを(特徴量にAgeを含めず)予測

としています。(以降のモデルでもAgeに対する方針は基本的に同じです)

他は特に細かく解説する内容もないのでコードをベタ張りします。

```python

import pandas as pd
import csv as csv
import math
from numpy import *
from sklearn.svm import LinearSVC
from sklearn.ensemble import AdaBoostClassifier,ExtraTreesClassifier ,GradientBoostingClassifier, RandomForestClassifier
from sklearn.decomposition import TruncatedSVD
from sklearn import datasets
from sklearn.cross_validation import cross_val_score
from sklearn import linear_model

#Ageに値が入っているものはAgeも使って予測し、そうでないものはAgeを抜いて予測する
#Embarkedを説明変数に加える。ただし、nullになっているレコードはdropする
#Ageに値が入っているもの... Pclass,SibSp,Parch,Age,Gender,EmbarkedIntからSurvivedを予測する
#Ageに値が入っていないもの... Pclass,SibSp,Parch,Gender,EmbarkedIntからSurvivedを予測する

train_df = pd.read_csv("train.csv", header=0)
train_df = train_df[train_df["Embarked"].notnull()]
#print(train2_df)
# Convert "Sex" to be a dummy variable (female = 0, Male = 1)
train_df["Gender"] = train_df["Sex"].map({"female": 0, "male": 1}).astype(int)
train_df["EmbarkedInt"] = train_df["Embarked"].map({"S": 0, "C": 1, "Q": 2}).astype(int)
age1_df = train_df[train_df.Age >= 0]	#Ageあり
age0_df = train_df[train_df["Age"].isnull()]	#Ageなし

age1_features = age1_df[["Pclass","Age","SibSp","Parch","Gender","EmbarkedInt"]]         #特徴量のデータ 
age1_labels   = age1_df["Survived"]          #特徴量に対する正解データ
age0_features = age0_df[["Pclass","SibSp","Parch","Gender","EmbarkedInt"]]         #特徴量のデータ 
age0_labels   = age0_df["Survived"]          #特徴量に対する正解データ

# Load test data, Convert "Sex" to be a dummy variable
test_df = pd.read_csv("test.csv", header=0)
test_df = test_df[test_df["Embarked"].notnull()]
test_df["Gender"] = test_df["Sex"].map({"female": 0, "male": 1}).astype(int)
test_df["EmbarkedInt"] = test_df["Embarked"].map({"S": 0, "C": 1, "Q": 2}).astype(int)
age1_t_df = test_df[test_df.Age >= 0]	#Ageあり
age0_t_df = test_df[test_df["Age"].isnull()]	#Ageなし

# Copy test data's "PassengerId" column, and remove un-used columns
ids_age1 = age1_t_df["PassengerId"].values
ids_age0 = age0_t_df["PassengerId"].values
#test-data
age1_t_df = age1_t_df.drop(["Name", "Ticket", "Sex", "Fare", "Cabin", "Embarked", "PassengerId"], axis=1)
age0_t_df = age0_t_df.drop(["Name", "Ticket", "Age", "Sex", "Fare", "Cabin", "Embarked", "PassengerId"], axis=1)
#train-data
age1_df = age1_df.drop(["Name", "Ticket", "Sex", "Fare", "Cabin", "Embarked", "PassengerId"], axis=1)
age0_df = age0_df.drop(["Name", "Ticket", "Age", "Sex", "Fare", "Cabin", "Embarked", "PassengerId"], axis=1)

#5つぐらいのモデルを適当に試して一番CVスコアが良いモデルで予測
#age1は最もスコアの良かったGradientBoostingClassifierを使って予測
train_data_age1 = age1_df.values
test_data_age1 = age1_t_df.values
model_age1 = GradientBoostingClassifier(n_estimators=100)
output_age1 = model_age1.fit(train_data_age1[0::, 1::], train_data_age1[0::, 0]).predict(test_data_age1).astype(int)
#age0は最もスコアの良かったAdaBoostClassifierを使って予測
train_data_age0 = age0_df.values
test_data_age0 = age0_t_df.values
model_age0 = AdaBoostClassifier(n_estimators=50)
output_age0 = model_age0.fit(train_data_age0[0::, 1::], train_data_age0[0::, 0]).predict(test_data_age0).astype(int)

# export result to be "titanic_submit.csv"
submit_file = open("titanic_submit_r5.csv", "w", newline="")
file_object = csv.writer(submit_file)
file_object.writerow(["PassengerId", "Survived"])
file_object.writerows(zip(ids_age1, output_age1))
file_object.writerows(zip(ids_age0, output_age0))
submit_file.close()
```


### モデル2(ルールベース)詳細
参考リンク：https://www.kaggle.com/ldfreeman3/a-data-science-framework-to-achieve-99-accuracy
単体スコアは0.77511。

trainデータの各特徴量についてgroupbyを行い、値ごとの生存率を確認。

```python

train = train[train["Embarked"].notnull()]
test['Fare'].fillna(test['Fare'].median(), inplace = True)

print(train.groupby(["Pclass"]).agg(["count","mean"])["Survived"])
print(train.groupby(["Sex"]).agg(["count","mean"])["Survived"])

        count      mean
Pclass                 
1         216  0.629630
2         184  0.472826
3         491  0.242363


        count      mean
Sex                    
female    314  0.742038
male      577  0.188908
```

全部は載せませんが、特徴量の中ではSexとPclassが比較的生存と相関が強そう。
ということで、この2属性で再度groupbyを行い、値の組み合わせごとに生存率を確認。


```
count = train.groupby(["Sex","Pclass"],as_index=False).count().loc[:, ["Sex","Pclass","Survived"]].rename(columns={"Survived":"count"})
mean = train.groupby(["Sex","Pclass"],as_index=False).mean().loc[:, ["Sex","Pclass","Survived"]].rename(columns={"Survived":"ratio"})
rule = pd.merge(count, mean, on=["Sex","Pclass"])
print(rule)

      Sex  Pclass  count     ratio
0  female       1     94  0.968085
1  female       2     76  0.921053
2  female       3    144  0.500000
3    male       1    122  0.368852
4    male       2    108  0.157407
5    male       3    347  0.135447
```

Pclass = 1 or 2の女性の生存率と、Pclass = 2 or 3の男性の死亡率が極めて高いことがわかります。
今回はAccuracy 0.8を目安としているので、(trainとtestのデータの傾向が同じであれば)testデータに対して

- Pclass = 1 or 2の女性 => 生存
- Pclass = 2 or 3の男性 => 死亡

と一律で予測しても、少なくともこの条件を満たす人についてはAccuracy 0.8を超えられそうです。
それ以外(Pclass = 3の女性、Pclass = 1の男性)については複雑な条件で生死が分かれていそうなので、この条件に該当するtestデータのみrandomforestで予測していきます。

```python

passenger_id = list()
survived = list()

for i in range(len(test)):
    data = test.iloc[i, :]
    if data["Sex"]=="female" and data["Pclass"]<=2:
        passenger_id.append(data["PassengerId"])
        survived.append(1)
    elif data["Sex"]=="male" and data["Pclass"]>=2:
        passenger_id.append(data["PassengerId"])
        survived.append(0)
                
output_df = pd.concat([pd.Series(passenger_id),pd.Series(survived)],axis=1)
output_df.columns = ["PassengerId", "Survived"]
```

上記データに対するrandomforestでの予測ですが、新たに下記のような特徴量を生成しています。
- FamilySize: 同乗している家族人数(SibSpとParchの値に本人分の1を足したもの)
- IsAlone: 上記FamilySizeが1か否かのフラグ
- Title_Code: 敬称(Mr., Mrs.等)のコード
- Cabin_Flag: Cabinカラムに値が入っているか否かのフラグ
- FareBin_Code: FareをBinで適当に分割(今回は4分割)した後の各FareのBin位置
- Sex_Code: 性別のコード
- Embarked_Code: Embarkedのコード
- AgeBin_Code: AgeをBinで適当に分割(今回は4分割)した後の各AgeのBin位置(※Ageに値が入っているデータのみ)

```python

#学習データとテストデータを先に結合しておくことで同時にcleaningが可能
data_cleaner = [train, test]
#print(data_cleaner)

label = LabelEncoder()

for dataset in data_cleaner:
    #特徴量:同乗家族人数(自身も含む)を生成し追加
    dataset['FamilySize'] = dataset ['SibSp'] + dataset['Parch'] + 1    
    #特徴量:単独乗船フラグを生成し追加
    dataset['IsAlone'] = 1 #initialize to yes/1 is alone
    dataset['IsAlone'].loc[dataset['FamilySize'] > 1] = 0 # now update to no/0 if family size is greater than 1
    #特徴量:敬称を生成し追加
    dataset['Title'] = dataset['Name'].str.split(", ", expand=True)[1].str.split(".", expand=True)[0]
    dataset['Title_Code'] = label.fit_transform(dataset['Title'])
    #下記Cabin_Flagを追加
	#Cabinに値が入っているなら1,そうでないなら0
    dataset["Cabin_Flag"] = dataset["Cabin"].notnull().replace({True:1, False:0})

    #特徴量:Fareのbinをqcutで指定し追加(qcut:境界を自動的に設定し分類)
    dataset['FareBin'] = pd.qcut(dataset['Fare'], 4)
    dataset['FareBin_Code'] = label.fit_transform(dataset['FareBin'])
    
    dataset["Sex_Code"] = label.fit_transform(dataset['Sex'])
    dataset["Embarked_Code"] = label.fit_transform(dataset['Embarked'])

#特殊な敬称をクリーニング
stat_min = 10
title_names_train = (train['Title'].value_counts() < stat_min)
title_names_test = (test['Title'].value_counts() < stat_min)
train['Title'] = train['Title'].apply(lambda x: 'Misc' if title_names_train.loc[x] == True else x)
test['Title'] = test['Title'].apply(lambda x: 'Misc' if title_names_test.loc[x] == True else x)

train_age1 = train[train["Age"].notnull()]
test_age1 = test[test["Age"].notnull()]
data_cleaner_age1 = [train_age1, test_age1]
for dataset in data_cleaner_age1:
    dataset['AgeBin'] = pd.cut(dataset['Age'].astype(int), 5)
    dataset['AgeBin_Code'] = label.fit_transform(dataset['AgeBin'])

train_age1 = train_age1.loc[:, ["PassengerId", "AgeBin_Code"]]
test_age1 = test_age1.loc[:, ["PassengerId", "AgeBin_Code"]]

train = pd.merge(train, train_age1, on="PassengerId", how="left")
test = pd.merge(test, test_age1, on="PassengerId", how="left")
```

上記のようにデータを整形した後、GridSearchして最適なパラメータで予測。

```python

train_rf = train[((train["Sex"]=="female")&(train["Pclass"]==3))|((train["Sex"]=="male")&(train["Pclass"]==1))]
test_rf = test[((test["Sex"]=="female")&(test["Pclass"]==3))|((test["Sex"]=="male")&(test["Pclass"]==1))]
#print(test_rf.groupby(["Sex","Pclass"],as_index=False).mean())
train_age1_rf = train_rf[train_rf["Age"].notnull()]	#Ageあり
train_age0_rf = train_rf[train_rf["Age"].isnull()]	#Ageなし
test_age1_rf = test_rf[test_rf["Age"].notnull()]	#Ageあり
test_age0_rf = test_rf[test_rf["Age"].isnull()]	#Ageなし
ids_age1 = list(test_age1_rf["PassengerId"])
ids_age0 = list(test_age0_rf["PassengerId"])

#train-data
train_data_age1 = train_age1_rf.loc[:, ['Survived', 'Pclass', 'SibSp', 'Parch', 'FamilySize', 'IsAlone', 'Title_Code', 
                                      'Cabin_Flag', 'FareBin_Code', 'Sex_Code', 'Embarked_Code', 'AgeBin_Code']].values
#test-data
test_data_age1 = test_age1_rf.loc[:, ['Pclass', 'SibSp', 'Parch', 'FamilySize', 'IsAlone', 'Title_Code', 
                                      'Cabin_Flag', 'FareBin_Code', 'Sex_Code', 'Embarked_Code', 'AgeBin_Code']].values

train_data_age0 = train_age0_rf.loc[:, ['Survived', 'Pclass', 'SibSp', 'Parch', 'FamilySize', 'IsAlone', 'Title_Code', 
                                      'Cabin_Flag', 'FareBin_Code', 'Sex_Code', 'Embarked_Code']].values
test_data_age0 = test_age0_rf.loc[:, ['Pclass', 'SibSp', 'Parch', 'FamilySize', 'IsAlone', 'Title_Code', 
                                      'Cabin_Flag', 'FareBin_Code', 'Sex_Code', 'Embarked_Code']].values

xs_1 = train_data_age1[0::, 1::]
y_1 = train_data_age1[0::, 0]

xs_0 = train_data_age0[0::, 1::]
y_0 = train_data_age0[0::, 0]

xs_test1 = test_data_age1
xs_test0 = test_data_age0

parameters = {'max_depth': [2,4,6,8,10], 'n_estimators': [50,100,200]}

clf = grid_search.GridSearchCV(XGBClassifier(), parameters)
clf.fit(xs_1, y_1)

print(clf.best_score_)

clf_final = XGBClassifier(**clf.best_params_)
clf_final.fit(xs_1, y_1)
Y_pred1 = clf.predict(xs_test1).astype(int)

clf = grid_search.GridSearchCV(XGBClassifier(), parameters)
clf.fit(xs_0, y_0)

print(clf.best_score_)

clf_final = XGBClassifier(**clf.best_params_)
clf_final.fit(xs_0, y_0)
Y_pred0 = clf.predict(xs_test0).astype(int)

ids = pd.Series(ids_age1 + ids_age0)
pred = pd.Series(list(Y_pred1)+list(Y_pred0))
output_df2 = pd.concat([pd.Series(ids),pd.Series(pred)],axis=1)
output_df2.columns = ["PassengerId", "Survived"]
final_output = pd.concat([output_df,output_df2],axis=0).sort_values(by="PassengerId").reset_index(drop=True)

final_output.to_csv("predict_hybrid_r1.csv",index=False)
```


なお、上記リンクではもっときめ細やかに、丁寧にEDAをされているので参考にしてみてください。


### モデル3(特徴量追加、xgboost)詳細
参考リンク：https://www.kaggle.com/pliptor/divide-and-conquer-0-82296
単体スコアは0.71291。しかし、手元でのCVの結果は断トツで、0.98近い値をたたき出していました。

モデル2までで生成した特徴量(FamilySizeとか)に加えて、それなりに手の込んだ特徴量を生成しました。
詳細は上記リンクにありますが、簡単に言えば**「家族でくくれないグループの概念を表す特徴量」**です。
例えば、下記のようにそれぞれファミリーネームも異なり、FamilySizeも皆1なのにTicketとFareが全て同じ値、という場合です。

```python


     PassengerId  Survived  Pclass             Name   Sex   Age  SibSp  Parch  \
74            75         1       3    Bing, Mr. Lee  male  32.0      0      0   
169          170         0       3    Ling, Mr. Lee  male  28.0      0      0   
509          510         1       3   Lang, Mr. Fang  male  26.0      0      0   
643          644         1       3  Foo, Mr. Choong  male   NaN      0      0   
692          693         1       3     Lam, Mr. Ali  male   NaN      0      0   
826          827         0       3     Lam, Mr. Len  male   NaN      0      0   
838          839         1       3  Chip, Mr. Chang  male  32.0      0      0   

    Ticket     Fare Cabin Embarked  
74    1601  56.4958   NaN        S  
169   1601  56.4958   NaN        S  
509   1601  56.4958   NaN        S  
643   1601  56.4958   NaN        S  
692   1601  56.4958   NaN        S  
826   1601  56.4958   NaN        S  
838   1601  56.4958   NaN        S  
```

また、この7人と全く同じTicket、Fareの値を持つ人がtestデータにも存在しています。

```
    PassengerId  Pclass           Name   Sex  Age  SibSp  Parch Ticket  \
39          931       3  Hee, Mr. Ling  male  NaN      0      0   1601   

       Fare Cabin Embarked  
39  56.4958   NaN        S
```

この8人は明らかに家族ではありませんが、チケット番号が全て同じであることから、友人・あるいは知り合い同士であり、誰かがまとめて8枚チケットを購入して乗船しているであろうことが読み取れます。
乗船してから完全にばらばらで行動している可能性は低いでしょうから、沈没の際にも一緒に行動していたと考えて良さそうです。
そうすると、予測対象の人が所属していたグループ内の他の人の生死が、予測対象の人の生死にも大きく影響してくるのではないでしょうか。
現に、上記例は「Pclass = 3の男性」という、きわめて生存率の低いカテゴリであるにも関わらず、(testデータを除く)7人中5人が生存しています。
このグループの概念を組み入れられるかどうかで、カテゴリだけでは当てづらい予測対象を当てることが出来るようになると思われます。

さて、先ほどまでの特徴量だけではこのグループの概念は定義できません。
そこで、前段階として下記特徴量を生成します。

- Tfreq: Ticketの値の出現回数
- Ffreq: Fareの値の出現回数(結局使いませんでしたが…)
- Surname: ファミリーネーム

例えば上記ケースでは、Tfreqが8となりFamilySizeよりも大きくなるので、「家族以外の誰かと一緒にチケットを購入し乗船している」ことが読み取れます。
また、家族それぞれを識別するためにSurnameを導入します。

```python

train = pd.read_csv("train.csv")
test = pd.read_csv("test.csv", header=0)

#同グループの人がtrainとtest両方に分布している場合はmergeしないと拾えない
merge = pd.concat([train,test],axis=0).reset_index(drop=True)
merge.sample(10)

#Ticketの値とFareの値のnullレコード数を調べる
print("Ticket Null Records => {0}, Fare Null Records => {1}".format(sum(merge["Ticket"].isnull()),sum(merge["Fare"].isnull())))
#Fareの値が欠損しているレコードを確認する
print(merge[merge["Fare"].isnull()])

Ticket Null Records => 0, Fare Null Records => 1
       Age Cabin Embarked  Fare                Name  Parch  PassengerId  \
1043  60.5   NaN        S   NaN  Storey, Mr. Thomas      0         1044   

      Pclass   Sex  SibSp  Survived Ticket  
1043       3  male      0       NaN   3701  

#Ticketの値とFareの値の出現回数をそれぞれ計算し、列に追加
import collections as col
tfreq = list()
ffreq = list()
for i in range(len(merge)):
    if merge["Ticket"][i] == merge["Ticket"][i]:
        ticket = str(merge["Ticket"][i])
        tfreq.append(col.Counter(merge["Ticket"])[ticket])
    else:
        tfreq.append("")
    if merge["Fare"][i] == merge["Fare"][i]:
        fare = float(merge["Fare"][i])
        ffreq.append(col.Counter(merge["Fare"])[fare])
    else:
        ffreq.append("")

merge["Tfreq"] = tfreq
merge["Ffreq"] = ffreq

merge.sample(5)

#Nameの値のnullレコード数を調べる
print("Name Null Records => {0}".format(sum(merge["Name"].isnull())))

#Surnameを取得
import re
surname_list = list()
for i in range(len(merge)):
    surname_list.append(re.split('[,.]', merge["Name"][i])[0])

merge["Surname"] = surname_list
merge.sample(5)

#Tfreq, Ffreq, Surnameの出現回数を確認
#print(col.Counter(merge["Tfreq"]))
#print(col.Counter(merge["Ffreq"]))
#print(col.Counter(merge["Surname"]))
#TfreqとSurnameは出現回数の最大値が11なのに対し、Ffreqは35～60の値がある。
#これは運賃が同じだからといって必ずしも同じグループではないことを暗示している。
#そこで、Ffreqは使わないこととする。
print(merge.columns)
```

ここからは、上記特徴量を基にして下記特徴量を作成します。
- グループID(GID) 過学習になりそうだったので結局使わず
- グループの人数(GroupSize)
- グループ内で生き残った人の割合(SurvivedRateInGroup) データがあるものに限る

```python

#GIDの割り振り
merge["FamilySize"] = merge["SibSp"]+merge["Parch"]+1
name_size_key = list()
for i in range(len(merge)):
    name_size_key.append(str(merge["Surname"][i])+"_"+str(merge["FamilySize"][i]))
name_size_counter = col.Counter(name_size_key)
#GID列とgroupsize列、さらにsurvived_in_group列の用意
merge["GID"] = [-1]*len(merge) #16列目
merge["GroupSize"] = [1]*len(merge) #17列目
merge["SurvivedRateInGroup"] = [0]*len(merge) #18列目
gid = 1
for key,value in name_size_counter.items():
    surname = str(key.split("_")[0])
    familysize = int(key.split("_")[1])
    if familysize >= 2: #家族がいる
        if familysize >= value: #同姓の人がいない
            data_part = merge[(merge["Surname"]==surname)&(merge["FamilySize"]==familysize)]
            tfreq = np.unique(data_part["Tfreq"])[0]
            #1. 家族だけで乗船している
            if tfreq <= value:
                #該当するレコードのindexを抽出
                family_index = list(data_part.index)
                #家族内の生存率を計算(結果があるもののみ)
                survived_rate = np.sum(np.array(data_part[data_part["Survived"].notnull()]["Survived"]))/len(data_part[data_part["Survived"].notnull()])
                #データがなければsurvived_rateは-1とする
                if survived_rate != survived_rate:
                    survived_rate = -1
                for i in family_index:
                    merge.iloc[i, 16] = gid
                    merge.iloc[i, 17] = value
                    merge.iloc[i, 18] = survived_rate
                #print("{0} family => GID {1}, Group Size {2}, Survived Rate {3}".format(surname,gid,value,survived_rate))
                gid += 1
            #2. 家族以外(お手伝いさんとか？)と一緒に乗船している
            else:
                #チケット番号を参照
                ticket_number = list(set(list(data_part["Ticket"])))[0]
                #デバッグ
                if len(np.unique(data_part["Ticket"])) >= 2:
                    print("Ticket separated: Family Name {0}, Family Size {1}, Ticket Pattern {2}".format(surname,familysize,len(data_part["Ticket"])))
                #該当するレコードのindexを抽出
                data_group = merge[merge["Ticket"]==ticket_number]
                group_index = list(data_group.index)
                survived_rate = np.sum(np.array(data_group[data_group["Survived"].notnull()]["Survived"]))/len(data_group[data_group["Survived"].notnull()])
                #データがなければsurvived_rateは-1とする
                if survived_rate != survived_rate:
                    survived_rate = -1
                for i in group_index:
                    merge.iloc[i, 16] = gid
                    merge.iloc[i, 17] = len(group_index)
                    merge.iloc[i, 18] = survived_rate
                #print("Ticket {0} group => GID {1}, Group Size {2}, Survived Rate {3}".format(ticket_number,gid,len(group_index),survived_rate))
                gid += 1
        else: #同姓の人が他に存在する
            data_part = merge[(merge["Surname"]==surname)&(merge["FamilySize"]==familysize)]
            #チケット番号ごとにデータを分割してそれぞれにGIDを振る
            ticket_number_list = list(set(list(data_part["Ticket"])))
            for tn in ticket_number_list:
                data_part2 = data_part[data_part["Ticket"]==tn]
                value = len(data_part2)
                tfreq = np.unique(data_part2["Tfreq"])[0]
                if value == 1: #分割した結果、一人になった場合
                    if tfreq == 1: #同じチケット番号で乗船している家族以外の人がいない
                        person_index = int(data_part2.index[0])
                        survived_rate = -1
                        if int(data_part2["PassengerId"]) < 892:
                            survived_rate = int(data_part2["Survived"])
                        merge.iloc[person_index, 16] = gid
                        merge.iloc[person_index, 17] = value
                        merge.iloc[person_index, 18] = survived_rate
                        #print("Single Surname {0} => GID {1}, Group Size {2}, Survived Rate {3}".format(surname,gid,value,survived_rate))
                        gid += 1
                    else:
                        #該当するレコードのindexを抽出
                        data_group = merge[merge["Ticket"]==tn]
                        group_index = list(data_group.index)
                        survived_rate = np.sum(np.array(data_group[data_group["Survived"].notnull()]["Survived"]))/len(data_group[data_group["Survived"].notnull()])
                        #データがなければsurvived_rateは-1とする
                        if survived_rate != survived_rate:
                            survived_rate = -1
                        for i in group_index:
                            merge.iloc[i, 16] = gid
                            merge.iloc[i, 17] = len(group_index)
                            merge.iloc[i, 18] = survived_rate
                        #print("Ticket {0} group => GID {1}, Group Size {2}, Survived Rate {3}".format(tn,gid,len(group_index),survived_rate))
                        gid += 1
                else:
                    #1. 家族だけで乗船している
                    if tfreq <= value:
                        #該当するレコードのindexを抽出
                        family_index = list(data_part2.index)
                        #家族内の生存率を計算(結果があるもののみ)
                        survived_rate = np.sum(np.array(data_part2[data_part2["Survived"].notnull()]["Survived"]))/len(data_part2[data_part2["Survived"].notnull()])
                        #データがなければsurvived_rateは-1とする
                        if survived_rate != survived_rate:
                            survived_rate = -1
                        for i in family_index:
                            merge.iloc[i, 16] = gid
                            merge.iloc[i, 17] = value
                            merge.iloc[i, 18] = survived_rate
                        #print("{0} family => GID {1}, Group Size {2}, Survived Rate {3}".format(surname,gid,value,survived_rate))
                        gid += 1
                    #2. 家族以外(お手伝いさんとか？)と一緒に乗船している
                    else:
                        #該当するレコードのindexを抽出
                        data_group = merge[merge["Ticket"]==tn]
                        group_index = list(data_group.index)
                        survived_rate = np.sum(np.array(data_group[data_group["Survived"].notnull()]["Survived"]))/len(data_group[data_group["Survived"].notnull()])
                        #データがなければsurvived_rateは-1とする
                        if survived_rate != survived_rate:
                            survived_rate = -1
                        for i in group_index:
                            merge.iloc[i, 16] = gid
                            merge.iloc[i, 17] = len(group_index)
                            merge.iloc[i, 18] = survived_rate
                        #print("Ticket {0} group => GID {1}, Group Size {2}, Survived Rate {3}".format(tn,gid,len(group_index),survived_rate))
                        gid += 1     
    else: #乗船している家族がいない
        data_part = merge[(merge["Surname"]==surname)&(merge["FamilySize"]==familysize)]
        if value == 1: #同姓でfamilysize==1の人もいない                
            tfreq = int(data_part["Tfreq"])
            if tfreq == 1: #同じチケット番号で乗船している家族以外の人もいない
                person_index = int(data_part.index[0])
                survived_rate = -1
                if int(data_part["PassengerId"]) < 892:
                    survived_rate = float(data_part["Survived"])
                merge.iloc[person_index, 16] = gid
                merge.iloc[person_index, 17] = value
                merge.iloc[person_index, 18] = survived_rate
                #print("Single Surname {0} => GID {1}, Group Size {2}, Survived Rate {3}".format(surname,gid,value,survived_rate))
                gid += 1
            else:
                #print(surname)
                #チケット番号を参照
                ticket_number = str(data_part["Ticket"].values[0])
                #print(ticket_number)
                #該当するレコードのindexを抽出
                data_group = merge[merge["Ticket"]==ticket_number]
                group_index = list(data_group.index)
                #print(group_index)
                survived_rate = np.sum(np.array(data_group[data_group["Survived"].notnull()]["Survived"]))/len(data_group[data_group["Survived"].notnull()])
                #データがなければsurvived_rateは-1とする
                if survived_rate != survived_rate:
                    survived_rate = -1
                for i in group_index:
                    merge.iloc[i, 16] = gid
                    merge.iloc[i, 17] = len(group_index)
                    merge.iloc[i, 18] = survived_rate
                #print("Ticket {0} group => GID {1}, Group Size {2}, Survived Rate {3}".format(ticket_number,gid,len(group_index),survived_rate))
                gid += 1
        else: #同姓でfamilysize==1の人がいる
            #チケット番号ごとにデータを分割してそれぞれにGIDを振る
            ticket_number_list = list(set(list(data_part["Ticket"])))
            for tn in ticket_number_list:
                data_part2 = data_part[data_part["Ticket"]==tn]
                value = len(data_part2)
                tfreq = np.unique(data_part2["Tfreq"])[0]
                if tfreq == 1: #同じチケット番号で乗船している家族以外の人がいない
                    person_index = int(data_part2.index[0])
                    survived_rate = -1
                    if int(data_part2["PassengerId"]) < 892:
                        survived_rate = float(data_part2["Survived"])
                    merge.iloc[person_index, 16] = gid
                    merge.iloc[person_index, 17] = value
                    merge.iloc[person_index, 18] = survived_rate
                    #print("Single Surname {0} => GID {1}, Group Size {2}, Survived Rate {3}".format(surname,gid,value,survived_rate))
                    gid += 1
                else:
                    #該当するレコードのindexを抽出
                    data_group = merge[merge["Ticket"]==tn]
                    group_index = list(data_group.index)
                    survived_rate = np.sum(np.array(data_group[data_group["Survived"].notnull()]["Survived"]))/len(data_group[data_group["Survived"].notnull()])
                    #データがなければsurvived_rateは-1とする
                    if survived_rate != survived_rate:
                        survived_rate = -1
                    for i in group_index:
                        merge.iloc[i, 16] = gid
                        merge.iloc[i, 17] = len(group_index)
                        merge.iloc[i, 18] = survived_rate
                    #print("Ticket {0} group => GID {1}, Group Size {2}, Survived Rate {3}".format(tn,gid,len(group_index),survived_rate))
                    gid += 1
```

かなり複雑な条件分岐で書くのが大変でした。。

これで特徴量の生成は完了したので、学習・予測をさせます。
使う特徴量は以下。
- Pclass
- GroupSize
- SurvivedRateInGroup
- Sex_Code
- AgeBin_Code

この特徴量選択が最適かどうかはわかりません。。
が、GroupSizeを含めるなら少なくともFamilySize,SibSp,Parchあたりは外した方が良いはずです。(GroupSizeと相関が高いため)

```python

train_age1 = train[train["Age"].notnull()]
train_age0 = train[train["Age"].isnull()]
test_age1 = test[test["Age"].notnull()]
test_age0 = test[test["Age"].isnull()]

ids_age1 = test_age1["PassengerId"].values
ids_age0 = test_age0["PassengerId"].values

#train-data
train_data_age1 = train_age1.loc[:, ['Survived', 'Pclass', 'GroupSize', 'SurvivedRateInGroup', 
                                     'Sex_Code', 'AgeBin_Code']].values
train_data_age0 = train_age0.loc[:, ['Survived', 'Pclass', 'GroupSize', 'SurvivedRateInGroup', 
                                     'Sex_Code']].values
#test-data
test_data_age1 = test_age1.loc[:, ['Pclass', 'GroupSize', 'SurvivedRateInGroup',
                                   'Sex_Code', 'AgeBin_Code']].values
test_data_age0 = test_age0.loc[:, ['Pclass', 'GroupSize', 'SurvivedRateInGroup',
                                   'Sex_Code',]].values

xs_1 = train_data_age1[0::, 1::]
y_1 = train_data_age1[0::, 0]

xs_0 = train_data_age0[0::, 1::]
y_0 = train_data_age0[0::, 0]

xs_test1 = test_data_age1
xs_test0 = test_data_age0

parameters = {'max_depth': [2,4,6,8,10], 'n_estimators': [50,100,200]}
clf = grid_search.GridSearchCV(XGBClassifier(), parameters, verbose=1)
clf.fit(xs_1, y_1)
print(clf.best_params_)
print(clf.best_score_)
clf_final = XGBClassifier(**clf.best_params_)
clf_final.fit(xs_1, y_1)
Y_pred1 = clf.predict(xs_test1).astype(int)

clf = grid_search.GridSearchCV(XGBClassifier(), parameters, verbose=1)
clf.fit(xs_0, y_0)
print(clf.best_params_)
print(clf.best_score_)
clf_final = XGBClassifier(**clf.best_params_)
clf_final.fit(xs_0, y_0)
Y_pred0 = clf.predict(xs_test0).astype(int)

# export result to be "titanic_submit.csv"
submit_file = open("predict_xgboost_r6.csv", "w", newline="")
file_object = csv.writer(submit_file)
file_object.writerow(["PassengerId", "Survived"])
file_object.writerows(zip(ids_age1, Y_pred1))
file_object.writerows(zip(ids_age0, Y_pred0))
submit_file.close()
```

CVのスコアは大変高かったので期待しましたが、スコアは3つのモデルの中で最低。。
ほぼ間違いなく過学習しているんですが、何が原因かもよくわからないので改善は断念。

### Voting
解説することはなく、モデル1～3の予測結果を多数決取っただけです。
結果的にはこれが一番効果ありました。

## 所感
- EDAをやるだけでもいろんなヒントが得られる
- (このコンペに関しては)学習器の違いはほとんど影響しなかった
- アンサンブル大事。単純なvotingなら実装コスト殆どないし、方向性の違うモデルをいくつか作って試してみるべき。

次はHome Credit Default Riskコンペに挑もうと思います。
kaggleでは初めてのOngoingコンペ参加なので楽しみ。銅メダルを取りたい！