Do larger Local Government Areas (LGAs) necessarily experience higher crime rates? Using data from the Crime Statistics Agency (CSA, year ending June 2025) and the Australian Bureau of Statistics (ABS, 2024), this visual analysis compares crime rates per 100,000 residents across Victoria’s LGAs. The findings reveal that several mid-sized LGAs record higher crime rates than some large metropolitan areas, suggesting that population size alone cannot explain variations in crime risk.
Population: ABS Estimated Resident Population (ERP) by LGA (2024) — used as the denominator for calculating crime rates.
Crime: Crime Statistics Agency (CSA) Recorded Offences by LGA (year ending June 2025) — provides total offences and the official crime rate per 100,000 residents.
Method: Both datasets were imported and cleaned to include only the latest year of data. Totals and unincorporated areas were excluded. The cleaned datasets were then merged by LGA name to enable comparative analysis.
Bar Chart: Highlights the top 10 LGAs with the highest crime rates (per 100,000 residents).
Scatter Plot: Displays the relationship between population size (x-axis) and crime rate (y-axis) across LGAs.
Why use rates instead of counts? Crime rates standardise comparisons by accounting for differences in population size, providing a fairer and more accurate basis for analysing crime across LGAs.
### ---- Load Required Libraries ----
library(tidyverse)
library(readxl)
library(scales)
library(plotly)
library(stringr)
library(magrittr)
p_path <- "C:/Users/nprad/Downloads/ABS_LGA_ERP_2001_2024.xlsx"
crime_path <- "C:/Users/nprad/Downloads/Data_Tables_LGA_Recorded_Offences_Year_Ending_June_2025.xlsx"
if (!file.exists(p_path)) {
message("Select the ABS population file...")
p_path <- file.choose()
}
if (!file.exists(crime_path)) {
message("Select the CSA offences file...")
crime_path <- file.choose()
}
stopifnot(file.exists(p_path), file.exists(crime_path))
# ---- ABS Population Data (Table 1) ----
abs_dataset <- suppressMessages(read_excel(p_path, sheet = "Table 1", skip = 5))
abs_lga_field <- names(abs_dataset)[2]
abs_year_candidates <-names(abs_dataset)[str_detect(names(abs_dataset),"^(X)?[0-9]{4}$")]
if (length(abs_year_candidates) == 0) stop("ABS check: no year columns detected. Verify sheet structure.")
abs_year_selected <- case_when(
"2024" %in% abs_year_candidates ~ "2024",
"X2024" %in% abs_year_candidates ~ "X2024",
TRUE ~ tail(abs_year_candidates, 1)
)
pop_table <- abs_dataset %>%
filter(!tolower(as.character(.data[[abs_year_selected]])) %in% c("no.", "no")) %>%
filter(
!is.na(.data[[abs_lga_field]]),
!str_detect(.data[[abs_lga_field]], regex("Total|Unincorp", ignore_case = TRUE))
) %>%
transmute(
LGA_Name = str_squish(as.character(.data[[abs_lga_field]])),
ERP_2024 = suppressWarnings(as.numeric(.data[[abs_year_selected]]))
)
# ---- CSA Recorded Offences Data (Table 01) ----
csa_dataset <- read_excel(crime_path, sheet = "Table 01")
col_lga <- names(csa_dataset)[str_detect(names(csa_dataset), "^Local Government Area$")][1]
col_year <- names(csa_dataset)[str_detect(names(csa_dataset), "^Year$")][1]
col_count <- names(csa_dataset)[str_detect(names(csa_dataset), "^Offence Count$")][1]
col_rate <- names(csa_dataset)[str_detect(names(csa_dataset), "^Rate per 100,000 population$")][1]
if (any(is.na(c(col_lga, col_year, col_count, col_rate)))) {
stop("CSA check: missing expected columns. Verify file headers.")
}
latest_csa_year <- suppressWarnings(max(as.numeric(csa_dataset[[col_year]]), na.rm = TRUE))
offence_table <- csa_dataset %>%
filter(
.data[[col_year]] == latest_csa_year,
!is.na(.data[[col_lga]]),
!str_detect(.data[[col_lga]], regex("Total|Unincorporated", ignore_case = TRUE))
) %>%
transmute(
LGA_Name = str_squish(as.character(.data[[col_lga]])),
Offence_Total = suppressWarnings(as.numeric(.data[[col_count]])),
Crime_Rate = suppressWarnings(as.numeric(.data[[col_rate]]))
)
# ---- Merge Datasets ----
filtered_pop <- pop_table %>% filter(LGA_Name %in% unique(offence_table$LGA_Name))
merged_data <- filtered_pop %>%
left_join(offence_table, by = "LGA_Name") %>%
filter(!is.na(Offence_Total), !is.na(ERP_2024), ERP_2024 > 0) %>%
mutate(Crime_Rate = replace_na(Crime_Rate, 0)) %>%
arrange(desc(Crime_Rate))
# ---- Sanity Check ----
list(
ABS_rows = nrow(filtered_pop),
CSA_rows = nrow(offence_table),
Joined_rows = nrow(merged_data),
Sample_LGAs = head(merged_data$LGA_Name, 5)
)
## $ABS_rows
## [1] 75
##
## $CSA_rows
## [1] 80
##
## $Joined_rows
## [1] 75
##
## $Sample_LGAs
## [1] "Melbourne" "Yarra" "Greater Shepparton"
## [4] "Mildura" "Ararat"
Several mid-sized LGAs rank among the top ten in crime rate, revealing that localised risk factors extend beyond overall population size.
When comparing total offences and crime rates, Melbourne records high counts; however, its relative ranking changes notably once population is taken into account.
Policy implication: Crime prevention and resource allocation should prioritise LGAs with disproportionately high crime rates rather than those with the highest offence counts
### ---- Visual 1 - Top 10 LGAs by Crime Rate ----
top_10 <- merged_data %>% slice_head(n = 10)
p1 <- ggplot(top_10, aes(x = reorder(LGA_Name, -Crime_Rate), y = Crime_Rate)) +
geom_col(fill = "#0073C2FF") +
labs(
title = "Top 10 Victorian LGAs by Recorded Offence Rate",
subtitle = paste0("Year ending June ", latest_csa_year, " (per 100,000 residents)"),
x = "Local Government Area (LGA)",
y = "Crime Rate (per 100,000 residents)"
) +
theme_light(base_size = 13) +
theme(axis.text.x = element_text(angle = 45, hjust = 1))
if (knitr::is_html_output()) {
p1 <- plotly::ggplotly(
p1 + aes(text = paste0(
"LGA: ", LGA_Name,
"<br>Crime Rate: ", round(Crime_Rate, 1),
"<br>Total Offences: ", scales::comma(Offence_Total)
)),
tooltip = "text"
)
}
p1
### ---- Visual 2 - Population vs Crime Rate ----
p2 <- ggplot(merged_data, aes(x = ERP_2024, y = Crime_Rate)) +
geom_point(alpha = 0.85, colour = "#2E86C1") +
labs(
title = "Population vs Recorded Offence Rate Across Victorian LGAs",
subtitle = paste0("ERP 2024 (ABS) vs Crime Rate per 100,000 (CSA ", latest_csa_year, ")"),
x = "Estimated Resident Population (ERP 2024)",
y = "Crime Rate (per 100,000 residents)"
) +
scale_x_continuous(labels = label_number(scale_cut = cut_short_scale())) +
theme_bw(base_size = 13)
if (knitr::is_html_output()) {
p2 <- plotly::ggplotly(
p2 + aes(text = paste0(
"LGA: ", LGA_Name,
"<br>Population: ", scales::comma(ERP_2024),
"<br>Rate: ", round(Crime_Rate, 1),
"<br>Offences: ", scales::comma(Offence_Total)
)),
tooltip = "text"
)
}
p2
Crime rates rely on Estimated Resident Population (ERP) data; estimation errors or changes in LGA boundaries or names may affect accuracy.
The offence composition (e.g., property versus person offences) is not disaggregated in this analysis.
Reporting and recording practices may vary across regions and over time, which can influence comparability.
Population size alone does not determine crime risk. Effective prevention and resource strategies should prioritise LGAs that show unusually high crime rates, focusing on localised factors rather than overall population counts.
Australian Bureau of Statistics. (2024). Regional population, 2023-24: Estimated resident population by Local Government Area (ERP by LGA) [Data set]. https://www.abs.gov.au/
Crime Statistics Agency Victoria. (2025). Recorded offences by Local Government Area - Year ending June 2025 [Data set]. https://www.crimestatistics.vic.gov.au/