LESSON

演習:特徴量エンジニアリングを実践しよう

田中VPoE:「さて、ここからが本番だ。NetShop の顧客データに対して、特徴量エンジニアリングを一通り実践してもらう。良い特徴量が作れるかどうかで、最終的なモデルの精度が大きく変わる。」

あなた:「選択、エンコーディング、スケーリング、生成を全部やるんですね。」

田中VPoE:「そうだ。実務ではこれらを組み合わせて使う。Pipeline で再現可能な形にまとめることも意識してくれ。」

ミッション概要

NetShop の顧客データに対して、特徴量エンジニアリングのフルパイプラインを構築します。データの前処理からモデルに入力できる状態までの一連の流れを実装してください。


Mission 1: データの準備と探索

サンプルデータを生成し、特徴量の特性を把握してください。

要件

  1. 以下の特徴量を持つ顧客データを生成する
    • 数値: purchase_count, avg_amount, days_inactive, page_views, support_tickets
    • カテゴリカル: device_type, membership_rank, payment_method
    • 日付: registration_date, last_purchase_date
  2. 各特徴量の基本統計量を確認する
  3. 欠損値と外れ値の有無を確認する
import numpy as np
import pandas as pd

np.random.seed(42)
n = 3000

# ここにデータ生成と探索のコードを書く
解答例
import numpy as np
import pandas as pd
from datetime import datetime, timedelta

np.random.seed(42)
n = 3000

# データ生成
df = pd.DataFrame({
    'customer_id': range(1, n + 1),
    'purchase_count': np.random.poisson(8, n),
    'avg_amount': np.random.lognormal(8, 1, n).astype(int),
    'days_inactive': np.random.exponential(25, n).astype(int),
    'page_views': np.random.poisson(30, n),
    'support_tickets': np.random.poisson(1.5, n),
    'device_type': np.random.choice(['PC', 'スマホ', 'タブレット'], n, p=[0.3, 0.55, 0.15]),
    'membership_rank': np.random.choice(['ブロンズ', 'シルバー', 'ゴールド', 'プラチナ'], n, p=[0.4, 0.3, 0.2, 0.1]),
    'payment_method': np.random.choice(['クレジット', '銀行振込', 'コンビニ', 'PayPay'], n, p=[0.4, 0.2, 0.15, 0.25]),
})

# 日付特徴量
base_date = datetime(2026, 3, 1)
df['registration_date'] = [base_date - timedelta(days=int(d)) for d in np.random.exponential(365, n)]
df['last_purchase_date'] = [base_date - timedelta(days=int(d)) for d in df['days_inactive']]

# 離反ラベル(days_inactive が大きいほど離反しやすい)
churn_prob = 1 / (1 + np.exp(-(df['days_inactive'] - 40) / 15 + df['purchase_count'] * 0.1))
df['is_churned'] = (np.random.random(n) < churn_prob).astype(int)

# 欠損値を一部に追加
mask = np.random.random(n) < 0.05
df.loc[mask, 'avg_amount'] = np.nan
mask2 = np.random.random(n) < 0.03
df.loc[mask2, 'page_views'] = np.nan

# 基本統計量
print("=== 基本統計量 ===")
print(df.describe())
print(f"\n=== データ型 ===")
print(df.dtypes)
print(f"\n=== 欠損値 ===")
print(df.isnull().sum())
print(f"\n=== 離反率 ===")
print(f"{df['is_churned'].mean():.1%}")
print(f"\n=== カテゴリカル変数の分布 ===")
for col in ['device_type', 'membership_rank', 'payment_method']:
    print(f"\n{col}:")
    print(df[col].value_counts())

Mission 2: 特徴量の前処理パイプラインを構築する

欠損値処理、エンコーディング、スケーリングを含む前処理パイプラインを構築してください。

要件

  1. 欠損値を適切な方法で補完する
  2. カテゴリカル変数を適切にエンコーディングする
    • device_type, payment_method: One-hot Encoding
    • membership_rank: Ordinal Encoding(順序あり)
  3. 数値特徴量をスケーリングする
  4. scikit-learn の Pipeline / ColumnTransformer で実装する
from sklearn.pipeline import Pipeline
from sklearn.compose import ColumnTransformer
from sklearn.preprocessing import StandardScaler, OneHotEncoder, OrdinalEncoder
from sklearn.impute import SimpleImputer

# ここに前処理パイプラインのコードを書く
解答例
from sklearn.pipeline import Pipeline
from sklearn.compose import ColumnTransformer
from sklearn.preprocessing import StandardScaler, OneHotEncoder, OrdinalEncoder
from sklearn.impute import SimpleImputer
from sklearn.model_selection import train_test_split

# 特徴量とラベルを分離
feature_cols = ['purchase_count', 'avg_amount', 'days_inactive',
                'page_views', 'support_tickets',
                'device_type', 'membership_rank', 'payment_method']
X = df[feature_cols]
y = df['is_churned']

# データ分割
X_train, X_test, y_train, y_test = train_test_split(
    X, y, test_size=0.2, random_state=42, stratify=y
)

# 特徴量の分類
numeric_features = ['purchase_count', 'avg_amount', 'days_inactive',
                    'page_views', 'support_tickets']
onehot_features = ['device_type', 'payment_method']
ordinal_features = ['membership_rank']

# 数値特徴量パイプライン
numeric_pipeline = Pipeline([
    ('imputer', SimpleImputer(strategy='median')),
    ('scaler', StandardScaler()),
])

# One-hot エンコーディングパイプライン
onehot_pipeline = Pipeline([
    ('imputer', SimpleImputer(strategy='most_frequent')),
    ('encoder', OneHotEncoder(drop='first', sparse_output=False)),
])

# 順序エンコーディングパイプライン
rank_order = [['ブロンズ', 'シルバー', 'ゴールド', 'プラチナ']]
ordinal_pipeline = Pipeline([
    ('imputer', SimpleImputer(strategy='most_frequent')),
    ('encoder', OrdinalEncoder(categories=rank_order)),
])

# ColumnTransformer で統合
preprocessor = ColumnTransformer([
    ('numeric', numeric_pipeline, numeric_features),
    ('onehot', onehot_pipeline, onehot_features),
    ('ordinal', ordinal_pipeline, ordinal_features),
])

# フィットと変換
X_train_processed = preprocessor.fit_transform(X_train)
X_test_processed = preprocessor.transform(X_test)

# 変換後の特徴量名を取得
feature_names_out = preprocessor.get_feature_names_out()
print(f"変換前の特徴量数: {X_train.shape[1]}")
print(f"変換後の特徴量数: {X_train_processed.shape[1]}")
print(f"\n特徴量名:")
for name in feature_names_out:
    print(f"  {name}")

Mission 3: ドメイン知識に基づく特徴量を生成する

NetShop のビジネス文脈を活かした新しい特徴量を生成し、パイプラインに組み込んでください。

要件

  1. 以下の特徴量を生成する
    • RFM 関連: recency, frequency, monetary のスコア
    • エンゲージメント: 訪問あたりの購入率
    • トレンド: 活動の低下フラグ
    • 時間系: 会員期間(日数)
  2. 生成した特徴量と元の特徴量を組み合わせる
  3. 特徴量重要度を確認し、有効な特徴量を特定する
from sklearn.ensemble import RandomForestClassifier

# ここに特徴量生成と重要度分析のコードを書く
解答例
import numpy as np
import pandas as pd
from sklearn.ensemble import RandomForestClassifier
from sklearn.metrics import f1_score
import matplotlib.pyplot as plt

# 特徴量生成関数
def create_features(df):
    """ドメイン知識に基づく特徴量生成"""
    df_new = df.copy()

    # RFM特徴量
    df_new['recency'] = df_new['days_inactive']
    df_new['frequency'] = df_new['purchase_count']
    df_new['monetary'] = df_new['avg_amount']

    # エンゲージメント指標
    df_new['purchase_per_view'] = (
        df_new['purchase_count'] / df_new['page_views'].replace(0, 1)
    )

    # サポート負荷
    df_new['support_per_purchase'] = (
        df_new['support_tickets'] / df_new['purchase_count'].replace(0, 1)
    )

    # 会員期間
    base_date = pd.Timestamp('2026-03-01')
    df_new['membership_days'] = (base_date - pd.to_datetime(df_new['registration_date'])).dt.days

    # 購入頻度(会員期間あたりの購入回数)
    df_new['purchase_frequency'] = (
        df_new['purchase_count'] / (df_new['membership_days'] / 30).replace(0, 1)
    )

    # 非活動フラグ
    df_new['is_long_inactive'] = (df_new['days_inactive'] > 30).astype(int)
    df_new['is_low_purchase'] = (df_new['purchase_count'] < 3).astype(int)

    # 対数変換
    df_new['log_amount'] = np.log1p(df_new['avg_amount'])

    return df_new

# 特徴量生成
df_featured = create_features(df)

# 生成した特徴量で学習
new_numeric_features = [
    'purchase_count', 'avg_amount', 'days_inactive', 'page_views',
    'support_tickets', 'purchase_per_view', 'support_per_purchase',
    'membership_days', 'purchase_frequency', 'is_long_inactive',
    'is_low_purchase', 'log_amount',
]

X_new = df_featured[new_numeric_features].fillna(0)
y = df_featured['is_churned']

X_train, X_test, y_train, y_test = train_test_split(
    X_new, y, test_size=0.2, random_state=42, stratify=y
)

# ランダムフォレストで特徴量重要度を計算
rf = RandomForestClassifier(n_estimators=100, random_state=42)
rf.fit(X_train, y_train)

y_pred = rf.predict(X_test)
print(f"F1スコア: {f1_score(y_test, y_pred):.3f}")

# 特徴量重要度
importance = pd.Series(rf.feature_importances_, index=new_numeric_features)
importance = importance.sort_values(ascending=True)

plt.figure(figsize=(10, 8))
importance.plot(kind='barh')
plt.title('特徴量重要度')
plt.xlabel('重要度')
plt.tight_layout()
plt.show()

print("\n=== 特徴量重要度ランキング ===")
for i, (feat, imp) in enumerate(importance.sort_values(ascending=False).items(), 1):
    print(f"{i}. {feat}: {imp:.4f}")

達成度チェック

  • サンプルデータを生成し、基本統計量を確認した
  • 欠損値処理を適切に行った
  • カテゴリカル変数のエンコーディングを実装した
  • スケーリングを Pipeline/ColumnTransformer で実装した
  • ドメイン知識に基づく特徴量を5つ以上生成した
  • 特徴量重要度を確認し、有効な特徴量を特定した

推定所要時間: 90分