# Forecast timeseries together

> For AI agents: the complete documentation index is at
> [https://docs.retrocast.com/llms.txt](https://docs.retrocast.com/llms.txt) — every page is also available as
> markdown by appending `.md` to its URL.

By default every timeseries is forecast from its own data alone: the model never
sees the other stores while it forecasts one. Stores in the same city often move
together. `cross_inference` forecasts timeseries jointly, in groups you choose, so
the model learns across them at inference time (cross-learning): it reads each
group's targets, and any historical and future covariates, side by side.

```json
{
  "input": {
    "source": {
      "inline": {
        "date":  ["2024-01-01", "2024-01-01", "2024-01-01", "2024-01-01", "2024-01-02", "2024-01-02", "2024-01-02", "2024-01-02"],
        "store": ["S1", "S2", "S3", "S4", "S1", "S2", "S3", "S4"],
        "city":  ["Lyon", "Lyon", "Paris", "Paris", "Lyon", "Lyon", "Paris", "Paris"],
        "sales": [30, 28, 42, 58, 29, 31, 40, 62]
      }
    },
    "columns": {
      "date":  { "kind": "time", "frequency": "1d" },
      "store": "identifier",
      "city":  "identifier",
      "sales": { "kind": "target" }
    },
    "hierarchies": {
      "store": [["store", "city"]]
    }
  },
  "selector": { "store": "any" },
  "cross_inference": { "group_by": ["city"] },
  "prediction_length": "1w"
}
```

Post it to `https://api.retrocast.com/v1-beta/forecast?model=t0-alpha`.
`model` names the forecasting model and is required — without it the
request is rejected with `missing field "model"`.

What each part does:

- `selector: { "store": "any" }` still asks for one forecast per store.
- `cross_inference: { "group_by": ["city"] }` forecasts the stores of each
  city together: `S1` and `S2` in one group, `S3` and `S4` in another. Each
  store still gets its own forecast.
- `"group_by": []` puts every timeseries the selector picks into one group.

Each level in `group_by` must be a level the selector picks, or one it rolls
up to in `hierarchies` — `city` here, because `store` sits inside it. Any
other level is rejected with an error naming it. The order of the levels
doesn't matter.

Leave `cross_inference` out and each timeseries is forecast on its own.

Joint forecasting is supported by the t0 models and `chronos-2`.

## Which timeseries share a group

Timeseries are forecast together when they share a value at every `group_by`
level **and** end at the same time: they are forecast from the same cutoff
instant. A group never mixes
cutoffs, so:

- In a [backtest](/api-v1/contents/cookbook/backtesting), each cutoff forms
  its own groups. With two cutoffs, the request above makes four groups: each
  city, at each cutoff. A forecast never sees another timeseries past its own
  cutoff.
- With `cutoff: "latest"` (the default), each timeseries is cut at its own last
  date. If `S2` ended two days before `S1`, they fall at different instants:
  each is forecast on its own.
- With `selectors`, each selector's timeseries are grouped separately, and every
  `group_by` level must suit every selector.

Groups are never split. A group with more timeseries than the model can read at
once fails with an error.

## What comes back

The usual table, one row per (store, time step), with one more column:
`cross_inference_group`. Rows that share a value were forecast together. Four
stores over seven days make 28 rows, trimmed here to each store's first step:

```json
{
  "data": {
    "time":      ["2024-01-03T00:00:00Z", "2024-01-03T00:00:00Z", "2024-01-03T00:00:00Z", "2024-01-03T00:00:00Z"],
    "cutoff":    ["2024-01-02T00:00:00Z", "2024-01-02T00:00:00Z", "2024-01-02T00:00:00Z", "2024-01-02T00:00:00Z"],
    "lead_time": ["PT0S", "PT0S", "PT0S", "PT0S"],
    "span":      ["P1D", "P1D", "P1D", "P1D"],
    "identifiers": {
      "store": ["S1", "S2", "S3", "S4"],
      "city":  ["Lyon", "Lyon", "Paris", "Paris"]
    },
    "targets": {
      "sales": {
        "quantiles": {
          "0.1":  [26.9, 28.1, 37.2, 56.8],
          "0.5":  [29.3, 30.4, 40.5, 61.2],
          "0.9":  [32.0, 33.1, 44.1, 66.0],
          "mean": [29.4, 30.5, 40.6, 61.3]
        }
      }
    },
    "cross_inference_group": [0, 0, 1, 1]
  }
}
```

A group number is only a name for the group within this response: it isn't
stable from one request to the next. A timeseries that was forecast on its own,
with no other timeseries at its cutoff instant, has `null`. The column is there
whenever the request sets `cross_inference`, even when no timeseries ended up in
a group.

The other formats carry the same information:

- `application/x-ndjson` and `application/vnd.msgpack`: each timeseries record has
  a `cross_inference_group` field, `null` when the timeseries was forecast on its
  own.
- `application/vnd.apache.parquet`: a nullable `cross_inference_group` column,
  present whenever the request sets `cross_inference`.
