Skip to content

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.

Verified against Python Prophet

Regressors match Python Prophet 1.4.0 on all three trend types. The seasonality, events and regressors benchmarks 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
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
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
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: { 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 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.
  • 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.
  • Options for each regressor:
OptionDefaultMeaning
priorScaleholidaysPriorScaleLower values keep the effect small unless the data clearly supports it
modeseasonalityMode"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.
predictornonetrue or options to forecast the regressor with its own model

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 for how factor and contribution appear in forecasts.

Inspired by Prophet from Meta. Not affiliated with Meta.