Forecasting · Feature Store

A Feature Store & Time-Series Architecture for Demand Forecasting

Accurate demand forecasting is what lets a business stock the right amount, in the right place, at the right time. The modelling gets the attention, but in practice the forecast is only as good as the features behind it — and the single most common way forecasting projects fail is feeding models information that wouldn't have been available at prediction time. This article covers the feature-store and time-series architecture we use to forecast demand reliably and at scale, and how it pairs with the MLOps platform that operates the resulting model fleet.

The architecture

Forecasting features come from many sources at different cadences: transactional sales, slow-moving product attributes, and external regressors like weather and holidays. A feature store unifies them with two guarantees that matter more than anything else — point-in-time correctness for training and train/serve parity for inference.

POS / salestransactions
Feature pipelineslags, windows
Feature storeoffline + online
Weather / eventsexternal
Calendar builderholidays, promos
— join —as-of key
Trainquantile GBM
Reconcilehierarchy
Forecast table+ intervals
Internal and external signals are engineered into a feature store, then consumed identically for training and serving.

Point-in-time correctness, by construction

If today's forecast model trains on a sales figure that was only finalised days after the target date, it learns a relationship it can never exploit in production — and backtests look great while live accuracy collapses. The feature store enforces an as-of join: for a label at time t, every feature is read at its value as known at t. Getting this right once, in the platform, removes the most damaging class of forecasting bug for every model that uses it.

Defining features with event timestamps for as-of joins

from feast import FeatureView, Field
from feast.types import Float32, Int64

sales_fv = FeatureView(
    name="store_item_sales",
    entities=[store_item],                 # composite key: (store, item)
    ttl=timedelta(days=90),
    schema=[
        Field(name="sales_lag_7",   dtype=Float32),
        Field(name="sales_lag_28",  dtype=Float32),
        Field(name="roll_mean_28",  dtype=Float32),
        Field(name="roll_std_28",   dtype=Float32),
        Field(name="dow",           dtype=Int64),
    ],
    online=True, source=sales_source,       # event_timestamp drives as-of joins
)

# training pull is point-in-time correct against the label timestamps
train_df = store.get_historical_features(
    entity_df=labels[["store", "item", "event_timestamp", "y"]],
    features=["store_item_sales:sales_lag_7", "store_item_sales:roll_mean_28",
              "calendar:is_holiday", "weather:tmax"]).to_df()

Features that actually move demand

Three families do most of the work, and a feature store makes them reusable across every model:

Quantile models for decisions, not just point forecasts

Planning needs ranges, not a single number — a stockout and an overstock have very different costs. We train gradient-boosted quantile models (e.g. LightGBM with a pinball objective) to predict the median and the upper/lower quantiles directly, giving calibrated prediction intervals the planning system can turn into safety stock. GBMs handle the mixed numeric/categorical feature space, missing values, and non-linear calendar interactions without heavy preprocessing.

Quantile forecasting with backtest validation

import lightgbm as lgb

def fit_quantiles(df, quantiles=(0.1, 0.5, 0.9)):
    models = {}
    for q in quantiles:
        models[q] = lgb.LGBMRegressor(objective="quantile", alpha=q,
                                      n_estimators=500, learning_rate=0.05,
                                      num_leaves=63).fit(df.X, df.y)
    return models

def backtest(df, horizon=28):                  # rolling-origin evaluation
    errs = []
    for cutoff in expanding_cutoffs(df, folds=5):
        m = fit_quantiles(df[df.t <= cutoff])
        pred = m[0.5].predict(df[(df.t > cutoff) & (df.t <= cutoff + horizon)].X)
        errs.append(smape(actuals(cutoff, horizon), pred))
    return float(np.mean(errs))
Validate with rolling origins, never a random split. Shuffling time-series rows leaks the future into training and produces backtests that look excellent and fail in production. Expanding-window, rolling-origin evaluation mirrors how the model is actually used — always predicting forward from a cutoff.

Hierarchical reconciliation

Demand is forecast at many levels — item, store, region, total — and independent forecasts at each level won't sum up consistently. The sum of store forecasts rarely equals the regional forecast. Reconciliation (e.g. MinT) adjusts the whole hierarchy so forecasts are coherent across levels, which is essential when different planning teams consume different aggregations and need them to agree.

Serving and the path to scale

Forecasts are written to a date- and key-partitioned table with their prediction intervals, refreshed on the planning cadence; the online store serves the same feature definitions for any series that needs intra-day updates. From here, the templated pipeline runs across hundreds of thousands of series — the subject of our MLOps-at-scale article — with the feature store guaranteeing every one of them gets correct, consistent inputs.

What this delivers

The result is sharper, calibrated forecasts that planning systems can trust, free of the leakage and skew bugs that quietly wreck forecasting projects — and a feature foundation that every model in the fleet inherits for free. Demand predicted per store, per product, per day becomes a reliable input to inventory, staffing, and speed-to-service rather than a hopeful guess.

All insights