---
url: https://jer-k.github.io/effect-prophet/examples/regressors.md
description: >-
  Add regressors such as temperature or ad spend, and forecast a regressor with
  its own model.
---

# Extra factors (regressors)

Sometimes you know something that helps explain your data: the temperature, how much you spent
on ads, whether a promotion was running. Add these as **regressors**.

::: tip Verified against Python Prophet
Regressors match Python Prophet 1.4.0 on all three trend types. The
[seasonality, events and regressors benchmarks](/benchmarks/features) show every case side by side.
:::

```ts
import { Effect } from "effect";
import { fit, getRegressorCoefficients, predict, prophetFittingBackendLayer } from "effect-prophet";

const date = (day: number) => new Date(Date.UTC(2025, 0, 1 + day)).toISOString();

const temperature = (day: number) => 15 + 10 * Math.sin((2 * Math.PI * day) / 365);

// Ice cream sales follow the temperature: about 4 extra sales per degree.
const history = Array.from({ length: 200 }, (_, day) => ({
  timestamp: date(day),
  value: 40 + 4 * temperature(day),
  regressors: { temperature: temperature(day) },
}));

const model = fit(history, {
  regressors: [{ name: "temperature" }],
});

// You must supply the regressor for every future row, e.g. from a weather forecast.
const future = Array.from({ length: 3 }, (_, offset) => ({
  timestamp: date(200 + offset),
  regressors: { temperature: 30 },
}));

const result = await Effect.runPromise(
  Effect.gen(function* () {
    const fitted = yield* model;

    return { fitted, forecast: yield* predict(fitted, future) };
  }).pipe(Effect.provide(prophetFittingBackendLayer)),
);

for (const coefficient of getRegressorCoefficients(result.fitted)) {
  console.log(coefficient.name, coefficient.mode, coefficient.coefficient.toFixed(2));
}

console.log(result.forecast.map((row) => row.value.toFixed(1)).join(", "));

```

```txt [Output]
temperature additive 3.99
159.9, 159.9, 159.9
```

The model learned the real relationship: about 4 extra sales for every degree.

## In one call

If you don't need the coefficients, `forecast` fits and predicts at once. Pass full rows, each
with its future regressor value:

```ts
import { Effect } from "effect";
import { forecast, prophetFittingBackendLayer } from "effect-prophet";

const date = (day: number) => new Date(Date.UTC(2025, 0, 1 + day)).toISOString();

const temperature = (day: number) => 15 + 10 * Math.sin((2 * Math.PI * day) / 365);

const history = Array.from({ length: 200 }, (_, day) => ({
  timestamp: date(day),
  value: 40 + 4 * temperature(day),
  regressors: { temperature: temperature(day) },
}));

// Full rows: each future date carries the regressor value the model needs.
const future = [20, 25, 30].map((degrees, offset) => ({
  timestamp: date(200 + offset),
  regressors: { temperature: degrees },
}));

const rows = await Effect.runPromise(
  forecast(history, future, { regressors: [{ name: "temperature" }] }).pipe(
    Effect.provide(prophetFittingBackendLayer),
  ),
);

console.log(rows.map((row) => row.value.toFixed(1)).join(", "));

```

```txt [Output]
120.0, 139.9, 159.9
```

## Forecasting a regressor

When you don't know a regressor's future values, give it its own model with `predictor`, like
Python's `regressor_predictor`. `fit` fits a second model to the regressor's history, and `predict`
uses that model's forecast for every row after the history:

```ts
import { Effect } from "effect";
import { fit, makeFutureTimestamps, predict, prophetFittingBackendLayer } from "effect-prophet";

const date = (day: number) => new Date(Date.UTC(2025, 0, 1 + day)).toISOString();

// Foot traffic grows and peaks once a week, and sales follow it.
const traffic = (day: number) => 100 + 0.5 * day + 20 * Math.sin((2 * Math.PI * day) / 7);

const history = Array.from({ length: 120 }, (_, day) => ({
  timestamp: date(day),
  value: 50 + 2 * traffic(day),
  regressors: { traffic: traffic(day) },
}));

const forecast = await Effect.runPromise(
  Effect.gen(function* () {
    // Traffic gets its own model, fitted to its history, like Python's regressor_predictor.
    const model = yield* fit(history, { regressors: [{ name: "traffic", predictor: true }] });

    // The future rows need no traffic values: the traffic model forecasts them.
    const future = yield* makeFutureTimestamps(history, { periods: 5, includeHistory: false });

    return yield* predict(model, future);
  }).pipe(Effect.provide(prophetFittingBackendLayer)),
);

console.log("forecast", forecast.map((row) => row.value.toFixed(1)).join(", "));

console.log(
  "actual  ",
  [120, 121, 122, 123, 124].map((day) => (50 + 2 * traffic(day)).toFixed(1)).join(", "),
);

```

```txt [Output]
forecast 401.3, 410.0, 389.4, 355.6, 335.0
actual   401.3, 410.0, 389.4, 355.6, 335.0
```

`predictor: true` uses the default settings. To change them, pass options instead, such as
`{ growth: "flat", builtInSeasonalities: { yearly: "off" } }`. As in Python, where these are
`Prophet` constructor arguments:

* The options are `growth` (`"linear"` or `"flat"`), `seasonalityMode`, `holidaysMode`,
  `seasonalityPriorScale`, `holidaysPriorScale`, `builtInSeasonalities`, `events`, `scaling`,
  `map.changepoints` and `map.changepointPriorScale`, and `mcmc`. Custom seasonalities, regressors
  and country holidays aren't available, and logistic growth isn't either, because it needs a
  capacity no future row has.
* `mcmc` samples the regressor model's posterior, like `mcmc_samples` in Python's options, with
  the same settings as the main model's [`mcmc`](./bayesian): `{ mcmc: { samples: 300, seed: 7 } }`.
  It needs its own seed, and the main model's `mcmc` never applies to it, so a regressor model
  optimizes unless its own options sample.
* The regressor model doesn't inherit the main model's settings, except `map.warmStart`, and
  `map.optimizer` when the regressor model optimizes, when you set them, as Python passes its fit
  arguments on. A warm start's `delta` and `beta` are used only if their lengths match the
  regressor model's.
* In rows after the history, the forecast replaces any value you pass for that regressor. In
  earlier rows, a value you pass is used, and a missing one is the history's value at the same
  timestamp. If that timestamp isn't in the history, or the history repeats timestamps, `predict`
  fails and asks for the value.
* Regressors without a `predictor` still need a value in every row.
* [Cross-validation](./cross-validation) and model search fit each fold's regressor models to that
  fold's history, so assessment rows are forecast as a fit at the cutoff would forecast them.
* Saved models keep the regressor models, so a loaded model forecasts the same way. Python's saved
  models drop them.
* **Ranges include the regressor's uncertainty.** As in Python, `predictWithIntervals` and
  `predictUncertainty` give each simulated future one of the regressor model's own simulated
  values, so ranges after the history are wider than with the point forecast alone. Ranges for
  rows inside the history use the values those rows have. A sampled regressor model simulates
  `ceil(samples / draws)` values per posterior draw, like a sampled main model, and the futures
  take them in turn.

## Things to know

* **Every row needs every regressor**, in both the history and the future rows, unless the
  regressor has its own model. If you don't know a future value, use your own forecast, such as a
  weather forecast, or [forecast it with its own model](#forecasting-a-regressor).
* **Regressors are numbers.** Use `1` and `0` for yes/no factors.
* **`getRegressorCoefficients(model)`** shows what the model learned for each regressor: its
  `mode`, `coefficient` and `center`. How to read the coefficient depends on the mode. See
  [Reading coefficients](#reading-coefficients).
* **Options for each regressor:**

| Option            | Default              | Meaning                                                                                                    |
| ----------------- | -------------------- | ---------------------------------------------------------------------------------------------------------- |
| `priorScale`      | `holidaysPriorScale` | Lower values keep the effect small unless the data clearly supports it                                     |
| `mode`            | `seasonalityMode`    | `"multiplicative"` makes the effect a percentage of the trend. `seasonalityMode` defaults to `"additive"`. |
| `standardization` | `"auto"`             | Rescales the values internally for a stable fit. `"auto"` leaves yes/no (0/1) values alone.                |
| `predictor`       | none                 | `true` or options to [forecast the regressor with its own model](#forecasting-a-regressor)                 |

As in Python Prophet, a regressor without its own `priorScale` uses the model's
`holidaysPriorScale` (default `10`), not `seasonalityPriorScale`. Changing `holidaysPriorScale`
therefore changes those regressors too. To tune holidays alone, give each regressor its own
`priorScale`.

## Reading coefficients

For a regressor with coefficient `c`, center `center`, and a row whose regressor value is `x`, in
the regressor's own units:

* **Additive:** the regressor adds `c × (x − center)` to the forecast, in your data's units. One
  more unit of the regressor changes the forecast by `c`.
* **Multiplicative:** `c × (x − center)` is a fraction of the trend, the `factor` in the
  forecast's regressor entry. The `contribution`, in your data's units, is
  `trend × c × (x − center)`. One more unit of the regressor changes the forecast by `trend × c`,
  so the same coefficient has a larger effect where the trend is higher.

For example, a multiplicative `promotion` regressor with coefficient `0.2` on a trend of `100`
adds `20` to the forecast on promotion days, not `0.2`. See
[Additive and multiplicative pieces](../guide/reading-forecasts#additive-and-multiplicative-pieces)
for how `factor` and `contribution` appear in forecasts.
