📚 背景知識(読んでから問題へ)
📓
Kaggle コンペでは「自分でゼロから考える」よりも、公開されている上位ノートブックを素早く読み解き、自分の解法に組み込むスキルが非常に重要です。上位 Kaggler ほど、このサイクルが速いです。
🏅 公開ノートブックの品質基準
金メダル (100+ votes)
ほぼ必読。上位手法 or 完璧な EDA が含まれる。コンペ参加直後に精読する。
銀メダル (30-100 votes)
良質なアイデアが含まれる。EDA or 特定のモデル手法に特化していることが多い。
銅メダル (10-30 votes)
特定テクニックに特化。タイトルを見て関連するものだけ読む。
メダルなし (10未満 votes)
玉石混交。Public LB スコアを確認して取捨選択する。
🔍 上位ノートブックの読み方(5ステップ)
1
スコアと votes を確認
フォーク数・投票数・LB スコアを見て、読む価値があるかを 30 秒で判断する
フォーク数・投票数・LB スコアを見て、読む価値があるかを 30 秒で判断する
↓
2
EDA セクションを読む
データの分布・欠損値パターン・目的変数の特徴など、自分が気づかなかった洞察を探す
データの分布・欠損値パターン・目的変数の特徴など、自分が気づかなかった洞察を探す
↓
3
特徴量エンジニアリングを抽出
どんな特徴量を作っているか、なぜその変換をするかに着目する
どんな特徴量を作っているか、なぜその変換をするかに着目する
↓
4
モデルと CV スコアを確認
どのモデルを使い、CV はどう設計しているかを把握する
どのモデルを使い、CV はどう設計しているかを把握する
↓
5
自分のコードに組み込む(再実装)
直接コピーではなく、手法の理解をもとに自分のコードとして書き直す
直接コピーではなく、手法の理解をもとに自分のコードとして書き直す
🎯 問題
📝
シナリオ: Titanic コンペの上位ノートブックから抜粋したコードを読み解いてください。
import pandas as pd import numpy as np train = pd.read_csv('train.csv') test = pd.read_csv('test.csv') # ─── 特徴量エンジニアリング ─── # (A) 敬称の抽出 for df in [train, test]: df['Title'] = df['Name'].str.extract(r',\s*([^\.]+)\.') df['Title'] = df['Title'].replace(['Lady', 'Countess', 'Capt', 'Col', 'Don', 'Dr', 'Major', 'Rev', 'Sir', 'Jonkheer', 'Dona'], 'Rare') df['Title'] = df['Title'].map({'Mr': 0, 'Miss': 1, 'Mrs': 2, 'Master': 3, 'Rare': 4}) df['Title'] = df['Title'].fillna(0) # (B) 家族サイズの作成 for df in [train, test]: df['FamilySize'] = df['SibSp'] + df['Parch'] + 1 df['IsAlone'] = (df['FamilySize'] == 1).astype(int) # (C) 年齢の欠損値補完 for df in [train, test]: age_median = df.groupby(['Pclass', 'Title'])['Age'].transform('median') df['Age'] = df['Age'].fillna(age_median) # (D) 運賃のbin化 for df in [train, test]: df['FareBin'] = pd.qcut(df['Fare'].fillna(df['Fare'].median()), q=4, labels=False)
問題1: 正規表現 r',\s*([^\.]+)\.' を解説する
'Mr. John Smith' から何を取り出しているのか、正規表現の各要素を説明してください。
問題2: より良い年齢補完方法を実装する
コード (C) の補完より精度が高い、Sex も考慮した補完方法をコードで示してください。
問題3: FamilyGroup を作成する関数を実装する
FamilySize から FamilyGroup(1人=Alone, 2-4人=Small, 5人以上=Large)を作る関数を完成させてください。
def make_family_group(df): """FamilySize を3カテゴリに変換する""" # ここを実装してください pass for df in [train, test]: make_family_group(df)
💡 ヒント
ヒント1 — 方向性
正規表現の
([^\.]+) は「ドット以外の文字が1文字以上」を捕捉するグループです。Titanic の Name 列は「Smith, Mr. John」という形式なので、カンマの後の「Mr」を取り出しています。
ヒント2 — アプローチ
- 問題2:
groupby(['Pclass', 'Title'])の代わりにgroupby(['Pclass', 'Sex'])を使うと、より細かく年齢の中央値を計算できる - 問題3:
np.select()を使うと条件分岐を簡潔に書ける
ヒント3 — コード骨格
# 問題2(Sexも考慮) for df in [train, test]: age_median = df.groupby(['Pclass', 'Sex'])['Age'].transform('median') df['Age'] = df['Age'].fillna(age_median) # 問題3 def make_family_group(df): conditions = [ df['FamilySize'] == 1, (df['FamilySize'] >= 2) & (df['FamilySize'] <= 4), df['FamilySize'] >= 5 ] choices = ['Alone', 'Small', 'Large'] df['FamilyGroup'] = np.select(conditions, choices, default='Alone') return df
✅ 解答1: 正規表現の解説
r',\s*([^\.]+)\.'
,
カンマ
→
\s*
0個以上のスペース
→
(
[^\.]
+
)
キャプチャ: ドット以外を1文字以上
→
\.
ドット
| パターン要素 | 意味 | マッチ例 |
|---|---|---|
, |
カンマにそのままマッチ | "Braund, Mr. John" |
\s* |
0個以上のスペース(\s=空白、*=0以上) |
", Mr." のスペース |
([^\.]+ ) |
キャプチャグループ。[^\.]=ドット以外の文字、+=1文字以上 |
"Mr." → Mr を取り出す |
\. |
ドット(. はエスケープ必要) |
"Mr." のドット |
動作確認コード
実行可能
import re test_names = [ "Braund, Mr. Owen Harris", "Heikkinen, Miss. Laina", "Allen, Mrs. William Henry", "Palsson, Master. Gosta Leonard" ] for name in test_names: match = re.search(r',\s*([^\.]+)\.', name) print(f"{name:<40} → Title: {match.group(1) if match else None}") # 出力: # Braund, Mr. Owen Harris → Title: Mr # Heikkinen, Miss. Laina → Title: Miss # Allen, Mrs. William Henry → Title: Mrs # Palsson, Master. Gosta Leonard → Title: Master
💡
なぜ Title が有効な特徴量か: Title は性別・年齢・社会的地位と強く相関します。「Master」(未成年男子)は生存率が高く、「Mr」(成人男子)は最も低い。
Sex 単独より細かい情報を提供します。✅ 解答2: より良い年齢補完
改善版: Pclass × Sex の中央値で補完
推奨パターン
# ─── 方法3: Pclass × Sex の中央値で補完(改善版)─── for df in [train, test]: age_med = df.groupby(['Pclass', 'Sex'])['Age'].transform('median') df['Age'] = df['Age'].fillna(age_med) # ─── 方法4: 3段階フォールバック(さらに丁寧)─── for df in [train, test]: # まず細粒度 Pclass×Sex×Title で補完 age_fine = df.groupby(['Pclass', 'Sex', 'Title'])['Age'].transform('median') # 残った NaN を Pclass×Sex で補完 age_coarse = df.groupby(['Pclass', 'Sex'])['Age'].transform('median') # それでも残った NaN を全体中央値で補完 df['Age'] = (df['Age'] .fillna(age_fine) .fillna(age_coarse) .fillna(df['Age'].median())) # グループ別の年齢中央値を確認 print(train.groupby(['Pclass', 'Sex'])['Age'].median()) # Pclass Sex # 1 female 35.0 # 1 male 40.0 # 2 female 28.0 # 2 male 30.0 # 3 female 21.5 # 3 male 25.0
✅ 解答3: FamilyGroup の作成
make_family_group — np.select 使用
実装パターン
import numpy as np def make_family_group(df): """FamilySize を3カテゴリに変換する""" conditions = [ df['FamilySize'] == 1, # Alone (df['FamilySize'] >= 2) & (df['FamilySize'] <= 4), # Small df['FamilySize'] >= 5 # Large ] choices = ['Alone', 'Small', 'Large'] df['FamilyGroup'] = np.select(conditions, choices, default='Alone') return df # train, test に適用 for df in [train, test]: make_family_group(df) # 分布確認 print(train['FamilyGroup'].value_counts()) # グループ別生存率確認(なぜこの分け方が有効か) print("\n--- FamilyGroup 別生存率 ---") print(train.groupby('FamilyGroup')['Survived'].mean().sort_values(ascending=False)) # Small 0.58 ← 小家族は互いに助け合えて生存率高 # Alone 0.30 ← 孤独な旅行者は低め # Large 0.16 ← 大家族は脱出困難で最も低い
🪜 Step-by-Step: ノートブック活用サイクル
1
コンペ参加直後(Gold ノートブックを2〜3件精読)
Notebooks タブ → Votes 降順 → 上位ノートブックを精読する
Notebooks タブ → Votes 降順 → 上位ノートブックを精読する
↓
2
EDA ノートブックで洞察を吸収
データの隠れた構造・ドメイン知識を学ぶ
データの隠れた構造・ドメイン知識を学ぶ
↓
3
ベースラインをフォークして改良
動くコードをフォークし、特徴量を追加・変更しながら CV スコアを改善する
動くコードをフォークし、特徴量を追加・変更しながら CV スコアを改善する
↓
4
Discussion で追加ヒントを収集
「重要な特徴量」「リーク情報」スレッドを探す
「重要な特徴量」「リーク情報」スレッドを探す
↓
5
コンペ終了後: 上位 Writeup を精読
上位者の考え方・手法を次のコンペへの学習として記録する
上位者の考え方・手法を次のコンペへの学習として記録する
🛠️ よくある特徴量エンジニアリングパターン
パターン1: 文字列から情報抽出
Name列から敬称・長さを抽出。正規表現を活用。
パターン2: 組み合わせ特徴量
SibSp + Parch + 1 = FamilySize。合計・積・比率で新特徴量を作成。
パターン3: Bin化(離散化)
連続値をカテゴリに変換。pd.cut / pd.qcut を使用。
パターン4: 交互作用特徴量
Pclass + Sex の組み合わせを文字列連結で新しいカテゴリを作成。
パターン5: グループ集計
チケット番号ごとの人数など、groupby で集計した値を特徴量として追加。
パターン6: 欠損値を特徴量化
欠損しているかどうか自体が情報になる場合がある(Cabin の有無など)。
# 6パターンの実装例(Titanic) for df in [train, test]: # パターン1: 文字列から抽出 df['NameLength'] = df['Name'].str.len() # パターン2: 組み合わせ df['FamilySize'] = df['SibSp'] + df['Parch'] + 1 # パターン3: Bin化 df['AgeBin'] = pd.cut(df['Age'], bins=[0, 12, 18, 35, 60, 100], labels=['Child', 'Teen', 'Adult', 'Middle', 'Senior']) # パターン4: 交互作用 df['Pclass_Sex'] = df['Pclass'].astype(str) + '_' + df['Sex'] # パターン5: グループ集計 df['Ticket_Count'] = df.groupby('Ticket')['Ticket'].transform('count') # パターン6: 欠損値を特徴量化 df['Has_Cabin'] = df['Cabin'].notna().astype(int) # Cabin がある = 裕福 → 生存率高
📊 Titanic 生存率の可視化(特徴量の有効性確認)
Sex > Pclass > FamilyGroup の順で生存率への影響が大きい。特徴量の有効性をこの視点で確認する。
生存率の差 = 特徴量の有効性
グループ間の差が大きいほど、その特徴量はモデルにとって有益。
📋 Titanic で有効な特徴量(上位ノートブックより)
| 特徴量 | 重要度 | 作り方 | なぜ有効か |
|---|---|---|---|
| Sex | 最高 | そのまま or Label Encoding | 女性優先避難で生存率が大きく違う |
| Pclass | 高 | そのまま(序数) | 客室の位置・脱出しやすさと相関 |
| Title | 高 | Name から正規表現で抽出 | 性別・年齢・社会的地位を内包 |
| FamilySize | 中 | SibSp + Parch + 1 | 家族人数と生存率が非線形関係 |
| IsAlone | 中 | FamilySize == 1 | 単独旅行者の生存率が低い |
| AgeBin | 中 | Age を区間に分割 | 子供(Master)は優先避難 |
| Has_Cabin | 中 | Cabin.notna().astype(int) | Cabin 記録あり = 裕福 → 生存率高 |
| Ticket_Count | 低〜中 | groupby('Ticket').transform('count') | 同チケットグループ → 家族構成の代理 |
⚡ よくある誤解・ミス
| 誤解・ミス | なぜ起こるか | 正しい理解 |
|---|---|---|
| ノートブックをそのままコピーして提出 | 手間を省こうとする | 理解なしのコピーはスキルが身につかない。手法を学んで再実装する |
| votes が多ければ正しいと思い込む | 人気 = 正確と誤解 | コンペ固有の問題がある場合も。必ず CV スコアで自分の環境で検証する |
| EDA ノートブックを飛ばす | コーディングを急ぐ | EDA は特徴量のアイデア源。飛ばすと重要な洞察を見逃す |
| test データで transform を再計算する | データリークを意識しない | test に train の統計量を適用しないと情報漏洩。train で計算した値を test にマッピングする |
| 特徴量を増やすほど良いと思う | 多い = 良いという直感 | 不要な特徴量はノイズになる。CV スコアで取捨選択する |
🚀 次のステップ
- 発展: クロスバリデーションの設計(Stratified K-Fold / Group K-Fold の使い分け)
- 実践: Titanic コンペの上位ノートブックを1件精読し、自分のコードに1つ手法を追加してみる
- 次回予告 (Day 080): CV の設計戦略 — Titanic で Stratified K-Fold を使った正確なモデル評価