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.
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:
- Autoregressive — lags (7, 28), rolling mean/std, and trend over recent windows capture the series' own momentum.
- Calendar — day-of-week, holidays, promotional flags, and paydays; these explain most of the predictable spikes operators care about.
- External regressors — weather, local events, and traffic. On the QSR program, weather and holidays were decisive for per-store, per-protein demand.
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))
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