| """Trend features: slope of order frequency, rolling averages, spend change.""" |
|
|
| import numpy as np |
| import pandas as pd |
|
|
| from src.utils.logger import get_logger |
|
|
| logger = get_logger(__name__) |
|
|
| _N_TREND_PERIODS = 6 |
|
|
|
|
| def _ols_slope(values: np.ndarray) -> float: |
| """Return the OLS slope of a 1-D time series (no intercept shift needed).""" |
| n = len(values) |
| if n < 2: |
| return 0.0 |
| x = np.arange(n, dtype=float) |
| x -= x.mean() |
| y = values.astype(float) - values.mean() |
| denom = float((x * x).sum()) |
| if denom == 0.0: |
| return 0.0 |
| return float((x * y).sum() / denom) |
|
|
|
|
| def compute_trend_features( |
| orders: pd.DataFrame, |
| reference_date: pd.Timestamp, |
| n_periods: int = _N_TREND_PERIODS, |
| ) -> pd.DataFrame: |
| """Compute trend-based features for each customer. |
| |
| Features produced: |
| - order_frequency_slope β OLS slope of monthly order counts (last n_periods months) |
| - spend_slope β OLS slope of monthly spend |
| - rolling_avg_orders_3m β mean monthly order count over last 3 months |
| - spend_change_ratio β rolling_avg_orders_3m / (avg over previous 3 months + 1) |
| |
| Args: |
| orders: Full orders fact table. |
| reference_date: Upper bound for feature computation (exclusive). |
| n_periods: Number of monthly buckets used for slope computation. |
| |
| Returns: |
| DataFrame with one row per customer. |
| """ |
| hist = orders[orders["order_date"] < reference_date].copy() |
| hist["year_month"] = hist["order_date"].dt.to_period("M") |
|
|
| monthly = ( |
| hist.groupby(["customer_id", "year_month"]) |
| .agg(monthly_orders=("order_id", "count"), monthly_spend=("total_value", "sum")) |
| .reset_index() |
| ) |
|
|
| |
| all_months = pd.period_range( |
| end=reference_date - pd.Timedelta(days=1), periods=n_periods, freq="M" |
| ) |
|
|
| records: list[dict] = [] |
| for cust_id, grp in monthly.groupby("customer_id"): |
| ts = grp.set_index("year_month").reindex(all_months, fill_value=0) |
|
|
| order_vals = ts["monthly_orders"].to_numpy() |
| spend_vals = ts["monthly_spend"].to_numpy() |
|
|
| order_slope = _ols_slope(order_vals) |
| spend_slope = _ols_slope(spend_vals) |
|
|
| last_3 = float(order_vals[-3:].mean()) if len(order_vals) >= 3 else float(order_vals.mean()) |
| prev_3 = float(order_vals[-6:-3].mean()) if len(order_vals) >= 6 else 0.0 |
| spend_change_ratio = round(last_3 / (prev_3 + 1.0), 4) |
|
|
| records.append( |
| { |
| "customer_id": cust_id, |
| "order_frequency_slope": round(order_slope, 6), |
| "spend_slope": round(spend_slope, 6), |
| "rolling_avg_orders_3m": round(last_3, 4), |
| "spend_change_ratio": spend_change_ratio, |
| } |
| ) |
|
|
| result = pd.DataFrame(records) |
| logger.info(f"compute_trend_features: {len(result):,} customers") |
| return result |
|
|