Prophet

Where We Are

Look at how far our model has come since Module 1:

Module What we added Model
1 Decomposition + benchmark STL + Drift + SNAIVE
2 Smarter trend-cycle STL + ETS / ARIMA
3.1–2 External context TSLM, dynamic regression
3.3 Harmonic regression ARIMA + Fourier terms
3.4 Automated & interpretable Prophet

Every previous model required us to make choices manually: how many AR terms? Which regressors? Where do the knots go?

The knot problem we left open

In Linear Regression, we saw that piecewise TSLM is sensitive to where we place the knots — different choices produce dramatically different forecasts, even with similar in-sample fit. Prophet solves this automatically.

What Is Prophet?

Prophet is a forecasting procedure developed by Meta (Facebook) and released as open source in 2017. It was designed for business time series — daily, weekly, and yearly data with strong seasonal effects, holidays, and shifting trends.

“Prophet is a procedure for forecasting time series data based on an additive model where non-linear trends are fit with yearly, weekly, and daily seasonality, plus holiday effects. It works best with time series that have strong seasonal effects and several seasons of historical data.”

facebook.github.io/prophet

At its core, Prophet fits a decomposition model:

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

  • g(t)trend: piecewise linear or logistic growth, with changepoints detected automatically.
  • s(t)seasonality: Fourier series approximating yearly, weekly, or daily patterns.
  • h(t)holidays / special events: user-supplied dummy variables with windowed effects.
  • \varepsilon_tnoise: assumed i.i.d. normal.

Sound familiar?

This is exactly what we’ve been building all semester — trend + seasonality + external effects. Prophet just automates the specification and fits it in a Bayesian framework using Stan.

The key innovation over piecewise TSLM is automatic changepoint detection. Prophet:

  1. Places a large number of potential changepoints uniformly in the first changepoint_range proportion of the data (default: 80%).
  2. Uses a sparse prior (Laplace) to shrink most changepoint magnitudes to zero — only genuine structural breaks survive.
  3. The user can tune n_changepoints (default 25) and changepoint_prior_scale (flexibility of trend changes).

No more guessing where the knots go.

When to use Prophet

Situation Use Prophet?
Sub-daily data (hourly, daily) with multiple seasonal cycles
Business data with holidays and known events
Need interpretable components for stakeholders
Several seasons of history available
Short series (< 2 full seasonal cycles)
Series with no clear trend or seasonality
Need formal inference on model parameters
Need prediction intervals from theory, not simulation ⚠️

:::

Prophet is not always better than ARIMA

Prophet was designed for at-scale, analyst-friendly forecasting. On many classical monthly or quarterly economic series, a well-specified ARIMA or ETS will outperform it. Always compare on a held-out test set.

Setup: fable.prophet

Prophet is available in R through two packages:

  • prophet — the original package. Works standalone, does not integrate with fable.
  • fable.prophet — a fable-compatible wrapper by Mitchell O’Hara-Wild. Lets us use Prophet inside model(), forecast(), accuracy(), and all the tools we already know.
install.packages("fable.prophet")
library(fable.prophet)
1
fable.prophet is now available on CRAN.
2
Load it alongside fpp3 — the prophet() function is then available inside model().

Dependency: Stan

fable.prophet depends on rstan and prophet. On first install, R will also install Stan and its dependencies. This can take a few minutes — plan accordingly before class.

Model Specification

Inside model(), Prophet is specified with the prophet() function. Like ARIMA() and ETS(), it can be fully automatic or manually specified.

# Automatic — Prophet chooses everything
prophet(y)

# Manual — explicit components
prophet(y ~ growth("linear") + season("year", type = "multiplicative"))

The growth() term specifies the trend model:

  • growth("linear") — piecewise linear trend. Best for series that grow or decline without a natural ceiling.
  • growth("logistic") — logistic growth (S-curve). Requires a capacity column in the data specifying the theoretical maximum.
prophet(y ~ growth("linear",
                   n_changepoints    = 25,
                   changepoint_range = 0.8,
                   changepoint_prior_scale = 0.05))
1
Number of potential changepoints to consider (default: 25).
2
Proportion of history where changepoints can occur (default: 0.8).
3
Flexibility of trend changes — larger = more flexible, smaller = more rigid (default: 0.05).

The season() term adds a Fourier-approximated seasonal pattern:

prophet(y ~
  season("year",  period = 365.25, order = 10, type = "additive") +
  season("week",  period = 7,      order = 3,  type = "multiplicative") +
  season("day",   period = 1,      order = 5,  type = "additive"))
1
Annual seasonality — 10 Fourier pairs; additive (level of seasonality does not change with the series level).
2
Weekly seasonality — 3 Fourier pairs; multiplicative (seasonality scales with the series level).
3
Daily seasonality — only relevant for sub-daily data.

order = Fourier K

The order argument in season() is the same K we used in harmonic regression (fourier(K = ...)). Higher K → more flexible seasonal shape, but more parameters. Start with the default and adjust if residuals show seasonal structure.

Application: LAX Passengers

We’ll apply Prophet to monthly passenger counts at Los Angeles International Airport (LAX), broken down by domestic and international flights — a dataset with a clear piecewise trend, multiplicative seasonality, and a major structural break (the 2001 and 2008 shocks).

lax_passengers <- read.csv(
  "https://raw.githubusercontent.com/mitchelloharawild/fable.prophet/master/data-raw/lax_passengers.csv"
)

lax_passengers <- lax_passengers |>
  mutate(datetime = lubridate::mdy_hms(ReportPeriod)) |>
  group_by(
    month = yearmonth(datetime),
    type  = Domestic_International
  ) |>
  summarise(passengers = sum(Passenger_Count), .groups = "drop") |>
  as_tsibble(index = month, key = type)

lax_passengers
1
Parse the date-time string into a proper datetime object.
2
Sum all passenger categories within each month and travel type.
3
Convert to tsibbletype (Domestic / International) is the key variable.

The meme check

Spending three hours choosing knots manually vs. discovering Prophet does it for you.

Train / test split

lax_train <- lax_passengers |> filter_index(~ "2017 Dec.")
lax_test  <- lax_passengers |> filter_index("2018 Jan." ~ .)

Fit models

lax_fit <- lax_train |>
  model(
    Prophet     = prophet(passengers ~
                    growth("linear") +
                    season("year", type = "multiplicative")),
    Prophet_auto = prophet(passengers),
    ARIMA       = ARIMA(passengers),
    ETS         = ETS(passengers),
    Harmonic    = ARIMA(passengers ~ fourier(K = 3) + PDQ(0,0,0))
  )

lax_fit
1
Manual Prophet: piecewise linear trend + multiplicative annual seasonality.
2
Automatic Prophet: lets the algorithm decide on all components.
3
Automatic ARIMA — our Module 2 benchmark.
4
Automatic ETS — our other Module 2 benchmark.
5
Harmonic regression: ARIMA errors + Fourier seasonality (Module 3.3).

One of Prophet’s biggest advantages in practice: interpretable components that you can show to a non-technical audience.

lax_fit |>
  select(type, Prophet) |>
  components() |>
  autoplot()

We can also visualize the seasonal component overlaid by year — useful to check if the seasonal shape is stable over time:

lax_fit |>
  select(type, Prophet) |>
  components() |>
  ggplot(aes(
    x      = lubridate::month(month, label = TRUE),
    y      = year,
    colour = type,
    group  = interaction(type, lubridate::year(month))
  )) +
  geom_line(alpha = 0.7) +
  labs(
    title = "Annual seasonal component by year",
    x     = "Month",
    y     = "Seasonal effect",
    color = "Type"
  ) +
  theme(legend.position = "top")
1
Extract the month name from the time index for the x-axis.
2
year is the name of the annual seasonal component extracted by components().

What to look for

If the seasonal lines are stacked consistently, the shape is stable across years — a multiplicative model is appropriate. If lines diverge or change shape dramatically, you may need to reconsider the specification.

lax_fc <- lax_fit |>
  forecast(h = "2 years")

lax_fc |>
  autoplot(
    lax_passengers |> filter_index("2012 Jan." ~ .),
    level = 80
  ) +
  facet_wrap(~ type, ncol = 1, scales = "free_y") +
  labs(
    title  = "LAX passenger forecasts — 2018–2019",
    y      = "Passengers",
    x      = NULL,
    color  = "Model"
  ) +
  theme(legend.position = "top")

lax_accu <- lax_fc |>
  accuracy(lax_test) |>
  select(.model, type, RMSE, MAE, MAPE) |>
  arrange(type, MAPE)
Warning: The future dataset is incomplete, incomplete out-of-sample data will be treated as missing. 
9 observations are missing between 2019 Apr and 2019 Dec
lax_accu

Check accuracy by key

When your tsibble has a key variable (like type here), accuracy() returns one row per model per key. A model that performs best for Domestic passengers may not be best for International — always check both.

lax_fit |>
  select(type, Prophet) |>
  filter(type == "Domestic") |>
  gg_tsresiduals()

Changepoints: Diagnosing the Trend

We can extract the changepoints Prophet detected and plot them against the original series — a powerful diagnostic for communicating with stakeholders.

# Extract trend component to visualize changepoints
lax_fit |>
  select(type, Prophet) |>
  filter(type == "Domestic") |>
  components() |>
  autoplot(trend) +
  labs(
    title = "Prophet trend component with detected changepoints",
    y     = "Trend",
    x     = NULL
  )

Model Comparison

Let’s put all models side by side — zooming in on the test period:

p <- lax_fc |>
  ggplot(aes(x = month, y = .mean, color = .model)) +
  geom_line() +
  geom_line(
    data  = lax_passengers |> filter_index("2015 Jan." ~ .),
    aes(y = passengers, color = NULL),
    color = "grey30",
    linewidth = 0.8
  ) +
  facet_wrap(~ type, ncol = 1, scales = "free_y") +
  labs(
    title = "All models: point forecast comparison",
    y     = "Passengers",
    x     = NULL,
    color = "Model"
  ) +
  theme(legend.position = "top")

ggplotly(p)

Summary

What we covered

Key takeaways:

  • Prophet is a decomposition model — trend + seasonality + holidays + noise — fitting the same structure we’ve built all semester, but in an automated, Bayesian framework.
  • The key innovation is automatic changepoint detection: no manual knot selection required.
  • In fable, Prophet fits into the same model()forecast()accuracy() workflow — no new syntax to learn.
  • Prophet is not universally better: on short, classical economic series, ARIMA and ETS often win. Always compare on a held-out test set.
  • Components are interpretable and easy to communicate to non-technical stakeholders — a real practical advantage.

Coming up in Module 4: We’ve now seen all the main model families. In Module 4, we tackle what happens when data has multiple seasonal periods simultaneously (daily + weekly + yearly), and how to make our models robust to outliers, missing values, and real-world messiness — including ensembling the models we’ve built throughout the semester.

Further resources

Talk by the Prophet team at PyData — covers the motivation, the math, and practical use cases (30 min).