Forecasts with actuals¶
You fit a model one time, but new numbers continue to come. For example, the sales of last month arrive, or a preliminary figure gets a revision. A forecast request can send these actuals together with the dates to forecast. The model updates its state with the actuals and the parameters of the fit. It does not estimate the parameters again. Then it forecasts from the last actual. Thus, you do not have to fit again to move the forecast origin forward.
The model does not ignore the data that you send. If a model cannot use actuals, it refuses
the request with ACTUALS_NOT_SUPPORTED. Each forecast answer tells which data the model
conditioned on.
Sending actuals and future dates¶
Use mode: forecast with a payload (data, or an inline dataset on an endpoint). The payload
must have the target along the time dim. For each series:
- A period with a target value is an actual.
- A period with an empty target (
null, a blank cell, NaN) after the last actual is a period to forecast.
{
"mode": "forecast",
"horizon": {"dim": "t", "periods": 8},
"future_covariates": "carry_last",
"output": {"summary": ["median", "q5", "q95"]},
"data": {
"coords": {"t": {"dims": ["t"], "data": ["1961-01-01", "1961-02-01", "1961-03-01",
"1961-04-01", "1961-05-01"]}},
"data_vars": {"y": {"dims": ["t"], "data": [417, 391, 419, null, null]}}
}
}
This request updates the airline model with January to March 1961, and forecasts April and May. In a spreadsheet, the Input sheet of the endpoint is the same table. It has one row for each month. Fill the target column where you have actuals. Leave it blank where you want a forecast. The driver columns are next to the target column.
| t | y | price |
|---|---|---|
| 1961-01-01 | 417 | 10.5 |
| 1961-02-01 | 391 | 10.5 |
| 1961-03-01 | 419 | 10.9 |
| 1961-04-01 | 11.0 | |
| 1961-05-01 |
GET /v1/endpoints/{alias}/schema gives actuals_example for endpoints with a model that can
do this. It is a call with inline data for the periods after the fit, with all values empty,
for you to fill. The schema also gives input_table for all endpoints. It is the same table as
columns: the time column first, then the target, the drivers and the other inputs. Each column
tells what it contains. For a forecast endpoint, the table also has the periods after the fit as
empty rows. All clients read that layout:
| From | How |
|---|---|
| Google Sheets | The Input sheet of the generated Apps Script (Connect Google Sheets). A second function makes the sheet |
| Python | api.endpoints.forecast(alias, rows) with a DataFrame or rows, or actuals= and future= (The Python client) |
| The terminal | mapi endpoints forecast ALIAS input.csv (The command line) |
| The web app | The Schema tab of the endpoint: Your latest actuals and the dates to forecast, then Use in the call |
| curl, TypeScript, R | The snippets of the endpoint add the call for the periods after the fit, for you to fill |
The rules¶
The source of the forecast periods. If the payload has periods after the last actual, the
model forecasts only those periods. It does not use horizon.periods. If the two are different,
a warning in issues tells you. If the payload ends with an actual, the model forecasts
horizon.periods periods after it. If the actuals only revise fitted periods, the forecast
starts after the end of the fit. The fitted periods between them keep their values and get
one-step-ahead predictions. Some payloads have no period after the fit and no actual: no rows,
or only fitted periods with an empty target. For these payloads, the model forecasts
horizon.periods periods after the fit, as for a call without data. horizon.dim always
gives the time dim.
Actuals continue the fitted history. The first actual can be the period immediately after
the fit, or an earlier period. Actuals in the fitted range replace the fitted values for
those periods (revised data), and the forecast conditions on the revision. An empty target in
the fitted range keeps the fitted value. An empty target between the end of the fit and the
last actual is a gap, and gives ACTUALS_INVALID. The exception is a model that accepts missing
observations (its input schema accepts a missing target, as the state-space models do). This
model filters the gap as missing data. Empty targets in the fitted range are never gaps. Thus,
all models accept revisions of separate fitted periods, with or without new actuals after the
fit. Coordinates that skip periods give NON_CONTIGUOUS_EXTENSION, as for all score requests.
Only the values that you send are actuals. Some pipeline steps fill missing targets. An
example is core.impute with ffill or mean, which fills the gaps of the fitted history.
Such a step never changes an empty target that you send into an actual. The model forecasts
the periods that you leave blank.
Drivers. A model with drivers needs them on the actual periods after the fit. These are
the values that occurred with the actuals. If they are missing, the result is
ACTUALS_INVALID. On the periods to forecast, empty drivers follow future_covariates:
supplied(the default) refuses them (MISSING_FUTURE_COVARIATE).carry_last,seasonal_naiveandzerofill them from the fitted history and the drivers of the actual periods. Thus,carry_lastrepeats the latest value that you sent.
The model never changes the values that you send.
Panels. For a univariate model, each series conditions on its own actuals and forecasts
from its own last actual. (A series is a cell of the other dims of the target, for example a
store or a geo.) Thus, the series can end at different dates. Series that the model models
jointly are one system: the variables of ts.var, ts.vecm, ts.varmax and
ts.dynamic_factor. Each of these series needs a value on each actual period after the fit.
If not, the result is ACTUALS_INVALID.
The parameters do not change. Conditioning is a filter, not a fit. For a state-space model,
it is results.append(new, refit=False) of statsmodels (or apply when actuals revise the
fitted range). For an autoregression, it is the new lags. Fit the model again at intervals, so
that the parameters use the new data.
What the answer says¶
Each forecast has a conditioning record. It is in the inline summary
(ScoreCallResult.summary.conditioning), in the result.json of the run and in the root
attribute conditioning of the result tree:
{
"dim": "t",
"fit_end": "1960-12-01",
"conditioned": true,
"n_actuals": 3,
"actuals_through": "1961-03-01",
"revised": 0,
"series_conditioned": 1,
"n_series": 1,
"horizon_source": "rows",
"periods": 2,
"parameters": "fitted",
"series": [{"series": {}, "n_actuals": 3, "first_actual": "1961-01-01",
"actuals_through": "1961-03-01", "revised": 0, "missing": 0,
"origin": "1961-03-01", "forecast_from": "1961-04-01",
"forecast_to": "1961-05-01", "n_forecast": 2}],
"message": "Forecast updated with 3 actuals (1961-01-01 to 1961-03-01) after the fitted history, which ends 1960-12-01; the model's parameters were not re-estimated. 2 periods forecast from 1961-04-01 to 1961-05-01."
}
The summary table covers all the periods that you sent and the added horizon. On the actual
periods, it shows one-step-ahead predictions (how well the model predicts each period). On the
other periods, it shows the forecast. A forecast without actuals gives "conditioned": false
and the end of the fitted history where it started.
Which models can¶
All the built-in statsmodels models
can do this (ts.sarimax, ts.arima, ts.unobserved_components, ts.ets, ts.theta,
ts.ardl, ts.ecm, ts.var, ts.vecm, ts.varmax, ts.dynamic_factor,
ts.markov_switching). This includes the models that the ts_readiness_arimax workflow fits.
The PyMC time-series models (ts.hsgp_trend, ts.gaussian_hmm), the marketing-mix models and
the tabular models refuse. As an alternative, ts.gaussian_hmm runs its forward filter on new
observations with mode: filter.
A model declares this with capabilities.conditions_on_actuals (see
Capabilities). GET …/schema shows it as snapshot.conditions_on_actuals.
The check uses the plugin build that scores. Thus, a model that you fitted before its plugin
could condition can do it now, without a new fit. Some promotions move a version that cannot
condition onto an endpoint with a version that can. This is a breaking change for the callers
of the endpoint. The promotion gate counts it together with the input-schema changes, under the
schema_change policy of the endpoint (blocked by default). All other models refuse a forecast
with a target value in the payload:
{
"type": "urn:mapi:error:ACTUALS_NOT_SUPPORTED",
"title": "The model cannot update its forecast with supplied actuals",
"status": 422,
"code": "ACTUALS_NOT_SUPPORTED",
"errors": [{
"code": "ACTUALS_NOT_SUPPORTED",
"pointer": "/data",
"message": "model 'ts.hsgp_trend@1.0.0' cannot update a forecast with actuals: the payload has values of the target 'y' for 3 periods (1961-01-01 to 1961-03-01). It would forecast from the end of its fitted history (1960-12-01) without them.",
"suggested_action": "Refit the model with the new data to forecast from it, or leave the target empty (null) to forecast from the end of the fitted history (1960-12-01)."
}]
}
Only forecast requests read targets as actuals. predict and evaluate payloads have
targets for other reasons (metrics, features), and this function does not change them. If a
predict payload has target values after the fitted history, a warning in issues tells you
that the model does not use them. To condition on them, use mode: forecast.
Models fitted in a transformed space. Some workflow models fit on the output of
transform.log, transform.box_cox or transform.difference. These transforms do not have an
equivalent preprocessing step, so the endpoint cannot apply them to the data that you send.
Such a model has the label mapi.io/data-space: transformed. It refuses actuals with
ACTUALS_NOT_SUPPORTED, because it would read them as transformed values. Its
snapshot.conditions_on_actuals is false. Each answer has a warning that the predictions are
in the transformed space. To prevent this, put the transform in the model (an ARIMA order
d). Or put it in the pipeline of the model (core.log1p through transform.steps). Then
the model transforms the actuals, and the forecasts come back in the units of the data.
| Code | HTTP | Raised when | What to do |
|---|---|---|---|
ACTUALS_NOT_SUPPORTED |
422 | The payload has target values and the model cannot condition on them | Fit again with the new data, or leave the target empty |
ACTUALS_INVALID |
422 | Actuals leave a gap after the fit (and the model needs a complete history), or actuals after the fit do not have their drivers | Fill the gap, or send only the actuals before it. Send the drivers |
MISSING_FUTURE_COVARIATE |
422 | A driver is empty on a period to forecast and future_covariates is supplied |
Fill it, or set future_covariates to carry_last, seasonal_naive or zero |
SCHEMA_INVALID |
422 | A target or driver cell has text where the schema needs a number ("#N/A", or a number as text from a client that sends it with no change) |
Send numbers. Leave the cell empty for no value |
The API checks inline payloads before it starts a run. It does a null check of the JSON for
the target and for the drivers that a forecast needs. It also does a type check for text. (The
platform never decodes values outside the sandbox.) These checks see the union of all series.
The sandbox checks each series again. Thus, a problem in only one series of a panel causes the
run to fail with the same code. An example is one store without its driver on an actual month.
In this case, the answer is 200 with the problem in run.problem, not a 422.
For plugin authors¶
Declare conditions_on_actuals=True and override CustomModel.forecast_with_actuals:
def forecast_with_actuals(
self,
fitted: Fitted,
ds: xr.Dataset, # the canonicalised payload: actuals, periods to forecast, drivers
actuals: ForecastActuals, # which period is which, per series
req: ScoreRequest,
ctx: ScoreContext,
) -> xr.DataTree: ...
ForecastActuals (mapi_sdk.actuals) has these attributes:
dim,fit_endandtargets.- The boolean mask
observed: a supplied actual. In the fitted range, it replaces the fitted value. - The boolean mask
future: a period to forecast. - The derived
revised,gapandlast_actual(). - The
conditioningrecord of the answer.
Return the predictions group for all periods of ds, as score does. Before the hook, the
harness refuses the requests that the model cannot do. It also adds the horizon, fills the
future drivers and checks the gaps and the drivers. The default implementation refuses. Thus,
a plugin that declares the capability but has no hook never ignores actuals. toy.ar1 in
mapi_sdk.testing.toy is a small reference. The forecast_actuals check of the contract kit
makes sure of two things. Conditioning on the fitted values gives the plain forecast again,
and a new actual changes the forecast.