File size: 3,849 Bytes
fef33ce
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
09fcfe1
fef33ce
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
"""
User-level feature engineering.

Computes aggregate statistics over a user's interaction history:
  - avg_rating, rating_std
  - num_ratings (activity level)
  - avg_review_recency (days since first interaction, normalised)
  - top_categories (mode category in rated items)
  - category_affinity vector (fraction of positive ratings per category)

All features are derived from training data only to prevent leakage.
"""

from __future__ import annotations

import numpy as np
import pandas as pd

from src.utils.logging import get_logger

logger = get_logger(__name__)


def compute_user_features(
    train_df: pd.DataFrame,
    metadata_df: pd.DataFrame,
    positive_threshold: float = 4.0,
) -> pd.DataFrame:
    """
    Compute per-user aggregate features from the training split.

    Args:
        train_df: Training interactions with columns [user_idx, item_idx, rating, timestamp].
        metadata_df: Item metadata with columns [item_idx, category, brand, price].
        positive_threshold: Rating threshold for a positive interaction.

    Returns:
        DataFrame indexed by user_idx with one row per user.
    """
    # Merge metadata into interactions
    merged = train_df.merge(
        metadata_df[["item_idx", "category", "brand", "price"]],
        on="item_idx",
        how="left",
    )
    merged["is_positive"] = (merged["rating"] >= positive_threshold).astype(int)

    # Basic rating stats
    rating_stats = (
        merged.groupby("user_idx")["rating"]
        .agg(["mean", "std", "count"])
        .rename(columns={"mean": "user_avg_rating", "std": "user_rating_std", "count": "user_num_ratings"})
    )
    rating_stats["user_rating_std"] = rating_stats["user_rating_std"].fillna(0.0)

    # Positive interaction count
    pos_count = (
        merged[merged["is_positive"] == 1]
        .groupby("user_idx")["item_idx"]
        .count()
        .rename("user_num_positives")
    )

    # Most frequent category among positively-rated items
    pos_merged = merged[merged["is_positive"] == 1].copy()
    top_category = (
        pos_merged.groupby("user_idx")["category"]
        .agg(lambda x: x.mode().iat[0] if len(x) > 0 and len(x.mode()) > 0 else "Unknown")
        .rename("user_top_category")
    )

    # Number of unique categories interacted with
    n_categories = (
        merged.groupby("user_idx")["category"]
        .nunique()
        .rename("user_n_categories")
    )

    # Recency: mean timestamp normalised to [0, 1]
    if merged["timestamp"].notna().any():
        ts_min = merged["timestamp"].min()
        ts_max = merged["timestamp"].max()
        ts_range = max(ts_max - ts_min, 1)
        recency = (
            merged.groupby("user_idx")["timestamp"]
            .mean()
            .apply(lambda t: (t - ts_min) / ts_range)
            .rename("user_recency")
        )
    else:
        recency = pd.Series(0.5, index=rating_stats.index, name="user_recency")

    # Average price of positively rated items
    avg_price = (
        pos_merged.groupby("user_idx")["price"]
        .mean()
        .rename("user_avg_item_price")
    )

    user_features = (
        rating_stats
        .join(pos_count, how="left")
        .join(top_category, how="left")
        .join(n_categories, how="left")
        .join(recency, how="left")
        .join(avg_price, how="left")
    )

    user_features["user_num_positives"] = user_features["user_num_positives"].fillna(0)
    user_features["user_top_category"] = user_features["user_top_category"].fillna("Unknown")
    user_features["user_avg_item_price"] = user_features["user_avg_item_price"].fillna(
        user_features["user_avg_item_price"].median()
    )

    logger.info("Computed user features for %d users, %d features",
                len(user_features), len(user_features.columns))
    return user_features.reset_index()