Day 075 — EDA体系的手法⑦ — カテゴリ変数のエンコーディング手法比較

2026-06-25 水 / Phase 3 コーディング LabelEncoder / OneHot / Target Encoding / Frequency Encoding

📚 背景知識(読んでから問題へ)

🎯
Day 075 — Phase 3 継続: EDA第7弾はカテゴリ変数のエンコーディング。機械学習モデルは数値しか扱えないため、文字列カテゴリを数値に変換する必要がある。House Prices の Neighborhood(25種類)・ExterQual(4種類)を題材に4手法を実装・比較する。

なぜカテゴリ変数を数値に変換するのか?

機械学習モデルは数値しか扱えないため、文字列のカテゴリ変数を数値に変換する必要があります。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 の落とし穴

⚠️
次元爆発に注意: Neighborhood が 25種類ある場合、OHEで 25列が追加される。
線形モデルでは過学習のリスク増大。GBDTでは One-Hot より Label の方が速く精度も同等以上のことが多い。

スムージング付き Target Encoding の公式

スムージング付き Target Encoding(Bayesian Target Encoding)

TE(c) = (nc × meanc + k × meanglobal) / (nc + k)
記号意味
ncカテゴリ c のサンプル数
meancカテゴリ c 内の目的変数の平均
meanglobal全体の目的変数の平均
kスムージングパラメータ(小さいほどカテゴリ平均を重視、大きいほど全体平均に引き寄せる。デフォルト=10)

📊 カテゴリ変数のカーディナリティ(エンコード選択の起点)

House Prices 主要カテゴリ変数のユニーク値数。これを見てエンコード手法を決める。

ユニーク値数(カーディナリティ)— バーの長さがユニーク種類数を表す

Neighborhood
25種類 → Target Encoding 推奨
25 unique
Exterior1st
15種類 → Target / Frequency
15 unique
SaleType
9種類 → One-Hot か Target
9 unique
MSZoning
5種類 → One-Hot 推奨
5 unique
ExterQual
4種類(順序あり)→ Label
4 unique
CentralAir
2種類 → Label / OHE
2 unique

経験則: カーディナリティ ≤ 10 → One-Hot または Label | カーディナリティ > 10 → Target または Frequency

⚖️ エンコーディング手法 詳細比較

Label Encoding
リスク: 低(順序ありのみ) 次元変化: なし

カテゴリを整数値にマッピング。順序情報が意味を持つカテゴリ(品質系)に適する。

sklearn の LabelEncoder はアルファベット順に割り当てるため、順序を保証したい場合は map() を使うこと。

# 正しい実装(順序を明示)
quality_map = {'Ex': 4, 'Gd': 3, 'TA': 2, 'Fa': 1, 'Po': 0}
df['ExterQual_enc'] = df['ExterQual'].map(quality_map)
One-Hot Encoding (OHE)
リスク: 低(リークなし) 次元変化: 大(列が増加)

各カテゴリを独立したバイナリ列に展開。順序のないカテゴリ・線形モデルに最適。

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)
Target Encoding(スムージング付き)
リスク: 中(リーク注意) 次元変化: なし

カテゴリ別の目的変数平均で置換。高カーディナリティ・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)
Frequency Encoding
リスク: 低(リークなし) 次元変化: なし

カテゴリの出現頻度(件数)で置換。情報量は薄いが安全・高速。

「よく登場するカテゴリ = 一般的」という暗黙の意味を数値化できる場合に有効。

freq_map = df['Neighborhood'].value_counts()
df['Neighborhood_freq'] = df['Neighborhood'].map(freq_map)

🎯 Target Encoding スムージング効果の視覚化

k=10 でのスムージング効果。バーの長さは TE値(全体平均=180,000 を基準に正規化)。赤い縦線が全体平均。

NoRidge
n=30
TE: 285,000
NridgHt
n=40
TE: 276,000
CollgCr
n=150
TE: 217,500
NAmes
n=225
TE: 151,276
MeadowV
n=17
TE: 129,629
Blueste (n=2 少数)
n=2 生平均70k
→ スムージング後
TE: 161,666

Blueste は生平均 70,000 だが、n=2 しかなく信頼性が低い。スムージングで全体平均 180,000 に大きく引き寄せられ 161,666 になる(過学習防止)。

スムージング強度とサンプル数の関係(k=10の場合)

100% カテゴリ平均 50% 全体平均 n=1 n=5 n=10 n=30 n=100 n=200 9% 33% 50% 75% 91% TE のカテゴリ平均への重み(k=10) サンプル数 n

n が少ないほどカテゴリ平均への重みが小さく(全体平均に引き寄せられる)、n が大きくなるほど実際のカテゴリ平均を重視する。

🗂️ データスキーマ(House Prices スタイル)

カラム名説明ユニーク数推奨エンコード
Neighborhoodobject住宅地区名25Target Encoding
MSZoningobjectゾーニング区分5One-Hot Encoding
ExterQualobject外観品質 (Ex/Gd/TA/Fa/Po)4 (順序あり)Label Encoding (map)
SalePricefloat64販売価格(目的変数)

🧩 問題

💻
House Prices スタイルのサンプルデータを使って、カテゴリ変数のエンコーディングを実装してください。

タスク1 — カーディナリティの確認:
各カテゴリ変数のユニーク値の数・最多頻度カテゴリ・欠損数を一覧表示する。

タスク2 — Label Encoding(順序あり):
ExterQual(外観品質: Ex > Gd > TA > Fa > Po)を順序情報を保ったまま数値に変換せよ。
pd.Categoricalmap の 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 解説

1
カーディナリティの確認(エンコード手法選択の起点)
df.select_dtypes(include=['object']).columns でカテゴリ列を抽出し、.nunique() でユニーク数を確認。25種類の Neighborhood に One-Hot を使うと 25列が追加される — まず確認してから手法を選ぶ。
2
順序ありカテゴリは map() で明示的マッピング
sklearn の LabelEncoder はアルファベット順(Ex=0, Fa=1, Gd=2, Po=3, TA=4)に割り当てる。品質の序列(Ex > Gd > TA > Fa > Po)を崩してしまうため、明示的な辞書マッピングが必須。
3
One-Hot の drop_first=True の意味
MSZoning が5種類なら、4列が 0 のとき残り1列が必ず 1 になる。つまり5列目は冗長(ダミー変数トラップ)。線形モデルでは多重共線性を引き起こすため、drop_first=True で1列を削除する。GBDTではこの問題を回避できるが、列数削減のために付ける習慣は良い。
4
Target Encoding にスムージングを加える理由
n=2 しかない Blueste の生平均は偶然性が大きく信頼できない。スムージングで全体平均に引き寄せ、過学習を防ぐ。本番では category_encoders ライブラリや sklearn の TargetEncoder(v1.3+)を使い、CV内でフィットしてリークを防ぐ。
5
Frequency Encoding の使い所
df[col].map(df[col].value_counts()) で頻度をマッピング。「登場頻度が高いカテゴリ = よくある地区 = 郊外の一般住宅」という暗黙の意味を数値化できる場合に有効。Target Encoding よりリスクが低く高速に実装できる。

📐 数学・統計の補足(文系向け)

スムージングの直感的な説明

「少ないデータだけで判断するのは危険」という考え方をコードで表現したものです。

🍽️
例え: 新しいレストランが開店して、最初の2人のレビューが「星5・星5」だとしたら、本当に「最高の店」と判断すべき?
それとも「まだデータが少ないから全体平均の評価(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+)TargetEncoderCVサポート・リーク防止内蔵
category_encodersLeaveOneOutEncoderLOOでリーク防止
category_encodersWOEEncoder二値分類特化(Weight of Evidence)
pandasget_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体系的手法⑧ — 特徴量エンジニアリング基礎(交互作用特徴量・多項式特徴量)

📝 自己評価(解いた後に記入)

自分の回答・気づき・メモ: