Uncertainty ranges
A single number is never the whole story. predictUncertainty gives a range for each forecast: "between 133 and 140, most likely around 136".
Benchmarked against Python Prophet
Both libraries simulate ranges with random draws, so individual values never match exactly. The uncertainty benchmarks check every range against Python Prophet 1.4.0 on all three trend types, within the variation expected from that many draws, and time the work.
ts
import { Effect } from "effect";
import { fit, predict, predictUncertainty, 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: 7 }, (_, offset) => date(90 + offset));
const result = await Effect.runPromise(
Effect.gen(function* () {
const model = yield* fit(history);
const forecast = yield* predict(model, nextWeek);
// The seed makes the simulation repeatable: same seed, same intervals.
const intervals = yield* predictUncertainty(model, nextWeek, {
seed: 42,
samples: 1000,
intervalWidth: 0.8,
});
return { forecast, intervals };
}).pipe(Effect.provide(prophetFittingBackendLayer)),
);
if (result.intervals.kind === "intervals") {
for (const [index, row] of result.intervals.rows.entries()) {
const point = result.forecast[index]?.value ?? Number.NaN;
console.log(
new Date(row.timestamp).toISOString().slice(0, 10),
`${point.toFixed(1)} (80% range ${row.value.lower.toFixed(1)} to ${row.value.upper.toFixed(1)})`,
);
}
}txt
2025-04-01 136.5 (80% range 133.1 to 140.1)
2025-04-02 144.7 (80% range 141.2 to 148.4)
2025-04-03 153.0 (80% range 149.6 to 156.8)
2025-04-04 155.5 (80% range 152.0 to 159.1)
2025-04-05 150.7 (80% range 147.2 to 154.3)
2025-04-06 142.5 (80% range 138.9 to 146.0)
2025-04-07 137.6 (80% range 134.4 to 141.3)How it works
The model simulates many possible futures. In each one, the trend might change direction the way it did in the past, and day-to-day noise is added. The range covers the middle 80% of those simulated futures.
| Option | Default | Meaning |
|---|---|---|
seed | required | Any whole number. The same seed always gives the same ranges. |
samples | 1000 | How many futures to simulate (up to 2,048). More is smoother but slower. |
intervalWidth | 0.8 | How much of the simulated futures to cover. 0.95 gives a wider range. |
output | "intervals" | "samples" returns every simulated value instead of a range |
Working with the raw samples
Use output: "samples" to answer questions like "what's the chance we go over 150?":
ts
import { Effect } from "effect";
import { fit, predictUncertainty, 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 + 4 * Math.sin(day * 12.9898),
}));
const nextThreeDays = [date(90), date(91), date(92)];
const result = await Effect.runPromise(
Effect.gen(function* () {
const model = yield* fit(history);
return yield* predictUncertainty(model, nextThreeDays, {
seed: 7,
samples: 500,
output: "samples",
});
}).pipe(Effect.provide(prophetFittingBackendLayer)),
);
if (result.kind === "samples") {
// Samples are stored row by row: all of day 1's samples, then all of day 2's, and so on.
const firstDay = result.value.subarray(0, result.sampleCount);
const chanceAbove150 = firstDay.filter((value) => value > 150).length / result.sampleCount;
console.log(`Chance tomorrow is above 150: ${(chanceAbove150 * 100).toFixed(0)}%`);
}Things to know
- Ranges get wider further out. The further ahead you forecast, the more the trend could change.
- The ranges reflect trend changes and noise only. They don't account for uncertainty in the seasonal patterns, so treat them as a minimum, not a guarantee.
- The seed is required so that results are repeatable. Pick any number and keep it.
- Size limits. One call takes up to 10,000 rows. Ranges work with any sample count up to 2,048, but
output: "samples"returns every simulated value, so rows × samples can be at most 1,000,000. Very long forecasts are also capped: with the default 25 changepoints, ranges reach about 10 times the length of the history ahead. Past any limit,predictUncertaintyfails with aPredictionErrorwhose reason issimulation-limit.