Grouped Forecasting

Forecast a set of related series that roll up to a total — MRR by plan and region, demand by warehouse, revenue by category — and get back one coherent result: every parent equals the sum of its children, in every period, at every level.

Why a dedicated endpoint

Forecast five segments and the total independently and the numbers won't add up — each series picks its own model and trend. That's fine for exploration and embarrassing in a board deck. Grouped forecasting reconciles the whole hierarchy so the parts sum to the whole by construction.

How it works

You send the leaves of your hierarchy — every combination of the dimensions you declare — each with its own history. The API derives every aggregate level (each region, and the grand total) by summing the leaf histories, forecasts the nodes, reconciles them to coherence, and stores each node as its own entity with independently tracked accuracy.

Processing is asynchronous: submission returns 202 with an id to poll, and the run executes in the background as one all-or-nothing job. A hierarchy is one coherent answer, so there is no partial success — if any node cannot be forecast, the run fails with the reason rather than returning numbers that don't add up.

Submit a hierarchy

POST /v2/forecast/grouped — the identifier names the measure, and each series carries a structured segment naming exactly the declared hierarchy dimensions. segment is independent of tenant_context: tenancy keeps meaning "whose data is this", segment means "which slice".

{
  "identifier": "mrr",
  "hierarchy": ["region", "plan"],
  "reconciliation": "bottom_up",
  "frequency": "M",
  "periods": 6,
  "data_type": "revenue",
  "series": [
    {"segment": {"region": "eu", "plan": "pro"},
     "data": [{"date": "2024-01-01", "value": 42000}, {"date": "2024-02-01", "value": 43800}]},
    {"segment": {"region": "eu", "plan": "free"},
     "data": [{"date": "2024-01-01", "value": 3100}, {"date": "2024-02-01", "value": 3300}]},
    {"segment": {"region": "us", "plan": "pro"},
     "data": [{"date": "2024-01-01", "value": 61000}, {"date": "2024-02-01", "value": 62500}]}
  ]
}
{
  "grouped_forecast_id": "k3n9x2m4p8q1w5r7",
  "status": "pending",
  "series_count": 3,
  "node_count": 6,
  "reconciliation": "bottom_up",
  "time_taken_ms": 41.2
}

node_count is what will actually be forecast and reconciled: the leaves plus every derived aggregate and the total. The three series above expand to six nodes — total, two regions, three leaves. A request may expand to at most 250 nodes.

Poll for the result

GET /v2/forecast/grouped/{id} returns the run's status, and once completed, a node map keyed by segment (total, region=eu, plan=pro|region=eu, …). Every node carries its own forecast rows, its place in the tree, and the reconciliation details.

{
  "grouped_forecast_id": "k3n9x2m4p8q1w5r7",
  "status": "completed",
  "node_count": 6,
  "results": {
    "identifier": "mrr",
    "hierarchy": ["region", "plan"],
    "reconciliation": {"method": "bottom_up", "correlation": 0.5, "correlation_source": "default", "measured": false},
    "confidence_level": 0.8,
    "nodes": {
      "total": {
        "segment": {},
        "parent": null,
        "children": ["region=eu", "region=us"],
        "forecasts": [{"period": 1, "date": "2024-07-01", "forecast": 118400.0, "lower": 112300.5, "upper": 124499.5}]
      },
      "region=eu": {
        "segment": {"region": "eu"},
        "parent": "total",
        "children": ["plan=free|region=eu", "plan=pro|region=eu"],
        "forecasts": [{"period": 1, "date": "2024-07-01", "forecast": 51900.0, "lower": 48700.2, "upper": 55099.8}]
      }
    }
  }
}

Reconciliation methods

bottom_up (default)

Only the leaves are forecast; every aggregate is the exact sum of its children. Fast, cheap, and the leaves keep their own models untouched. Best when the leaf histories are substantial enough to forecast well on their own.

min_trace

Every node — aggregates included — is forecast, then MinTrace optimally redistributes the disagreement between levels into a coherent set. Costs one forecast per node, but lets the usually-smoother aggregate series inform the result, which tends to help when leaves are short or noisy. Each node reports its unreconciled path under model_info.reconciliation.base_forecast.

Confidence bands on aggregates

Summing the children's lower values is not a lower bound for the parent — it assumes every child misses low at once. With bottom_up, an aggregate's band is instead built from its children's uncertainties under a stated correlation assumption between sibling errors (correlation, default 0.5, overridable per request between 0 and 1). The assumption is echoed in the response and is never labelled as measured. With min_trace, each node keeps its own band, re-centred on the reconciled point with its width preserved.

When a node's band coverage is unknown (a heuristic interval), no aggregate band is emitted above it and the reason is stated in model_info.reconciliation.interval_reason — an honest absence rather than a decorative interval.

Rules and limits

  • Every series must declare exactly the hierarchy dimensions in its segment, and each combination may appear once.
  • All series must end on the same date so forecast periods align. Histories may start whenever they like — a segment that launched later simply contributes zero before its first data point.
  • periods, frequency, model and confidence are request-level only — every node of a hierarchy must share them, so per-series overrides are rejected rather than silently ignored.
  • Up to 4 hierarchy levels and 250 expanded nodes per request (10 leaf series on the free plan).
  • quantiles, value_bounds, adjustments and accumulate are not yet supported on grouped forecasts and are rejected explicitly.

Segments without reconciliation?

If you just want independent forecasts per slice — no coherent roll-up — the batch endpoint with per-series identifiers or tenant_context is simpler and tolerates per-series failure. See Patterns & Segmentation.