📚 背景知識(読んでから問題へ)
なぜカテゴリ変数を数値に変換するのか?
機械学習モデルは数値しか扱えないため、文字列のカテゴリ変数を数値に変換する必要があります。House Prices データには Neighborhood(住宅地区名)や ExterQual(外観品質)など多くのカテゴリ変数が含まれています。
主要なエンコーディング手法一覧
| 手法 | 概要 | 向いている場面 | 次元変化 |
|---|---|---|---|
| Label Encoding | カテゴリを整数(0,1,2...)に置換 | 順序があるカテゴリ(Ex>Gd>TA>Fa) | 変化なし(1列→1列) |
| One-Hot Encoding | カテゴリ数のバイナリ列に展開 | 順序のないカテゴリ・線形モデル | 増加(1列→n列) |
| Target Encoding | カテゴリ別の目的変数平均で置換 | 高カーディナリティ・GBDT | 変化なし(1列→1列) |
| Frequency Encoding | カテゴリの出現頻度で置換 | 高カーディナリティの簡易処理 | 変化なし(1列→1列) |
One-Hot Encoding の落とし穴
線形モデルでは過学習のリスク増大。GBDTでは One-Hot より Label の方が速く精度も同等以上のことが多い。
スムージング付き Target Encoding の公式
スムージング付き Target Encoding(Bayesian Target Encoding)
| 記号 | 意味 |
|---|---|
nc | カテゴリ c のサンプル数 |
meanc | カテゴリ c 内の目的変数の平均 |
meanglobal | 全体の目的変数の平均 |
k | スムージングパラメータ(小さいほどカテゴリ平均を重視、大きいほど全体平均に引き寄せる。デフォルト=10) |
📊 カテゴリ変数のカーディナリティ(エンコード選択の起点)
House Prices 主要カテゴリ変数のユニーク値数。これを見てエンコード手法を決める。
ユニーク値数(カーディナリティ)— バーの長さがユニーク種類数を表す
経験則: カーディナリティ ≤ 10 → One-Hot または Label | カーディナリティ > 10 → Target または Frequency
⚖️ エンコーディング手法 詳細比較
カテゴリを整数値にマッピング。順序情報が意味を持つカテゴリ(品質系)に適する。
sklearn の LabelEncoder はアルファベット順に割り当てるため、順序を保証したい場合は map() を使うこと。
# 正しい実装(順序を明示) quality_map = {'Ex': 4, 'Gd': 3, 'TA': 2, 'Fa': 1, 'Po': 0} df['ExterQual_enc'] = df['ExterQual'].map(quality_map)
各カテゴリを独立したバイナリ列に展開。順序のないカテゴリ・線形モデルに最適。
drop_first=True でダミー変数トラップ(多重共線性)を防ぐ。
# drop_first=True で 1列削除(線形モデル必須) ohe = pd.get_dummies(df['MSZoning'], prefix='MSZ', drop_first=True) df = pd.concat([df, ohe], axis=1).drop('MSZoning', axis=1)
カテゴリ別の目的変数平均で置換。高カーディナリティ・GBDTに最も有効。
スムージングで少数カテゴリの過学習を防ぐ。必ずCV内でフィットしてリークを避ける。
def target_encode_smooth(df, col, target, k=10): global_mean = df[target].mean() stats = df.groupby(col)[target].agg(['mean', 'count']) smooth = (stats['count'] * stats['mean'] + k * global_mean) / (stats['count'] + k) return df[col].map(smooth)
カテゴリの出現頻度(件数)で置換。情報量は薄いが安全・高速。
「よく登場するカテゴリ = 一般的」という暗黙の意味を数値化できる場合に有効。
freq_map = df['Neighborhood'].value_counts() df['Neighborhood_freq'] = df['Neighborhood'].map(freq_map)
🎯 Target Encoding スムージング効果の視覚化
k=10 でのスムージング効果。バーの長さは TE値(全体平均=180,000 を基準に正規化)。赤い縦線が全体平均。
Blueste は生平均 70,000 だが、n=2 しかなく信頼性が低い。スムージングで全体平均 180,000 に大きく引き寄せられ 161,666 になる(過学習防止)。
スムージング強度とサンプル数の関係(k=10の場合)
n が少ないほどカテゴリ平均への重みが小さく(全体平均に引き寄せられる)、n が大きくなるほど実際のカテゴリ平均を重視する。
🗂️ データスキーマ(House Prices スタイル)
| カラム名 | 型 | 説明 | ユニーク数 | 推奨エンコード |
|---|---|---|---|---|
Neighborhood | object | 住宅地区名 | 25 | Target Encoding |
MSZoning | object | ゾーニング区分 | 5 | One-Hot Encoding |
ExterQual | object | 外観品質 (Ex/Gd/TA/Fa/Po) | 4 (順序あり) | Label Encoding (map) |
SalePrice | float64 | 販売価格(目的変数) | — | — |
🧩 問題
タスク1 — カーディナリティの確認:
各カテゴリ変数のユニーク値の数・最多頻度カテゴリ・欠損数を一覧表示する。
タスク2 — Label Encoding(順序あり):
ExterQual(外観品質: Ex > Gd > TA > Fa > Po)を順序情報を保ったまま数値に変換せよ。
pd.Categorical と map の 2通りの実装を比較すること。
タスク3 — One-Hot Encoding:
MSZoning(ゾーニング区分)に pd.get_dummies() を適用し、ダミー変数トラップを避ける設定も確認せよ。
タスク4 — Target Encoding(スムージング付き):
Neighborhood カラムに対してスムージング付きの Target Encoding を実装し、カテゴリ別エンコード値を表示せよ。
タスク5 — Frequency Encoding:
全カテゴリ変数に Frequency Encoding を一括適用する関数を実装せよ。
期待する出力形式
=== タスク1: カーディナリティ確認 === MSZoning : 5種類 | 最多: RL(1151) | 欠損: 0 Neighborhood: 25種類 | 最多: NAmes(225)| 欠損: 0 ExterQual : 4種類 | 最多: TA(906) | 欠損: 0 === タスク2: Label Encoding === Ex=4, Gd=3, TA=2, Fa=1, Po=0 変換前 unique: ['Ex', 'Gd', 'TA', 'Fa'] 変換後 unique: [4, 3, 2, 1] === タスク3: One-Hot Encoding === MSZoning展開後の列数: 4 (drop_first=True) / 5 (drop_first=False) === タスク4: Target Encoding === NoRidge n= 30 生平均: 320,000 → TE: 285,000 ...
💡 ヒント
ヒント1(方向性)
タスク2は順序付きのカテゴリエンコードなので sklearn.preprocessing.LabelEncoder ではなく明示的なマッピング辞書を使うこと。LabelEncoder はアルファベット順に割り当てるため順序が保証されない。
ヒント2(アプローチ)
- タスク2:
quality_map = {'Ex':4, 'Gd':3, 'TA':2, 'Fa':1, 'Po':0}→df['ExterQual'].map(quality_map) - タスク3:
pd.get_dummies(df['MSZoning'], prefix='MSZ', drop_first=True) - タスク4:
df.groupby('Neighborhood')['SalePrice'].mean()+ スムージング計算 - タスク5:
df[col].map(df[col].value_counts())で頻度マッピング
ヒント3(コード骨格)
# タスク4: スムージング付き Target Encoding def target_encode_smooth(df, col, target, k=10): global_mean = df[target].mean() stats = df.groupby(col)[target].agg(['mean', 'count']) smooth = (stats['count'] * stats['mean'] + k * global_mean) / (stats['count'] + k) return df[col].map(smooth) # タスク5: Frequency Encoding def frequency_encode(df, cat_cols): df_enc = df.copy() for col in cat_cols: freq_map = df[col].value_counts() df_enc[col + '_freq'] = df[col].map(freq_map) return df_enc
✅ 模範解答
import numpy as np import pandas as pd import matplotlib.pyplot as plt import warnings warnings.filterwarnings('ignore') # ─── サンプルデータ生成(House Prices スタイル) ────────────────── np.random.seed(42) n = 1460 neighborhoods = ['OldTown', 'CollgCr', 'Crawfor', 'NoRidge', 'Mitchel', 'Somerst', 'NWAmes', 'OldTown', 'BrkSide', 'Sawyer', 'NridgHt', 'NAmes', 'SawyerW', 'IDOTRR', 'Timber', 'CollgCr', 'NAmes', 'Veenker', 'Blmngtn', 'MeadowV', 'ClearCr', 'NPkVill', 'Gilbert', 'StoneBr', 'Blueste'] # (nb_weights, mszoning_cats, quality_cats は省略 — MDファイル参照) df = pd.DataFrame({ 'Neighborhood': np.random.choice(neighborhoods, n, p=nb_weights), 'MSZoning' : np.random.choice(mszoning_cats, n, p=mszoning_w), 'ExterQual' : np.random.choice(quality_cats, n, p=quality_w), 'SalePrice' : np.random.lognormal(12.0, 0.4, n).clip(34900, 755000), }) # ─── タスク2: Label Encoding(順序あり) ──────────────────────── quality_map = {'Ex': 4, 'Gd': 3, 'TA': 2, 'Fa': 1, 'Po': 0} df['ExterQual_label'] = df['ExterQual'].map(quality_map) # pd.Categorical で順序情報を保持 quality_order = ['Po', 'Fa', 'TA', 'Gd', 'Ex'] df['ExterQual_cat'] = pd.Categorical(df['ExterQual'], categories=quality_order, ordered=True) df['ExterQual_cat_code'] = df['ExterQual_cat'].cat.codes # ─── タスク3: One-Hot Encoding ────────────────────────────────── ohe_with_drop = pd.get_dummies(df['MSZoning'], prefix='MSZ', drop_first=True) ohe_no_drop = pd.get_dummies(df['MSZoning'], prefix='MSZ', drop_first=False) df_with_ohe = pd.concat([df.drop('MSZoning', axis=1), ohe_with_drop], axis=1) # ─── タスク4: Target Encoding(スムージング付き) ──────────────── def target_encode_smooth(df, col, target, k=10): global_mean = df[target].mean() stats = df.groupby(col)[target].agg(['mean', 'count']) smooth = (stats['count'] * stats['mean'] + k * global_mean) / (stats['count'] + k) return df[col].map(smooth) df['Neighborhood_te'] = target_encode_smooth(df, 'Neighborhood', 'SalePrice', k=10) # ─── タスク5: Frequency Encoding ──────────────────────────────── def frequency_encode(df: pd.DataFrame, cat_cols: list, suffix: str = '_freq') -> pd.DataFrame: df_enc = df.copy() for col in cat_cols: freq_map = df[col].value_counts() df_enc[col + suffix] = df[col].map(freq_map) return df_enc orig_cat_cols = ['Neighborhood', 'MSZoning', 'ExterQual'] df_freq = frequency_encode(df, orig_cat_cols)
🪜 Step-by-Step 解説
df.select_dtypes(include=['object']).columns でカテゴリ列を抽出し、.nunique() でユニーク数を確認。25種類の Neighborhood に One-Hot を使うと 25列が追加される — まず確認してから手法を選ぶ。
sklearn の
LabelEncoder はアルファベット順(Ex=0, Fa=1, Gd=2, Po=3, TA=4)に割り当てる。品質の序列(Ex > Gd > TA > Fa > Po)を崩してしまうため、明示的な辞書マッピングが必須。
MSZoning が5種類なら、4列が 0 のとき残り1列が必ず 1 になる。つまり5列目は冗長(ダミー変数トラップ)。線形モデルでは多重共線性を引き起こすため、
drop_first=True で1列を削除する。GBDTではこの問題を回避できるが、列数削減のために付ける習慣は良い。
n=2 しかない Blueste の生平均は偶然性が大きく信頼できない。スムージングで全体平均に引き寄せ、過学習を防ぐ。本番では
category_encoders ライブラリや sklearn の TargetEncoder(v1.3+)を使い、CV内でフィットしてリークを防ぐ。
df[col].map(df[col].value_counts()) で頻度をマッピング。「登場頻度が高いカテゴリ = よくある地区 = 郊外の一般住宅」という暗黙の意味を数値化できる場合に有効。Target Encoding よりリスクが低く高速に実装できる。
📐 数学・統計の補足(文系向け)
スムージングの直感的な説明
「少ないデータだけで判断するのは危険」という考え方をコードで表現したものです。
それとも「まだデータが少ないから全体平均の評価(3.5)寄りに考えよう」とすべき?
スムージングはサンプル数が少ないほど全体平均に引き寄せることで、過剰反応を防ぎます。
One-Hot と Label の数学的な違い
| 手法 | 数学的解釈 | 問題 |
|---|---|---|
| Label Encoding | TA=2, Gd=3 → 差は「1」。モデルは「Gd は TA より1単位良い」と解釈する | 順序なしカテゴリには誤った数値関係を学習させてしまう |
| One-Hot Encoding | 各列は「その地域かどうか」の 0/1 情報のみ。カテゴリ間に数値的距離を設定しない | 次元数が増加する(高カーディナリティでは使えない) |
🏆 Kaggleでの実践的な使い方
House Prices の典型的なエンコードパイプライン
import pandas as pd import numpy as np # 順序あり品質系カテゴリは手動マッピング qual_map = {'Ex': 5, 'Gd': 4, 'TA': 3, 'Fa': 2, 'Po': 1, 'NA': 0} qual_cols = ['ExterQual', 'ExterCond', 'BsmtQual', 'KitchenQual', 'HeatingQC'] for col in qual_cols: df[col] = df[col].map(qual_map).fillna(0) # 低カーディナリティ(≤10種類)→ One-Hot low_card = [c for c in df.select_dtypes('object').columns if df[c].nunique() <= 10] df = pd.get_dummies(df, columns=low_card, drop_first=True) # 高カーディナリティ(>10種類)→ Target Encoding(CV内で使用) # pip install category_encoders from category_encoders import TargetEncoder from sklearn.model_selection import cross_val_score high_card = [c for c in df.select_dtypes('object').columns if df[c].nunique() > 10] enc = TargetEncoder(cols=high_card) # fit は必ず訓練データのみで。テストデータは transform のみ。
Kaggleでよく使われるエンコーダライブラリ
| ライブラリ | エンコーダ名 | 特徴 |
|---|---|---|
| sklearn (v1.3+) | TargetEncoder | CVサポート・リーク防止内蔵 |
| category_encoders | LeaveOneOutEncoder | LOOでリーク防止 |
| category_encoders | WOEEncoder | 二値分類特化(Weight of Evidence) |
| pandas | get_dummies | 手軽・低カーディナリティ向き |
⚠️ よくある誤解・ミス
| 誤解・ミス | なぜ起こるか | 正しい理解 |
|---|---|---|
| 全カテゴリに LabelEncoder を使う | sklearn の LabelEncoder が有名すぎる | 順序なしカテゴリへの LabelEncoder は誤った数値関係を学習させる |
| Target Encoding でデータリーク | 訓練データ全体の平均でエンコードして同じデータで評価 | CVのfoldで分けてエンコード(category_encoders 等を使う) |
| One-Hot でメモリ不足 | 高カーディナリティ変数に OHE を適用 | nunique > 10 はまず Target/Frequency Encoding を検討 |
| テストデータに fit してしまう | エンコーダを test でも fit する | エンコーダは train のみで fit、test には transform のみ |
| ダミー変数トラップを無視 | GBDTでは問題ないと思っている | 線形モデルでは必ず drop_first=True か 1列削除 |
🚀 次のステップ
- 発展:
category_encodersライブラリのLeaveOneOutEncoderでリークを完全排除する実装 - 次回予告: EDA体系的手法⑧ — 特徴量エンジニアリング基礎(交互作用特徴量・多項式特徴量)
📝 自己評価(解いた後に記入)
自分の回答・気づき・メモ: