Nori V1 — Replaces XGBoost

AI & Machine LearningTime Series10 min read

Better Cold-Start Forecasting with Nori: New and Short-History SKUs

Forecasting a product that has no sales history of its own, by borrowing demand patterns from the SKUs it resembles.

#Cold Start#Demand Forecasting#Tabular Foundation Models#In-Context Learning#Feature Engineering
Better Cold-Start Forecasting with Nori: New and Short-History SKUs

The cold-start problem

New products are the hardest thing to forecast, because they have no sales history to forecast from. Products with only a few months of sales have the same problem to a lesser degree: a few noisy months isn't enough to separate trend, seasonality, and level.

Why it has been hard

Forecasting a Product That Has Never Sold Anything
Looking at one product on its ownmonthly salesnext monthProduct Aselling for two years?we can estimate it from its own pastProduct Bselling for two years?we can estimate it from its own pastNew productlaunches this monthno sales yet?there is no past to estimate fromthis is how most forecasting works: each product is predicted from its own sales, and nothing elseNorilooks at every product at onceProduct Aselling for two yearsProduct Bselling for two yearsNew productlaunches this monthno sales yetthe new product's number comes fromhow similar products have soldnothing about the new product changed. what changed is that the other products are in the room

Figure 1Why a new product breaks the usual approach. Forecast a product from its own sales alone and two years of history is plenty. Ask the same method for a product that has never sold anything and there is nothing to fit at all, so the number falls back to a guess. Nori reads every product at once, and it is that cross-product learning that makes the last row answerable: the new product's forecast comes from how comparable products have already sold.

The reason is simple. The per-series models most teams run (ARIMA, ETS, Prophet) are fit one per product, so each one only ever sees that product's own sales. Two years of sales and it can separate trend from seasonality. Two months and it is mostly fitting noise. None at all and there is nothing to fit, so the forecast falls back to a manual guess or a category average. What would actually help, how comparable products behaved when they launched, sits in series the model never looks at.

Throughout this post we use the public Online Retail II dataset (UCI): two years of a UK online gift retailer, with products that first appear on different dates, so it has genuine new and short-history products. Demand is strongly seasonal (a gift shop peaks at Christmas), which sharpens the problem, because a product launched in autumn has no December of its own to learn from.

How Nori changes it

Nori reads the whole catalog at once, so a product with no past of its own can be forecast from the products it resembles. It is a tabular foundation model that does regression by in-context learning: you give it a table of examples at inference time and it predicts the unlabeled rows, with no training step and no per-product fitting. Nothing about the new product changed. What changed is that the other products are in the table with it.

The call to Nori is two lines. You pass the labeled rows as context and the unlabeled rows as queries:

Python
1from synthefy_nori import NoriRegressor
2
3model = NoriRegressor(model="nori-6m")
4model.fit(X_context, y_context)      # labeled rows (established + the new product)
5predictions = model.predict(X_new)   # unlabeled rows (the months to forecast)

There's no training step here. With XGBoost you'd fit a model on the catalog, tune hyperparameters, and retrain whenever the data changes. With SARIMAX you'd fit a separate model per product, which for a brand-new one means you can't even start. Nori skips all of that: the labeled rows are the model's context, not its training data, so adding a new product or a new month of history is just adding rows to the table.

The call never changes. Every decision that affects forecast quality has moved into X_context instead: which rows you hand Nori, and which columns describe them.

Building the context

Because Nori learns from the rows you give it at inference time, improving a cold-start forecast is less about model tuning and more about what you put in the table. This is the tabular equivalent of context engineering for LLMs: you're optimizing the input, not the model. The better the context rows represent the new SKU's situation, the better the prediction.

Good demand planners have been doing exactly this for decades, without calling it that. The discipline has a whole toolkit for the no-history problem: forecast the item from its attributes instead of its past, since pack count, price tier, and design theme are known on day one; borrow a launch curve from how comparable products ramped over their first few months; net out cannibalization when a new variant eats a sibling's volume; scale by planned distribution, because demand follows how many stores actually stock the product. Every one of those is a claim about which products a new SKU should be read alongside — and every one is a column you can put in the table.

X_context starts as the pooled table from above, one row per (SKU, month) with every product in the catalog stacked together, and then gains a column per feature. A new SKU's own history is nearly empty, so the features that carry its forecast have to describe the products it resembles instead. We build them in two blocks.

Why a New SKU Breaks Per-Series Forecasting
Classicalown historyone cell = one monthnext monthSKU A?a model fit on SKU A aloneforecastSKU B?a model fit on SKU B aloneforecastSKU Conly 2?a model fit on SKU C alonepoor forecastSKU Dno history?nothing to fitno forecasteach model sees one SKU, so a short or missing past is all it has to fitNoriown historysame as abovecategory + calendar featuresbuilt from the other SKUsnextmonthSKU A?SKU B?SKU Conly 2?SKU Dno history?every SKU's past demand is pooled into those shared columns, which is what a SKU with no past of its own readsonetableNorireads across all of itforecastforecastforecastforecaststill the hardest case

Figure 2The same comparison as Figure 1, now with the columns Nori actually reads. Above, one model is fit per SKU: six months of history forecasts, two months forecasts badly, and no history has nothing to fit at all. Below, the same four SKUs sit in one table carrying a second block of columns, category and calendar features pooled from every SKU's past demand, so they are populated even on the row with no past of its own. One call returns a forecast for all four, though a cold SKU is still the hardest case.

Category and calendar features

These two blocks are the cold-start enabler: the calendar features are properties of the date, and the category features come from the SKU's neighbors, so both are fully populated for a product with no history of its own.

Python
1# `rows` is the monthly panel joined to its per-category aggregates, which are
2# already shifted so a row only ever sees earlier months.
3rows = panel.merge(category_monthly, on=["category", "month"], how="left")
4by_sku = rows.groupby("sku")
5
6# ---- calendar: properties of the date, so known for any row, cold or not ----
7m = rows["month"].dt.month
8feat["fcst_month_sin"] = np.sin(2 * np.pi * m / 12)   # Dec and Jan are neighbors,
9feat["fcst_month_cos"] = np.cos(2 * np.pi * m / 12)   # not 12 apart
10
11# ---- category: years of history even when the SKU itself has none ----
12feat["cat_units_lag1m"] = rows["cat_units_lag1m"]     # the category last month
13feat["cat_units_lag12m"] = rows["cat_units_lag12m"]   # the seasonal anchor
14feat["cat_yoy_lagged"] = rows["cat_yoy_lagged"]       # growing or shrinking
15
16# a cold SKU has no price history, so this falls back to the category median
17feat["price_vs_cat_lagged"] = (by_sku["avg_price"].shift(1)
18                               / rows["cat_price"]).clip(0.2, 5.0).fillna(1.0)

Own-history features (1+ months of history)

Once a SKU has a month of sales its own past becomes usable: demand 1, 2, 3, 6, 12 and 13 months back, a trailing three-month mean, a year-over-year ratio, and a seasonal-naive forecast built from the last two. Four launch-and-maturity columns (months_since_launch, launch_units, peak_units_lagged, cat_share_lag1m) fill in alongside them. All of it is computed in log-ratio space (log1p(units) - log1p(scale)), so a 10-unit SKU and a 10,000-unit SKU produce comparable values. For a cold row every one of these is zero, which is why the category and calendar columns have to carry the forecast.

Stacking the blocks, then predicting

Stack the two blocks horizontally and that is X_context: 25 columns, one row per (SKU, month), every product in the catalog in the same table. Nori forecasts from it as it stands.

XGBoost needs a tuning step first, because a foundation model beating an untuned GBDT would not be worth writing about. We searched a 576-config grid on an earlier window, so the test months never inform the choice, and a well-regularized default turned out to be hard to beat. That default is the baseline we report; the notebook has the search.

The split is by date, not by row sampling: everything strictly before the forecast month is context, and the forecast month itself is what you ask for. Both models get the same columns, the same rows and the same split. One stores the table and reads it; the other fits parameters to it.

Two brand-new wrap designs, forecast against actual
actualNori (P50)XGBoost
SKU 23549 · Wrap Bird Garden
launch WAPE Nori 25% vs XGBoost 50%
full cycle Nori 21% vs XGBoost 31%
launch (h=0)0125250375500unitsJulh=0Augh=1Seph=2Octh=3Novh=4
SKU 23547 · Wrap Flower Shop
launch WAPE Nori 10% vs XGBoost 41%
full cycle Nori 35% vs XGBoost 36%
launch (h=0)0125250375500unitsJulh=0Augh=1Seph=2Octh=3Novh=4

Figure 3The two new wrap designs, forecast vs actual. Each point is a separate one-step-ahead forecast at that month's origin; h counts the months of history the SKU had at the time, not a forecast horizon. In the shaded launch month, with no history whatsoever, Nori is off by 25% on one and 10% on the other, against XGBoost's 50% and 41%. Over the full cycle Nori holds the lead on Wrap Bird Garden (21% vs 31%) and draws on Wrap Flower Shop (35% vs 36%); the launch month is where the gap is real.

Proof: the full backtest

The two SKUs above are anecdotes. The full comparison is the whole catalog: 16,203 forecasts over a 6-origin rolling window (June–November 2011), scored by WAPE, where a lower number is a better forecast. The heuristic column is a category-mean baseline, roughly the fallback a team reaches for when there's no history to fit.

One practical note, because it's usually the first question: Nori takes the entire prior panel as context (47,603 rows at the June origin, growing to 60,883 by November), with no subsampling. All six passes run in a few minutes on a single GPU. XGBoost retrains from scratch at every origin; Nori's fit just stores the rows.

The full rolling backtest, and the featurization behind it, are in the accompanying notebook.

Forecast Error by Cohort
HeuristicXGBoost (pooled)Nori (P50)WAPE %, lower is better
0255075100WAPE %102.0104.293.7Brand-new (cold)n = 44776.751.850.8Short (1–11 months)n = 3,22266.652.351.6Established (≥12 months)n = 12,53470.253.652.6Alln = 16,203

Figure 4Forecast error by cohort, pooled over the six rolling origins. Lower is better. Nori is ahead of both baselines in every cohort, and the margin is widest exactly where there is no history to fit: on brand-new SKUs the tuned XGBoost (104.2%) is beaten by the naive category-mean heuristic (102.0%), while Nori lands at 93.7%.

The same cohorts, month by month
Nori (P50)XGBoostWAPE %, lower is better · each panel has its own scale
Brand-new (cold) (n = 447)
overall: Nori 93.7% · XGBoost 104.2%
8095110125JunJulAugSepOctNov
Short (1–11 months) (n = 3,222)
overall: Nori 50.8% · XGBoost 51.8%
4246505458JunJulAugSepOctNov
Established (≥12 months) (n = 12,534)
overall: Nori 51.6% · XGBoost 52.3%
46505458JunJulAugSepOctNov

Figure 5The same three cohorts as Figure 4, broken out by month; each panel's overall number is the bar from that chart. Nori is ahead in four of the six months in every cohort (not a clean sweep), and June is the one month it loses across all three, presumably because it starts with the least history behind it.

What to try next

  • Ask for the interval, not just the point. output_type="quantiles" returns P10/P50/P90 from the same call, no extra machinery. On a launch the spread is the more useful output: a band that is honestly wide tells a planner something a single number cannot.
  • Encode planner intuition as a number. Cold-start forecasting is usually done by a person, weighing how similar a new item is to ones they already know. If that judgment can be turned into a numeric feature (some measure of similarity or expected resemblance to existing products), the model can use it alongside everything else in the table.
  • Feed in next month's price if you set it yourself. It's exogenous, not a look into the future, since a planned price is already decided before the month starts (everything else in this post assumes you know nothing about the forecast month but the calendar, which is why a cold SKU's price feature falls back to the category median). Elasticity is real signal, and it costs one column with no change to the call: drop it in unlagged and see what it moves.
  • Try a different size. Swap model="nori-30m" into the same call for the larger checkpoint, same two lines, no other changes needed. Thinking variants, for more test-time compute than a single forward pass, run on the hosted API only, not the local package used in this post. See the quickstart for both.

Beyond retail and e-commerce

Although this post focuses on new SKUs, the cold-start problem appears anywhere a new entity must be forecast before it has accumulated enough history of its own. The same approach, borrowing patterns from comparable entities and combining them with the limited features available at launch, can apply across industries.

In energy and utilities, this could mean forecasting power consumption or grid load for a newly connected building, or estimating generation from a newly installed solar array. In digital media and technology, it could mean predicting early web traffic, adoption metrics, or network resource utilization for a newly deployed cloud service or web page.

These are only a few possibilities. We're always interested in learning about novel ways teams are using Nori. Join us on Discord and tell us what you're building.

Get started

Questions? contact@synthefy.com