LESSON

推薦の特徴量エンジニアリング

「アルゴリズムの選択も大事だが、推薦の精度を決定的に左右するのは特徴量だ。」

田中VPoEがH&Mコンペの上位解法を分析した資料を見せる。

「Kaggleの推薦コンペでは、アルゴリズムよりも特徴量エンジニアリングで差がつく。ユーザー、アイテム、コンテキストの3軸で良い特徴量を設計しよう。」

特徴量の3つの軸

推薦システムの特徴量は、ユーザー特徴量、アイテム特徴量、コンテキスト特徴量の3軸で設計する。

推薦特徴量の3軸:

ユーザー特徴量: 「誰が」
  → ユーザーの嗜好、行動パターン、属性

アイテム特徴量: 「何を」
  → アイテムの属性、人気度、品質

コンテキスト特徴量: 「いつ・どこで・どうやって」
  → 時間、場所、デバイス、セッション状況

ユーザー特徴量

基本属性

import pandas as pd
import numpy as np

def build_user_features(transactions_df, customers_df):
    """ユーザー特徴量を構築"""
    features = customers_df.copy()

    # --- 基本属性 ---
    # 会員ランク
    features['member_rank'] = customers_df['rank']
    # 登録からの日数
    features['days_since_registration'] = (
        pd.Timestamp.now() - pd.to_datetime(customers_df['registered_at'])
    ).dt.days

    return features

行動統計量

def build_user_behavior_features(transactions_df, user_id_col='user_id'):
    """ユーザー行動統計量"""
    features = pd.DataFrame()

    # --- 購買統計 ---
    user_txn = transactions_df.groupby(user_id_col)
    features['total_purchases'] = user_txn.size()
    features['total_spend'] = user_txn['price'].sum()
    features['avg_order_value'] = user_txn['price'].mean()
    features['std_order_value'] = user_txn['price'].std().fillna(0)
    features['max_order_value'] = user_txn['price'].max()
    features['min_order_value'] = user_txn['price'].min()

    # --- 購買頻度 ---
    features['unique_items'] = user_txn['item_id'].nunique()
    features['unique_categories'] = user_txn['category'].nunique()

    # --- 時間的特徴量 ---
    features['days_since_last_purchase'] = (
        pd.Timestamp.now() - user_txn['purchase_date'].max()
    ).dt.days
    features['days_since_first_purchase'] = (
        pd.Timestamp.now() - user_txn['purchase_date'].min()
    ).dt.days

    # 購入間隔の平均
    def avg_purchase_interval(group):
        dates = group['purchase_date'].sort_values()
        if len(dates) < 2:
            return np.nan
        intervals = dates.diff().dt.days.dropna()
        return intervals.mean()

    features['avg_purchase_interval'] = (
        transactions_df.groupby(user_id_col).apply(avg_purchase_interval)
    )

    # --- リーセンシー・フリークエンシー・マネタリー (RFM) ---
    features['recency'] = features['days_since_last_purchase']
    features['frequency'] = features['total_purchases']
    features['monetary'] = features['total_spend']

    return features

嗜好特徴量

def build_user_preference_features(transactions_df, user_id_col='user_id'):
    """ユーザー嗜好特徴量"""
    features = pd.DataFrame()

    # カテゴリ別購入割合
    cat_counts = transactions_df.groupby(
        [user_id_col, 'category']
    ).size().unstack(fill_value=0)
    cat_ratios = cat_counts.div(cat_counts.sum(axis=1), axis=0)
    cat_ratios.columns = [f'cat_ratio_{c}' for c in cat_ratios.columns]
    features = pd.concat([features, cat_ratios], axis=1)

    # 好みの価格帯
    price_stats = transactions_df.groupby(user_id_col)['price'].agg([
        'mean', 'median', 'std'
    ])
    price_stats.columns = ['preferred_price_mean', 'preferred_price_median',
                           'preferred_price_std']
    features = pd.concat([features, price_stats], axis=1)

    # 好みのブランド(最頻ブランド)
    top_brand = transactions_df.groupby(user_id_col)['brand'].agg(
        lambda x: x.mode()[0] if len(x.mode()) > 0 else 'unknown'
    )
    features['top_brand'] = top_brand

    # 嗜好の多様性(エントロピー)
    def category_entropy(group):
        probs = group['category'].value_counts(normalize=True)
        return -(probs * np.log2(probs + 1e-10)).sum()

    features['preference_entropy'] = (
        transactions_df.groupby(user_id_col).apply(category_entropy)
    )

    return features

アイテム特徴量

def build_item_features(articles_df, transactions_df, item_id_col='item_id'):
    """アイテム特徴量を構築"""
    features = articles_df.copy()

    # --- 基本属性 ---
    # カテゴリ、ブランド、色、サイズ等はそのまま使用

    # --- 人気度指標 ---
    item_stats = transactions_df.groupby(item_id_col)
    features['total_sales'] = item_stats.size()
    features['total_revenue'] = item_stats['price'].sum()
    features['unique_buyers'] = item_stats['user_id'].nunique()

    # 直近7日間の売上
    recent = transactions_df[
        transactions_df['purchase_date'] >= pd.Timestamp.now() - pd.Timedelta(days=7)
    ]
    recent_sales = recent.groupby(item_id_col).size()
    features['sales_last_7d'] = recent_sales.reindex(features.index, fill_value=0)

    # 売上トレンド(直近7日 / 過去30日の日平均比)
    last_30d = transactions_df[
        transactions_df['purchase_date'] >= pd.Timestamp.now() - pd.Timedelta(days=30)
    ]
    daily_avg_30d = last_30d.groupby(item_id_col).size() / 30
    daily_avg_7d = recent_sales / 7
    features['sales_trend'] = (daily_avg_7d / (daily_avg_30d + 1e-6)).reindex(
        features.index, fill_value=0
    )

    # --- 品質指標 ---
    if 'rating' in transactions_df.columns:
        features['avg_rating'] = item_stats['rating'].mean()
        features['rating_count'] = item_stats['rating'].count()

    # --- 新しさ ---
    features['days_since_launch'] = (
        pd.Timestamp.now() - pd.to_datetime(features['launch_date'])
    ).dt.days

    return features

コンテキスト特徴量

def build_context_features(timestamp, user_session):
    """コンテキスト特徴量を構築"""
    features = {}

    # --- 時間特徴量 ---
    features['hour'] = timestamp.hour
    features['day_of_week'] = timestamp.dayofweek
    features['is_weekend'] = 1 if timestamp.dayofweek >= 5 else 0
    features['month'] = timestamp.month

    # 季節性
    month = timestamp.month
    if month in [3, 4, 5]:
        features['season'] = 'spring'
    elif month in [6, 7, 8]:
        features['season'] = 'summer'
    elif month in [9, 10, 11]:
        features['season'] = 'autumn'
    else:
        features['season'] = 'winter'

    # イベント(セール期間など)
    features['is_sale_period'] = is_sale_period(timestamp)

    # --- セッション特徴量 ---
    features['session_length'] = len(user_session['viewed_items'])
    features['session_duration_min'] = user_session['duration_seconds'] / 60
    features['n_categories_viewed'] = len(set(
        item['category'] for item in user_session['viewed_items']
    ))

    # --- デバイス ---
    features['device'] = user_session.get('device', 'unknown')
    features['is_mobile'] = 1 if features['device'] == 'mobile' else 0

    return features

H&Mコンペの特徴量設計

# H&M Personalized Fashion Recommendationsでの実践的な特徴量

h_and_m_features = {
    "ユーザー特徴量": {
        "基本": ["age", "club_member_status", "fashion_news_frequency"],
        "購買統計": [
            "total_purchases_all_time",
            "total_purchases_last_7d / 14d / 30d / 90d",
            "unique_articles_purchased",
            "unique_product_groups",
            "avg_price", "max_price",
        ],
        "嗜好": [
            "favorite_product_group(最頻カテゴリ)",
            "favorite_colour(最頻カラー)",
            "preference_entropy",
            "price_sensitivity(購入価格の分散)",
        ],
    },
    "アイテム特徴量": {
        "基本": [
            "product_type_name", "product_group_name",
            "colour_group_name", "perceived_colour_value_name",
            "department_name", "section_name", "garment_group_name",
        ],
        "人気度": [
            "total_sales", "sales_last_7d",
            "sales_trend(直近/過去比)",
            "unique_buyers",
        ],
        "テキスト": [
            "detail_desc_tfidf(TF-IDFベクトル)",
            "detail_desc_embedding(Sentence Embedding)",
        ],
    },
    "インタラクション特徴量": {
        "ユーザー×アイテム": [
            "user_has_purchased_same_product_type",
            "user_has_purchased_same_colour",
            "user_has_purchased_same_department",
            "price_diff_from_user_avg(ユーザー平均価格との差)",
        ],
    },
}

特徴量選択のポイント

推薦での特徴量選択の指針:

1. リーセンシーが最重要
   - 直近の行動は遠い過去の行動より予測力が高い
   - 時間窓を複数設定(7日/14日/30日/90日)

2. 相対指標が有効
   - 「購入数10」より「カテゴリ内での購入割合30%」の方が有用
   - ユーザー平均との差分、カテゴリ内順位

3. ターゲットリーケージに注意
   - 未来の情報を使わないこと
   - 時系列分割の徹底

4. 特徴量の鮮度管理
   - 定期更新が必要な特徴量を明確にする
   - リアルタイム更新 vs バッチ更新の切り分け

まとめ

代表的な特徴量ポイント
ユーザーRFM、嗜好エントロピー、カテゴリ比率リーセンシーが最重要
アイテム人気度、売上トレンド、テキストEmb鮮度と品質指標
コンテキスト時間、デバイス、セッション状況今の意図を捉える
インタラクションユーザー×アイテムの関係性最も予測力が高い

チェックリスト

  • ユーザー特徴量のRFM分析を実装できる
  • 嗜好の多様性(エントロピー)を計算できる
  • アイテムの人気度と売上トレンドを算出できる
  • コンテキスト特徴量(時間・デバイス・セッション)の重要性を理解した
  • H&Mコンペでの特徴量設計をイメージできる

次のステップへ

特徴量エンジニアリングを理解したところで、次は推薦結果にビジネスルールを適用するリランキングについて学ぼう。

推定読了時間: 30分