Hierarchical and Grouped Time Series
1 Introduction
In many real-world applications, time-series data are organized in structured forms — for example, products within categories, stores within regions, or states within a country. When these series are related through aggregation, they are referred to as hierarchical or grouped time series.
These structures arise naturally in:
- Retail forecasting (products, stores, regions)
- Tourism demand (states, purposes, demographics)
- Supply chain management (warehouses, distribution centers)
- Economic data (national, regional, sectoral aggregates)
Understanding the difference between hierarchical and grouped structures is crucial for forecast reconciliation — keeping forecasts consistent across aggregation levels.
2 Hierarchical Time Series (HTS)
2.1 Definition
A hierarchical time series is organized in a tree-like structure, where observations at higher levels are obtained by summing the series at lower levels.
Key characteristics:
- Each parent node is the aggregate of its child nodes.
- Aggregation flows along a single path, from the bottom (most detailed level) to the top (overall total).
- Strict tree structure — no cross-classification.
2.2 Example: Retail Sales Hierarchy
Consider a retail company tracking sales across a geographic hierarchy:
Total (Country)
├── State A
│ ├── Region A1
│ │ ├── Store A1a
│ │ └── Store A1b
│ └── Region A2
│ ├── Store A2a
│ └── Store A2b
└── State B
├── Region B1
│ └── Store B1a
└── Region B2
└── Store B2a
Store-level sales sum to region totals, region totals sum to state totals, and state totals sum to the national total.
2.3 Mathematical Representation
Let \(\mathbf{b}_t\) denote the vector of bottom-level series at time \(t\). The hierarchy can be expressed through a summing matrix \(\mathbf{S}\):
\[ \mathbf{y}_t = \mathbf{S} \mathbf{b}_t \]
where \(\mathbf{y}_t\) contains all series (from top to bottom) and \(\mathbf{S}\) encodes the aggregation structure.
2.3.1 Example with 3 bottom-level series
Three stores (A, B, C) in two regions: Region 1 = {A, B}, Region 2 = {C}.
\[ \mathbf{S} = \begin{bmatrix} 1 & 1 & 1 \\ 1 & 1 & 0 \\ 0 & 0 & 1 \\ 1 & 0 & 0 \\ 0 & 1 & 0 \\ 0 & 0 & 1 \end{bmatrix}, \qquad \mathbf{y}_t = \begin{bmatrix} \text{Total} \\ \text{Region 1} \\ \text{Region 2} \\ \text{Store A} \\ \text{Store B} \\ \text{Store C} \end{bmatrix} = \mathbf{S} \begin{bmatrix} y_{A,t} \\ y_{B,t} \\ y_{C,t} \end{bmatrix} \]
3 Grouped Time Series (GTS)
3.1 Definition
A grouped time series does not follow a single hierarchical tree. The same observations can be aggregated along multiple independent dimensions — for example, region, product type, or customer segment.
Key characteristics:
- Each dimension gives a different aggregation view of the same base data.
- Cross-classification: a single observation can belong to several groups simultaneously.
- Multiple valid aggregation paths exist.
3.2 Example: Tourism Data
A tourism company tracks visitor numbers by both state and purpose of travel:
| State | Purpose | Visitors |
|---|---|---|
| NSW | Business | 1,200 |
| NSW | Leisure | 3,400 |
| VIC | Business | 800 |
| VIC | Leisure | 2,100 |
Two aggregation dimensions exist simultaneously:
- By State (NSW, VIC) — geographical aggregation
- By Purpose (Business, Leisure) — functional aggregation
The same base data can be summarized in multiple ways: state totals, purpose totals, and the grand total.
3.3 Grouped Structure Visualization
A GTS cannot be drawn as a single tree because each bottom cell has two parents — one in each aggregation dimension. We show it as two parallel aggregation paths on top, sharing a common bottom grid:
Total
/ \
┌──────┘ └──────┐
By State By Purpose
/ \ / \
NSW VIC Business Leisure
│ │ │ │
└──┐ └──────┐ ┌──────┘ ┌────┘
│ │ │ │
▼ ▼ ▼ ▼
Bottom level — each cell rolls up BOTH ways:
┌─────────┬───────────┬────────────┐
│ │ Business │ Leisure │
├─────────┼───────────┼────────────┤
│ NSW │ NSW×Bus │ NSW×Leis │
│ VIC │ VIC×Bus │ VIC×Leis │
└─────────┴───────────┴────────────┘
The bottom grid is shared across both aggregation paths — NSW×Bus contributes to the NSW total and to the Business total. Only the intermediate levels differ. This is exactly why the summing matrix \(\mathbf{S}\) has extra rows compared to an HTS: it must encode both aggregation views simultaneously.
3.4 Mathematical Representation
As with HTS, a grouped series uses a summing matrix:
\[ \mathbf{y}_t = \mathbf{S} \mathbf{b}_t \]
but here \(\mathbf{S}\) carries rows for each independent grouping path.
3.4.1 Example: Two-way Grouping
For 2 states × 2 purposes = 4 bottom-level series, the relationship \(y = S \cdot b\) writes out as:
\[ \begin{bmatrix} \text{Total} \\ \text{NSW} \\ \text{VIC} \\ \text{Business} \\ \text{Leisure} \\ \text{NSW×Bus} \\ \text{NSW×Leis} \\ \text{VIC×Bus} \\ \text{VIC×Leis} \end{bmatrix} = \begin{bmatrix} 1 & 1 & 1 & 1 \\ 1 & 1 & 0 & 0 \\ 0 & 0 & 1 & 1 \\ 1 & 0 & 1 & 0 \\ 0 & 1 & 0 & 1 \\ 1 & 0 & 0 & 0 \\ 0 & 1 & 0 & 0 \\ 0 & 0 & 1 & 0 \\ 0 & 0 & 0 & 1 \end{bmatrix} \begin{bmatrix} \text{NSW×Bus} \\ \text{NSW×Leis} \\ \text{VIC×Bus} \\ \text{VIC×Leis} \end{bmatrix} \]
Do the matrix multiplication row-by-row to read off each aggregation rule:
- Row 1 (
Total): \(1 \cdot \text{NSW×Bus} + 1 \cdot \text{NSW×Leis} + 1 \cdot \text{VIC×Bus} + 1 \cdot \text{VIC×Leis}\) = grand total. - Row 2 (
NSW): \(1,1,0,0\) picks out the two NSW cells →NSW = NSW×Bus + NSW×Leis. - Row 4 (
Business): \(1,0,1,0\) picks out the two Business cells →Business = NSW×Bus + VIC×Bus. - Rows 6–9 each have a single 1 → the bottom series equal themselves (identity block).
3.4.2 Reading the Matrix: Terminology
Using the 2×2 example to anchor each term:
| Term | Meaning | In this example |
|---|---|---|
| Bottom-level series (\(\mathbf{b}_t\)) | The most disaggregated observations — the “atoms” of the structure. Every other series is a sum of these. |
NSW×Bus, NSW×Leis, VIC×Bus, VIC×Leis — 4 series (columns of \(\mathbf{S}\)) |
| Top-level series | The fully aggregated series at the root of the structure. |
Total — 1 series (row 1 of \(\mathbf{S}\)) |
| Intermediate / middle-level series | Any aggregate series that sits between the top and the bottom. In a GTS, these are the partial aggregations along each grouping dimension. |
NSW, VIC (by State); Business, Leisure (by Purpose) — 4 series |
| Aggregate series | Any series above the bottom level — includes both the top and all intermediate series. | All rows of \(\mathbf{y}_t\) except the last 4 |
| Full series vector (\(\mathbf{y}_t\)) | All series stacked: top first, intermediate next, bottom last. Length \(n\). | 9 series (1 top + 4 intermediate + 4 bottom) |
| Summing matrix (\(\mathbf{S}\)) | An \(n \times m\) 0/1 matrix that expresses each series as a sum of bottom-level series. Each row = one series in \(\mathbf{y}_t\); each column = one bottom-level series. | \(9 \times 4\) |
| Identity block (\(\mathbf{I}_m\)) | The bottom \(m \times m\) block of \(\mathbf{S}\) — the trivial “a bottom series equals itself” rows. | Rows 6–9 of \(\mathbf{S}\) |
How to read a row of \(\mathbf{S}\):
- Row 1 =
Total: all four 1’s →Totalis the sum of every bottom cell. - Row 2 =
NSW: 1’s in the NSW columns only →NSW = NSW×Bus + NSW×Leis. - Row 4 =
Business: 1’s in the Business columns only →Business = NSW×Bus + VIC×Bus. - Row 6 =
NSW×Bus: a single 1 → it is a bottom series (this is the \(\mathbf{I}_m\) block).
Counting the series. For a GTS with \(p\) grouping dimensions of sizes \(k_1, k_2, \dots, k_p\):
\[ m = \prod_{i=1}^{p} k_i \quad (\text{bottom series}), \qquad n = 1 + \sum_{i=1}^{p} k_i + m \quad (\text{total series in } \mathbf{y}_t) \]
In the tourism example (\(k_{\text{state}} = 2\), \(k_{\text{purpose}} = 2\)): \(m = 4\), \(n = 1 + 2 + 2 + 4 = 9\) — matching the 9 rows of \(\mathbf{S}\).
3.4.3 Forecasting Vocabulary
Two more terms you’ll meet once you start forecasting:
-
Base forecasts (\(\hat{\mathbf{y}}_h\)) — the independent forecasts produced by your base model (ETS, ARIMA, TSLM, …) for every series in \(\mathbf{y}_t\), before any adjustment. These are usually incoherent: the forecast for
NSWdoes not equalNSW×Bus + NSW×Leis. - Reconciled forecasts (\(\tilde{\mathbf{y}}_h = \mathbf{S}\mathbf{G}\hat{\mathbf{y}}_h\)) — the adjusted forecasts that satisfy the aggregation structure, i.e., they are coherent across every level and every grouping path.
Coherent vs incoherent: a set of forecasts is coherent if every aggregate equals the sum of its parts — for example, \(\tilde{y}_{\text{Total}} = \tilde{y}_{\text{NSW}} + \tilde{y}_{\text{VIC}} = \tilde{y}_{\text{Business}} + \tilde{y}_{\text{Leisure}} = \sum \tilde{y}_{\text{bottom}}\). That is exactly what reconciliation enforces.
4 HTS vs GTS at a Glance
| Feature | Hierarchical (HTS) | Grouped (GTS) |
|---|---|---|
| Structure | Pure tree, one parent per series | Cross-classification, multiple valid groupings |
| Aggregation path | Unique (e.g., Country → State → Region) | Multiple (State and Purpose both roll up to Total) |
| Summing matrix S | Smaller, fewer constraints | Larger, overlapping constraints |
| Computational cost | Lower | Higher |
| Example | Country → State → City | State × Purpose (two-way grouping) |
| Typical notation | a / b / c |
a * b |
Choosing HTS vs GTS is not a different research question. You are asking the same economic question (e.g., forecasting tourism demand), but you are telling the software how the data are organized and should be reconciled.
That structure determines:
- how forecasts aggregate or cross-classify,
- which reconciliation methods are valid, and
- how
is_aggregated()recognizes levels for plotting or filtering.
You are defining what belongs to what, not yet what to forecast. The base model (ETS, ARIMA, …) doesn’t “know” whether it’s HTS or GTS — the reconciliation step uses the structure.
5 Forecast Reconciliation
5.1 The Coherence Problem
When forecasting hierarchical or grouped time series, independent forecasts made at different levels may be incoherent — upper-level forecasts do not equal the sum of lower-level forecasts.
Example of incoherence:
- Forecast for Total = 1,000 units
- Forecast for State A = 600 units
- Forecast for State B = 500 units
- Problem: 600 + 500 = 1,100 ≠ 1,000
Reconciliation adjusts forecasts so they are internally consistent across all levels.
Base models vs. reconciliation methods — do not confuse these two steps.
| Step | Type | Examples | Purpose |
|---|---|---|---|
| Base forecasting | Statistical / ML models |
ETS(), ARIMA(), TSLM(), NN |
Produce initial (unreconciled) forecasts for each series |
| Reconciliation | Coherence adjustment |
bottom_up(), top_down(), middle_out(), min_trace()
|
Enforce coherence across the structure |
Reconciliation happens after base models produce forecasts.
5.2 The Four Reconciliation Methods
All four methods apply to the general form
\[ \tilde{\mathbf{y}}_{h} = \mathbf{S}\mathbf{G}\,\hat{\mathbf{y}}_{h}, \]
where \(\hat{\mathbf{y}}_h\) are the base forecasts and \(\mathbf{G}\) is the matrix that maps base forecasts to reconciled bottom-level forecasts. What differs across methods is the choice of \(\mathbf{G}\).
5.2.1 Bottom-Up (BU)
Forecast at the bottom level, then aggregate upward.
\[ \hat{y}_{\text{total}} = \sum \hat{y}_{\text{bottom}} \]
- Use when bottom-level data are rich and reliable.
- Works for both HTS and GTS.
- Always coherent by construction; noise at the bottom propagates upward.
5.2.2 Top-Down (TD)
Forecast only the top, then disaggregate using proportions.
- Use when the data form a strict, single-tree hierarchy and totals are stable.
-
method = "forecast_proportions"(dynamic) or"average_proportions"(historical). -
HTS only — not valid for grouped or crossed structures.
reconcile()will error on GTS.
5.2.3 Middle-Out (MO)
Forecast at an intermediate level, then aggregate up and disaggregate down.
- Use when there are three or more levels (e.g., Country → State → Region) and the middle level is the most stable signal.
- With only 2 levels, middle-out defaults to bottom-up (redundant but safe).
- Not defined for grouped/crossed structures.
5.2.4 Optimal Reconciliation — MinT
Uses all base forecasts and finds the optimal \(\mathbf{G}\) that minimizes total forecast-error variance subject to the coherence constraint \(\mathbf{S}\mathbf{G}\mathbf{S} = \mathbf{S}\):
\[ \mathbf{G} = (\mathbf{S}' \mathbf{\Sigma}^{-1} \mathbf{S})^{-1} \mathbf{S}' \mathbf{\Sigma}^{-1} \]
where \(\mathbf{\Sigma}\) is the forecast-error covariance matrix.
Common variants in fable:
-
method = "ols"— assumes equal forecast-error variance (simple, ignores covariances). -
method = "mint_shrink"— shrinkage estimator for \(\mathbf{\Sigma}\); more stable in practice.
Use when you want the statistically efficient answer. Works for any structure — HTS, GTS, or mixed — and borrows strength across correlated series.
5.3 Method Selection Summary
| Method | Works For | Needs Strict Hierarchy? | Behavior at 2 Levels | Strength | Limitation |
|---|---|---|---|---|---|
| Bottom-Up | HTS + GTS | No | Simple summation | Coherent, interpretable | Propagates bottom-level noise |
| Top-Down | HTS only | Yes | Valid (splits total) | Uses stable totals | Fails for grouped structures |
| Middle-Out | HTS (3+ levels) | Yes | Collapses to Bottom-Up | Balances top & bottom | Not meaningful with only 2 levels |
| MinT (OLS / Shrink) | HTS + GTS | No | Weighted optimum | Statistically efficient | Requires covariance estimation |
Quick rule: Bottom-Up for simplicity, Top-Down for stable aggregates, Middle-Out for deep hierarchies, and MinT when you want statistical optimality.
6 Software Implementation
Both HTS and GTS are handled in R with the fable and fabletools packages.
6.1 Australian domestic overnight trips: (State / Region) * Purpose
Quarterly overnight trips from 1998 Q1 to 2016 Q4 across Australia — a tsibble with 23,408 rows and 5 variables:
- Quarter — year quarter (index)
- Region — tourism regions formed by aggregating Statistical Local Areas
- State — Australian states and territories
-
Purpose — visit purpose:
Holiday,Visiting friends and relatives,Business,Other reason - Trips — overnight trips (thousands)
In a tsibble (tidy time series), each observation is uniquely identified by a time index (Quarter) and a key (one or more variables that define individual series).
The attribute key = tsibble[304x4] tells us there are 304 unique series, each defined by Region + State + Purpose.
6.2 Confirming the Key
key_vars(tourism)[1] "Region" "State" "Purpose"
index_var(tourism)[1] "Quarter"
table(tourism$State)
ACT New South Wales Northern Territory Queensland
320 4160 2240 3840
South Australia Tasmania Victoria Western Australia
3840 1600 6720 1600
table(tourism$Region, tourism$State)
ACT New South Wales Northern Territory
Adelaide 0 0 0
Adelaide Hills 0 0 0
Alice Springs 0 0 320
Australia's Coral Coast 0 0 0
Australia's Golden Outback 0 0 0
Australia's North West 0 0 0
Australia's South West 0 0 0
Ballarat 0 0 0
Barkly 0 0 320
Barossa 0 0 0
Bendigo Loddon 0 0 0
Blue Mountains 0 320 0
Brisbane 0 0 0
Bundaberg 0 0 0
Canberra 320 0 0
Capital Country 0 320 0
Central Coast 0 320 0
Central Highlands 0 0 0
Central Murray 0 0 0
Central NSW 0 320 0
Central Queensland 0 0 0
Clare Valley 0 0 0
Darling Downs 0 0 0
Darwin 0 0 320
East Coast 0 0 0
Experience Perth 0 0 0
Eyre Peninsula 0 0 0
Fleurieu Peninsula 0 0 0
Flinders Ranges and Outback 0 0 0
Fraser Coast 0 0 0
Geelong and the Bellarine 0 0 0
Gippsland 0 0 0
Gold Coast 0 0 0
Goulburn 0 0 0
Great Ocean Road 0 0 0
High Country 0 0 0
Hobart and the South 0 0 0
Hunter 0 320 0
Kakadu Arnhem 0 0 320
Kangaroo Island 0 0 0
Katherine Daly 0 0 320
Lakes 0 0 0
Lasseter 0 0 320
Launceston, Tamar and the North 0 0 0
Limestone Coast 0 0 0
MacDonnell 0 0 320
Macedon 0 0 0
Mackay 0 0 0
Mallee 0 0 0
Melbourne 0 0 0
Melbourne East 0 0 0
Murray East 0 0 0
Murraylands 0 0 0
New England North West 0 320 0
North Coast NSW 0 320 0
North West 0 0 0
Northern 0 0 0
Outback 0 0 0
Outback NSW 0 320 0
Peninsula 0 0 0
Phillip Island 0 0 0
Riverina 0 320 0
Riverland 0 0 0
Snowy Mountains 0 320 0
South Coast 0 320 0
Spa Country 0 0 0
Sunshine Coast 0 0 0
Sydney 0 320 0
The Murray 0 320 0
Tropical North Queensland 0 0 0
Upper Yarra 0 0 0
Western Grampians 0 0 0
Whitsundays 0 0 0
Wilderness West 0 0 0
Wimmera 0 0 0
Yorke Peninsula 0 0 0
Queensland South Australia Tasmania Victoria
Adelaide 0 320 0 0
Adelaide Hills 0 320 0 0
Alice Springs 0 0 0 0
Australia's Coral Coast 0 0 0 0
Australia's Golden Outback 0 0 0 0
Australia's North West 0 0 0 0
Australia's South West 0 0 0 0
Ballarat 0 0 0 320
Barkly 0 0 0 0
Barossa 0 320 0 0
Bendigo Loddon 0 0 0 320
Blue Mountains 0 0 0 0
Brisbane 320 0 0 0
Bundaberg 320 0 0 0
Canberra 0 0 0 0
Capital Country 0 0 0 0
Central Coast 0 0 0 0
Central Highlands 0 0 0 320
Central Murray 0 0 0 320
Central NSW 0 0 0 0
Central Queensland 320 0 0 0
Clare Valley 0 320 0 0
Darling Downs 320 0 0 0
Darwin 0 0 0 0
East Coast 0 0 320 0
Experience Perth 0 0 0 0
Eyre Peninsula 0 320 0 0
Fleurieu Peninsula 0 320 0 0
Flinders Ranges and Outback 0 320 0 0
Fraser Coast 320 0 0 0
Geelong and the Bellarine 0 0 0 320
Gippsland 0 0 0 320
Gold Coast 320 0 0 0
Goulburn 0 0 0 320
Great Ocean Road 0 0 0 320
High Country 0 0 0 320
Hobart and the South 0 0 320 0
Hunter 0 0 0 0
Kakadu Arnhem 0 0 0 0
Kangaroo Island 0 320 0 0
Katherine Daly 0 0 0 0
Lakes 0 0 0 320
Lasseter 0 0 0 0
Launceston, Tamar and the North 0 0 320 0
Limestone Coast 0 320 0 0
MacDonnell 0 0 0 0
Macedon 0 0 0 320
Mackay 320 0 0 0
Mallee 0 0 0 320
Melbourne 0 0 0 320
Melbourne East 0 0 0 320
Murray East 0 0 0 320
Murraylands 0 320 0 0
New England North West 0 0 0 0
North Coast NSW 0 0 0 0
North West 0 0 320 0
Northern 320 0 0 0
Outback 320 0 0 0
Outback NSW 0 0 0 0
Peninsula 0 0 0 320
Phillip Island 0 0 0 320
Riverina 0 0 0 0
Riverland 0 320 0 0
Snowy Mountains 0 0 0 0
South Coast 0 0 0 0
Spa Country 0 0 0 320
Sunshine Coast 320 0 0 0
Sydney 0 0 0 0
The Murray 0 0 0 0
Tropical North Queensland 320 0 0 0
Upper Yarra 0 0 0 320
Western Grampians 0 0 0 320
Whitsundays 320 0 0 0
Wilderness West 0 0 320 0
Wimmera 0 0 0 320
Yorke Peninsula 0 320 0 0
Western Australia
Adelaide 0
Adelaide Hills 0
Alice Springs 0
Australia's Coral Coast 320
Australia's Golden Outback 320
Australia's North West 320
Australia's South West 320
Ballarat 0
Barkly 0
Barossa 0
Bendigo Loddon 0
Blue Mountains 0
Brisbane 0
Bundaberg 0
Canberra 0
Capital Country 0
Central Coast 0
Central Highlands 0
Central Murray 0
Central NSW 0
Central Queensland 0
Clare Valley 0
Darling Downs 0
Darwin 0
East Coast 0
Experience Perth 320
Eyre Peninsula 0
Fleurieu Peninsula 0
Flinders Ranges and Outback 0
Fraser Coast 0
Geelong and the Bellarine 0
Gippsland 0
Gold Coast 0
Goulburn 0
Great Ocean Road 0
High Country 0
Hobart and the South 0
Hunter 0
Kakadu Arnhem 0
Kangaroo Island 0
Katherine Daly 0
Lakes 0
Lasseter 0
Launceston, Tamar and the North 0
Limestone Coast 0
MacDonnell 0
Macedon 0
Mackay 0
Mallee 0
Melbourne 0
Melbourne East 0
Murray East 0
Murraylands 0
New England North West 0
North Coast NSW 0
North West 0
Northern 0
Outback 0
Outback NSW 0
Peninsula 0
Phillip Island 0
Riverina 0
Riverland 0
Snowy Mountains 0
South Coast 0
Spa Country 0
Sunshine Coast 0
Sydney 0
The Murray 0
Tropical North Queensland 0
Upper Yarra 0
Western Grampians 0
Whitsundays 0
Wilderness West 0
Wimmera 0
Yorke Peninsula 0
The data structure is confirmed as (State / Region) * Purpose. Read the data dictionary, eyeball with View() or glimpse(), then use the commands above to confirm whether the data are HTS or GTS — and only then choose the matching reconciliation step.
6.3 Build, Model, Reconcile, Forecast
6.3.1 Why We Model This as GTS (not HTS)
The tourism dataset has the full structure (State / Region) * Purpose — geography is strictly nested and crossed with Purpose. So we have a real choice of how to model it:
-
HTS on
State / Region— a clean nested tree. All four reconciliation methods (BU, TD, MO, MinT) are valid, but we lose thePurposedimension entirely. -
HTS on
State / Purpose— forces Purpose into a single tree under State. Works, but treats Purpose as subordinate to geography, which is not how tourism analysts actually think about it. -
GTS on
State * Purpose— keeps State totals and Purpose totals as first-class aggregation views at the same time. This matches the real business question (“what’s happening by state?” and “what’s happening by purpose?”) and it’s the case where method choice actually matters —top_down()andmiddle_out()are not valid here, so you are forced to think about which reconciliation is appropriate.
We go with GTS because (i) it reflects how the data are actually consumed by decision-makers, and (ii) it’s the pedagogically interesting case — the one where “just pick any reconciliation method” fails.
The pipeline below is written so you only change one line (step 1) to swap between HTS and GTS. Everything downstream — fit, reconcile, forecast — stays the same.
# ──────────────────────────────────────────────────────────────
# 1. DECLARE THE STRUCTURE ← change this ONE line to experiment
# ──────────────────────────────────────────────────────────────
# Uncomment exactly ONE option. The code below it works for all three.
tourism_struct <- tourism %>%
aggregate_key(State * Purpose, Trips = sum(Trips)) # GTS (used in this lecture)
# aggregate_key(State / Region, Trips = sum(Trips)) # HTS — strict geographic tree
# aggregate_key(State / Purpose, Trips = sum(Trips)) # HTS — State then Purpose
# ──────────────────────────────────────────────────────────────
# 2. FIT BASE MODELS (creates a mable)
# ──────────────────────────────────────────────────────────────
fit <- tourism_struct %>%
model(ets = ETS(Trips))
# ──────────────────────────────────────────────────────────────
# 3. RECONCILE
# If you stay on GTS: leave top_down() commented (it errors on GTS).
# If you switch to an HTS above: feel free to uncomment td_fp and/or td_ap.
# ──────────────────────────────────────────────────────────────
recon <- fit %>%
reconcile(
bu = bottom_up(ets), # HTS + GTS
# td_fp = top_down(ets, method = "forecast_proportions"), # HTS only
# td_ap = top_down(ets, method = "average_proportions"), # HTS only
mint_shrink = min_trace(ets, method = "mint_shrink"), # HTS + GTS
mint_ols = min_trace(ets, method = "ols") # HTS + GTS
)
# ──────────────────────────────────────────────────────────────
# 4. FORECAST
# ──────────────────────────────────────────────────────────────
fc <- recon %>% forecast(h = 8) # 8 quarters = 2 years
head(fc)Play with the pipeline — the one-line swap at step 1 is the whole point.
-
Swap to HTS by uncommenting one of the
State / RegionorState / Purposelines. Rerun everything. Does the pipeline still work? -
Now uncomment
td_fp = top_down(...)in step 3. On HTS it runs fine; on GTS it errors. Read the error — that’sfabletelling you reconciliation validity is structure-dependent, not a bug. -
Change the base model — swap
ETS(Trips)forARIMA(Trips)orTSLM(Trips ~ trend() + season()). The reconciliation menu doesn’t change (it’s a property of the structure, not the model). -
Change the horizon — try
h = 4(1 year) vsh = 16(4 years). Do MinT and Bottom-Up diverge more at longer horizons?
Note: the visualizations below use filter(... Purpose ...). If you switch to State / Region, you’ll need to swap Purpose for Region in those filters to see non-empty plots.
7 Visualizing Reconciled Forecasts
After generating the reconciled forecasts (fc), we visualize them to confirm:
- Coherence — forecasts add up correctly across hierarchy levels.
- Behavior — different reconciliation methods produce consistent, realistic trends.
# 1. Compare all reconciliation methods
autoplot(fc, tourism_struct) +
labs(
title = "Forecast Reconciliation Methods (8-Quarter Horizon)",
subtitle = "Comparing Bottom-Up, MinT (OLS), and MinT (Shrinkage)",
y = "Trips", x = "Year"
) +
facet_wrap(~ .model, scales = "free_y") +
theme_minimal()# 2. Focus on one state
fc %>%
filter(State == "Victoria") %>%
autoplot(tourism_struct) +
labs(
title = "Reconciled Forecasts for Victoria",
subtitle = "Comparison across Bottom-Up and MinT methods",
y = "Trips", x = "Year"
) +
facet_wrap(~ .model) +
theme_minimal()# 3. Top-level total (aggregated over State AND Purpose)
fc %>%
filter(is_aggregated(State), is_aggregated(Purpose)) %>%
autoplot(tourism_struct) +
labs(
title = "Total Tourism Trips — Coherent Forecasts",
subtitle = "Top-level series after reconciliation",
y = "Trips", x = "Year"
) +
facet_wrap(~ .model) +
theme_minimal()# 4. State-level totals (aggregated over Purpose)
fc %>%
filter(!is_aggregated(State), is_aggregated(Purpose)) %>%
autoplot(tourism_struct) +
labs(
title = "State-Level Total Tourism Trips (Aggregated over Purpose)",
subtitle = "Useful for comparing geographic performance",
y = "Trips", x = "Year"
) +
facet_wrap(~ State) +
theme_minimal()# 5. Purpose-level totals (aggregated over State)
fc %>%
filter(is_aggregated(State), !is_aggregated(Purpose)) %>%
autoplot(tourism_struct) +
labs(
title = "Purpose-Level Total Tourism Trips (Aggregated over State)",
subtitle = "Useful for analyzing trends by travel purpose",
y = "Trips", x = "Year"
) +
facet_wrap(~ Purpose) +
theme_minimal()| Plot | Aggregation Level | Key Insight |
|---|---|---|
| 1 | All series | Compare reconciliation methods visually |
| 2 | Victoria only | Assess local forecast coherence |
| 3 | Total | Check top-level consistency |
| 4 | State totals | Compare states geographically |
| 5 | Purpose totals | Understand motivation trends |
Once the structure is declared and reconciled, you can forecast at any level — bottom, middle, or top — because coherence is guaranteed.
HTS:
- forecast at
Region→ sums automatically toStateandTotal - forecast at
State→ middle-out - forecast at
Total→ top-down
GTS:
- forecast at
State × Purpose→ bottom-up gives totals - forecast at
State(aggregated over Purpose) orPurpose(aggregated over State)
You choose the forecast level at the end — the structure keeps every level coherent.
8 Step-by-Step Workflow
When starting with hierarchical or grouped time series (e.g., the tourism data):
| Step | Task | Example Command | Purpose |
|---|---|---|---|
| 1 | Read the data dictionary | documentation / metadata | Understand variables (State, Region, Purpose) |
| 2 | Eyeball the data |
View(tourism), glimpse(tourism)
|
Inspect the organization |
| 3 | Confirm structure type |
State / Region (HTS) or State * Purpose (GTS)? |
Hierarchical or grouped? |
| 4 | Build structure | aggregate_key(State / Region, Trips = sum(Trips)) |
Define aggregation |
| 5 | Model | model(ETS(Trips)) |
Fit base forecasting models |
| 6 | Reconcile | reconcile(bu = bottom_up(...), mint = min_trace(...)) |
Enforce coherence |
| 7 | Forecast | forecast(h = 8) |
Produce coherent forecasts at any level |
9 Summary
- Hierarchical Time Series (HTS) — single tree; each level aggregates the ones below it. Simpler reconciliation.
- Grouped Time Series (GTS) — multiple, possibly intersecting hierarchies; several valid aggregation paths. More complex reconciliation.
-
Reconciliation (BU, TD, MO, MinT) is essential for coherence; the valid choice depends on the structure declared via
aggregate_key(). - Base models (ETS, ARIMA, TSLM, …) are unchanged across HTS and GTS — only the reconciliation rule changes.
10 Further Reading
- Hyndman, R.J., & Athanasopoulos, G. (2021). Forecasting: Principles and Practice (3rd ed.), Chapter 11: Forecasting hierarchical and grouped time series. OTexts.
- 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.
- Athanasopoulos, G., Hyndman, R.J., Kourentzes, N., & Petropoulos, F. (2017). Forecasting with temporal hierarchies. European Journal of Operational Research, 262(1), 60-74.