Machine Learning: 特徴量エンジニアリング — MLプロジェクトの競争力を決める体系的ガイド

最終更新:2026-08-26

データが上限を決め、モデルはそこに近づくだけ — 特徴量エンジニアリングこそが、その上限を引き上げる鍵です。

1. この章で学ぶこと


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つの主要アプローチ

100%
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で、その特徴量をランダムにシャッフルした際に性能がどれだけ低下するかを測定する。

📖 まとめ


📝 練習問題

  1. 基礎(難易度 ⭐):歪んだ分布を持つデータセットを用意し、StandardScalerのみで処理した場合と、対数変換 + StandardScalerで処理した場合を比較してください。各処理後の分布統計量を比較します。ヒント:np.log1p() + StandardScaler()
  2. 中級(難易度 ⭐⭐):完全な特徴量エンジニアリングのパイプラインを構築してください — 数値列にはStandardScaler、カテゴリカル列にはOneHotEncoder、datetime列からmonth/week/is_weekendを抽出し、RFEで最も重要な5つの特徴量を選択します。ヒント:ColumnTransformer + RFE。
  3. 上級(難易度 ⭐⭐⭐):BobのECサイト特徴量ファクトリーを構築してください — 生の取引データから、少なくとも10個のビジネス特徴量(RFM、カテゴリ嗜好エントロピー、価格感度、購入頻度などを含む)を構築し、Lassoで特徴量選択を行い、選択前後のモデル性能を比較してください。ヒント:第6節の特徴量ファクトリーのサンプルを参照してください。

← 前へ:KNNとクラスタリング | 次へ:アンサンブル学習 — XGBoostとLightGBM →

Web-Tutorial.com

Web-Tutorial 技術チーム

複数の開発者によって共同維持されているプログラミングチュートリアルプラットフォーム。各チュートリアルは専門分野の開発者が執筆・レビューしています。正確で信頼性の高いコンテンツを目指しています — 問題を見つけた場合はお知らせください。

100%