Skip to content

Preprocessing

polars_ts.applications.perishables.preprocessing

Growth-aware preprocessing for rapidly scaling perishables demand.

A double-digit YoY growth business breaks the stationarity assumption of most models: two-year-old observations describe a much smaller company. Three complementary levers are provided:

  • Growth detrending — estimate a per-SKU exponential growth rate and rescale history to today's level (and re-apply it to forecasts).
  • Recency weighting — exponential-decay sample weights for models that accept them.
  • Adaptive windowing — per-SKU training-window length chosen so the level drift across the window stays below a tolerance; fast-growing SKUs train on less (but recent) history.

fit_growth(df, id_col='unique_id', time_col='ds', target_col='y', smooth_window=28)

Estimate a per-SKU daily exponential growth rate.

Fits ordinary least squares of log(rolling_mean(y) + eps) against time (in days) per SKU. The rolling mean smooths over zero-demand days so intermittent SKUs get a level trend, not noise.

Parameters

df Gap-free canonical sales frame. id_col, time_col, target_col Canonical column names. smooth_window Rolling-mean window (days) applied before the log-linear fit.

Returns

pl.DataFrame Columns [id_col, "daily_growth"] where daily_growth is the fitted slope of the log-level per day (0.0 for SKUs with fewer than smooth_window observations).

remove_growth(df, growth, id_col='unique_id', time_col='ds', target_col='y')

Rescale history to today's demand level.

Each observation is multiplied by exp(g * (t_last - t)) so that a value observed a year ago on a SKU growing at rate g is expressed at the level the SKU sells at now. Models then see an (approximately) level-stationary series.

Parameters

df Gap-free canonical sales frame. growth Output of :func:fit_growth. id_col, time_col, target_col Canonical column names.

Returns

pl.DataFrame Input frame with target_col replaced by its level-adjusted value and the original preserved as "{target_col}_raw".

reapply_growth(forecast, growth, last_date=None, id_col='unique_id', time_col='ds', target_col='y_hat')

Project the fitted growth back onto level-space forecasts.

Parameters

forecast Forecast frame with [id_col, time_col, target_col] produced from growth-removed history. growth Output of :func:fit_growth. last_date Optional [id_col, time_col] frame with each SKU's last training date (defaults to one day before the first forecast date per SKU, which is correct for gap-free daily data). id_col, time_col, target_col Column names.

Returns

pl.DataFrame Forecast frame with target_col scaled by exp(g * days_ahead).

recency_weights(df, half_life=90.0, id_col='unique_id', time_col='ds', weight_col='weight')

Add exponential-decay sample weights favouring recent history.

Parameters

df Canonical sales frame. half_life Age in days at which a sample's weight halves. id_col, time_col Canonical column names. weight_col Name of the added weight column.

Returns

pl.DataFrame Input frame with weight_col in (0, 1] (1 at each SKU's most recent date).

select_adaptive_window(growth, tolerance=0.25, min_days=56, max_days=730, id_col='unique_id')

Choose a per-SKU training-window length from its growth rate.

The window is the longest span over which the fitted exponential level drifts by at most tolerance (25% by default): window = ln(1 + tolerance) / |daily_growth|, clamped to [min_days, max_days]. Stable SKUs keep long histories; SKUs doubling every few months train on short, recent windows.

Parameters

growth Output of :func:fit_growth. tolerance Maximum tolerated relative level drift across the window. min_days, max_days Window clamp bounds in days. id_col SKU column name.

Returns

pl.DataFrame Columns [id_col, "window_days"].

apply_training_window(df, windows, id_col='unique_id', time_col='ds')

Trim each SKU's history to its adaptive training window.

Parameters

df Canonical sales frame. windows Output of :func:select_adaptive_window. id_col, time_col Canonical column names.

Returns

pl.DataFrame Rows within window_days of each SKU's last date; SKUs absent from windows are kept in full.