Full Bayesian fitting
By default, fit finds the single most likely set of parameters (the MAP estimate). With the mcmc option it instead draws thousands of plausible parameter sets from the posterior with Stan's NUTS sampler, exactly as Python's mcmc_samples does. Ranges then cover how unsure the model is about its trend and seasonality, not just future trend changes and noise.
import { Effect } from "effect";
import { fit, isMcmcModel, predictWithIntervals, prophetFittingBackendLayer } from "effect-prophet";
const date = (day: number) => new Date(Date.UTC(2025, 0, 1 + day)).toISOString();
const history = Array.from({ length: 90 }, (_, day) => ({
timestamp: date(day),
value: 100 + day * 0.5 + 10 * Math.sin((2 * Math.PI * day) / 7) + 4 * Math.sin(day * 12.9898),
}));
const nextWeek = Array.from({ length: 3 }, (_, offset) => date(90 + offset));
const { model, forecast } = await Effect.runPromise(
Effect.gen(function* () {
// Like Python's `mcmc_samples=300`: 4 chains, each with 150 warmup and 150 kept draws.
const model = yield* fit(history, { mcmc: { samples: 300, seed: 7 } });
const forecast = yield* predictWithIntervals(model, nextWeek, { seed: 42 });
return { model, forecast };
}).pipe(Effect.provide(prophetFittingBackendLayer)),
);
const diagnostics = isMcmcModel(model) ? model.fitSummary.diagnostics : null;
console.log(
`${model.model}: ${diagnostics?.divergences} divergences, max R-hat ${diagnostics?.maxRhat?.toFixed(2)}`,
);
console.log(`${forecast.sampleCount} simulated paths`);
for (const row of forecast.rows) {
const weekly = row.componentIntervals?.seasonalities.find(({ name }) => name === "weekly");
console.log(
new Date(row.timestamp).toISOString().slice(0, 10),
`${row.value.toFixed(1)} (80% range ${row.valueInterval.lower.toFixed(1)} to ${row.valueInterval.upper.toFixed(1)})`,
`weekly ${weekly?.interval.lower.toFixed(2)} to ${weekly?.interval.upper.toFixed(2)}`,
);
}linear-piecewise-mcmc: 0 divergences, max R-hat 1.03
1200 simulated paths
2025-04-01 135.9 (80% range 131.8 to 139.8) weekly -8.80 to -6.84
2025-04-02 143.9 (80% range 139.8 to 148.1) weekly -1.20 to 0.65
2025-04-03 152.3 (80% range 148.2 to 156.5) weekly 6.82 to 8.68Options
| Option | Default | Meaning |
|---|---|---|
samples | required | Iterations per chain, like mcmc_samples. Each chain keeps half, rounded down |
seed | required | Any whole number. The same seed always gives the same draws |
chains | 4 | Independent chains, up to 16 |
warmup | samples / 2 | Iterations each chain spends tuning itself before it keeps draws |
Python's mcmc_samples=300 is mcmc: { samples: 300, seed }: 4 chains of 150 warmup and 150 kept iterations, 600 draws in all. Model kinds are "flat-mcmc", "linear-piecewise-mcmc" and "logistic-piecewise-mcmc"; isMcmcModel tells them from MAP models. Python picks a random seed when you don't give one; here the seed is required, so a fit is repeatable.
The sampler is a port of Stan 2.37.0's adaptive NUTS with CmdStan's default settings, run on Prophet's own model. Its chains can't match CmdStan's draw for draw, since tiny rounding differences grow along a chain, so the benchmarks compare the posterior means and ranges with Python's instead, within the variation expected from that many draws.
What changes
- Point forecasts use the posterior means, as Python's do.
model.slope,model.coefficientsand the other fields hold those means, andmodel.posteriorholds every draw. - Ranges come from
ceil(samples / draws)simulated futures for each draw, as in Python, sosampleCountcan be more than thesamplesyou asked for: 1,200 for 1,000 samples over 600 draws.simulationisprophet-mcmc-scalar-xoshiro128ss-v1. - Component ranges.
predictWithIntervalsaddscomponentIntervalsto each row: the range of the additive and multiplicative totals and of every seasonality, event and regressor over the draws, like Python'sweekly_lowerandweekly_uppercolumns. Multiplicative ranges are factors, as in Python. - Regressor coefficients.
getRegressorCoefficientsgives each coefficient's posterior mean andcoefficientInterval, its 80% range over the draws, like Python'scoef_lowerandcoef_upper. - Warm starts.
warmStartParametersreturns the posterior means, as Python'swarm_start_paramsdoes, andmap.warmStartsets where every chain starts. - Saved models keep every draw, so a loaded model forecasts and simulates exactly as before.
Checking the sampler
model.fitSummary.diagnostics reports the sampler's health, as CmdStan's diagnose and summary do:
| Field | Look for |
|---|---|
divergences | Zero. Any mean the sampler struggled, and the ranges may be too narrow |
maxTreeDepthHits | Few. Many mean slow, awkward exploration |
maxRhat | Below about 1.01 to 1.05. Higher means the chains disagree: sample more |
minBulkEss, minTailEss | At least a few hundred, for stable means and ranges |
meanAcceptStat, stepSizes | Acceptance near or above the 0.8 target, and each chain's tuned step size |
warmupDivergences | Divergences while tuning, which CmdStan doesn't report; usually harmless |
A flat or linear history whose values are all the same keeps Prophet's single fixed state, as Python does: its termination is "constant-target-shortcut", it has one draw and no diagnostics. Logistic growth always samples.
Things to know
- Short warmups never tune the metric. CmdStan only adapts the sampler's per-parameter scales once warmup is long enough for its tuning windows, so Python fits with
mcmc_samplesunder 300 keep the default scales and smaller steps. This sampler does the same, to match Python. - Sampling is slower than MAP fitting: hundreds of gradient evaluations per draw instead of a few dozen in all. The 90-day example above takes about a second. Linear and flat fits take about as long as Python's, but logistic fits take 4–5 times as long as Python's native CmdStan. See the benchmarks, which also time ranges from a posterior. Python's calls there also compute point forecasts and components, so those times aren't compared.
- Limits. Up to 16 chains, 100,000 warmup iterations per chain and 20,000 kept draws in all, and at most 2,000,000 kept values (draws times parameters), so models stay a reasonable size. The first three are checked against your
mcmcoptions before sampling: past them,fitfails with anOptionsErrorwhosepathpoints atmcmc.chains,mcmc.warmupormcmc.samples. Kept values depend on how many parameters the model has, so only fitting can check them: past that,fitfails with aFittingErrorwhosereasonis"sampling-limit". - Every growth and feature. Flat, linear and logistic growth all sample, with events, country holidays, conditional seasonalities and regressors. Regressors forecast by their own models optimize unless their own options set
mcmc, as in Python, wheremcmc_samplesdoesn't reach them; when a posterior runs more futures than the regressor models drew, it reuses their draws in turn, as Python does. - Evaluation.
crossValidate,searchModelsandevaluateHoldoutsample a posterior for every fit. Each round's ranges runceil(samples / draws)futures per draw, and holdout reports record the posterior's draw count, divergences and largest R-hat instead of optimizer progress.