Every Victorian election, hundreds of thousands of people line up, take a ballot paper and cast a vote that is never counted. At the 2022 state election, 211,791 Lower House ballots were informal (5.53% of votes cast). The Victorian Electoral Commission’s own review of those ballots found that most were not protest votes: about 56% showed a preference, meaning the voter tried and made a mistake (Victorian Electoral Commission [VEC], 2023).

With the 2026 election on 28 November, this report follows those lost votes: how they have grown, where they fall, who they fall on, and what that means for whose voice is heard.

1. The ballot keeps getting longer, and more votes go uncounted

c1_rate <- plot_ly(statewide, x = ~year) |>
  add_trace(y = ~lower, name = "Lower House", type = "scatter", mode = "lines+markers",
            line = list(color = "#C0392B", width = 3), marker = list(size = 8, color = "#C0392B"),
            hovertemplate = "%{x}: %{y:.2f}% informal (Lower House)<extra></extra>") |>
  add_trace(y = ~upper, name = "Upper House", type = "scatter", mode = "lines+markers",
            line = list(color = "#7F8C8D", width = 2, dash = "dot"),
            marker = list(size = 6, color = "#7F8C8D"),
            hovertemplate = "%{x}: %{y:.2f}% informal (Upper House)<extra></extra>") |>
  layout(yaxis = list(title = "Informal votes (% of votes cast)", rangemode = "tozero",
                      ticksuffix = "%"))

c1_cand <- plot_ly(statewide, x = ~year, y = ~candidates, type = "bar", name = "Candidates",
                   marker = list(color = "#34495E"),
                   hovertemplate = "%{x}: %{y:,} candidates (both houses)<extra></extra>") |>
  layout(yaxis = list(title = "Candidates (both houses)"))

subplot(c1_rate, c1_cand, nrows = 2, shareX = TRUE, heights = c(0.62, 0.38),
        titleY = TRUE, margin = 0.04) |>
  layout(title = list(text = "Informal voting has nearly doubled since 1999 as ballots grew",
                      x = 0, font = list(size = 16)),
         xaxis = list(title = "State election year", tickvals = statewide$year),
         legend = list(orientation = "h", x = 0, y = 1.02, yanchor = "bottom"),
         margin = list(t = 80),
         hovermode = "x unified") |>
  config(displaylogo = FALSE)

Lower House informality rose from 3.02% in 1999 to 5.53% in 2022, while the number of candidates nearly tripled. Every extra name is another box a voter has to number correctly.

2. Where the votes go missing

pal_rate <- colorNumeric("YlOrRd", domain = map_df$informal_rate)
pal_cand <- colorNumeric("Purples", domain = map_df$n_candidates)

popup_txt <- sprintf(
  "<strong>%s</strong><br/>Informal: %.2f%% (%s ballots)<br/>Candidates on ballot: %s<br/>Turnout: %.1f%%",
  map_df$district, map_df$informal_rate, comma(map_df$informal),
  map_df$n_candidates, map_df$turnout) |> lapply(htmltools::HTML)

leaflet(map_df) |>
  addProviderTiles(providers$CartoDB.Positron) |>
  addPolygons(group = "Informal rate", fillColor = ~pal_rate(informal_rate),
              fillOpacity = 0.8, color = "white", weight = 1, label = popup_txt,
              highlightOptions = highlightOptions(weight = 3, color = "#222",
                                                  bringToFront = TRUE)) |>
  addPolygons(group = "Candidates on ballot", fillColor = ~pal_cand(n_candidates),
              fillOpacity = 0.8, color = "white", weight = 1, label = popup_txt,
              highlightOptions = highlightOptions(weight = 3, color = "#222",
                                                  bringToFront = TRUE)) |>
  addLegend("bottomright", pal = pal_rate, values = ~informal_rate,
            title = "Informal votes<br/>(% of votes cast)",
            labFormat = labelFormat(suffix = "%"), className = "info legend leg-rate") |>
  addLegend("bottomright", pal = pal_cand, values = ~n_candidates,
            title = "Candidates<br/>on ballot", className = "info legend leg-cand") |>
  addLayersControl(baseGroups = c("Informal rate", "Candidates on ballot"),
                   options = layersControlOptions(collapsed = FALSE)) |>
  setView(lng = 145.0, lat = -37.8, zoom = 9) |>
  addControl(htmltools::HTML("<b>Informal votes by district, 2022</b><br/>Hover a district; switch layers top-right; zoom out for regional Victoria"),
             position = "topleft") |>
  # Show only the legend that matches the selected layer
  htmlwidgets::onRender("
    function(el, x) {
      var map = this;
      function show(name) {
        el.querySelectorAll('.leg-rate').forEach(function(d){ d.style.display = name === 'Informal rate' ? '' : 'none'; });
        el.querySelectorAll('.leg-cand').forEach(function(d){ d.style.display = name === 'Candidates on ballot' ? '' : 'none'; });
      }
      show('Informal rate');
      map.on('baselayerchange', function(e) { show(e.name); });
    }")

The spread is wide: Hawthorn lost 2.59% of its votes, while Melton lost 11.14%, more than one ballot in every nine. Switch the map to Candidates on ballot to compare where the longest ballots were.

3. More names on the ballot, more votes lost

c3 <- districts |> filter(!is.na(lote_pct))
fit <- lm(informal_rate ~ n_candidates, data = districts)
r_cand <- cor(districts$informal_rate, districts$n_candidates)

colour_opts <- list(
  list(var = "lote_pct",          lab = "Speak a language<br>other than English<br>at home (%)"),
  list(var = "born_overseas_pct", lab = "Born overseas (%)"),
  list(var = "median_age",        lab = "Median age<br>(years)"),
  list(var = "median_hh_income",  lab = "Median household<br>income ($/week)"))

buttons <- map(colour_opts, function(o) list(
  method = "restyle",
  label  = str_remove_all(str_replace_all(o$lab, "<br>", " "), " \\(.*\\)"),
  args   = list(list(marker.color = list(c3[[o$var]]),
                     marker.colorbar.title = o$lab),
                list(0))))     # restyle the district points only, not the trend line

set.seed(2026)
plot_ly(c3, x = ~jitter(n_candidates, 0.6), y = ~informal_rate,
        type = "scatter", mode = "markers",
        marker = list(color = ~lote_pct, colorscale = "Viridis", showscale = TRUE,
                      size = ~rescale(enrolment, c(8, 22)), opacity = 0.85,
                      line = list(color = "white", width = 1),
                      colorbar = list(title = colour_opts[[1]]$lab)),
        text = ~sprintf("<b>%s</b><br>Candidates: %d<br>Informal: %.2f%%<br>Language other than English: %.0f%%<br>Born overseas: %.0f%%<br>Median age: %s<br>Enrolled: %s",
                        district, n_candidates, informal_rate, lote_pct,
                        born_overseas_pct, median_age, comma(enrolment)),
        hoverinfo = "text", name = "District") |>
  add_lines(data = tibble(n_candidates = range(districts$n_candidates)) |>
              mutate(y = predict(fit, newdata = pick(everything()))),
            x = ~n_candidates, y = ~y, inherit = FALSE,
            line = list(color = "#555", dash = "dash"), hoverinfo = "skip",
            name = "Linear trend") |>
  layout(title = list(text = sprintf("More names on the ballot, more spoiled votes (r = %.2f)", r_cand),
                      x = 0, font = list(size = 16)),
         xaxis = list(title = "Candidates on the Lower House ballot (jittered)", dtick = 1),
         yaxis = list(title = "Informal votes (% of votes cast)", ticksuffix = "%"),
         showlegend = FALSE,
         margin = list(t = 130),
         updatemenus = list(list(type = "dropdown", x = 0.09, y = 1.04, xanchor = "left",
                                 yanchor = "bottom", buttons = buttons)),
         annotations = list(list(text = "Colour by:", x = 0, y = 1.055, xref = "paper",
                                 yref = "paper", showarrow = FALSE, xanchor = "left",
                                 yanchor = "bottom"))) |>
  config(displaylogo = FALSE)

Each point is a district, sized by enrolment. Use the dropdown to recolour by Census characteristics. Ballot length explains part of the pattern (r = 0.66), but districts with the same number of candidates still differ by several points, and the colours show which communities sit at the top of each column.

4. Who carries the burden?

long <- districts |>
  filter(!is.na(lote_pct)) |>
  select(district, informal_rate, lote_pct, born_overseas_pct, median_age, median_hh_income) |>
  pivot_longer(-c(district, informal_rate), names_to = "measure", values_to = "value") |>
  group_by(measure) |>
  mutate(r = cor(informal_rate, value)) |>
  ungroup() |>
  mutate(panel = recode(measure,
                        lote_pct = "Language other than English at home (%)",
                        born_overseas_pct = "Born overseas (%)",
                        median_age = "Median age (years)",
                        median_hh_income = "Median household income ($/week)"),
         panel = sprintf("%s  ·  r = %.2f", panel, r))

shared <- SharedData$new(long, key = ~district, group = "district")

p4 <- ggplot(shared, aes(value, informal_rate,
                         text = sprintf("<b>%s</b><br>Informal: %.2f%%<br>Value: %s",
                                        district, informal_rate, comma(value, 0.1)))) +
  geom_point(colour = "#2C3E50", alpha = 0.7, size = 2) +
  facet_wrap(~panel, scales = "free_x", ncol = 2) +
  labs(x = NULL, y = "Informal votes (% of votes cast)") +
  theme_minimal(base_size = 11) +
  theme(strip.text = element_text(face = "bold", hjust = 0))

ggplotly(p4, tooltip = "text") |>
  layout(title = list(text = "Which communities lose the most votes?",
                      x = 0, font = list(size = 16)),
         margin = list(t = 70)) |>
  highlight(on = "plotly_click", off = "plotly_doubleclick", color = "#E67E22",
            selectize = TRUE, persistent = FALSE) |>
  config(displaylogo = FALSE)

Informal voting correlates r = 0.37 with the share of residents speaking a language other than English at home, 0.22 with the share born overseas, -0.47 with median household income and -0.23 with median age. Type a district name in the search box (or click a point) to highlight it in all four panels; double-click to clear. Districts created in the 2021 redivision (Ashwood, Berwick, Eureka, Glen Waverley, Greenvale, Kalkallo, Laverton, Pakenham, Point Cook) have no matching 2021 Census division and are left out of charts 3–4.

5. Of every 100 people on the roll, how many were actually heard?

c5 <- districts |>
  arrange(informal_share) |>          # most informal votes at the top
  mutate(district = factor(district, levels = district))

plot_ly(c5, y = ~district) |>
  add_bars(x = ~counted_share, orientation = "h", name = "Vote counted", marker = list(color = "#2E86AB"),
           hovertemplate = "%{y}: %{x:.1f} in 100 counted<extra></extra>") |>
  add_bars(x = ~informal_share, orientation = "h", name = "Voted, but informal", marker = list(color = "#E4572E"),
           hovertemplate = "%{y}: %{x:.1f} in 100 voted informally<extra></extra>") |>
  add_bars(x = ~absent_share, orientation = "h", name = "Did not vote", marker = list(color = "#CFCFCF"),
           hovertemplate = "%{y}: %{x:.1f} in 100 did not vote<extra></extra>") |>
  layout(barmode = "stack",
         title = list(text = "Out of every 100 enrolled voters, how many votes counted?",
                      x = 0, font = list(size = 16)),
         xaxis = list(title = "", range = c(0, 100), side = "top", ticksuffix = " "),
         yaxis = list(title = "", tickfont = list(size = 9), dtick = 1, automargin = TRUE),
         legend = list(orientation = "h", x = 0, y = 1.045, yanchor = "bottom",
                       traceorder = "normal"),
         margin = list(l = 140, t = 140)) |>
  config(displaylogo = FALSE)

Hover any district, or click a legend item to hide it. Across Victoria, only 83.3 of every 100 enrolled people cast a vote that counted in 2022.

Turnout gets the headlines, but in Melton, 9.4 of every 100 enrolled voters turned up and still went uncounted.

Most of these votes are fixable. A shorter ballot is not in anyone’s gift, but clearer instructions in community languages and better how-to-vote guidance could be in place before 28 November 2026.


Data notes

  • Informal rate = informal votes ÷ total votes cast, per VEC district results (recheck figures). Narracan is excluded because its 2022 election failed and was re-held in January 2023.
  • Candidate numbers in chart 1 include both houses (VEC statistics page); charts 2–3 count Lower House candidates per district.
  • Census values are 2021 Census State Electoral Division approximations, matched to 2022 districts by name. Where a district kept its name but its boundary moved in the 2021 redivision, the Census figures describe the old boundary, so treat them as approximate.
  • Correlations are district-level and say nothing about individual voters (ecological fallacy).

References

Australian Bureau of Statistics. (2022a). 2021 Census of Population and Housing: General Community Profile DataPack – State Electoral Divisions, Victoria [Data set]. https://www.abs.gov.au/census/find-census-data/datapacks

Australian Bureau of Statistics. (2022b). Australian Statistical Geography Standard (ASGS) Edition 3: State Electoral Divisions 2022 [Data set]. https://www.abs.gov.au/statistics/standards/australian-statistical-geography-standard-asgs-edition-3/jul2021-jun2026/access-and-downloads/digital-boundary-files

Cheng, J., Schloerke, B., Karambelkar, B., Xie, Y., & Aden-Buie, G. (2025). leaflet: Create interactive web maps with the JavaScript ‘Leaflet’ library (Version 2.2.3) [R package]. https://doi.org/10.32614/CRAN.package.leaflet

Cheng, J., & Sievert, C. (2025). crosstalk: Inter-widget interactivity for HTML widgets (Version 1.2.2) [R package]. https://doi.org/10.32614/CRAN.package.crosstalk

Pebesma, E. (2018). Simple features for R: Standardized support for spatial vector data. The R Journal, 10(1), 439–446. https://doi.org/10.32614/RJ-2018-009

R Core Team. (2025). R: A language and environment for statistical computing (Version 4.5.2) [Computer software]. R Foundation for Statistical Computing. https://www.R-project.org/

Raue, B. (2023). Victoria 2022 – informal vote drops despite bigger ballots. The Tally Room. https://www.tallyroom.com.au/50650

Sievert, C. (2020). Interactive web-based data visualization with R, plotly, and shiny. Chapman and Hall/CRC. https://plotly-r.com

Victorian Electoral Commission. (2022). 2022 state election results [Data set]. https://www.vec.vic.gov.au/results/state-election-results/2022-state-election-results

Victorian Electoral Commission. (2023). Review of informal voting: 2022 State election. https://www.vecs.vec.vic.gov.au/-/media/dbb32440950847748761ea3481a397d4.pdf

Victorian Electoral Commission. (2024). State election statistics [Data set]. https://www.vec.vic.gov.au/results/electoral-statistics/state-election-statistics

Wickham, H. (2025). rvest: Easily harvest (scrape) web pages (Version 1.0.5) [R package]. https://doi.org/10.32614/CRAN.package.rvest

Wickham, H., Averick, M., Bryan, J., Chang, W., McGowan, L. D., François, R., Grolemund, G., Hayes, A., Henry, L., Hester, J., Kuhn, M., Pedersen, T. L., Miller, E., Bache, S. M., Müller, K., Ooms, J., Robinson, D., Seidel, D. P., Spinu, V., … Yutani, H. (2019). Welcome to the tidyverse. Journal of Open Source Software, 4(43), 1686. https://doi.org/10.21105/joss.01686

Wickham, H., & Bryan, J. (2026). readxl: Read Excel files (Version 1.5.0) [R package]. https://doi.org/10.32614/CRAN.package.readxl