---
url: https://jer-k.github.io/effect-prophet/examples/saturating-growth.md
description: >-
  Logistic growth with a capacity, and an optional floor, for values that level
  off.
---

# 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 [Output]
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.
