---
url: https://jer-k.github.io/effect-prophet/examples/seasonality.md
description: >-
  Built-in yearly, weekly and daily seasonalities, custom periods,
  multiplicative effects and conditional seasonalities.
---

# Seasonality

Seasonality is any pattern that repeats on a schedule: busier weekends, a summer peak, a spike at
the start of each month.

::: tip Verified against Python Prophet
Seasonal patterns match Python Prophet 1.4.0 on all three trend types. The
[seasonality, events and regressors benchmarks](/benchmarks/features) show every case side by side.
:::

## Built-in patterns

There are three built-in patterns. Each one switches on automatically once you have enough data
for it:

| Pattern  | Turns on when the history covers                   |
| -------- | -------------------------------------------------- |
| `daily`  | At least 2 days, with several rows per day         |
| `weekly` | At least 2 weeks, with rows less than a week apart |
| `yearly` | At least 2 years                                   |

You can override each one with `"off"`, or with `{ mode: "on" }` plus optional settings.

## Adding your own patterns

Add any repeating cycle you know about with `seasonalities`. Give it a `name`, a length in days
(`periodDays`) and a `fourierOrder`.

```ts
import { Effect } from "effect";
import { fit, predict, prophetFittingBackendLayer } from "effect-prophet";

const date = (day: number) => new Date(Date.UTC(2024, 0, 1 + day)).toISOString();

// Two years of daily data with a weekly rhythm and a monthly billing cycle.
const history = Array.from({ length: 730 }, (_, day) => ({
  timestamp: date(day),
  value:
    200 +
    day * 0.1 +
    15 * Math.sin((2 * Math.PI * day) / 7) +
    8 * Math.cos((2 * Math.PI * day) / 30.5),
}));

const model = fit(history, {
  // Built-in patterns: "auto" (the default), "off", or "on" with your own settings.
  builtInSeasonalities: {
    weekly: { mode: "on", fourierOrder: 5 },
    yearly: "auto",
    daily: "off",
  },
  // Add any repeating pattern you know about.
  seasonalities: [{ name: "monthly", periodDays: 30.5, fourierOrder: 5 }],
});

const forecast = await Effect.runPromise(
  Effect.gen(function* () {
    return yield* predict(yield* model, [date(731)]);
  }).pipe(Effect.provide(prophetFittingBackendLayer)),
);

for (const seasonality of forecast[0]?.seasonalities ?? []) {
  if (seasonality.mode === "additive") {
    console.log(seasonality.name, seasonality.value.toFixed(2));
  }
}

```

```txt [Output]
monthly 7.83
weekly 6.51
```

::: tip What is `fourierOrder`?
It controls how detailed the pattern can be. Low values (3 to 5) give smooth, gentle curves.
Higher values (10 or more) can capture sharp shapes, like a spike on one specific day, but are
more likely to chase noise. The built-in defaults are 3 for weekly, 10 for yearly and 4 for
daily.
:::

Each pattern also accepts a `priorScale`. Lower it to keep a pattern's effect small unless the
data strongly supports it. Patterns without one, including the built-in ones, use
`seasonalityPriorScale`, which defaults to `10`. Set it to tune them all at once.

## Effects that grow with the trend

By default, a pattern adds a fixed amount: "+20 on Saturdays". If the swings grow as your data
grows, use `seasonalityMode: "multiplicative"` so the pattern acts as a percentage instead:
"+15% on Saturdays".

```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();

// The weekly swing grows as the business grows: it is a percentage, not a fixed amount.
const history = Array.from({ length: 120 }, (_, day) => {
  const trend = 100 + day * 2;

  return {
    timestamp: date(day),
    value: trend * (1 + 0.2 * Math.sin((2 * Math.PI * day) / 7)),
  };
});

const model = fit(history, { seasonalityMode: "multiplicative" });

const forecast = await Effect.runPromise(
  Effect.gen(function* () {
    return yield* predict(yield* model, [date(121)]);
  }).pipe(Effect.provide(prophetFittingBackendLayer)),
);

for (const seasonality of forecast[0]?.seasonalities ?? []) {
  if (seasonality.mode === "multiplicative") {
    // `factor` is the percentage effect; `contribution` is the same effect in your data's units.
    console.log(
      seasonality.name,
      `${(seasonality.factor * 100).toFixed(1)}%`,
      seasonality.contribution.toFixed(1),
    );
  }
}

```

```txt [Output]
weekly 19.5% 66.7
```

To mix the two kinds in one model, set `mode` on a single custom seasonality or regressor.
Events don't take their own `mode`: `holidaysMode` sets one mode for every event and defaults to
`seasonalityMode`. See [Holidays and events](/examples/events#things-to-know).

## Patterns that switch on and off

Sometimes a pattern only exists part of the time. A campus coffee shop has a weekly rhythm during
the school year, but not in summer. Give the pattern a `conditionName`, and put a matching
true/false value in `conditions` on every row, past and future.

```ts
import { Effect } from "effect";
import { fit, predict, prophetFittingBackendLayer } from "effect-prophet";

const date = (day: number) => new Date(Date.UTC(2025, 0, 1 + day));

// A campus coffee shop: the weekly pattern only exists while classes are in session.
const inSession = (day: number) => {
  const month = date(day).getUTCMonth();

  return month < 5 || month > 7;
};

const history = Array.from({ length: 365 }, (_, day) => ({
  timestamp: date(day).toISOString(),
  value: 80 + (inSession(day) ? 25 * Math.sin((2 * Math.PI * day) / 7) : 0),
  // Every row says whether the condition is true on that day.
  conditions: { inSession: inSession(day) },
}));

const model = fit(history, {
  builtInSeasonalities: { weekly: "off" },
  seasonalities: [
    { name: "school-week", periodDays: 7, fourierOrder: 3, conditionName: "inSession" },
  ],
});

// Future rows need the condition too.
const future = Array.from({ length: 7 }, (_, offset) => ({
  timestamp: date(365 + offset).toISOString(),
  conditions: { inSession: inSession(365 + offset) },
}));

const forecast = await Effect.runPromise(
  Effect.gen(function* () {
    return yield* predict(yield* model, future);
  }).pipe(Effect.provide(prophetFittingBackendLayer)),
);

console.log(forecast.map((row) => row.value.toFixed(1)).join(", "));

```
