Time Series and Forecasting

Prophet

Prophet forecasts by adding up a bendy trend, repeating seasonal shapes and named holiday effects, which makes moving festivals and missing days easy to handle.

On this page 10
  1. The short answer
  2. The analogy you have already lived
  3. Why it exists
  4. How it works
  5. The bendy trend
  6. The part that makes it worth learning
  7. Where you have already seen this shape of thinking
  8. What is honestly hard here
  9. Remember this
  10. What to learn next

One lesson, three depths. Pick the one that fits you today — you can switch any time.

Beginner — No maths. Plain English.

The short answer

Prophet builds a forecast by adding up separate pieces: a slow trend, a weekly shape, a yearly shape and named festival days. It never studies how yesterday relates to today.

The analogy you have already lived

Think about how your mother decides how much rice to cook for the week.

There is a base amount for the family. There is extra on Saturday and Sunday because everyone eats at home. There is a lot extra on the day cousins visit for the festival. And the base amount has crept up over the years as the children grew.

She never thinks "today's quantity depends on yesterday's quantity". She adds up named reasons: the base, the weekend, the festival, the growth.

Prophet works exactly that way. Every forecast is a sum of pieces you can name.

Why it exists

ARIMA is powerful and fussy. It needs you to choose orders, to make the series stop drifting, to have no gaps, and to have no wild outliers. Analysts who know it well get excellent results. Everyone else struggles.

Facebook's data science team had thousands of business series and not enough forecasting specialists. They wanted something a competent analyst could run without a statistics degree. It had to handle what business data always has. Holidays, missing days, sudden growth changes, and the occasional nonsense value.

Prophet was their answer, released free in 2017.

How it works

   Stack these four pieces on top of each other,
   and the height of the stack is the forecast:

      TREND       the slow direction, allowed to bend
      WEEKLY      what a typical Monday, Tuesday ... looks like
      YEARLY      what a typical January, February ... looks like
      HOLIDAYS    named dates you supply, each with its own effect

      ... and whatever is left over is called the leftovers

Each piece is fitted separately and added. That has one very practical consequence: you can look at each piece on its own. Prophet will draw you the weekly shape, the yearly shape and the size of each holiday effect. That makes it easy to explain a forecast to someone who does not code.

The bendy trend

Straight-line trends break on real business data. A shop grows slowly, then opens a second branch and grows fast, then a competitor arrives and it flattens.

Prophet handles this by allowing the trend line to change slope at a number of points along the history. It picks where those bends go by looking for places the growth genuinely shifted.

You control how freely it may bend. Let it bend too freely and it chases noise, then extrapolates that noise into the future. Let it bend too little and it misses a genuine change of direction. This single dial causes most of the disappointing Prophet forecasts in the world.

The part that makes it worth learning

Festivals move.

Diwali fell in early November in 2021, late October in 2022, mid November in 2023 and end of October in 2024. Any method that learns "October is high" will get this wrong in half the years.

Prophet lets you hand it a list of the actual dates, plus how many days before and after the effect lasts. It then learns the size of the bump once and places it on the right dates in future years.

For Indian retail data this is not a nice extra. It is the difference between a usable forecast and a useless one, and the developer section measures exactly how much difference.

Where you have already seen this shape of thinking

  • Staff rosters built from a base number, plus weekend extra, plus festival extra.
  • Electricity demand planning for a state, which adds a growth term, a summer term and a holiday term.
  • Hotel pricing that lifts rates for weekends and for known event dates.
  • School canteen ordering, which drops on exam days someone marked in a calendar.

What is honestly hard here

Prophet's reputation is better than its record, and you should know that before you rely on it.

Independent comparisons have repeatedly found Prophet losing to simple methods on standard benchmark data. It is easy to run, which is not the same as accurate. Always score it against a seasonal naive baseline before you believe it. That baseline is the cheapest forecast there is: repeat whatever happened on the same weekday last week.

It is also weak at exactly one thing: short-horizon forecasts of a series with strong day-to-day memory. Prophet is fitting a curve through time. It does not model "today looks like yesterday" at all. For predicting tomorrow's value from today's, ARIMA usually wins.

Use Prophet when your series is driven by the calendar. Use something else when it is driven by its own recent past.

Remember this

  • Prophet adds up named pieces: trend, weekly, yearly, holidays.
  • Its best feature is handling moving festivals and gaps in the data.
  • It is convenient, not automatically accurate. Always compare it to a simple baseline.

What to learn next

Developer — Code and libraries.

Setup

bash
pip install prophet pandas numpy

Prophet ships a compiled Stan backend in the wheel, so no compiler is needed. The first import is slow; fitting a few thousand rows takes a second or two on a laptop CPU.

A note on reproducibility. The yhat column is produced by a deterministic optimiser and is stable across runs on the same Prophet version. The yhat_lower and yhat_upper columns are simulated, so they move slightly unless you seed NumPy first. Across different Prophet or Stan versions, the last digit of any of these may differ from what is printed here.

The API in one script

Prophet requires a DataFrame with two columns named exactly ds (the dates) and y (the values). That naming requirement catches everyone once.

prophet_basic.py
import logging
import numpy as np
import pandas as pd
from prophet import Prophet

logging.getLogger("cmdstanpy").disabled = True    # Stan prints progress lines otherwise

rng = np.random.default_rng(4)
n = 800
days = pd.date_range("2023-01-01", periods=n, freq="D")
weekend = np.where(days.dayofweek >= 5, 180, 0)
yearly = 120 * np.sin(2 * np.pi * (days.dayofyear - 80) / 365.25)
visits = np.linspace(800, 1400, n) + weekend + yearly + rng.normal(0, 40, n)

df = pd.DataFrame({"ds": days, "y": visits.round()})   # Prophet insists on these two column names
train, test = df.iloc[:770], df.iloc[770:]

m = Prophet(yearly_seasonality=True, weekly_seasonality=True, daily_seasonality=False)
m.fit(train)

np.random.seed(0)                                 # the interval columns are simulated, so pin them
future = m.make_future_dataframe(periods=30)
fc = m.predict(future).tail(30)[["ds", "yhat", "yhat_lower", "yhat_upper"]]
out = fc.merge(test, on="ds")

print(out.head(8).round(0).to_string(index=False))
print("\nMAE:", round(float(np.mean(np.abs(out.y - out.yhat))), 1))
print("inside the 80% band:", int(((out.y >= out.yhat_lower) & (out.y <= out.yhat_upper)).sum()), "of", len(out))
Output
        ds   yhat  yhat_lower  yhat_upper      y
2025-02-09 1475.0      1422.0      1526.0 1422.0
2025-02-10 1298.0      1247.0      1348.0 1355.0
2025-02-11 1299.0      1248.0      1348.0 1323.0
2025-02-12 1309.0      1256.0      1361.0 1289.0
2025-02-13 1302.0      1250.0      1350.0 1370.0
2025-02-14 1318.0      1271.0      1373.0 1333.0
2025-02-15 1501.0      1446.0      1549.0 1511.0
2025-02-16 1495.0      1444.0      1541.0 1447.0

MAE: 38.2
inside the 80% band: 22 of 30

The MAE of 38.2 sits at the noise floor. This series has noise with a standard deviation of 40, so an average absolute error under 40 means the structure was captured almost completely.

The weekend jump is visible in the forecast, not only in the data. February 15 and 16 are a Saturday and Sunday, and yhat rises to about 1500 for both while weekdays sit near 1300.

22 of 30 inside an 80 percent band is 73 percent, slightly under nominal. Prophet's intervals account for uncertainty in the trend and in the noise, but not for uncertainty in the seasonal terms. They are known to be somewhat narrow. Treat them as a rough guide, and check coverage on your own data before quoting them to anyone.

make_future_dataframe extends the existing dates. It returns history plus the new period, which is why the script takes .tail(30).

The feature that earns its place: moving festivals

This is the measurement that decides whether Prophet belongs in your stack. Diwali moves by weeks between years. Here is the same model, with and without being told about it.

prophet_diwali.py
import logging
import numpy as np
import pandas as pd
from prophet import Prophet

logging.getLogger("cmdstanpy").disabled = True

diwali = pd.to_datetime(["2021-11-04", "2022-10-24", "2023-11-12", "2024-10-31", "2025-10-21"])

rng = np.random.default_rng(6)
days = pd.date_range("2021-01-01", "2025-11-30", freq="D")
sales = np.linspace(300, 450, len(days)) + rng.normal(0, 15, len(days))
for d in diwali:                      # a rush that starts about 5 days before and ends 1 day after
    lift = np.exp(-0.5 * ((days - d).days / 3.0) ** 2) * 400
    sales = sales + np.where((days - d).days <= 1, lift, 0)

df = pd.DataFrame({"ds": days, "y": sales.round()})
train = df[df.ds < "2025-10-01"]
test = df[df.ds >= "2025-10-01"]

holidays = pd.DataFrame({"holiday": "diwali", "ds": diwali, "lower_window": -6, "upper_window": 2})

for label, kwargs in [("no holiday column", {}), ("with holiday column", {"holidays": holidays})]:
    m = Prophet(yearly_seasonality=True, weekly_seasonality=False,
                daily_seasonality=False, **kwargs).fit(train)
    pred = m.predict(m.make_future_dataframe(periods=len(test))).tail(len(test))
    err = np.abs(test.y.to_numpy() - pred.yhat.to_numpy())
    peak = test.ds.to_numpy() == np.datetime64("2025-10-21")
    print(f"{label:22s} MAE over Oct-Nov: {err.mean():6.1f}   error on Diwali day: {err[peak][0]:6.1f}")
Output
no holiday column      MAE over Oct-Nov:   64.0   error on Diwali day:  356.8
with holiday column    MAE over Oct-Nov:   14.2   error on Diwali day:   10.2

The error on Diwali day fell from 356.8 to 10.2. Without the holiday column, the yearly seasonality smeared a bump across late October and mid November, so it was wrong on both. With it, the model learned one bump shape from four past Diwalis and placed it on the correct 2025 date.

lower_window=-6 and upper_window=2 say the effect runs from six days before to two days after. Getting this window right matters. Too narrow and the shoulder days are missed; too wide and the model spends parameters on days with no effect.

Prophet also ships a country holiday table. m.add_country_holidays(country_name="IN") loads Indian public holidays automatically. It covers gazetted holidays, so check whether the dates that actually move your business are in there before relying on it.

The dial that causes most Prophet failures

python
Prophet(changepoint_prior_scale=0.05)   # default

This controls how freely the trend may bend. It is the first thing to tune, and often the only thing.

  • Forecast shoots off in an unlikely direction → lower it, toward 0.01. The trend becomes stiffer.
  • Forecast ignores a real change of level in recent months → raise it, toward 0.5.

A related trap: by default, changepoints are only placed in the first 80 percent of the history. A growth change in the final months is invisible to the model. Raise changepoint_range if your data recently turned.

Other settings worth knowing

python
Prophet(
    growth="logistic",              # needs a 'cap' column; keeps forecasts under a ceiling
    seasonality_mode="multiplicative",  # when the weekly swing grows with the level
    interval_width=0.95,            # default is 0.80, which surprises people
    weekly_seasonality=10,          # more Fourier terms = a wigglier weekly shape
)
m.add_seasonality(name="monthly", period=30.5, fourier_order=5)
m.add_regressor("temperature")      # an outside variable; you must supply its future values too

seasonality_mode="multiplicative" is the setting most often left wrong. If your festival bump grows as the business grows — see trend, seasonality and noise — the default additive mode will underfit your busy years.

add_regressor carries the same warning as every method that uses outside variables: you must know the regressor's future values to forecast with it. That is covered properly in multivariate forecasting.

Common mistakes

Wrong column names. Anything other than ds and y raises ValueError: Dataframe must have columns "ds" and "y" with the dates and values respectively. Rename before fitting.

Timezone-aware timestamps. Prophet rejects them. Use df.ds = df.ds.dt.tz_localize(None) first.

Forgetting a baseline. Prophet always returns a smooth, plausible-looking forecast, which makes it easy to skip the comparison. Score y.shift(7) on the same test window. If Prophet does not beat it, do not deploy it.

Trusting the default 80 percent interval as 95 percent. interval_width defaults to 0.80. Many dashboards display it labelled as a confidence band and readers assume 95.

Using it for hourly data with several cycles. Prophet handles daily plus weekly plus yearly, but heavy sub-daily seasonality with holiday interactions gets slow and awkward. NeuralProphet or a feature-based model handles that better.

Tuning on the test set. changepoint_prior_scale is tempting to nudge until the test MAE looks good. Use prophet.diagnostics.cross_validation with a rolling origin, and keep a final untouched holdout.

Try it yourself

In the Diwali script, change lower_window=-6 to lower_window=0, so the model is told the effect starts on the day itself.

Predict what happens to the October-November MAE before you run it. The true rush builds up over the preceding days, so the model now has to explain that build-up with something else. Watch where the error goes.

What to learn next

Researcher — Mathematics and papers.

The model

Taylor and Letham (2018) specify a decomposable additive model — a generalised additive model in time, with no autoregressive component whatsoever:

$$ y(t) = g(t) + s(t) + h(t) + \varepsilon_t $$

  • $g(t)$ — the trend, piecewise linear or logistic.
  • $s(t)$ — periodic seasonality, represented by a truncated Fourier series.
  • $h(t)$ — holiday and event effects, from a user-supplied date table.
  • $\varepsilon_t \sim \mathrm{N}(0,\sigma^2)$ — assumed i.i.d.

The last assumption is the design's defining commitment and its defining weakness. Prophet models $y$ as a function of $t$, not as a function of $y_{t-1}$. Fitting is a curve-fitting problem, which is why it tolerates gaps, unequal spacing and outliers, and why it is uncompetitive when the dominant signal is short-lag autocorrelation.

Trend

The piecewise-linear form with changepoints $s_1,\dots,s_S$ is

$$ g(t) = (k + \mathbf{a}(t)^\top \boldsymbol{\delta})\,t + (m + \mathbf{a}(t)^\top \boldsymbol{\gamma}) $$

  • $k$ — the base growth rate.
  • $\boldsymbol{\delta} \in \mathbb{R}^S$ — the rate adjustments at each changepoint.
  • $a_j(t) = \mathbf{1}[t \ge s_j]$ — the changepoint indicator vector.
  • $m$ — the offset, with $\gamma_j = -s_j\delta_j$ chosen to keep $g$ continuous.

Sparsity is imposed by a Laplace prior $\delta_j \sim \mathrm{Laplace}(0, \tau)$, where $\tau$ is changepoint_prior_scale. This is the Bayesian analogue of an $L_1$ penalty, so most $\delta_j$ shrink to near zero and a few survive. Candidate changepoints default to 25 points uniformly spaced over the first 80 percent of history.

Forecast uncertainty in the trend is generated by simulating future changepoints at the historical rate, with magnitudes drawn from a Laplace fitted to the observed $\delta_j$. This is an explicitly heuristic device, acknowledged as such in the paper: it assumes the future will bend as often as the past did.

The logistic form is

$$ g(t) = \frac{C(t)}{1 + \exp!\left(-(k + \mathbf{a}(t)^\top\boldsymbol{\delta})(t - (m + \mathbf{a}(t)^\top\boldsymbol{\gamma}))\right)} $$

with a time-varying capacity $C(t)$ that the user must supply. Prophet does not estimate the ceiling.

Seasonality and holidays

$$ s(t) = \sum_{n=1}^{N}\left[a_n \cos!\left(\frac{2\pi n t}{P}\right) + b_n \sin!\left(\frac{2\pi n t}{P}\right)\right] $$

  • $P$ — the period in days (365.25 yearly, 7 weekly).
  • $N$ — the Fourier order, defaulting to 10 for yearly and 3 for weekly.

The $2N$ coefficients get a prior $\mathrm{N}(0, \sigma_s^2)$ with $\sigma_s$ set by seasonality_prior_scale. Fourier terms rather than seasonal indices are what allow non-integer periods and multiple simultaneous seasonalities at $2N$ parameters each.

Holidays are a design matrix of indicators over the union of each event's window $[\text{lower}, \text{upper}]$, with coefficients $\kappa \sim \mathrm{N}(0, \nu^2)$. Each window day gets its own coefficient, so an event with a seven-day window costs seven parameters and needs several past occurrences to estimate them.

Estimation is MAP by L-BFGS in Stan by default, with mcmc_samples > 0 switching to full HMC and giving seasonality uncertainty that MAP omits. MAP fitting is the reason the point forecast is deterministic while the intervals are not.

The empirical record, stated plainly

Prophet's adoption far exceeds its measured accuracy, and this is worth being precise about.

Makridakis, Spiliotis and Assimakopoulos (2018), Statistical and Machine Learning forecasting methods: Concerns and ways forward, evaluated a range of methods on M3 series and found Prophet among the weaker performers relative to statistical baselines. Later reproductions on M-competition data have consistently placed Prophet below well-tuned ETS and ARIMA on standard benchmarks, and no Prophet-based entry has featured in the top of any M competition.

The reason is structural rather than a matter of tuning. M-competition series are mostly short, monthly or quarterly, and dominated by autocorrelation rather than calendar effects. Prophet has no autoregressive term, so on those series it is fitting a smooth curve to something whose predictability lives in the lags.

Where Prophet does earn its place is the regime it was designed for: long daily business series with strong multi-scale seasonality, irregular holiday effects, missing observations, outliers, and level shifts — combined with an analyst who needs a defensible, decomposable explanation rather than the last percent of accuracy. That is a real and common regime, and it is not the one the benchmarks measure.

Extensions

NeuralProphet (Triebe et al., 2021) keeps the decomposable structure and adds an autoregressive module (AR-Net, a sparse linear or shallow network over lags) plus lagged covariates, addressing the missing $y_{t-1}$ dependence directly while retaining interpretable components. It is a PyTorch implementation, so it also gets minibatch training over long series.

Orbit (Uber) and PyMC-based structural time series offer the same decomposition under full Bayesian inference with proper posterior intervals.

Structural time series in the classical sense (Harvey, 1989) is the closer statistical ancestor: local level plus local trend plus seasonal, estimated in state space by the Kalman filter, which does model the dependence and gives calibrated intervals. statsmodels.tsa.statespace.structural.UnobservedComponents implements it. If you like Prophet's decomposition but need honest intervals, that is where to look.

Reading

  • Taylor and Letham (2018), Forecasting at scale, The American Statistician 72(1) — the Prophet paper. Preprint at peerj.com/preprints/3190.
  • Triebe, Laptev and Rajagopal (2021), NeuralProphet: Explainable Forecasting at Scale — arxiv.org/abs/2111.15397.
  • Harvey (1989), Forecasting, Structural Time Series Models and the Kalman Filter, CUP.
  • Makridakis, Spiliotis and Assimakopoulos (2018), Statistical and Machine Learning forecasting methods: Concerns and ways forward, PLoS ONE 13(3).
  • Hastie and Tibshirani (1987), Generalized Additive Models: Some Applications, JASA 82(398) — the framework Prophet instantiates.

What to learn next