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.