Hierarchical Time Series Forecasting

Introduction to Hierarchical Time Series

Hierarchical time series are collections of related time series organized in a hierarchical structure. These structures naturally arise when data can be disaggregated by different categorical variables or attributes.

Key Concepts

Hierarchy: A nested structure where series at higher levels are the sum of series at lower levels. For example:

- Total tourism → State level → Purpose of travel

Grouped Time Series: A generalization of hierarchies where series can be disaggregated in multiple ways that don’t nest perfectly. Series can be grouped by different attributes simultaneously.

Example: In the tourism data:

  1. Hierarchy: State / Region / Purpose (State contains Regions)

  2. Grouped: State × Purpose AND Region × Purpose (cross-classification)

    - You can view by State + Purpose: “NSW Business”, “NSW Holiday”…

    - You can view by Region + Purpose: “Sydney Business”, “Melbourne Business”…

    - These groupings overlap but don’t nest cleanly

Why Use Hierarchical Forecasting?

  1. Coherence: Ensures forecasts are mathematically consistent (lower-level forecasts sum to upper-level forecasts)
  2. Information sharing: Allows information to flow between levels
  3. Flexibility: Can forecast at any level while maintaining consistency
  4. Improved accuracy: Often improves forecast accuracy through reconciliation

Reconciliation Methods

Bottom-Up

  • Forecast only the bottom-level series
  • Sum these forecasts to get higher-level forecasts
  • Pros: Simple, no information loss at bottom level
  • Cons: Ignores potentially useful information at aggregate levels

Top-Down

  • Forecast only the top-level series
  • Disaggregate using proportions
  • Pros: Can be more stable for volatile bottom-level series
  • Cons: Loses disaggregate-level information

Middle-Out

  • Forecast at a middle level
  • Combine top-down and bottom-up approaches

Optimal Reconciliation (MinT)

  • Uses all forecasts (base forecasts at all levels)
  • Finds optimal combination that minimizes forecast error variance
  • Methods include: OLS, WLS, MinT (minimum trace)

Example: Australian Tourism Data

Data Structure

library(fpp3)
library(dplyr)

remove(list=ls())

# Load tourism data
df <- tsibble::tourism
Examine the hierarchy structure (using length, unique and cat commands)
Time periods: 80 quarters
States: 8 states
Regions: 76 regions
Purpose categories: 4 
Expected series (State × Purpose): 32 

Understanding the Hierarchy

The tourism data has a natural hierarchy:

- Top level: Total Australian tourism

- Level 1: Disaggregated by State (8 states/territories)

- Level 2: Disaggregated by Purpose (4 categories: Business, Holiday, Visiting, Other)

- Bottom level: State × Purpose combinations (8 × 4 = 32 series)

Creating the Hierarchical Structure

# Create hierarchical time series using aggregate_key
tourism_hier <- tourism %>%
  aggregate_key(State / Purpose, Trips = sum(Trips))

# View the structure
tourism_hier
# A tsibble: 3,280 x 4 [1Q]
# Key:       State, Purpose [41]
   Quarter State        Purpose       Trips
     <qtr> <chr*>       <chr*>        <dbl>
 1 1998 Q1 <aggregated> <aggregated> 23182.
 2 1998 Q2 <aggregated> <aggregated> 20323.
 3 1998 Q3 <aggregated> <aggregated> 19827.
 4 1998 Q4 <aggregated> <aggregated> 20830.
 5 1999 Q1 <aggregated> <aggregated> 22087.
 6 1999 Q2 <aggregated> <aggregated> 21458.
 7 1999 Q3 <aggregated> <aggregated> 19914.
 8 1999 Q4 <aggregated> <aggregated> 20028.
 9 2000 Q1 <aggregated> <aggregated> 22339.
10 2000 Q2 <aggregated> <aggregated> 19941.
# ℹ 3,270 more rows
# The aggregate_key function creates:
# - Aggregated series at all levels
# - Special <aggregated> labels for higher levels

tourism_hier %>%
  as_tibble() %>%
  distinct(State, Purpose) %>%
  arrange(State, Purpose) %>%
  print(n = 50)
# A tibble: 41 × 2
   State              Purpose     
   <chr*>             <chr*>      
 1 ACT                Business    
 2 ACT                Holiday     
 3 ACT                Other       
 4 ACT                Visiting    
 5 ACT                <aggregated>
 6 New South Wales    Business    
 7 New South Wales    Holiday     
 8 New South Wales    Other       
 9 New South Wales    Visiting    
10 New South Wales    <aggregated>
11 Northern Territory Business    
12 Northern Territory Holiday     
13 Northern Territory Other       
14 Northern Territory Visiting    
15 Northern Territory <aggregated>
16 Queensland         Business    
17 Queensland         Holiday     
18 Queensland         Other       
19 Queensland         Visiting    
20 Queensland         <aggregated>
21 South Australia    Business    
22 South Australia    Holiday     
23 South Australia    Other       
24 South Australia    Visiting    
25 South Australia    <aggregated>
26 Tasmania           Business    
27 Tasmania           Holiday     
28 Tasmania           Other       
29 Tasmania           Visiting    
30 Tasmania           <aggregated>
31 Victoria           Business    
32 Victoria           Holiday     
33 Victoria           Other       
34 Victoria           Visiting    
35 Victoria           <aggregated>
36 Western Australia  Business    
37 Western Australia  Holiday     
38 Western Australia  Other       
39 Western Australia  Visiting    
40 Western Australia  <aggregated>
41 <aggregated>       <aggregated>
41 * 80 # 41 series * 80 quarters
[1] 3280

Creating a Grouped Time Series Structure

# Create GROUPED time series: State * Purpose (cross-classification)
tourism_grouped <- tourism %>%
  aggregate_key(State * Purpose, Trips = sum(Trips))

tourism_grouped
# A tsibble: 3,600 x 4 [1Q]
# Key:       State, Purpose [45]
   Quarter State        Purpose       Trips
     <qtr> <chr*>       <chr*>        <dbl>
 1 1998 Q1 <aggregated> <aggregated> 23182.
 2 1998 Q2 <aggregated> <aggregated> 20323.
 3 1998 Q3 <aggregated> <aggregated> 19827.
 4 1998 Q4 <aggregated> <aggregated> 20830.
 5 1999 Q1 <aggregated> <aggregated> 22087.
 6 1999 Q2 <aggregated> <aggregated> 21458.
 7 1999 Q3 <aggregated> <aggregated> 19914.
 8 1999 Q4 <aggregated> <aggregated> 20028.
 9 2000 Q1 <aggregated> <aggregated> 22339.
10 2000 Q2 <aggregated> <aggregated> 19941.
# ℹ 3,590 more rows
# This creates all combinations:
# - Total (all aggregated)
# - By State only (Purpose aggregated)
# - By Purpose only (State aggregated)  
# - By State AND Purpose (bottom level, no aggregation)

# Compare counts
cat("Hierarchical series:", NROW(tourism_hier), "\n")
Hierarchical series: 3280 
cat("Grouped series:", NROW(tourism_grouped), "\n")
Grouped series: 3600 

Difference: Grouped structure adds 4 ‘Purpose-only’ series.

4 purpose only series * 80 quarters = 320 obs

Total

├── By State (aggregated over Purpose)

├── By Purpose (aggregated over State)

└── By State × Purpose (bottom level)

Fitting Base Models

# Fit ETS models to ALL levels of the hierarchy
tourism_fit <- tourism_hier %>%
  model(base = ETS(Trips))

# This creates forecasts at every level, which may not be coherent
tourism_fit
# A mable: 41 x 3
# Key:     State, Purpose [41]
   State           Purpose              base
   <chr*>          <chr*>            <model>
 1 ACT             Business     <ETS(M,N,M)>
 2 ACT             Holiday      <ETS(M,N,A)>
 3 ACT             Other        <ETS(M,N,N)>
 4 ACT             Visiting     <ETS(M,N,N)>
 5 ACT             <aggregated> <ETS(M,A,N)>
 6 New South Wales Business     <ETS(M,N,A)>
 7 New South Wales Holiday      <ETS(M,N,A)>
 8 New South Wales Other        <ETS(A,N,N)>
 9 New South Wales Visiting     <ETS(A,N,A)>
10 New South Wales <aggregated> <ETS(A,N,A)>
# ℹ 31 more rows

Reconciliation with MinT

# Reconcile forecasts to ensure coherence
reconciled <- tourism_fit %>%
  reconcile(
    bu = bottom_up(base),           # Bottom-up reconciliation
    td = top_down(base),             # Top-down reconciliation  
    mint = min_trace(base, method = "mint_shrink")  # MinT with shrinkage
  )

reconciled
# A mable: 41 x 6
# Key:     State, Purpose [41]
   State          Purpose            base bu           td           mint        
   <chr*>         <chr*>          <model> <model>      <model>      <model>     
 1 ACT          … Business   <ETS(M,N,M)> <ETS(M,N,M)> <ETS(M,N,M)> <ETS(M,N,M)>
 2 ACT          … Holiday    <ETS(M,N,A)> <ETS(M,N,A)> <ETS(M,N,A)> <ETS(M,N,A)>
 3 ACT          … Other      <ETS(M,N,N)> <ETS(M,N,N)> <ETS(M,N,N)> <ETS(M,N,N)>
 4 ACT          … Visiting   <ETS(M,N,N)> <ETS(M,N,N)> <ETS(M,N,N)> <ETS(M,N,N)>
 5 ACT          … <aggregat… <ETS(M,A,N)> <ETS(M,A,N)> <ETS(M,A,N)> <ETS(M,A,N)>
 6 New South Wal… Business   <ETS(M,N,A)> <ETS(M,N,A)> <ETS(M,N,A)> <ETS(M,N,A)>
 7 New South Wal… Holiday    <ETS(M,N,A)> <ETS(M,N,A)> <ETS(M,N,A)> <ETS(M,N,A)>
 8 New South Wal… Other      <ETS(A,N,N)> <ETS(A,N,N)> <ETS(A,N,N)> <ETS(A,N,N)>
 9 New South Wal… Visiting   <ETS(A,N,A)> <ETS(A,N,A)> <ETS(A,N,A)> <ETS(A,N,A)>
10 New South Wal… <aggregat… <ETS(A,N,A)> <ETS(A,N,A)> <ETS(A,N,A)> <ETS(A,N,A)>
# ℹ 31 more rows

Generating Forecasts

# Generate 2-year ahead forecasts
tourism_fc <- reconciled %>%
  forecast(h = "2 years")

tourism_fc
# A fable: 1,312 x 6 [1Q]
# Key:     State, Purpose, .model [164]
   State  Purpose  .model Quarter
   <chr*> <chr*>   <chr>    <qtr>
 1 ACT    Business base   2018 Q1
 2 ACT    Business base   2018 Q2
 3 ACT    Business base   2018 Q3
 4 ACT    Business base   2018 Q4
 5 ACT    Business base   2019 Q1
 6 ACT    Business base   2019 Q2
 7 ACT    Business base   2019 Q3
 8 ACT    Business base   2019 Q4
 9 ACT    Business bu     2018 Q1
10 ACT    Business bu     2018 Q2
# ℹ 1,302 more rows
# ℹ 2 more variables: Trips <dist>, .mean <dbl>

Visualizing Reconciled Forecasts

# Plot forecasts for Queensland by purpose
tourism_fc %>%
  filter(State == "Queensland") %>% # try eliminating
  autoplot(tourism_hier, level = 95) +
  labs(
    title = "Reconciled Forecasts: Queensland Tourism by Purpose",
    y = "Trips ('000)",
    x = "Quarter"
  ) +
  facet_wrap(~ Purpose, scales = "free_y", ncol = 2) +
  theme_minimal()

Comparing Reconciliation Methods

# Compare different reconciliation methods for total Queensland
tourism_fc %>%
  filter(State == "Queensland", is_aggregated(Purpose)) %>% # try eliminating
  autoplot(tourism_hier, level = NULL) +
  labs(
    title = "Comparison of Reconciliation Methods: Total Queensland Tourism",
    y = "Trips ('000)",
    x = "Quarter"
  ) +
  theme_minimal()

Key Functions Reference

Creating Hierarchies

  • aggregate_key(): Creates hierarchical structure with aggregations
  • Syntax: aggregate_key(var1 / var2 / ..., measure = sum(measure))

Reconciliation Methods

  • bottom_up(): Bottom-up reconciliation
  • top_down(): Top-down reconciliation
  • middle_out(): Middle-out reconciliation
  • min_trace(): Optimal reconciliation (MinT)
    • Methods: "ols", "wls_struct", "wls_var", "mint_cov", "mint_shrink"

Workflow

  1. Create hierarchical structure with aggregate_key()
  2. Fit models at all levels with model()
  3. Reconcile forecasts with reconcile()
  4. Generate forecasts with forecast()
  5. Evaluate and visualize

Best Practices

  1. Always reconcile: Base forecasts at different levels are rarely coherent
  2. Use MinT for accuracy: Generally provides best forecast accuracy
  3. Check residuals: Ensure base models are well-specified
  4. Consider computational cost: MinT with many bottom-level series can be slow
  5. Visualize multiple levels: Check forecasts make sense at all levels

References

  • Hyndman, R.J., & Athanasopoulos, G. (2021). Forecasting: principles and practice (3rd ed.). OTexts: Melbourne, Australia. OTexts.com/fpp3
  • Wickramasuriya, S. L., Athanasopoulos, G., & Hyndman, R. J. (2019). Optimal forecast reconciliation for hierarchical and grouped time series through trace minimization. Journal of the American Statistical Association, 114(526), 804-819.