Most forecasting APIs ask you to flatten your world (ours did too, until today!). You take whatever lives in your warehouse (stores, SKUs, regions, daily and weekly grains, gaps and all), collapse it into anonymous arrays, and re-upload the whole thing on every call. That’s the “before”; drag the divider for a taste of the “after”:
{
"series": [
{ "index": ["2022-01-01", ...], "values": [142, 137, ...] },
{ "index": ["2024-01-01", ...], "values": [90, 84, ...] }
],
"horizon": 28
}
The pipeline to get to this query payload{
"input": <the schema you write once>,
"selector": { "daily": "any", "sku": "any", "store": "any" },
"prediction_length": 28
}
The pipeline to get to this query payloadIn the “before”, each series is a bare list of numbers with dates attached. Position in the list is the only identity: nothing says which store, which product, which city, or how one series relates to another. Your data has structure. It’s the first thing this shape throws away, leaving you to rebuild it by hand, in glue code, before every request.
Our beta API addresses this issue. You describe your data; we do the reshaping. You hand us a schema that says what your columns mean, and a selector that says which slice you want to forecast. Everything else — aggregating or interpolating to the right grain, filling gaps, rolling up a hierarchy, running a backtest across dozens of dates or a bulk forecast — folds in one description, automatically handled by our timeseries engine.
The rest of this post runs the whole loop on a tiny retail example: a few SKUs, a few stores, a few cities.
Describe a family once
So what do you describe, exactly? Not just one timeseries: a real dataset is a family of them, meaning you have many related timeseries that live together. Your sales spans multiple stores, multiple SKUs, multiple categories, maybe is transactional… We call this bag of timeseries a timeseries family.
Here’s the kind of raw, long-format table that lands in your data lake:
▸View data table
| date | sku | color | product_family | store | city | sales |
|---|---|---|---|---|---|---|
| 2024-01-01 | SKU_1 | black | shoes | S1 | Paris | 100 |
| 2024-01-01 | SKU_1 | black | shoes | S2 | Lyon | 80 |
| 2024-01-01 | SKU_2 | red | shirts | S1 | Paris | 60 |
| 2024-01-02 | SKU_1 | black | shoes | S1 | Paris | 102 |
| 2024-01-02 | SKU_1 | black | shoes | S2 | Lyon | 78 |
| 2024-01-02 | SKU_2 | red | shirts | S1 | Paris | 55 |
| … | … | … | … | … | … | … |
With our API v1, instead of reshaping this dataset into flat timeseries, you just describe it. Assign each column a role, say how each signal behaves when it’s aggregated or imputed, and declare how your identifiers roll up:
{
"input": {
"source": { "uri": "s3://acme/retail/sales/*.parquet" },
Thesourceyour data already lives somewhere — no need to serialize megabytes into a request body. Inline arrays, Parquet on object storage, a glob across many files; more connectors on the way. "columns": {
"date": { "kind": "time", "frequency": "1d" },
"sku": "identifier",
"color": "identifier",
"product_family": "identifier",
"store": "identifier",
"city": "identifier",
"sales": { "kind": "target",
"aggregate": "sum",
"impute": { "const": 0 } }
},
"hierarchies": {
"product": [["sku", "color"], ["sku", "product_family"]],
"geography": [["store", "city"]]
}
},
"selector": { "product_family": "any", "city": "any" },
"prediction_length": "3w"
}
Read the hierarchies block as aggregation paths, grouped into dimensions you
name. In this example, the most granular level you have in your product
dimension is SKU. For the geographical dimension, that’s the level
store.
The hierarchies ["sku", "color"] and ["sku", "product_family"] mean that
you can either aggregate multiple SKUs over their color, or over their
product_family. Also, individual stores aggregate into cities.
The selector is the dial: it picks which level you want to forecast. Click
around and watch the request change:
sku rolled up to product_family and store rolled up to city, the selector in the request above gives one forecast per (day, product_family, city).{
"input": {
"source": "s3://acme/retail/sales/*.parquet",
"columns": { ... },
"hierarchies": {
"product": [["sku", "color"], ["sku", "product_family"]],
"geography": [["store", "city"]]
}
},
"selector": {
"product_family": "any",
"city": "any"
},
"prediction_length": "3w"
}
Aggregate SKUs up to families, stores into cities, days into weeks or months,
and you never hand-roll that GROUP BY again. Missing days, mixed grains,
ragged history: you declared how to handle them once, in the schema above.
One query to rule them all 💍: full backtest in one go
Because you described a family, a single query fans out into a forecast for every timeseries it contains: every family, every city, in one request.
Once the API understands your data’s shape, backtesting is another parameter, not another pipeline. A backtest is just that forecast re-run from many points in time, so say so:
{
"input": {
"source": { "uri": "s3://acme/retail/sales/*.parquet" },
"columns": {
"date": { "kind": "time", "frequency": "1d" },
"sku": "identifier",
"color": "identifier",
"product_family": "identifier",
"store": "identifier",
"city": "identifier",
"sales": { "kind": "target",
"aggregate": "sum",
"impute": { "const": 0 } }
}
},
"selector": { "product_family": "any", "city": "any" },
"context": "90d",
"prediction_length": "28d",
"cutoff": [{ "every": "2w" }],
"quantiles": [0.1, 0.5, 0.9],
"compute_metrics": true
}
"cutoff": [{ "every": "2w" }] re-runs the forecast from a cutoff every two
weeks, across every timeseries, with 90 days of context each time. You get back
accuracy metrics and quantile forecasts for each cutoff date — a full rolling-origin
evaluation from the request you were already going to send. Pass a list of
explicit dates instead, and you control exactly when each forecast is made.
Each cutoff reuses the same request shape: historical cutoffs become backtests,
and future-facing cutoffs become forecasts. Two more knobs shape each run:
lead_time inserts a gap between the cutoff and the first predicted day (you
place Friday’s order for a week that starts Monday), and span sets how much
time each forecast value covers (daily history in, weekly values out). Expand
the controls to see how cutoff, lead_time, span, and prediction_length
carve up the timeline:
Hover a forecast to isolate it: each one sees only the context behind its own cutoff.
Forecasts you can keep
With our API v1, you get forecasts in a long-format table, shaped like what went in: one row for each predicted time of each series.
Each row answers three questions:
- Which timeseries is this? The row has one column per
identifier. For instance
cityorsku. - What is the period forecasted by this row? The row has one
timecolumn for the target datetime, and onespancolumn. For instance a forecast for the first week of 2026 hastime2026-01-05andspan1 week. - What knowledge was used for this forecast? The row has one
cutoffcolumn that says the last instant the forecast saw, and alead_timeduration that says how far this row is past the first forecastable datetime.
This shape is a context-free format for storing forecasts. Any level of
aggregation in the hierarchy, any information cutoff date, any horizon can fit
in the same format with the same columns. One table with no metadata sidecar
to tell them apart, and GROUP BY cutoff, span pulls each forecast back out.
The table below presents the output from two runs over the same data, interleaved (one daily, one weekly):
cutoff, lead_time, and span make every row self-describing. Hover a row to isolate its run.| predicted period | lead_time | cutoff | which timeseries | target: sales | ||||
|---|---|---|---|---|---|---|---|---|
| time | span | product_family | city | q0.1 | q0.5 | q0.9 | ||
| 2024-01-03 | 1d | 0d | 2024-01-02 | shoes | Paris | 99.7 | 100.9 | 103.3 |
| 2024-01-03 | 1d | 0d | 2024-01-02 | shoes | Lyon | 76.6 | 78.5 | 81.5 |
| 2024-01-03 | 1d | 0d | 2024-01-02 | shirts | Paris | 51.7 | 56.2 | 63.5 |
| … | … | … | … | … | … | … | … | … |
| 2024-01-08 | 1d | 5d | 2024-01-02 | shoes | Paris | 99.7 | 100.8 | 103.3 |
| 2024-01-08 | 1w | 0d | 2024-01-01 | shoes | Paris | 99.3 | 100.0 | 101.6 |
| 2024-01-08 | 1w | 0d | 2024-01-01 | shirts | Paris | 59.3 | 60.0 | 61.6 |
| … | … | … | … | … | … | … | … | … |
| 2024-01-15 | 1w | 1w | 2024-01-01 | shoes | Lyon | 79.3 | 80.1 | 82.2 |
| … | … | … | … | … | … | … | … | … |
cutoff and span to pull them apart7 of the runs' 72 rows shownMetrics arrive the same way — one row per series: CRPS, WAPE, bias — and it all lands in your lake next to the actuals. No glue code on the way out either.
Coming soon: attach covariates, forecast among peers
The two ideas below aren’t live yet, but you’ll get both for free: they only need the family description you already wrote.
Attach what the world knows
Your timeseries alone is often just one part of the story, the other parts live outside your sales table: promotions, prices, holidays, weather, events. Those external parts help explain why that SKU sold so well last March.
Turns out, a covariate is just a timeseries family like any other. All we need is to be able to attach timeseries families together. This can be done by declaring how their hierarchies match: joining a store-level timeseries with a city-level timeseries is possible because (1) we know which store is in which city, and (2) we know how to aggregate from the store level to the city level.
Your promotions calendar and your price list are families you already own: attach them and they stay yours, as private as the rest of your data. Public data is different: nobody should maintain a holiday calendar twice! So the world’s families will sit in a shared catalog, for you to pull:
"covariates": [
"soon://timeseries-lake/holidays?country=FR",
"soon://timeseries-lake/weather?city=Paris&frequency=weekly"
]
Surprisingly, soon:// doesn’t resolve yet, and probably needs a rename.
Forecast among peers
Much of the signal for forecasting one timeseries comes from its relatives (it’s a timeseries family, after all). A newly launched product has no history of its own, but its predecessors do. So instead of forecasting every timeseries in isolation, you can pair each target with the peers that should inform it: a small map from one selector to another.
Forecasting {"sku": ["jPhone10"]}? Let it borrow from {"sku": ["jPhone8", "jPhone9"]}. A fresh store leans on the established ones nearby. This season’s
launch leans on the last two generations. Each target points at its own
hand-picked peers, and the model sees them alongside the target at request time:
no fine-tuning, no separate training job.
Describe once, forecast anything
Describe your data where it lives, once and for all, and query forecasts or backtests at any aggregation level, soon with covariates and peers in context.
This API is in beta. Grab an API key, describe the data you already have, and forecast any slice of it you can name.
Questions or feedback? Reach us at support@theforecastingcompany.com.