Coming from Python
Effect Prophet fits the same model as Python Prophet, so the ideas carry over: changepoints, seasonalities, holidays, regressors, and the same defaults. What changes is the shape of the API. You pass plain data and options to functions instead of calling methods on a mutable Prophet object.
A side-by-side example
The Python version of a small forecast:
from prophet import Prophet
model = Prophet(changepoint_prior_scale=0.5, weekly_seasonality=False)
model.fit(df) # df has `ds` and `y` columns
future = model.make_future_dataframe(periods=7, include_history=False)
forecast = model.predict(future)
print(forecast[["ds", "yhat"]])And the same program here:
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();
// Python builds a dataframe with `ds` and `y` columns. Here each row is a plain object.
const history = Array.from({ length: 90 }, (_, day) => ({
timestamp: date(day),
value: 100 + day * 0.5 + 10 * Math.sin((2 * Math.PI * day) / 7),
}));
const forecast = await Effect.runPromise(
Effect.gen(function* () {
// `Prophet(changepoint_prior_scale=0.5, weekly_seasonality=False).fit(df)`
const model = yield* fit(history, {
map: { changepointPriorScale: 0.5 },
builtInSeasonalities: { weekly: "off" },
});
// `model.make_future_dataframe(periods=7, include_history=False)`
const future = yield* makeFutureTimestamps(history, { periods: 7, includeHistory: false });
// `model.predict(future)`
return yield* predict(model, future);
}).pipe(Effect.provide(prophetFittingBackendLayer)),
);
for (const row of forecast) {
console.log(new Date(row.timestamp).toISOString().slice(0, 10), row.value.toFixed(1));
}Three differences to notice. fit and predict return Effects, so you yield* them inside Effect.gen and run the result once at the edge. The forecasting engine comes from prophetFittingBackendLayer. And makeFutureTimestamps takes the history rather than the model. New to Effect? Start with the five-minute introduction.
Translation table
Data
| Python | Effect Prophet |
|---|---|
ds column | timestamp: an exact UTC ISO string |
y column | value |
cap and floor columns | capacity and floor fields on each row |
| extra regressor columns | a regressors: { name: number } object on each row |
| condition columns for seasonalities | a conditions: { name: boolean } object on each row |
make_future_dataframe(periods=n) | makeFutureTimestamps(history, { periods: n }); see Future rows |
dropping NaN rows | leave the row out; see Your data |
Model settings
| Python | Effect Prophet |
|---|---|
growth="linear", "flat", "logistic" | growth: "linear" | "flat" | "logistic" (linear is the default) |
n_changepoints, changepoint_range | map.changepoints: { mode: "auto", count, range } |
changepoints=[...] | map.changepoints: { mode: "explicit", timestamps } |
changepoint_prior_scale | map.changepointPriorScale |
seasonality_prior_scale | seasonalityPriorScale: the prior for every seasonality without its own priorScale |
holidays_prior_scale | holidaysPriorScale: the prior for every event, country holiday and regressor without its own priorScale |
fit(algorithm="Newton", iter=n) | map.optimizer: { algorithm: "newton", maxIterations: n } |
mcmc_samples=n | mcmc: { samples: n, seed }, with chains and warmup; see Full Bayesian fitting |
fit(df, init=warm_start_params(m)) | map.warmStart: warmStartParameters(model) |
yearly_seasonality, weekly_seasonality, daily_seasonality | builtInSeasonalities: { yearly, weekly, daily }: "auto", "off", or { mode: "on", fourierOrder, priorScale } |
seasonality_mode="multiplicative" | seasonalityMode: "multiplicative" |
holidays_mode | holidaysMode |
scaling="minmax" | scaling: "minmax" |
add_seasonality(name, period, fourier_order, prior_scale, mode, condition_name) | an entry in seasonalities: { name, periodDays, fourierOrder, priorScale, mode, conditionName } |
add_regressor(name, prior_scale, standardize, mode) | an entry in regressors: { name, priorScale, standardization, mode } |
add_regressor(name, regressor_predictor=True) or a dict of constructor options | predictor: true or options on that entry; see Forecasting a regressor |
holidays dataframe | events: one { name, date, lowerWindowDays, upperWindowDays, priorScale } per occurrence |
add_country_holidays(country_name="US") | countryHolidays: { country: "US", coverage, priorScale } |
The defaults are Prophet's, so a fit with no options produces the same model as Python. See How close are the results? for the evidence.
Output and evaluation
| Python | Effect Prophet |
|---|---|
fit(df) then predict(future) | forecast(history, future, options), which fits and predicts in one call |
predict(future) columns yhat, trend | predict(model, timestamps): value, trend, and every component broken out |
yhat_lower, yhat_upper | predictWithIntervals(model, timestamps, { seed, samples, intervalWidth }): valueInterval |
trend_lower, trend_upper | trendInterval on each predictWithIntervals row |
interval_width, uncertainty_samples | intervalWidth and samples on predictWithIntervals or predictUncertainty |
comparing y with yhat_lower, yhat_upper | detectAnomalies(model, observations, { seed }) |
| the same, on the history the model was fitted on | anomalies(history, { seed }), which fits and checks in one call |
df.y - m.predict(df).yhat | residuals(model, observations): actual, predicted and residual per row |
predictive_samples | predictUncertainty(..., { output: "samples" }) |
regressor_coefficients(model) | getRegressorCoefficients(model) |
cross_validation | crossValidate |
performance_metrics | performanceMetrics |
performance_metrics(df, monthly=True) | performanceMetrics(result, { ..., horizon: "calendar-month" }) |
register_performance_metric | a custom metric definition in metrics |
model_to_json, model_from_json | encodeFittedModel, decodeFittedModel; the format is not Python's |
| a loop over parameter grids | makeParameterGrid to build the candidates, then searchModels and evaluateHoldout |
Gotchas for Python users
- Timestamps must be exact UTC ISO strings, such as
"2025-01-31T00:00:00.000Z". There is no format guessing and no time-zone conversion. - Missing values: leave the row out instead of using
NaN. A row without a value is rejected. - Future rows carry their own inputs. Every future row needs its
capacity(andfloor), regressor values and condition flags, because there is no dataframe to look them up in. The exception is a regressor with its own model (regressor_predictor), which forecasts it. - The seed is required for uncertainty ranges, so results are repeatable.
- Intervals are a separate call.
predictreturns point forecasts only, so you never pay for simulations you didn't ask for. - Models are immutable. Fitting returns a new model. There is nothing to mutate; to refit from an earlier model, pass
warmStartParameters(model)asmap.warmStart. - Country coverage is explicit. The offline snapshot covers request years 1995–2060; reviewed gaps fail by default. Opting into
"allow-known-gaps"accepts actual partial provider data, not complete coverage. Fitted models embed their selected snapshot; updates require a refit. See Country holiday calendars. - No plotting. Forecasts are plain data for your own charts.
See What's different for where behavior differs on purpose, and the limits Python doesn't have.