Skip to content

Preparing your data ​

The history ​

Your history is an array of rows. Each row needs:

FieldTypeNotes
timestampstringUTC, in the exact form 2025-01-31T00:00:00.000Z
valuenumberAny finite number. NaN, Infinity and null are refused.
ts
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:

FieldUsed for
capacityGrowth with a ceiling
floorGrowth with a ceiling
regressorsExtra factors
conditionsSeasonality 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:

ts
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:

ts
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:

ts
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:

ts
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:

ts
predict(model, [{ timestamp: "2025-02-01T00:00:00.000Z", regressors: { temperature: 21 } }]);

Forecasts come back in the same order as the rows you passed.

Inspired by Prophet from Meta. Not affiliated with Meta.