Growth with a ceiling
Some things can't grow forever. A service in a town of 10,000 people can't have more than 10,000 users. Growth slows as it approaches that limit, in an S-shaped curve.
Use growth: "logistic" and give every row a capacity: the most the value could possibly be.
ts
import { Effect } from "effect";
import { fit, predict, prophetFittingBackendLayer } from "effect-prophet";
const date = (day: number) => new Date(Date.UTC(2025, 0, 1 + day)).toISOString();
// Sign-ups for a service in a town of 10,000 people: growth slows as it approaches the limit.
const history = Array.from({ length: 150 }, (_, day) => ({
timestamp: date(day),
value: 10_000 / (1 + Math.exp(-(day - 90) / 15)),
capacity: 10_000,
}));
// Every future row needs a capacity too. It can change over time, e.g. if the town grows.
const future = Array.from({ length: 5 }, (_, offset) => ({
timestamp: date(150 + offset * 30),
capacity: 10_000,
}));
const forecast = await Effect.runPromise(
Effect.gen(function* () {
const model = yield* fit(history, { growth: "logistic" });
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(0));
}txt
2025-05-31 9820
2025-06-30 9975
2025-07-30 9997
2025-08-29 10000
2025-09-28 10000The forecast levels off at the capacity instead of growing forever.
Adding a floor
You can also set a minimum with floor. If the history has floors, every future row needs one too.
ts
import { Effect } from "effect";
import { fit, predict, prophetFittingBackendLayer } from "effect-prophet";
const date = (day: number) => new Date(Date.UTC(2025, 0, 1 + day)).toISOString();
// A server's memory use, which can never drop below 2 GB or rise above 16 GB.
const history = Array.from({ length: 120 }, (_, day) => ({
timestamp: date(day),
value: 2 + 14 / (1 + Math.exp(-(day - 60) / 12)),
floor: 2,
capacity: 16,
}));
// When the history has a floor, every future row needs one as well.
const future = [{ timestamp: date(150), floor: 2, capacity: 16 }];
const forecast = await Effect.runPromise(
Effect.gen(function* () {
const model = yield* fit(history, { growth: "logistic" });
return yield* predict(model, future);
}).pipe(Effect.provide(prophetFittingBackendLayer)),
);
console.log(forecast[0]?.value.toFixed(2));Things to know
- The capacity is your judgement. The model doesn't work it out for you. It is usually a market size, a physical limit or a plan.
- Capacity can change over time. Give each row its own value, for example if the town grows.
- Capacity must be greater than the floor on every row.
- Seasonal patterns and events are added on top of the trend, so the final forecast can go slightly above the capacity or below the floor.