Target Encoding(Leave One Out)を使って分類器の精度向上させました。Kaggle Titanicで0.79665のScoreで約2500位/55000人程度なので、そこそこいいスコアかと思います。
Target Encodingを勉強したときは、どんなときに使うんだろうとぼんやりと考えていましたが、意外とすぐに使う機会ができました。Greedy Target Statisticsはリークしたので、Leave one-out Target Statisticsを使いました。
※Target Encodingは記事「カテゴリ変数系特徴量の前処理(scikit-learnとcategory_encoders)」に書いています。
前提: Target Encoding
手前味噌ですが、私の記事に簡単に書いていて、以下の記事にはもっと詳しい内容があります。
チャレンジ内容: Titanic
Kaggle TitanicでTarget Encoding を使いました。タイタニック号乗客の生死予測です。
特徴量
特徴量詳細
与えられた特徴量には、以下のものがあります。
列 | 変数 | 内容 | Value情報など |
---|---|---|---|
1 | PassengerId | 乗客IDでユニークキー | |
2 | Survival | 生死(目的変数) | 0 = No(死亡), 1 = Yes(生存)。テストデータには存在しない列 |
3 | Pclass | 席等級 | 1 = 1st(Upper), 2 = 2nd(Middle), 3 = 3rd(Lower) |
4 | Name | 姓名 | |
5 | Sex | 性 | male/female |
6 | Age | Age in years | |
7 | Sibsp | 同乗した姉妹兄弟と配偶者数 | |
8 | Parch | 同乗した両親子どもの数 | |
9 | Ticket | チケット番号 | |
10 | Fare | 料金 | 親子で2席買ったら合計料金になるっぽい |
11 | Cabin | キャビン番号 | 欠損値多い |
12 | Embarked | 乗船港 | C = Cherbourg, Q = Queenstown, S = Southampton |
新たな特徴量とTarget Encoding
そもそも、以下の kernel に触発されて自分なりに特徴量エンジニアリングをしてみた結果、Target Encodingに行き着きました。
家族や友人などのグループは生死を道連れにする、という仮説を元にグループの特徴量を作り、グループに対してTarget Encodingを使います。グループ作成の基準は以下の2点。
- 同じ Ticket(婚約者でまだ姓が違う場合や友人など)
- 姓と料金が同じ(姓だけだと偶然の場合も多いので)
※もっといいグルーピングの基準があるかもしれませんが、時間をかけて精査していません。
プログラム
全般的に、結構雑な処理をしています。プログラム全体はこちらのカーネル。
1. パッケージとファイル読込
最初はパッケージとファイル読込です。
import pandas as pd
import matplotlib.pyplot as plt
import category_encoders as ce
from sklearn.preprocessing import OrdinalEncoder
from sklearn.ensemble import RandomForestClassifier
train_csv = pd.read_csv("/kaggle/input/titanic/train.csv")
test_csv = pd.read_csv("/kaggle/input/titanic/test.csv")
訓練データとテストデータの内容(info
関数結果)を参考に載せておきます。
RangeIndex: 891 entries, 0 to 890
Data columns (total 12 columns):
# Column Non-Null Count Dtype
--- ------ -------------- -----
0 PassengerId 891 non-null int64
1 Survived 891 non-null int64
2 Pclass 891 non-null int64
3 Name 891 non-null object
4 Sex 891 non-null object
5 Age 714 non-null float64
6 SibSp 891 non-null int64
7 Parch 891 non-null int64
8 Ticket 891 non-null object
9 Fare 891 non-null float64
10 Cabin 204 non-null object
11 Embarked 889 non-null object
dtypes: float64(2), int64(5), object(5)
memory usage: 83.7+ KB
RangeIndex: 418 entries, 0 to 417
Data columns (total 11 columns):
# Column Non-Null Count Dtype
--- ------ -------------- -----
0 PassengerId 418 non-null int64
1 Pclass 418 non-null int64
2 Name 418 non-null object
3 Sex 418 non-null object
4 Age 332 non-null float64
5 SibSp 418 non-null int64
6 Parch 418 non-null int64
7 Ticket 418 non-null object
8 Fare 417 non-null float64
9 Cabin 91 non-null object
10 Embarked 418 non-null object
dtypes: float64(2), int64(4), object(5)
memory usage: 36.0+ KB
2. 特徴量生成
2.1. グループキーとなる特徴量生成
訓練データとテストデータを結合して、以下の2処理を行います。本当は訓練データとテストデータの結合って嫌いなのですが、今回は無視しています。
-
Name
から姓の抽出 - 姓と
Fare
の結合(次で処理しやすくするため)
both = pd.concat([train_csv, test_csv], ignore_index=True)
# Last Name作成
both['Last_Name'] = both['Name'].apply(lambda x: str.split(x, ",")[0])
# FareがNullのレコードは文字列"nan"となる
both['Name_Fare'] = both['Last_Name'] + both['Fare'].astype('str')
2.2. グルーピング
同じTicket
でグルーピング→同じ「姓」とFare
でグルーピングを再帰処理で繰り返します。今回、一番コーディングが面倒だった処理です(筆者のスキル不足が原因)。新しくGroup
列に数値を連番で振っていきます。
# 姓とFareでグルーピング
def process_name(i, name_fare):
tickets = both.loc[(both['Name_Fare'] == name_fare) & (both['Group'].isnull()),'Ticket'].unique().tolist()
both.loc[(both['Name_Fare'] == name_fare) & (both['Group'].isnull()), 'Group'] = i
for ticket in tickets:
process_ticket(i, ticket)
# チケットでグルーピング
def process_ticket(i, ticket):
name_fares = both.loc[(both['Ticket'] == ticket) & (both['Group'].isnull()),'Name_Fare'].unique().tolist()
both.loc[(both['Ticket'] == ticket) & (both['Group'].isnull()), 'Group'] = i
for name_fare in name_fares:
process_name(i, name_fare)
both['Group'] = None
# チケットでグルーピング(再帰処理で姓とFare、チケットでのグルーピングを繰り返す)
[process_ticket(i, ticket) for i, ticket in enumerate(both['Ticket'].unique().tolist())]
2.3. 訓練とテストに分離
再び、訓練とテストデータセットに分離します。
train = both[:891].copy()
test = both[891:].copy()
2.4. グループのカウント
Count Encoderを使ってグループ内人数のカウントを実施。Group_Count
という列に値を入れます。
テストデータセットには、自分自身のデータ数をカウントとして+1します。これは、例えば訓練データに「山田グループ」があり2人いたとして、テストデータに同じく「山田グループ」に所属する人がいたら(何人いても)テストデータを3と設定しています。訓練データとテストデータをまとめてカウントエンコーディングすればいいような気もしますが、そうすると訓練内容にも大きく影響与えそうなのでやっていません。
count_encoder = ce.CountEncoder(cols=['Group'], handle_unknown=0, return_df=True)
train['Group_Count'] = count_encoder.fit_transform(train['Group'])
test['Group_Count'] = count_encoder.transform(test['Group']).astype('int') + 1 #test自身のデータを1追加(本当はもっとあるかもしれないが雑に計算)
2.5. Target Encoding
Target Encoding としてLeave One Outを使っています。自分自身を抜いて目的変数Survived
の平均の特徴量をGroup_Target
に入れます。
te = ce.LeaveOneOutEncoder(cols=['Group'])
train['Group_Target'] = te.fit_transform(train['Group'], train['Survived'])
test['Group_Target'] = te.transform(test['Group'])
2.6. Target Encoding 微調整
訓練データに1人のみいたグループの場合、その1人の生死(Survived
の値)をそのままテストデータのGroup_Target
に入れます。
また、訓練データに1人もいなかったグループの場合は、平均生存率である0.38を入れておきます。
for index, row in test.query('Group_Count == 2').iterrows():
test.at[index, 'Group_Target'] = train[train['Group']==row['Group']]['Survived']
test.loc[test['Group_Count'] == 0, 'Group_Target'] = 0.38
2.7. 他カテゴリ変数のEncoding
Pclass
(席等級)とSex
(性)をEncodingします。
oe = OrdinalEncoder()
train.loc[:,['PclassEncoded', 'SexEncoded']] = oe.fit_transform(train[['Pclass', 'Sex']])
test.loc[:,['PclassEncoded', 'SexEncoded']] = oe.transform(test[['Pclass', 'Sex']])
3. グルーピングの確認
グルーピング結果をグラフで確認します。
_, axes = plt.subplots(nrows=1, ncols=3, figsize=(15, 4))
train['Group_Count'].value_counts().plot.pie(ax=axes[0], title='Group Count Pie Chart', autopct="%1.1f%%")
train['Group_Count'].plot.hist(ax=axes[1], title='Group Count Histgram')
train.query('Group_Count > 2')['Group_Target'].plot.hist(ax=axes[2], title='Survival Rate Histgram')
plt.show()
- 左(円グラフ): グループ内人数(
Group_Count
)の割合 - 中央(ヒストグラム): グループ内人数(
Group_Count
)のヒストグラム - 右(ヒストグラム): 3人以上のグループの生存率ヒストグラム
訓練データの半分弱がグループ(3人以上)です。右のグラフ(3人以上のグループの生存率ヒストグラム)では、ある程度左右に偏ってくれています。
2人グループを含めなかった理由は、2人グループはLeave One Outなので必ず0(死亡)か1(生存)になるからです。
グループ内人数で個別に確認しています。
_, ax = plt.subplots(figsize=(12, 10))
train.query('Group_Count > 2').Group_Target.hist(ax=ax, by=train['Group_Count'], range=(0, 1))
plt.show()
3 or 4人グループでは少しバラけています。一方で5人以上のグループでは、道連れ死パターンが多いですね。グループ人数情報と併せて決定木系の分類器であれば、判断してくれるのでは、と期待します。
4. 訓練実施
特徴量で使いやすいものだけピックアップして、使いやすいランダムフォレストで訓練します。Scoreは0.853でした。
※Greedy Target Statisticsを使ったときはリークがひどく、0.98くらいでした(ウル覚え)・・・
features = ["Group_Count", "Group_Target", 'PclassEncoded', 'SexEncoded', "SibSp", "Parch"]
X = train[features]
y = train["Survived"].astype('int')
tmp_sr = test['PassengerId']
X_test = test[features]
model = RandomForestClassifier(n_estimators=100, max_depth=5, random_state=1)
model.fit(X, y)
print(model.score(X, y))
特徴量の重要性を確認。
importances = pd.DataFrame({'Importance': model.feature_importances_}, index=X_test.columns)
importances.sort_values('Importance', ascending=False).head(10).sort_values('Importance', ascending=True).plot.barh(grid=True)
Group_Trarget
はいい感じの高さになってくれています(2人以上のグループは多くないのでこんなもの)。
5. 予測
モデルを使って予測して、CSVファイルに出力します。
predictions = model.predict(X_test)
output = pd.DataFrame({'PassengerId': tmp_sr, 'Survived': predictions})
output.to_csv('my_submission.csv', index=False)