Calendar-based exploratory time series analysis

30th June 2025 @ ISF

Mitchell O’Hara-Wild, Monash University

Cynthia Huang, Monash University

Rob Hyndman, Monash University

Matthew Kay, Northwestern University

Exploratory analysis in the forecasting workflow

Exploratory analysis in the forecasting workflow

Exploring special dimensions

Several types of data require unique care.

  • 🍀 uncertainty

    {distributional} / {ggdist}

  • 🕸️ graph

    {tidygraph} / {ggraph}

  • 🗺️ space

    {sf} / {ggplot2}

  • time

    {mixtime} / {ggtime}

Exploring temporal data

Time is comparatively simple dimension, it is a continuous and ordered variable.

Why do we need specialised tools?

📆 Calendrical time

While time itself is simple, calendrical patterns are complex!

Temporal data/patterns align with calendars.

  • Calender systems (Gregorian, Islamic, Chinese, Jewish, …)
  • Timezones (local/civil time, absolute time)
  • Granularities (hourly, daily, weekly, monthly, annual, …)
  • Corrections (leap years, leap seconds)
  • Seasonality (weekdays/weekends, financial years, holidays)

Exploratory time series analysis

There are two main tasks in ETSA:

💾 Data operations

  • Cleaning
  • Joining
  • Aggregating
  • Summaries / Features
  • Transformation
  • Decomposition

🎨 Data visualisation

  • Time plots
  • Season plots
  • Seasonal sub-series plots
  • ACF plots

Data operations

There are many tools for manipulating time series data, which are generally great!

Python packages

  • pandas
  • gluonts
  • aeon
  • darts

R packages

  • ts
  • xts
  • zoo
  • lubridate
  • tsibble
  • clock

Typical features

  • Represent time
  • Data cleaning
  • Time slicing
  • Missing values
  • Transformation
  • Aggregation

Representing time

Temporal data structures are often limited.

Most time series datasets look like this:

# A tibble: 24,320 × 5
   Quarter    Region   State           Purpose  Trips
   <date>     <chr>    <chr>           <chr>    <dbl>
 1 1998-01-01 Adelaide South Australia Business  135.
 2 1998-04-01 Adelaide South Australia Business  110.
 3 1998-07-01 Adelaide South Australia Business  166.
 4 1998-10-01 Adelaide South Australia Business  127.
 5 1999-01-01 Adelaide South Australia Business  137.
 6 1999-04-01 Adelaide South Australia Business  200.
 7 1999-07-01 Adelaide South Australia Business  169.
 8 1999-10-01 Adelaide South Australia Business  134.
 9 2000-01-01 Adelaide South Australia Business  154.
10 2000-04-01 Adelaide South Australia Business  169.
# ℹ 24,310 more rows

Representing time

In tsibble time needs the right granularity.

# A tsibble: 24,320 x 5 [1Q]
# Key:       Region, State, Purpose [304]
   Quarter Region   State           Purpose  Trips
     <qtr> <chr>    <chr>           <chr>    <dbl>
 1 1998 Q1 Adelaide South Australia Business  135.
 2 1998 Q2 Adelaide South Australia Business  110.
 3 1998 Q3 Adelaide South Australia Business  166.
 4 1998 Q4 Adelaide South Australia Business  127.
 5 1999 Q1 Adelaide South Australia Business  137.
 6 1999 Q2 Adelaide South Australia Business  200.
 7 1999 Q3 Adelaide South Australia Business  169.
 8 1999 Q4 Adelaide South Australia Business  134.
 9 2000 Q1 Adelaide South Australia Business  154.
10 2000 Q2 Adelaide South Australia Business  169.
# ℹ 24,310 more rows

Granular time vectors in tsibble

yearquarter(), yearmonth(), & yearweek()

Mixed temporal granularities

Existing time vectors are limited

  • Gregorian calendar only
  • Limited granularity options
  • Cannot mix temporal granularities

A better solution?

The {mixtime} package works with…

  • Many calendars via {calcal}

    Gregorian, Chinese, Islamic, Hebrew, …

  • Custom calendars / granularities

    Business hours, trading days, trimesters, …

  • Mixed temporal granularities in the same tsibble

    Critically important for temporal reconciliation!

Mixed temporal granularities

Mixed granularities enable more temporal tasks, e.g. joins, comparisons, & aggregation.

# A tsibble: 18 x 5 [1Y, 1Q, 1M]
        Time Region    State    Purpose Trips
   <mixtime> <chr>     <chr>    <chr>   <dbl>
 1      2020 Melbourne Victoria Holiday 7784.
 2      2021 Melbourne Victoria Holiday 7543.
 3   2020 Q1 Melbourne Victoria Holiday 1880.
 4   2020 Q2 Melbourne Victoria Holiday 1965.
 5   2020 Q3 Melbourne Victoria Holiday 1929.
 6   2020 Q4 Melbourne Victoria Holiday 2010.
 7  2020 Jan Melbourne Victoria Holiday  735.
 8  2020 Feb Melbourne Victoria Holiday  573.
 9  2020 Mar Melbourne Victoria Holiday  571.
10  2020 Apr Melbourne Victoria Holiday  607.
11  2020 May Melbourne Victoria Holiday  747.
12  2020 Jun Melbourne Victoria Holiday  611.
13  2020 Jul Melbourne Victoria Holiday  663.
14  2020 Aug Melbourne Victoria Holiday  632.
15  2020 Sep Melbourne Victoria Holiday  634.
16  2020 Oct Melbourne Victoria Holiday  582.
17  2020 Nov Melbourne Victoria Holiday  622.
18  2020 Dec Melbourne Victoria Holiday  806.

Types of time

Usually we work with continuous time points, however there are many other types of time.

  • continuous time [2024 Jan, 2024 Feb]
  • cyclical time [Oct, Nov, Dec, Jan]
  • origin-less time [Month 3, Month 4]
  • ranges [2024 Jan - 2024 Jun]
  • durations [3 months]
  • progress [60% through month]

Time series visualisation

ggplot2 already supports temporal data

Simply map a date (Date), datetime (POSIXct), or even time (hms) to any aesthetic. Scales to adjust temporal labels and breaks exist in ggplot2:

  • scale_*_date() for Date,
  • scale_*_datetime() for POSIXct,
  • scale_*_time() for hms.

So why do we need ggtime?

{ggtime} visualises time with calendrical patterns.

  • Alignment options for timezones, granularities & cycles
  • Cyclical and calendar layouts for seasonality & holidays

Common temporal graphics

These ‘visual idioms’ of time series plots can be categorised into two core groups:

  • Linear time plots
  • Circular time plots

A grammar of temporal graphics

{ggtime} aims to re-express these time series plots with common elements of a grammar.

Those elements can be further remixed to create other useful temporal visualisations.

Linear time plots

These show time as a continuous dimension.

Time plots

Maps time to the x/y-axis.

They reveal trends, seasons, cycles, outliers, and more.

⚛️ Notable grammar elements

  • mapping:
    • x: time
    • y: observations
  • coord: cartesian
Code
library(fpp3)
pbs_scripts <- tsibbledata::PBS |>
  summarise(Scripts = sum(Scripts))
pbs_scripts |>
  autoplot(Scripts)

Forecast plots

These are simply time plots.

They just happen to show forecasts, or uncertain values.

⚛️ Notable grammar elements

  • mapping:
    • x: time
    • y: observations
    • ydist: forecast
  • coord: cartesian
Code
pbs_scripts |> 
  model(ETS(Scripts)) |> 
  forecast() |> 
  autoplot(pbs_scripts) +
  theme(legend.position = "bottom")

Circular time plots

Transforms time to reveal circular patterns.

Seasonal plots

Loops seasons to align years.

⚛️ Notable grammar elements

  • mapping:
    • x: season (e.g. hms)
    • y: observations
    • colour: time
  • coord: cartesian
Code
pbs_scripts |>
  gg_season(Scripts)

Seasonal sub-series

Season facets to show changes over time.

⚛️ Notable grammar elements

  • mapping:
    • x: time
    • y: observations
  • coord: cartesian
  • facet: season
Code
pbs_scripts |>
  gg_subseries(Scripts)

Grammar of temporal graphics

{ggtime} re-expresses these common plots with a composable ggplot2-like grammar.

🎯 Goals

  • Decompose temporal graphics into modular elements.
  • Provide grammar extensions, and plot helpers using them.
  • Encourage users to experiment beyond standard plots.
  • Interoperability with other ggplot2 extensions (e.g. ggdist)
  • Overcome current limitations of time series graphics.

Data

{mixtime} directly associates time with a calendar to enable plotting:

  • Mixed calendars (Gregorian, astronomical, …)
  • Mixed granularities (daily, weekly, monthly, …)
  • Local/civil time and global/absolute time operations
    • e.g. "2 aweeks" vs "2 weeks" for absolute/civil
  • Censored calendars (trading days, working hours, …)
  • Origin-less time (time of day, month of year, …)
  • and much more…

Geometries / Layers

  • geom_time_line()

    A time-aware version of geom_line(). Shows calendrical jumps with dashed lines and/or open and closed circles.

    Code
    tz_shift <- as_tibble(tsibbledata::gafa_stock) |>
      filter(
        (Symbol == "AAPL" & Date <= "2014-01-15") | 
          (Symbol == "GOOG" & Date <= "2014-01-13")
      ) |>
      mutate(Date = Sys.Date() + hours(c(1:3, 3:9, 1:2, 4:9)), DST = ifelse(Symbol == "AAPL", "DST Ends", "DST Begins")) |> 
      slice(1:3, 3:12, 12:n()) |> 
      mutate(
        open = duplicated(Open),
        closed = c(open[-1], FALSE),
        Date = Date + open*3600*((DST=="DST Begins")*2-1)
      ) 
    
    tz_shift |> 
      ggplot(aes(x = Date, y = Close)) + 
      geom_path(aes(group = cumsum(open))) + 
      geom_path(linetype = "dashed", data = filter(tz_shift, open | closed)) +
      geom_point(aes(shape = closed), data = filter(tz_shift, open | closed), size = 3) +
      facet_wrap(vars(DST), ncol = 2, scales = "free_y") + 
      scale_shape_manual(values = c("TRUE" = 16, "FALSE" = 1)) + 
      guides(shape = "none")

  • geom_time_candle()

    Shows calendrical value changes/ranges (e.g. daily, weekly, monthly, annual).

Code
tsibbledata::gafa_stock |> 
  filter(Symbol == "GOOG") |> 
  filter(yearmonth(Date) == yearmonth("2014 Jun")) |> 
  ggplot(aes(x = Date)) + 
  tidyquant::geom_candlestick(
    aes(open = Open, high = High, low = Low, close = Close),
    colour_up = "#1FB974", fill_up = "#1FB974", colour_down = "#F4375D", fill_down = "#F4375D"
  ) + 
  scale_x_date(date_labels = "%d %b %Y") +
  labs(y = "GOOG Stock")

Scales

Much like ggplot2 temporal scales, {ggtime} has a unified scale_*_mixtime() with:

  • time_labels (strftime-based, e.g. "%Y %b")
  • time_breaks (calendar-based, e.g. "3 months")

The scales also control calendrical alignment for:

  • timezones (civil_time boolean)
  • granularity (time_align 0-1)
  • cycles (warp and time_warp)

Timezone alignment (local/absolute time)

Daylight savings time misaligns daily seasonal patterns.

Timezone alignment (local/absolute time)

📅 Daily seasonality in local time

Daily seasonal patterns are better aligned when local time is used with scale_x_mixtime(civil_time = TRUE).

Granularity alignment

{ggtime} aligns mixed granularities.

Imagine Australian births (annual) compared with total births by state (monthly).

📅 Temporal alignment across granularities

When constrained to Date and POSIXct, left alignment is commonly used for less-frequent granularities.

e.g. 2025-01-01 can be 2025, Jan 2025, or Jan 1 2025.

Consequently, plotting is also often left-aligned.

Granularity alignment

{ggtime} center aligns granularities.

📅 Aligning temporal granularities

Specify alignment of different granularities with scale_x_mixtime(align_time = <0-1>).

Time warping

{ggtime} defaults to center alignment.

Cycles are repeating patterns with an irregular duration (and shape).

Warping cycles to have the same length as “% of cycle” can help compare cycle shapes.

📅 Temporal alignment across cycles

In scale_x_mixtime(), warp time with

  • warp (stretch time between specific time points)
  • time_warp (stretch time by calendar, e.g. "1 month")

Facets / Coordinates

Two calendar-plot approaches:

  • facet_calendar()
  • coord_calendar()

📅 Calendar layouts for temporal graphics

Calendars are useful for plotting long time series, they:

  • use a familiar layout for quickly identifying dates.
  • reveal short annual patterns (e.g. holidays and events).
  • have a better aspect-ratio for dense time axis.
  • more effectively use vertical space.

Calendars are not limited to the standard weekly layout, but are hierarchical in nature over any calendar structure.

Calendar facets

Facets separate each day (or calendar period).

Calendar coordinates

Each day (or calendar period) shares the same panel.

Facets / Coordinates

Season plots loop time over seasonalities.

  • coord_time_loop()

The time loop points can be specified with:

  • loops (loop time over specific time points)
  • time_loops (loop time by calendar, e.g. "1 week")

Looping circular time periods

Looping continuous time

Looping the x-axis over seasonal granularities (e.g. day, week, or year) clearly shows seasonality.

coord_time_loop(time_loops = "1 year")

Looping circular time periods

Non-cartesian coordinates

Looping conceptually applies to other coordinate spaces too. Combining coord_loop() and coord_polar() shows seasonality in polar coordinates.

Thanks for your time!

Closing remarks

  • Use appropriate granularities for storing time
  • Create new graphics by combining each element
  • Composable code offer many benefits for exploration

Unsplash credits

Thanks to these Unsplash contributors for their photos