--- title: "Case study: Richmond electorate (2001–2025)" output: rmarkdown::html_vignette vignette: > %\VignetteIndexEntry{Case study: Richmond electorate (2001–2025)} %\VignetteEngine{knitr::rmarkdown} %\VignetteEncoding{UTF-8} --- ```{r setup, include = FALSE} knitr::opts_chunk$set( collapse = TRUE, comment = "#>", fig.width = 8, fig.height = 5, message = FALSE, warning = FALSE ) ``` Richmond is a federal electorate in northern New South Wales, covering the Northern Rivers region including Ballina, Lismore, Byron Bay, and Tweed Heads. Historically a Labor–National marginal, it has been contested by the Nationals, Labor, and (more recently) teal/independent candidates. This vignette walks through a complete single-electorate analysis using readaec. ```{r libs} library(readaec) library(dplyr) library(ggplot2) library(purrr) ``` --- ## 1. Available elections Start by confirming which elections we have data for. ```{r elections} list_elections() ``` --- ## 2. Two-party preferred trend Pull the TPP result for Richmond in every election since 2001. The `get_tpp()` function returns one row per division; we filter to Richmond and stack the years. ```{r tpp-trend} # AEC CSV downloads are available from 2007 onwards years <- list_elections()$year[list_elections()$has_downloads] tpp_richmond <- map_dfr(years, function(yr) { get_tpp(yr) |> filter(tolower(division) == "richmond") |> select(division, division_id, state, alp_pct, lnp_pct, total_votes, year) }) tpp_richmond ``` ```{r tpp-plot} ggplot(tpp_richmond, aes(x = year)) + geom_line(aes(y = alp_pct, colour = "ALP"), linewidth = 1.2) + geom_point(aes(y = alp_pct, colour = "ALP"), size = 3) + geom_line(aes(y = lnp_pct, colour = "LNP/Nat"), linewidth = 1.2) + geom_point(aes(y = lnp_pct, colour = "LNP/Nat"), size = 3) + geom_hline(yintercept = 50, linetype = "dashed", colour = "grey60") + annotate("text", x = min(years) + 0.3, y = 51, label = "50% — majority", size = 3, colour = "grey50", hjust = 0) + scale_colour_manual(values = c("ALP" = "#E4281B", "LNP/Nat" = "#1C4F9C")) + scale_x_continuous(breaks = years) + scale_y_continuous(limits = c(30, 70), labels = function(x) paste0(x, "%")) + labs( title = "Richmond (NSW): two-party preferred vote, 2001–2025", subtitle = "ALP vs LNP/National Coalition", x = NULL, y = "TPP vote share", colour = NULL, caption = "Source: Australian Electoral Commission via readaec" ) + theme_minimal(base_size = 13) + theme(legend.position = "bottom", panel.grid.minor = element_blank()) ``` --- ## 3. Who won each election? `get_members_elected()` returns the elected member for every division. We filter to Richmond to build a candidate-by-year summary. ```{r members} members_richmond <- map_dfr(years, function(yr) { tryCatch( get_members_elected(yr) |> filter(tolower(divisionnm) == "richmond") |> select(divisionnm, surname, givennm, partyab, stateab, year), error = function(e) NULL ) }) members_richmond |> select(year, given_name = givennm, surname, party = partyab) |> arrange(year) ``` --- ## 4. First preference vote breakdown First preferences show how votes were distributed across all candidates before preferences were distributed. This reveals the full competitive landscape beyond just the two-party contest. ```{r fp-data} fp_richmond <- map_dfr(years, function(yr) { get_fp(yr) |> filter(tolower(division) == "richmond") |> select(year, division, surname, given_name, party, party_name, total_votes) }) ``` ```{r fp-totals} # Total formal votes per year (for calculating shares) fp_totals <- fp_richmond |> group_by(year) |> summarise(total_formal = sum(total_votes, na.rm = TRUE)) # Major party shares fp_major <- fp_richmond |> filter(party %in% c("ALP", "NAT", "NP", "LIB", "GRN")) |> left_join(fp_totals, by = "year") |> mutate( pct = round(total_votes / total_formal * 100, 1), party_label = case_when( party %in% c("NAT", "NP") ~ "Nationals", party == "ALP" ~ "ALP", party == "LIB" ~ "Liberal", party == "GRN" ~ "Greens", TRUE ~ party ) ) ``` ```{r fp-plot} ggplot(fp_major, aes(x = year, y = pct, colour = party_label)) + geom_line(linewidth = 1.1) + geom_point(size = 2.5) + scale_colour_manual(values = c( "ALP" = "#E4281B", "Nationals" = "#006644", "Liberal" = "#1C4F9C", "Greens" = "#10C25B" )) + scale_x_continuous(breaks = years) + scale_y_continuous(labels = function(x) paste0(x, "%")) + labs( title = "Richmond (NSW): first preference vote shares", x = NULL, y = "First preference share", colour = NULL, caption = "Source: AEC via readaec" ) + theme_minimal(base_size = 13) + theme(legend.position = "bottom", panel.grid.minor = element_blank()) ``` --- ## 5. How did Richmond swing relative to NSW? Compare Richmond's election-to-election swing against all other NSW divisions to see whether it moved with or against the state trend. ```{r swing-pairs} # Build consecutive election pairs from years with downloads election_pairs <- Map(c, head(years, -1), tail(years, -1)) nsw_swings <- map_dfr(election_pairs, function(pair) { get_swing(pair[1], pair[2], state = "NSW") |> filter(!redistribution_flag) |> mutate(period = paste0(pair[1], "–", pair[2])) }) ``` ```{r swing-richmond} richmond_swing <- nsw_swings |> filter(tolower(division) == "richmond") |> select(period, division, alp_swing, year_from, year_to) nsw_avg_swing <- nsw_swings |> group_by(period, year_from, year_to) |> summarise(avg_alp_swing = mean(alp_swing, na.rm = TRUE), .groups = "drop") swing_comparison <- richmond_swing |> left_join(nsw_avg_swing, by = c("period", "year_from", "year_to")) |> mutate(relative_swing = alp_swing - avg_alp_swing) swing_comparison |> select(period, richmond_swing = alp_swing, nsw_avg = avg_alp_swing, relative_swing) ``` ```{r swing-plot} swing_comparison |> select(period, richmond_swing = alp_swing, nsw_avg = avg_alp_swing) |> tidyr::pivot_longer(c(richmond_swing, nsw_avg), names_to = "series", values_to = "swing") |> mutate(series = if_else(series == "richmond_swing", "Richmond", "NSW average")) |> ggplot(aes(x = period, y = swing, fill = series)) + geom_col(position = "dodge") + geom_hline(yintercept = 0, colour = "grey40") + scale_fill_manual(values = c("Richmond" = "#E4281B", "NSW average" = "grey70")) + scale_y_continuous(labels = function(x) paste0(ifelse(x > 0, "+", ""), x, "pp")) + labs( title = "Richmond ALP swing vs NSW average", x = NULL, y = "ALP swing (percentage points)", fill = NULL, caption = "Source: AEC via readaec" ) + theme_minimal(base_size = 13) + theme(legend.position = "bottom", axis.text.x = element_text(angle = 30, hjust = 1), panel.grid.minor = element_blank()) ``` --- ## 6. Enrolment and turnout over time Is the electorate growing? Is turnout holding up? ```{r turnout} turnout_richmond <- map_dfr(years, function(yr) { get_turnout(yr) |> filter(tolower(divisionnm) == "richmond") |> select(divisionnm, year, everything()) }) turnout_richmond ``` ```{r enrolment} enrolment_richmond <- map_dfr(years, function(yr) { get_enrolment(yr) |> filter(tolower(divisionnm) == "richmond") |> select(divisionnm, year, everything()) }) enrolment_richmond ``` --- ## Summary This case study shows how a few lines of readaec code can reconstruct the full electoral history of any Australian federal division: | Function | What it gives you | |---|---| | `get_tpp(year)` | ALP vs LNP two-party preferred by division | | `get_fp(year)` | First preferences by candidate | | `get_members_elected(year)` | Who won each seat | | `get_swing(from, to)` | Election-to-election TPP change | | `get_enrolment(year)` | Enrolled voters by division | | `get_turnout(year)` | Turnout metrics by division | | `get_fp_by_booth(year, state)` | First preferences at booth level | For spatial analysis, combine `get_fp_by_booth()` with `get_polling_places()` to map results at the polling-place level.