Skip to content

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 10000

The 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.

Inspired by Prophet from Meta. Not affiliated with Meta.