biplobgon/product-recommendation-system
0
1"""2features/user_features.py3--------------------------4Build user-level features from the events DataFrame.5 6EDA context7-----------8- Most visitors have only 1–3 events (highly sparse user history).9- Three event types: view (>95%), addtocart (<3%), transaction (<0.5%).10- Peak activity hours: 17:00–21:00.11- Session boundary: ~1 hour of inactivity.12"""13from __future__ import annotations14 15import pandas as pd16import numpy as np17 18from utils.logger import get_logger19 20logger = get_logger(__name__)21 22# Implicit feedback weights derived from EDA funnel analysis23EVENT_WEIGHTS = {"view": 1, "addtocart": 5, "transaction": 10}24 25 26def build_user_features(events: pd.DataFrame) -> pd.DataFrame:27 """Compute per-visitor aggregate features.28 29 Parameters30 ----------31 events:32 Raw events DataFrame with columns:33 [timestamp, visitorid, event, itemid, transactionid].34 35 Returns36 -------37 pd.DataFrame38 One row per visitor with columns:39 - visitorid40 - n_views, n_addtocart, n_transactions41 - n_unique_items42 - weighted_interaction_score43 - active_days44 - preferred_hour (mode hour of activity)45 - conversion_rate (transactions / views)46 - is_cold_start (True if only 1 interaction)47 """48 logger.info("Building user features from %d events …", len(events))49 50 df = events.copy()51 df["datetime"] = pd.to_datetime(df["timestamp"], unit="ms")52 df["hour"] = df["datetime"].dt.hour53 df["date"] = df["datetime"].dt.date54 df["weight"] = df["event"].map(EVENT_WEIGHTS).fillna(1)55 56 agg = df.groupby("visitorid").agg(57 n_views=("event", lambda x: (x == "view").sum()),58 n_addtocart=("event", lambda x: (x == "addtocart").sum()),59 n_transactions=("event", lambda x: (x == "transaction").sum()),60 n_unique_items=("itemid", "nunique"),61 weighted_interaction_score=("weight", "sum"),62 active_days=("date", "nunique"),63 preferred_hour=("hour", lambda x: x.mode()[0] if not x.empty else -1),64 first_seen=("datetime", "min"),65 last_seen=("datetime", "max"),66 ).reset_index()67 68 agg["conversion_rate"] = (69 agg["n_transactions"] / agg["n_views"].replace(0, np.nan)70 ).fillna(0.0)71 72 agg["is_cold_start"] = (73 agg["n_views"] + agg["n_addtocart"] + agg["n_transactions"]74 ) <= 175 76 logger.info(77 "User features built: %d users, %.1f%% cold-start.",78 len(agg),79 agg["is_cold_start"].mean() * 100,80 )81 return agg82 83 84def build_user_item_matrix(85 events: pd.DataFrame,86 event_weights: dict[str, int] | None = None,87) -> pd.DataFrame:88 """Build a sparse-friendly user × item interaction matrix.89 90 Returns a DataFrame in COO-style format (visitorid, itemid, score)91 suitable for passing to implicit ALS or SVD models.92 93 Parameters94 ----------95 events:96 Raw events DataFrame.97 event_weights:98 Override default EVENT_WEIGHTS mapping.99 100 Returns101 -------102 pd.DataFrame with columns [visitorid, itemid, score].103 """104 weights = event_weights or EVENT_WEIGHTS105 df = events.copy()106 df["score"] = df["event"].map(weights).fillna(1)107 108 matrix = (109 df.groupby(["visitorid", "itemid"])["score"]110 .sum()111 .reset_index()112 )113 logger.info(114 "User-item matrix: %d interactions across %d users × %d items.",115 len(matrix),116 matrix["visitorid"].nunique(),117 matrix["itemid"].nunique(),118 )119 return matrix120 