| """Time-based feature computation: rolling windows, inter-order stats.""" |
|
|
| import pandas as pd |
|
|
| from src.config import FEATURE_WINDOWS |
| from src.utils.logger import get_logger |
|
|
| logger = get_logger(__name__) |
|
|
|
|
| def compute_time_features( |
| orders: pd.DataFrame, |
| reference_date: pd.Timestamp, |
| windows: list[int] = FEATURE_WINDOWS, |
| ) -> pd.DataFrame: |
| """Compute time-based features per customer. |
| |
| Features produced: |
| - avg_inter_order_days β mean gap between consecutive orders |
| - std_inter_order_days β std dev of that gap (0 for single-order customers) |
| - orders_last_Nd β order count in the last N days (per ``windows``) |
| - spend_last_Nd β total spend in the last N days |
| - ratio_recent_historical β orders_last_30d / (orders_last_90d + 1) |
| |
| Args: |
| orders: Full orders fact table. |
| reference_date: Features are computed on orders strictly before this date. |
| windows: List of rolling-window sizes in days. |
| |
| Returns: |
| DataFrame with one row per customer. |
| """ |
| hist = orders[orders["order_date"] < reference_date].sort_values(["customer_id", "order_date"]) |
|
|
| |
| hist = hist.copy() |
| hist["prev_date"] = hist.groupby("customer_id")["order_date"].shift(1) |
| hist["inter_days"] = (hist["order_date"] - hist["prev_date"]).dt.days |
|
|
| inter_stats = ( |
| hist.groupby("customer_id")["inter_days"] |
| .agg(avg_inter_order_days="mean", std_inter_order_days="std") |
| .reset_index() |
| ) |
| inter_stats["avg_inter_order_days"] = inter_stats["avg_inter_order_days"].fillna(0).round(2) |
| inter_stats["std_inter_order_days"] = inter_stats["std_inter_order_days"].fillna(0).round(2) |
|
|
| result = inter_stats.copy() |
|
|
| |
| for w in sorted(windows): |
| w_start = reference_date - pd.Timedelta(days=w) |
| window_orders = hist[hist["order_date"] >= w_start] |
|
|
| cnt = window_orders.groupby("customer_id").size().reset_index(name=f"orders_last_{w}d") |
| spend = ( |
| window_orders.groupby("customer_id")["total_value"] |
| .sum() |
| .reset_index() |
| .rename(columns={"total_value": f"spend_last_{w}d"}) |
| ) |
|
|
| result = result.merge(cnt, on="customer_id", how="left") |
| result[f"orders_last_{w}d"] = result[f"orders_last_{w}d"].fillna(0).astype(int) |
|
|
| result = result.merge(spend, on="customer_id", how="left") |
| result[f"spend_last_{w}d"] = result[f"spend_last_{w}d"].fillna(0.0).round(2) |
|
|
| |
| if "orders_last_30d" in result.columns and "orders_last_90d" in result.columns: |
| result["ratio_recent_historical"] = ( |
| result["orders_last_30d"] / (result["orders_last_90d"] + 1) |
| ).round(4) |
| else: |
| result["ratio_recent_historical"] = 0.0 |
|
|
| logger.info(f"compute_time_features: {len(result):,} customers, windows={windows}") |
| return result |
|
|