Seasonality

Forecast models learn seasonal (cyclical) patterns and project them into the future. Understanding the seasonal patterns in your dataset can help you create a better forecast. Your goal is to identify which seasonality patterns are most important to capture, and which should be excluded from the model.

This tutorial explains how to identify the dominant seasonal patterns and check for interactions (e.g. daily seasonality that depends on day of week, or weekly seasonality increases in magnitude over time). Such interactions are important to model if they affect a large number of data points.

We use the Peyton Manning dataset as a running example.

Quick reference

You will learn how to use the function plot_quantiles_and_overlays in UnivariateTimeSeries to assess seasonal patterns.

Steps to detect seasonality:

  1. Start with the longest seasonal cycles to see the big picture, then proceed to shorter cycles. (yearly -> quarterly -> monthly -> weekly -> daily).

  2. For a given seasonality period:

    1. First, check for seasonal effect over the entire timeseries (main effect). Look for large variation that depends on the location in the cycle. Pick the time feature for your seasonality cycle. See available time features at build_time_features_df.

      • for yearly: "doy", "month_dom", "woy_dow"

      • for quarterly: "doq"

      • for monthly "dom"

      • for weekly: "str_dow", "dow_grouped", "is_weekend", "woy_dow"

      • for daily: "hour", "tod"

      • (“do” = “day of”, “to” = “time of”)

      fig = ts.plot_quantiles_and_overlays(
          groupby_time_feature="doy",  # day of year (yearly seasonality)
          show_mean=True,              # shows mean on the plot
          show_quantiles=[0.1, 0.9],   # shows quantiles [0.1, 0.9] on the plot
          xlabel="day of year",
          ylabel=ts.original_value_col,
          title="yearly seasonality",
      )
      
    2. Then, check for interactions by adding overlays and centering the values. These may be present even when there is no main effect:

      # random sample of individual overlays (check for clusters with similar patterns)
      fig = ts.plot_quantiles_and_overlays(
          groupby_time_feature="str_dow",  # day of week (weekly seasonality)
          show_mean=True,
          show_quantiles=False,
          # shows every 5th overlay. (accepts a list of indices/names, a number to sample, or `True` to show all)
          show_overlays=np.arange(0, ts.df.shape[0], 5),
          center_values=True,
          # each overlay contains 28 observations (4 weeks)
          overlay_label_sliding_window_size=28,
          xlabel="day of week",
          ylabel=ts.original_value_col,
          title="weekly seasonality with selected 28d averages",
      )
      # interactions with periodic time feature
      fig = ts.plot_quantiles_and_overlays(
          groupby_time_feature="str_dow",
          show_mean=True,
          show_quantiles=False,
          show_overlays=True,
          center_values=True,
          # splits overlays by month (try other features too)
          overlay_label_time_feature="month",
          # optional overlay styling, passed to `plotly.graph_objs.Scatter`
          overlay_style={"line": {"width": 1}, "opacity": 0.5},
          xlabel="day of week",
          ylabel=ts.original_value_col,
          title="weekly seasonality by month",
      )
      # interactions with an event (holiday, etc.)
      fig = ts.plot_quantiles_and_overlays(
          groupby_time_feature="str_dow",
          show_mean=True,
          show_quantiles=False,
          show_overlays=True,
          center_values=True,
          # splits overlays by custom pd.Series value
          overlay_label_custom_column=is_football_season,
          overlay_style={"line": {"width": 1}, "opacity": 0.5},
          # optional, how to aggregate values for each overlay (default=mean)
          aggfunc=np.nanmean,
          xlabel="day of week",
          ylabel=ts.original_value_col,
          title="weekly seasonality:is_football_season interaction",
      )
      # seasonality changepoints (option a): overlay against time (good for yearly/quarterly/monthly)
      fig = ts.plot_quantiles_and_overlays(
          groupby_time_feature="woy_dow",  # yearly(+weekly) seasonality
          show_mean=True,
          show_quantiles=True,
          show_overlays=True,
          overlay_label_time_feature="year",  # splits by time
          overlay_style={"line": {"width": 1}, "opacity": 0.5},
          center_values=True,
          xlabel="weekofyear_dayofweek",
          ylabel=ts.original_value_col,
          title="yearly and weekly seasonality for each year",
      )
      # seasonality changepoints (option b): overlay by seasonality value (good for daily/weekly/monthly)
      # see advanced version below, where the mean is removed.
      fig = ts.plot_quantiles_and_overlays(
          # The number of observations in each sliding window.
          # Should contain a whole number of complete seasonality cycles, e.g. 24*7*k for k weekly seasonality cycles on hourly data.
          groupby_sliding_window_size=7*13,  # x-axis, sliding windows with 13 weeks of daily observations.
          show_mean=True,
          show_quantiles=False,
          show_overlays=True,
          center_values=True,
          # overlays by the seasonality of interest (e.g. "hour", "str_dow", "dom")
          overlay_label_time_feature="str_dow",
          overlay_style={"line": {"width": 1}, "opacity": 0.5},
          ylabel=ts.original_value_col,
          title="daily averages over time (centered)",
      )
      
  3. For additional customization, fetch the dataframe for plotting via get_quantiles_and_overlays, compute additional stats as needed, and plot with plot_multivariate. For example, to remove the mean effect in seasonality changepoints (option b):

    grouped_df = ts.get_quantiles_and_overlays(
        groupby_sliding_window_size=7*13,  # accepts the same parameters as `plot_quantiles_and_overlays`
        show_mean=True,
        show_quantiles=False,
        show_overlays=True,
        center_values=False,  # note! does not center, to compute raw differences from the mean below
        overlay_label_time_feature="str_dow",
    )
    overlay_minus_mean = grouped_df[OVERLAY_COL_GROUP] - grouped_df[MEAN_COL_GROUP].values  # subtracts the mean
    x_col = overlay_minus_mean.index.name
    overlay_minus_mean.reset_index(inplace=True)  # `plot_multivariate` expects the x-value to be a column
    fig = plot_multivariate(  # plots the deviation from the mean
        df=overlay_minus_mean,
        x_col=x_col,
        ylabel=ts.original_value_col,
        title="day of week effect over time",
    )
    
  4. The yearly seasonality plot can also be used to check for holiday effects. Click and drag to zoom in on the dates of interest:

    fig = ts.plot_quantiles_and_overlays(
        groupby_time_feature="month_dom",  # date on x-axis
        show_mean=True,
        show_quantiles=False,
        show_overlays=True,
        overlay_label_time_feature="year",  # see the value for each year
        overlay_style={"line": {"width": 1}, "opacity": 0.5},
        center_values=True,
        xlabel="day of year",
        ylabel=ts.original_value_col,
        title="yearly seasonality for each year (centered)",
    )
    

Tip

  1. plot_quantiles_and_overlays allows grouping or overlays by (1) a time feature, (2) a sliding window, or (3) a custom column. See available time features at build_time_features_df.

  2. You can customize the plot style. See plot_quantiles_and_overlays for details.

Load data

To start, let’s plot the dataset. It contains daily observations between 2007-12-10 and 2016-01-20.

186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
 # necessary imports
 from datetime import datetime

 import numpy as np
 import plotly

 from greykite.framework.input.univariate_time_series import UnivariateTimeSeries
 from greykite.framework.constants import MEAN_COL_GROUP, OVERLAY_COL_GROUP
 from greykite.common.constants import TIME_COL
 from greykite.common.data_loader import DataLoader
 from greykite.common.viz.timeseries_plotting import add_groupby_column, plot_multivariate, plot_univariate

 # Loads dataset into pandas DataFrame
 dl = DataLoader()
 df = dl.load_peyton_manning()
 df.rename(columns={"y": "log(pageviews)"}, inplace=True)  # uses a more informative name

 # plots dataset
 ts = UnivariateTimeSeries()
 ts.load_data(
     df=df,
     time_col="ts",
     value_col="log(pageviews)",
     freq="D")
 fig = ts.plot()
 plotly.io.show(fig)

Yearly seasonality

Because the observations are at daily frequency, it is possible to see yearly, quarterly, monthly, and weekly seasonality. The name of the seasonality refers to the length of one cycle. For example, yearly seasonality is a pattern that repeats once a year.

Tip

It’s helpful to start with the longest cycle to see the big picture.

To examine yearly seasonality, plot the average value by day of year.

Use plot_quantiles_and_overlays with show_mean=True and groupby_time_feature="doy" (day of year). groupby_time_feature accepts any time feature generated by build_time_features_df.

230
231
232
233
234
235
236
237
 fig = ts.plot_quantiles_and_overlays(
     groupby_time_feature="doy",  # day of year
     show_mean=True,              # shows the mean
     xlabel="day of year",
     ylabel=f"mean of {ts.original_value_col}",
     title="yearly seasonality",
 )
 plotly.io.show(fig)