I am living in Tarneit, getting to a train station can be part of my journey in its own right. From my house bus connections can help link residents with stations such as Tarneit, Williams Landing and Hoppers Crossing. This personal experience raises a wider question: how has public transport use across Victoria changed, and what can statewide patronage data tell us—and not tell us—about the transport system people rely on?
This report investigates monthly public transport patronage across Victoria from January 2018 to June 2026. It compares transport modes, examines change over time, and looks for recurring monthly patterns.
# Install any missing packages once, in the RStudio Console:
# install.packages(c("tidyverse", "lubridate", "plotly", "scales", "knitr"))
library(tidyverse)
library(lubridate)
library(plotly)
library(scales)
library(knitr)
The dataset is published by the Victorian Department of Transport and Planning through the Victorian Government’s open data portal. It records monthly patronage for metropolitan train, tram and bus, V/Line train and coach, and regional bus. The official definition is patron boardings, not unique individual people. The department notes that some modes use estimates based on ticketing data and surveys, and that methodological changes and free-travel periods can affect comparisons.
The official data source is available at: https://opendata.transport.vic.gov.au/dataset/monthly-public-transport-patronage-by-mode.
source_url <- paste0(
"https://opendata.transport.vic.gov.au/dataset/",
"1ab35aa9-f21d-4f00-939b-60dade427d45/resource/",
"07141b34-08f7-430e-bf83-99e9462cc59f/download/",
"monthly_public_transport_patronage_by_mode.csv"
)
local_file <- "monthly_public_transport_patronage_by_mode.csv"
raw_data <- if (file.exists(local_file)) {
readr::read_csv(local_file, show_col_types = FALSE, name_repair = "minimal")
} else {
readr::read_csv(source_url, show_col_types = FALSE, name_repair = "minimal")
}
# Standardise column names and remove accidental spaces.
names(raw_data) <- names(raw_data) |>
stringr::str_trim() |>
stringr::str_replace_all("\\s+", "_") |>
stringr::str_replace_all("/", "_") |>
stringr::str_to_lower()
mode_columns <- c(
"metropolitan_train", "metropolitan_tram", "metropolitan_bus",
"v_line_train", "v_line_coach", "regional_bus"
)
data <- raw_data |>
mutate(
year = as.integer(year),
month = as.integer(month),
across(all_of(mode_columns), ~ readr::parse_number(as.character(.x))),
date = as.Date(sprintf("%04d-%02d-01", year, month))
) |>
arrange(date)
mode_labels <- c(
metropolitan_train = "Metropolitan train",
metropolitan_tram = "Metropolitan tram",
metropolitan_bus = "Metropolitan bus",
v_line_train = "V/Line train",
v_line_coach = "V/Line coach",
regional_bus = "Regional bus"
)
long_data <- data |>
pivot_longer(
cols = all_of(mode_columns),
names_to = "mode",
values_to = "boardings"
) |>
mutate(mode = recode(mode, !!!mode_labels))
complete_years <- data |>
filter(year <= max(year) - 1) |>
group_by(year) |>
summarise(months_recorded = n_distinct(month), .groups = "drop") |>
filter(months_recorded == 12) |>
pull(year)
annual_long <- long_data |>
filter(year %in% complete_years) |>
group_by(year, mode) |>
summarise(boardings = sum(boardings, na.rm = TRUE), .groups = "drop")
# Report basic data checks.
data_check <- tibble(
Check = c("Rows", "First month", "Last month", "Missing values in mode columns",
"Duplicate year-month records"),
Result = c(
nrow(data),
format(min(data$date), "%B %Y"),
format(max(data$date), "%B %Y"),
sum(is.na(as.matrix(data[mode_columns]))),
sum(duplicated(data[c("year", "month")]))
)
)
knitr::kable(data_check, caption = "Data validation checks")
| Check | Result |
|---|---|
| Rows | 102 |
| First month | January 2018 |
| Last month | June 2026 |
| Missing values in mode columns | 0 |
| Duplicate year-month records | 0 |
A boarding is not the same as a unique person or a complete door-to-door journey. A person may make multiple boardings during a trip. The dataset is statewide and aggregated by month, so it cannot identify the passengers using routes 150, 151 or 160, nor isolate Tarneit, Williams Landing, Hoppers Crossing or West Tarneit Station.
# Calculate total boardings for each mode across the full study period
mode_totals <- long_data |>
group_by(mode) |>
summarise(
total_boardings = sum(boardings, na.rm = TRUE),
.groups = "drop"
) |>
mutate(
share = total_boardings / sum(total_boardings) * 100,
hover_text = paste0(
"<b>", mode, "</b>",
"<br>Study period: January 2018–June 2026",
"<br>Total boardings: ",
scales::comma(total_boardings),
"<br>Share of all boardings: ",
round(share, 2), "%"
)
)
p1 <- plotly::plot_ly(
data = mode_totals,
labels = ~mode,
values = ~total_boardings,
type = "pie",
textinfo = "label+percent",
textposition = "inside",
insidetextfont = list(color = "white", size = 13),
hoverinfo = "text",
hovertext = ~hover_text,
sort = FALSE
) |>
plotly::layout(
title = list(
text = paste0(
"Public Transport Patronage: The Long View",
"<br><sup>Distribution across Victoria, January 2018–June 2026</sup>"
)
),
legend = list(
orientation = "h",
x = 0.05,
y = -0.15
),
margin = list(b = 100)
)
p1
The distribution of public transport boardings across Victoria shows that metropolitan trains account for the largest share at 38.6%, followed by metropolitan trams at 32% and metropolitan buses at 22.4%. Together, these three modes represent approximately 93% of all recorded boardings between January 2018 and June 2026, while V/Line trains, regional buses and V/Line coaches account for much smaller shares. This highlights the dominance of metropolitan transport in statewide patronage figures, while also emphasising the role buses can play in connecting residential areas with railway stations. From my experience travelling from Tarneit West, reaching stations such as Tarneit, Williams Landing and Hoppers Crossing involves considering bus connections as well as the train journey itself.
p2 <- ggplot(annual_long, aes(
x = factor(year), y = boardings, fill = mode,
text = paste0(
"<b>", mode, "</b>",
"<br>Year: ", year,
"<br>Annual boardings: ", scales::comma(boardings)
)
)) +
geom_col(position = "stack") +
scale_y_continuous(labels = label_number(scale = 1e-6, suffix = "M")) +
labs(
title = "The mix of transport modes is not evenly distributed",
subtitle = "Annual boardings in complete calendar years only",
x = "Year", y = "Annual boardings", fill = "Transport mode",
caption = "2026 is excluded because only January–June is available."
) +
theme_minimal(base_size = 12) +
theme(legend.position = "bottom", plot.title = element_text(face = "bold"))
ggplotly(p2, tooltip = "text") |>
layout(legend = list(orientation = "h", x = 0, y = -0.25))
As living in Tarneit West, I experience public transport as more than just a train journey; reaching a station often involves taking a bus first. This annual comparison shows a substantial decline in public transport boardings in 2020 and 2021 compared with 2018 and 2019, followed by a gradual increase from 2022 onwards. By 2024 and 2025, total boardings had risen considerably from the low recorded during 2020, although they remained below the levels shown for 2018 and 2019. Metropolitan trains, trams and buses make up the largest portions of the annual totals, highlighting the importance of metropolitan services in Victoria’s transport network. This connects with my experience of relying on bus connections to reach stations such as Tarneit, Williams Landing and Hoppers Crossing.
For a Tarneit West resident, the bus-to-train connection is part of the practical travel experience. The metropolitan bus series offers statewide context for that experience, but it is not route-level evidence. This chart focuses on monthly metropolitan bus patronage and compares it with metropolitan train patronage.
# Prepare monthly bus and train data
connection_data <- long_data |>
filter(mode %in% c("Metropolitan bus", "Metropolitan train")) |>
select(date, mode, boardings) |>
pivot_wider(
names_from = mode,
values_from = boardings
) |>
drop_na()
p3 <- ggplot(
connection_data,
aes(
x = `Metropolitan bus`,
y = `Metropolitan train`,
text = paste0(
"<b>Month: ", format(date, "%B %Y"), "</b>",
"<br>Metropolitan bus boardings: ",
scales::comma(`Metropolitan bus`),
"<br>Metropolitan train boardings: ",
scales::comma(`Metropolitan train`)
)
)
) +
geom_point(
colour = "purple",
alpha = 0.75,
size = 2.5
) +
scale_x_continuous(
labels = scales::label_number(scale = 1e-6, suffix = "M")
) +
scale_y_continuous(
labels = scales::label_number(scale = 1e-6, suffix = "M")
) +
labs(
title = "How are metropolitan bus and train boardings related?",
subtitle = "Each point represents one month across Victoria",
x = "Metropolitan bus boardings",
y = "Metropolitan train boardings",
caption = paste(
"Source: Victorian Department of Transport and Planning.",
"Statewide monthly totals; not individual routes or stations."
)
) +
theme_minimal(base_size = 12) +
theme(
plot.title = element_text(face = "bold"),
plot.caption = element_text(size = 9)
)
ggplotly(p3, tooltip = "text")
I experience public transport as a connected journey in which reaching a railway station can involve taking a bus first. This scatter plot shows a clear positive relationship between monthly metropolitan bus and train boardings across Victoria: months with higher bus patronage generally also have higher train patronage. The points rise from the lower-left to the upper-right, indicating that the two modes tend to experience similar broad changes in patronage over the study period. This is relevant to my experience because buses can connect residents with stations such as Tarneit, Williams Landing and Hoppers Crossing, making the accessibility of these connections an important part of everyday travel.
Month-to-month changes can reflect seasonality, school and work routines, service conditions, holidays and other factors. This heatmap helps the reader compare the pattern across years. Hover over a cell to see the exact number of boardings.
season_data <- long_data |>
filter(mode == "Metropolitan train") |>
mutate(month_name = factor(month.name[month], levels = month.name))
p4 <- ggplot(season_data, aes(
x = month_name, y = factor(year), fill = boardings,
text = paste0(
"<b>Metropolitan train</b>",
"<br>", month_name, " ", year,
"<br>Boardings: ", scales::comma(boardings)
)
)) +
geom_tile(colour = "white", linewidth = 0.25) +
scale_fill_viridis_c(
option = "C",
labels = label_number(scale = 1e-6, suffix = "M"),
name = "Boardings"
) +
labs(
title = "Monthly train patronage reveals recurring highs and lows",
subtitle = "Compare the same months across years",
x = NULL, y = "Year",
caption = "Colour represents monthly metropolitan train boardings; hover for exact values."
) +
theme_minimal(base_size = 11) +
theme(
axis.text.x = element_text(angle = 45, hjust = 1),
plot.title = element_text(face = "bold")
)
ggplotly(p4, tooltip = "text")
The heatmap illustrates how monthly metropolitan train patronage across Victoria changed between 2018 and June 2026, revealing a clear contrast between the higher patronage levels in 2018–2019 and the substantial decline during 2020 and 2021. The darker purple shades indicate lower monthly boardings, particularly across much of 2020, while the warmer orange and yellow shades in later years indicate a recovery in patronage. From 2022 onwards, the overall pattern becomes warmer, suggesting that train use increased, although monthly variations remain visible. Living in Tarneit West, these broader changes are relevant because train services are an important part of travelling across Melbourne’s western suburbs, often requiring a bus connection to reach a station.
# Calculate each transport mode's share of annual boardings
share_data <- annual_long |>
dplyr::group_by(year) |>
dplyr::mutate(
annual_total = sum(boardings, na.rm = TRUE),
share = boardings / annual_total
) |>
dplyr::ungroup()
# Create the line chart
p5 <- ggplot2::ggplot(
share_data,
ggplot2::aes(
x = year,
y = share,
colour = mode,
group = mode,
text = paste0(
"<b>", mode, "</b>",
"<br>Year: ", year,
"<br>Share of boardings: ",
scales::percent(share, accuracy = 0.1),
"<br>Annual boardings: ",
scales::comma(boardings)
)
)
) +
ggplot2::geom_line(linewidth = 1) +
ggplot2::geom_point(size = 2.5) +
ggplot2::scale_x_continuous(
breaks = sort(unique(share_data$year))
) +
ggplot2::scale_y_continuous(
labels = scales::label_percent()
) +
ggplot2::labs(
title = "How has Victoria's public transport mode mix changed?",
subtitle = "Each line shows a mode's share of annual boardings",
x = "Year",
y = "Share of annual boardings",
colour = "Transport mode",
caption = paste(
"Source: Victorian Department of Transport and Planning.",
"Complete calendar years only; shares represent boardings,",
"not unique passengers."
)
) +
ggplot2::theme_minimal(base_size = 12) +
ggplot2::theme(
legend.position = "bottom",
plot.title = ggplot2::element_text(face = "bold")
)
# Convert to an interactive Plotly chart
interactive_chart <- plotly::ggplotly(
p5,
tooltip = "text"
) |>
plotly::layout(
legend = list(
orientation = "h",
x = 0,
y = -0.25
)
)
# Return the widget as the final expression
interactive_chart
In Tarneit West, I have experienced how public transport connections can shape everyday travel. My journeys often involve taking a bus before connecting to a train, which made me curious about how people across Victoria use different transport modes. This visualisation explores changes in the share of annual boardings between 2018 and 2025. Metropolitan trains consistently accounted for the largest share, while metropolitan trams and buses also represented substantial proportions of public transport use. The chart also shows a noticeable change in the transport mix around 2020 and 2021, followed by shifts in the years after that. Exploring these patterns helped me understand that public transport is not just about individual journeys; it is also about how different modes contribute to the wider transport network.
I would like to acknowledge Transport Victoria Open Data for providing the public transport patronage dataset used in this project. I used R and RStudio to clean, analyse and visualise the data, with packages including ggplot2, dplyr, tidyr and plotly to create the visualisations and interactive features. I also referred to the official R documentation, Posit resources and Fundamentals of Data Visualization by Claus O. Wilke to support my understanding of effective data visualisation and storytelling techniques.
Department of Transport and Planning. (2026). Monthly public transport patronage by mode [Data set]. Transport Victoria Open Data. https://opendata.transport.vic.gov.au/dataset/monthly-public-transport-patronage-by-mode
Plotly. (n.d.). Plotly R open source graphing library. https://plotly.com/r/
Posit Software, PBC. (n.d.). Posit cheatsheets. https://rstudio.github.io/cheatsheets/
Posit Software, PBC. (n.d.). RStudio IDE user guide. https://docs.posit.co/ide/user/
R Core Team. (n.d.). R: A language and environment for statistical computing. R Foundation for Statistical Computing. https://www.R-project.org/
Wickham, H. (2016). ggplot2: Elegant graphics for data analysis. Springer. https://ggplot2.tidyverse.org/
Wickham, H., Çetinkaya-Rundel, M., & Grolemund, G. (2023). R for data science (2nd ed.). O’Reilly Media. https://r4ds.hadley.nz/
Wickham, H., Navarro, D., & Pedersen, T. L. (n.d.). ggplot2: Elegant graphics for data analysis (3rd ed., work in progress). https://ggplot2-book.org/
Wilke, C. O. (2019). Fundamentals of data visualization: A primer on making informative and compelling figures. O’Reilly Media. https://clauswilke.com/dataviz/
Xie, Y., Allaire, J. J., & Grolemund, G. (2018). R Markdown: The definitive guide. Chapman and Hall/CRC. https://bookdown.org/yihui/rmarkdown/
RStudio, PBC. (n.d.). R Markdown: Dynamic documents for R. https://rmarkdown.rstudio.com/