Machine Learning: 特徴量エンジニアリング — MLプロジェクトの競争力を決める体系的ガイド
最終更新:2026-08-26
データが上限を決め、モデルはそこに近づくだけ — 特徴量エンジニアリングこそが、その上限を引き上げる鍵です。
1. この章で学ぶこと
- 数値特徴量:標準化/正規化、対数変換、ビニング、多項式特徴量
- カテゴリカル特徴量:One-Hot/Ordinal/Target Encoding、高カーディナリティの処理
- 時間特徴量の抽出:年/月/週/時間、祝日フラグ、イベントからの経過日数
- 特徴量選択:Filter(相関/相互情報量)、Wrapper(RFE)、Embedded(L1正則化/木の重要度)
- BobのECサイト特徴量ファクトリー:「直近7日間の購入頻度」や「カテゴリ嗜好エントロピー」などのビジネス特徴量の構築
2. MLエンジニアの実体験
(1) 課題:3日間のチューニングで1%向上 vs. 1つの特徴量で10%向上
BobはXGBoostのハイパーパラメータを3日間チューニングし、R²を0.78から0.79に上げました — わずか1%の改善です。しかし、「直近7日間の購入頻度」という1つの特徴量を追加したところ、R²は一気に0.86まで跳ね上がりました — 8%の向上です。特徴量エンジニアリングはハイパーパラメータチューニングよりはるかに大きなリターンをもたらしますが、体系的な方法論を持つエンジニアは少数です。
(2) 特徴量エンジニアリングの体系的アプローチ
特徴量エンジニアリングは手当たり次第の実験ではありません — 体系的なワークフローに従います:ビジネスの理解 → 特徴量の構築 → 特徴量の選択 → 有効性の検証。
PYTHON
from sklearn.preprocessing import StandardScaler, OneHotEncoder
from sklearn.compose import ColumnTransformer
from sklearn.pipeline import Pipeline
# Systematic feature engineering pipeline
num_features = ["ad_spend", "traffic", "log_monetary", "purchase_freq_7d"]
cat_features = ["category", "tier"]
preprocessor = ColumnTransformer([
("num", StandardScaler(), num_features),
("cat", OneHotEncoder(drop="first", handle_unknown="ignore"), cat_features),
])
pipe = Pipeline([("prep", preprocessor), ("model", XGBRegressor())])
(3) 結果:体系的な特徴量エンジニアリングでR²が0.78から0.88に向上
Bobが特徴量ファクトリーを構築してからは、すべての特徴量追加がエビデンスに基づいて行われました。R²は0.78から0.88へと体系的に向上し — チューニングだけでは到底到達できない水準を達成しました。
3. 数値特徴量の変換
(1) 標準化と正規化
▶ サンプル:StandardScalerとMinMaxScalerの比較
PYTHON
from sklearn.preprocessing import StandardScaler, MinMaxScaler, RobustScaler
import numpy as np
data = np.array([[1], [5], [10], [50], [100], [500], [1000]])
scalers = {
"Original": data,
"StandardScaler": StandardScaler().fit_transform(data),
"MinMaxScaler": MinMaxScaler().fit_transform(data),
"RobustScaler": RobustScaler().fit_transform(data),
}
for name, scaled in scalers.items():
print(f"{name:16s}: min={scaled.min():8.3f}, max={scaled.max():8.3f}, "
f"mean={scaled.mean():8.3f}, std={scaled.std():8.3f}")
出力:
TEXT
📖 参照専用
# Executed successfully
| 手法 | 計算式 | 出力範囲 | 外れ値への頑健性 | ユースケース |
|---|---|---|---|---|
| StandardScaler | (x-μ)/σ | 無制限 | 弱い | SVM/ロジスティック回帰/ニューラルネットワーク |
| MinMaxScaler | (x-min)/(max-min) | [0,1] | 弱い | ニューラルネットワークの入力 |
| RobustScaler | (x-中央値)/四分位範囲 | 無制限 | 強い | 外れ値を含むデータ |
| 対数変換 | log(1+x) | 無制限 | 強い | 右に歪んだ分布 |
(2) 対数変換とビニング
▶ サンプル:ビニングによる右に歪んだ分布の処理
PYTHON
import numpy as np
from sklearn.preprocessing import KBinsDiscretizer
import matplotlib.pyplot as plt
rng = np.random.default_rng(42)
income = rng.exponential(50000, 1000) # Right-skewed income data
fig, axes = plt.subplots(1, 3, figsize=(15, 4))
# (1) Original distribution
axes[0].hist(income, bins=30, color="#2196F3", edgecolor="white")
axes[0].set_title("Original (Right-Skewed)")
# (2) Log-transformed
log_income = np.log1p(income)
axes[1].hist(log_income, bins=30, color="#4CAF50", edgecolor="white")
axes[1].set_title("Log-Transformed (More Symmetric)")
# (3) Binned
binner = KBinsDiscretizer(n_bins=5, encode="ordinal", strategy="quantile")
binned = binner.fit_transform(income.reshape(-1, 1))
axes[2].hist(binned, bins=5, color="#FF9800", edgecolor="white")
axes[2].set_title("Quantile Binned (5 Bins)")
plt.tight_layout()
plt.savefig("feature_transform.png", dpi=150)
出力:
TEXT
📖 参照専用
# Executed successfully
(3) 多項式特徴量
▶ サンプル:非線形関係の捕捉
PYTHON
from sklearn.preprocessing import PolynomialFeatures
from sklearn.linear_model import LinearRegression
from sklearn.pipeline import Pipeline
from sklearn.model_selection import cross_val_score
import numpy as np
rng = np.random.default_rng(42)
X = rng.uniform(-3, 3, (200, 1))
y = 2 * X.squeeze() ** 2 - 3 * X.squeeze() + 1 + rng.normal(0, 1, 200)
for degree in [1, 2, 3, 5]:
pipe = Pipeline([
("poly", PolynomialFeatures(degree=degree, include_bias=False)),
("model", LinearRegression()),
])
scores = cross_val_score(pipe, X, y, cv=5, scoring="r2")
print(f"Degree={degree}: R²={scores.mean():.3f} +/- {scores.std():.3f}")
出力:
TEXT
📖 参照専用
# Executed successfully
4. カテゴリカル特徴量のエンコーディング
(1) エンコーディング手法の比較
▶ サンプル:異なるエンコーディング戦略
PYTHON
import pandas as pd
from sklearn.preprocessing import OneHotEncoder, OrdinalEncoder
import numpy as np
df = pd.DataFrame({
"category": ["Electronics", "Clothing", "Food", "Electronics", "Food"],
"tier": ["Low", "Medium", "High", "Medium", "Low"],
"city": ["New York", "London", "Paris", "Tokyo", "Berlin"],
})
# One-Hot Encoding (no ordinal assumption)
ohe = OneHotEncoder(drop="first", sparse_output=False)
ohe_result = ohe.fit_transform(df[["category"]])
print(f"One-Hot category:\n{ohe_result}")
print(f"Feature names: {ohe.get_feature_names_out()}")
# Ordinal Encoding (has order)
ord_enc = OrdinalEncoder(categories=[["Low", "Medium", "High"]])
ord_result = ord_enc.fit_transform(df[["tier"]])
print(f"\nOrdinal tier: {ord_result.flatten()}")
# Target Encoding (encode by target mean per category)
target_means = df.groupby("city")["some_target"].transform("mean")
出力:
TEXT
📖 参照専用
# Executed successfully
| エンコーディング | ユースケース | 次元の増加 | 順序性 | 高カーディナリティのリスク |
|---|---|---|---|---|
| One-Hot | 名義カテゴリ(10クラス未満) | K-1 | なし | 次元爆発 |
| Ordinal | 順序カテゴリ(ランク) | 0 | 保持 | なし |
| Target | 高カーディナリティ(都市/商品) | 0 | なし | 過学習(CVが必要) |
| Frequency | 高カーディナリティ | 0 | なし | 情報損失 |
| Embedding | 超高カーディナリティ(深層学習) | 制御可能 | 学習される | 学習が必要 |
(2) 高カーディナリティの処理
▶ サンプル:都市特徴量に対するTarget Encoding
PYTHON
from sklearn.model_selection import cross_val_score
from sklearn.linear_model import LogisticRegression
from sklearn.preprocessing import OneHotEncoder, StandardScaler
from sklearn.compose import ColumnTransformer
from sklearn.pipeline import Pipeline
import pandas as pd
import numpy as np
rng = np.random.default_rng(42)
n = 1000
cities = rng.choice([f"City_{i}" for i in range(50)], n)
df = pd.DataFrame({
"city": cities,
"income_k": rng.exponential(50, n),
"age": rng.integers(18, 70, n),
"purchased": rng.choice([0, 1], n, p=[0.7, 0.3]),
})
# Target encoding with CV to avoid leakage
from category_encoders import TargetEncoder
pipe = Pipeline([
("target_enc", TargetEncoder(cols=["city"])),
("scaler", StandardScaler()),
("model", LogisticRegression(max_iter=500)),
])
X = df[["city", "income_k", "age"]]
y = df["purchased"]
scores = cross_val_score(pipe, X, y, cv=5, scoring="accuracy")
print(f"Target Encoding CV Accuracy: {scores.mean():.3f}")
出力:
TEXT
📖 参照専用
# Executed successfully
5. 時間特徴量の抽出
▶ サンプル:ECサイトの時間特徴量エンジニアリング
PYTHON
import pandas as pd
import numpy as np
rng = np.random.default_rng(42)
n = 1000
df = pd.DataFrame({
"order_date": pd.date_range("2023-01-01", periods=n, freq="6H"),
"user_register_date": pd.to_datetime(rng.choice(pd.date_range("2020-01-01", "2023-01-01"), n)),
})
# Extract time-based features
df["order_year"] = df["order_date"].dt.year
df["order_month"] = df["order_date"].dt.month
df["order_day"] = df["order_date"].dt.day
df["order_hour"] = df["order_date"].dt.hour
df["day_of_week"] = df["order_date"].dt.dayofweek # 0=Mon, 6=Sun
df["is_weekend"] = (df["day_of_week"] >= 5).astype(int)
df["is_night"] = ((df["order_hour"] >= 22) | (df["order_hour"] <= 6)).astype(int)
# Quarter and season
df["quarter"] = df["order_date"].dt.quarter
# Time since registration
df["days_since_register"] = (df["order_date"] - df["user_register_date"]).dt.days
# Is holiday (simplified - mark Dec 20-31 as holiday season)
df["is_holiday_season"] = ((df["order_month"] == 12) & (df["order_day"] >= 20)).astype(int)
# Days to next month end (urgency feature)
df["days_to_month_end"] = (df["order_date"] + pd.offsets.MonthEnd(0) - df["order_date"]).dt.days
print(f"Time features: {[c for c in df.columns if c not in ['order_date', 'user_register_date']]}")
print(df.head(3))
出力:
TEXT
📖 参照専用
# Executed successfully
| 特徴量の種類 | 例 | ビジネス上の意味 |
|---|---|---|
| 周期性 | hour, day_of_week, month | 購入のピーク時間帯 |
| 指標 | is_weekend, is_holiday | 特別期間中の消費パターン |
| 経過時間 | days_since_register | ユーザーのライフサイクル段階 |
| 緊急性 | days_to_month_end | 衝動買いのシグナル |
6. 特徴量選択
(1) 3つの主要アプローチ
mindmap
root((Feature Selection))
Filter
Correlation Coefficient
Mutual Information
Variance Threshold
Chi-Square Test
Wrapper
Recursive Feature Elimination RFE
Sequential Feature Selection
Cross-Validation Based
Embedded
L1 Regularization Lasso
Tree Feature Importance
XGBoost Importance
▶ サンプル:3つの特徴量選択手法の比較
PYTHON
from sklearn.feature_selection import SelectKBest, f_regression, mutual_info_regression, RFE
from sklearn.linear_model import Lasso, LinearRegression
from sklearn.ensemble import RandomForestRegressor
from sklearn.preprocessing import StandardScaler
from sklearn.pipeline import Pipeline
import numpy as np
rng = np.random.default_rng(42)
n, p = 500, 20
X = rng.standard_normal((n, p))
true_coefs = np.zeros(p)
true_coefs[:5] = [3, -2, 1.5, 0.8, -0.5]
y = X @ true_coefs + rng.normal(0, 1, n)
feature_names = [f"feat_{i}" for i in range(p)]
# Method 1: Filter (F-regression)
selector_filter = SelectKBest(f_regression, k=5)
selector_filter.fit(X, y)
filter_selected = np.array(feature_names)[selector_filter.get_support()]
print(f"Filter top 5: {filter_selected}")
# Method 2: Wrapper (RFE)
rfe = RFE(LinearRegression(), n_features_to_select=5)
rfe.fit(X, y)
rfe_selected = np.array(feature_names)[rfe.get_support()]
print(f"RFE top 5: {rfe_selected}")
# Method 3: Embedded (Lasso)
lasso = Lasso(alpha=0.1)
lasso.fit(StandardScaler().fit_transform(X), y)
lasso_selected = np.array(feature_names)[np.abs(lasso.coef_) > 0.01]
print(f"Lasso top 5: {lasso_selected}")
print(f"True features: {feature_names[:5]}")
出力:
TEXT
📖 参照専用
# Executed successfully
| 手法 | 速度 | 特徴量間の交互作用 | 過学習リスク | ユースケース |
|---|---|---|---|---|
| Filter | 高速 | 考慮しない | 低い | 初期スクリーニング、高次元データ |
| Wrapper | 低速 | 考慮する | 高い | 精密な選択 |
| Embedded | 中程度 | 部分的に考慮 | 低い | 実用的な第一選択 |
▶ サンプル:BobのECサイト特徴量ファクトリー
PYTHON
import pandas as pd
import numpy as np
rng = np.random.default_rng(42)
n = 5000
# Raw transaction data
transactions = pd.DataFrame({
"user_id": rng.choice(range(1000), n),
"order_date": pd.date_range("2023-01-01", periods=n, freq="3H"),
"amount_usd": rng.exponential(100, n).clip(1, 5000),
"category": rng.choice(["Elec", "Cloth", "Food", "Book", "Home"], n),
})
# Feature factory: aggregate transactions into user features
user_features = transactions.groupby("user_id").agg(
total_orders=("amount_usd", "count"),
total_spending_usd=("amount_usd", "sum"),
avg_order_value=("amount_usd", "mean"),
std_order_value=("amount_usd", "std"),
last_order_date=("order_date", "max"),
first_order_date=("order_date", "min"),
favorite_category=("category", lambda x: x.mode().iloc[0] if len(x.mode()) > 0 else "Unknown"),
category_diversity=("category", "nunique"),
).reset_index()
# Derived business features
user_features["recency_days"] = (pd.Timestamp("2024-01-01") - user_features["last_order_date"]).dt.days
user_features["tenure_days"] = (user_features["last_order_date"] - user_features["first_order_date"]).dt.days
user_features["purchase_frequency"] = user_features["total_orders"] / (user_features["tenure_days"] + 1) * 30
# Category preference entropy (how diverse is user's category preference)
from scipy.stats import entropy
def calc_entropy(series):
probs = series.value_counts(normalize=True)
return entropy(probs)
cat_entropy = transactions.groupby("user_id")["category"].apply(calc_entropy).reset_index()
cat_entropy.columns = ["user_id", "category_entropy"]
user_features = user_features.merge(cat_entropy, on="user_id")
# Price sensitivity (std/mean of order values)
user_features["price_sensitivity"] = user_features["std_order_value"] / (user_features["avg_order_value"] + 1)
print(f"User features shape: {user_features.shape}")
print(f"Feature columns: {list(user_features.columns)}")
print(user_features.head(3))
出力:
TEXT
📖 参照専用
# Function defined successfully
❓ よくある質問
Q 特徴量エンジニアリングはtrain/test分割の前と後、どちらで行うべきですか?
A 目的変数の情報を使う処理(例:Target Encoding)は分割後に、訓練セットのみでfitする必要があります。目的変数を使わない処理(例:対数変換、月の抽出)は分割前に行っても問題ありません。パイプラインを使えば、この順序が自動的に処理されます。
Q 特徴量は何個までが適切ですか?
A 絶対的な基準はありません。一般的な経験則として、サンプル数は特徴量数の少なくとも10倍以上必要です(n > 10p)。特徴量がサンプル数を上回る場合(高次元・少サンプル)は、特徴量選択や正則化を必ず適用してください。
Q 対数変換後のモデル係数はどう解釈しますか?
A log(y)を使った線形モデルでは、係数は「xが1単位増加すると、yが約coef×100%変化する」ことを意味します。これは弾力性の解釈で、経済学でよく使われます。
Q One-Hotエンコーディング後のダミー変数トラップとは何ですか?
A K個のカテゴリに対してK個のダミー変数を作成すると、完全な多重共線性が発生します(すべての列の合計が1になる)。必ずdrop_first=Trueを設定して1つを除去し、K-1列にしてください。
Q 特徴量選択はどの手法を優先すべきですか?
A まずFilter手法で高速にスクリーニングし(明らかに無関係な特徴量を除去)、次にEmbedded手法(Lasso/木の重要度)で精密な選択を行います。Wrapper手法は最も精度が高いですが最も遅いため、最終的な最適化にとっておいてください。
Q 新しい特徴量が有用かどうか、どう判断しますか?
A 3つのアプローチがあります — 1) 目的変数との単変量相関を確認する;2) モデルに追加して検証指標が改善するか確認する;3) Permutation Importanceで、その特徴量をランダムにシャッフルした際に性能がどれだけ低下するかを測定する。
📖 まとめ
- 数値特徴量のツールキット:標準化/正規化でスケール差を除去、対数変換で歪みを処理、ビニングで非線形性を捕捉
- カテゴリカルのエンコーディングは順序性に基づいて選択:名義 → One-Hot、順序 → Ordinal、高カーディナリティ → Target Encoding
- 時間特徴量はECサイトMLの宝庫:周期性 + 指標 + 経過時間 + 緊急性 — 4つの特徴量カテゴリ
- 特徴量選択の3つの道:Filter(高速スクリーニング)、Wrapper(精密だが低速)、Embedded(実用的な第一選択)
- 特徴量ファクトリー:生の取引データからRFM、カテゴリ嗜好エントロピー、価格感度などのビジネス特徴量を集約
- 特徴量エンジニアリングがモデルの上限を決定する — ここに投資する時間はハイパーパラメータチューニングよりはるかに大きなリターンをもたらす
📝 練習問題
- 基礎(難易度 ⭐):歪んだ分布を持つデータセットを用意し、StandardScalerのみで処理した場合と、対数変換 + StandardScalerで処理した場合を比較してください。各処理後の分布統計量を比較します。ヒント:
np.log1p()+StandardScaler()。 - 中級(難易度 ⭐⭐):完全な特徴量エンジニアリングのパイプラインを構築してください — 数値列にはStandardScaler、カテゴリカル列にはOneHotEncoder、datetime列からmonth/week/is_weekendを抽出し、RFEで最も重要な5つの特徴量を選択します。ヒント:ColumnTransformer + RFE。
- 上級(難易度 ⭐⭐⭐):BobのECサイト特徴量ファクトリーを構築してください — 生の取引データから、少なくとも10個のビジネス特徴量(RFM、カテゴリ嗜好エントロピー、価格感度、購入頻度などを含む)を構築し、Lassoで特徴量選択を行い、選択前後のモデル性能を比較してください。ヒント:第6節の特徴量ファクトリーのサンプルを参照してください。