Preparing your data
The history
Your history is an array of rows. Each row needs:
| Field | Type | Notes |
|---|---|---|
timestamp | string | UTC, in the exact form 2025-01-31T00:00:00.000Z |
value | number | Any finite number. NaN, Infinity and null are refused. |
const history = [
{ timestamp: "2025-01-01T00:00:00.000Z", value: 120 },
{ timestamp: "2025-01-02T00:00:00.000Z", value: 135 },
{ timestamp: "2025-01-03T00:00:00.000Z", value: 128 },
];Some features need extra fields on every row:
| Field | Used for |
|---|---|
capacity | Growth with a ceiling |
floor | Growth with a ceiling |
regressors | Extra factors |
conditions | Seasonality that switches on and off |
Timestamps
Timestamps must be strings exactly like the ones Date.prototype.toISOString() produces. The easiest way to get them right is to build them with Date:
new Date(Date.UTC(2025, 0, 31)).toISOString(); // "2025-01-31T00:00:00.000Z"Strings like "2025-01-31" or "2025-01-31T00:00:00+02:00" are refused rather than guessed at. That keeps time zones from silently shifting your data by a day.
Dates in your own time zone
For daily data, use midnight UTC for each calendar day, whatever your local time zone is. What matters is that every row uses the same rule.
Missing values
Leave out rows that have no value. Don't pass null, NaN or 0:
const history = rawRows
.filter((row) => row.sales !== null)
.map((row) => ({ timestamp: row.date, value: row.sales }));The model handles gaps in the dates without any trouble.
If rows carry extra columns such as regressors, drop the whole row, not just the value. This example removes a row with no target, holds out the last two rows, and predicts them using their known regressor values:
import { Effect } from "effect";
import {
fit,
predict,
prophetFittingBackendLayer,
type EncodedObservation,
type EncodedPredictionRow,
} from "effect-prophet";
// The application's source parser distinguishes a complete observation from a missing target.
// It does not pass the latter to the library as a partial observation.
type SourceRow =
| { readonly kind: "observed"; readonly row: EncodedObservation }
| { readonly kind: "missing-target"; readonly timestamp: string };
const sourceRows: ReadonlyArray<SourceRow> = [
{
kind: "observed",
row: { timestamp: "2025-01-01T00:00:00.000Z", value: 2, regressors: { promotion: 0 } },
},
{
kind: "observed",
row: { timestamp: "2025-01-02T00:00:00.000Z", value: 5, regressors: { promotion: 1 } },
},
{
kind: "observed",
row: { timestamp: "2025-01-03T00:00:00.000Z", value: 4, regressors: { promotion: 0 } },
},
{ kind: "missing-target", timestamp: "2025-01-04T00:00:00.000Z" },
{
kind: "observed",
row: { timestamp: "2025-01-05T00:00:00.000Z", value: 8, regressors: { promotion: 0 } },
},
{
kind: "observed",
row: { timestamp: "2025-01-06T00:00:00.000Z", value: 11, regressors: { promotion: 1 } },
},
{
kind: "observed",
row: { timestamp: "2025-01-07T00:00:00.000Z", value: 10, regressors: { promotion: 0 } },
},
{
kind: "observed",
row: { timestamp: "2025-01-08T00:00:00.000Z", value: 13, regressors: { promotion: 1 } },
},
{
kind: "observed",
row: { timestamp: "2025-01-09T00:00:00.000Z", value: 12, regressors: { promotion: 0 } },
},
];
const omittedCount = sourceRows.filter((row) => row.kind === "missing-target").length;
const completeRows = sourceRows.flatMap((row) => (row.kind === "observed" ? [row.row] : []));
// The supplied source data is already in canonical UTC order. A real source adapter owns
// validation, sorting, deduplication, aggregation and exclusion before this projection.
const training = completeRows.slice(0, -2);
const holdout = completeRows.slice(-2);
const futureRows: ReadonlyArray<EncodedPredictionRow> = holdout.map((row) => ({
timestamp: row.timestamp,
regressors: row.regressors ?? {},
}));
const program = Effect.gen(function* () {
const model = yield* fit(training, {
regressors: [{ name: "promotion", priorScale: 10 }],
});
return yield* predict(model, futureRows);
}).pipe(Effect.provide(prophetFittingBackendLayer));
const forecasts = await Effect.runPromise(program);
console.log(
`Omitted ${omittedCount} incomplete source row; predicted ${forecasts.length} held-out rows.`,
);Order and duplicates
Rows can be in any order, because the model sorts them. Two rows with the same timestamp are both kept and treated as two separate measurements.
How much history?
- At least two rows are required, but a useful forecast needs much more.
- Weekly patterns are detected automatically once you have two weeks of data.
- Yearly patterns are detected automatically once you have two years of data.
Future rows
To forecast, pass the timestamps you want:
predict(model, ["2025-02-01T00:00:00.000Z", "2025-02-02T00:00:00.000Z"]);If your model uses capacity, floor, regressors or conditions, pass full rows instead, because the model needs those values for the future too:
predict(model, [{ timestamp: "2025-02-01T00:00:00.000Z", regressors: { temperature: 21 } }]);Forecasts come back in the same order as the rows you passed.