This report explores changes in recorded offences across Victoria between 2017 and 2026, examining annual trends, differences in offence counts and population-adjusted rates across local government areas (LGAs), and changes across offence categories. It also investigates recorded breaches of family violence orders over time and across LGAs using interactive visualisations to highlight key patterns and differences.
The analysis uses Crime Statistics Agency Victoria data from Tables 01 and 02 (2017–2026), prepared and visualised in R using ggplot2 and plotly. It examines offence trends, LGA differences and offence categories. Recorded offences do not represent all crime or unique victims.
# Load necessary packages
library(readxl)
library(dplyr)
library(tidyr)
library(ggplot2)
library(plotly)
library(scales)
# Load data
file_path <- "Data_Tables_LGA_Recorded_Offences_Year_Ending_June_2026.xlsx"
table1 <- read_excel(file_path, sheet = "Table 01")
table2 <- read_excel(file_path, sheet = "Table 02")
# Check the reporting years and dataset dimensions
dim(table1)
## [1] 870 6
dim(table2)
## [1] 52809 10
range(table1$Year, na.rm = TRUE)
## [1] 2017 2026
range(table2$Year, na.rm = TRUE)
## [1] 2017 2026
# Remove aggregate and special-location rows from Table 01
table1_lga <- table1 %>%
filter(
!`Local Government Area` %in% c(
"Total", "Justice Institutions and Immigration Facilities", "Unincorporated Vic"
)
)
# Check each LGA appears once per reporting year
duplicate_lgas <- table1_lga %>% count(Year, `Local Government Area`) %>% filter(n > 1)
# Calculate annual recorded offence totals
annual_offences <- table1_lga %>%
group_by(Year) %>%
summarise(total_offences = sum(`Offence Count`, na.rm = TRUE), .groups = "drop") %>%
arrange(Year)
# Prepare 2026 offence counts and rates by LGA
lga_2026 <- table1_lga %>%
filter(Year == 2026, !is.na(`Rate per 100,000 population`)) %>%
select(`Local Government Area`, `Offence Count`, `Rate per 100,000 population`)
# Compare offence divisions in 2017 and 2026
division_comparison <- table2 %>%
filter(Year %in% c(2017, 2026)) %>%
group_by(Year, `Offence Division`) %>%
summarise(recorded_offences = sum(`Offence Count`, na.rm = TRUE), .groups = "drop") %>%
pivot_wider(names_from = Year, values_from = recorded_offences, names_prefix = "year_") %>%
mutate(change = year_2026 - year_2017,
percentage_change = if_else(year_2017 > 0, change / year_2017 * 100,NA_real_))
# Calculate the annual trend for family violence order breaches
family_violence_trend <- table2 %>%
filter(`Offence Subgroup` == "E21 Breach family violence order") %>%
group_by(Year) %>%
summarise(recorded_offences = sum(`Offence Count`, na.rm = TRUE), .groups = "drop") %>%
arrange(Year) %>%
mutate(
previous_year = lag(recorded_offences),
annual_change = recorded_offences - previous_year,
percentage_change = if_else(!is.na(previous_year) & previous_year > 0, annual_change / previous_year * 100, NA_real_)
)
# Compare LGA-level breaches in 2017 and 2026
family_violence_lga <- table2 %>%
filter(`Offence Subgroup` == "E21 Breach family violence order", Year %in% c(2017, 2026)) %>%
group_by(`Local Government Area`, Year) %>%
summarise(recorded_offences = sum(`Offence Count`, na.rm = TRUE), .groups = "drop") %>%
pivot_wider(names_from = Year, values_from = recorded_offences, names_prefix = "year_") %>%
mutate(
change = year_2026 - year_2017,
percentage_change = if_else(year_2017 > 0, change / year_2017 * 100, NA_real_)) %>%
arrange(desc(change))
# Select the ten LGAs with the largest absolute increases
top_increases <- family_violence_lga %>% slice_max(order_by = change, n = 10, with_ties = FALSE)
Recorded offences fluctuated over the decade, reaching their lowest annual total in 2022 before rising sharply to a peak in 2025. The total fell slightly in 2026, suggesting that the latest year did not continue the previous year’s increase.
chart1 <- ggplot(
annual_offences,
aes(
x = Year,
y = total_offences,
group = 1,
text = paste0(
"Year: ", Year,
"<br>Recorded offences: ",
scales::comma(total_offences)
)
)
) +
geom_line(
colour = "#276FBF",
linewidth = 1.1
) +
geom_point(
colour = "#276FBF",
size = 2.8
) +
geom_point(
data = filter(annual_offences, Year %in% c(2022, 2025)),
colour = "#E07A5F",
size = 4
) +
annotate(
"label",
x = 2022,
y = 490000,
label = "Lowest: 467,080",
size = 3.5,
fontface = "bold",
fill = "white",
label.size = 0
) +
annotate(
"label",
x = 2025,
y = 665000,
label = "Peak: 634,151",
size = 3.5,
fontface = "bold",
fill = "white",
label.size = 0
) +
scale_x_continuous(
breaks = 2017:2026
) +
scale_y_continuous(
labels = scales::label_comma(),
limits = c(0, 700000)
) +
labs(
title = "Recorded offences peaked in 2025 after a decade of uneven change",
subtitle = "Annual totals across 79 Victorian local government areas, 2017–2026",
x = "Year",
y = "Recorded offences",
caption = paste(
"Source: Crime Statistics Agency Victoria, Table 01.",
"Counts represent recorded offences, not unique victims."
)
) +
theme_minimal(base_size = 12) +
theme(
plot.title = element_text(face = "bold", size = 14),
plot.subtitle = element_text(size = 11),
axis.title = element_text(face = "bold"),
panel.grid.minor = element_blank()
)
ggplotly(chart1, tooltip = "text")
Comparing offence counts with rates per 100,000 people reveals different patterns across Victoria’s LGAs. Melbourne ranks highly on both measures, while other LGAs appear in only one group. Counts and rates offer different perspectives and do not directly measure personal safety.
# Select the ten highest LGAs by offence count
top_count_plot <- lga_2026 %>% slice_max(order_by = `Offence Count`, n = 10, with_ties = FALSE)
# Select the ten highest LGAs by offence rate
top_rate_plot <- lga_2026 %>% slice_max(order_by = `Rate per 100,000 population`, n = 10, with_ties = FALSE)
# Identify LGAs appearing in each top-ten group
count_lgas <- top_count_plot$`Local Government Area`
rate_lgas <- top_rate_plot$`Local Government Area`
# Combine the groups
chart2_data <- lga_2026 %>%
mutate(
chart_group = case_when(
`Local Government Area` %in% count_lgas &
`Local Government Area` %in% rate_lgas ~ "Highest counts and rates",
`Local Government Area` %in% count_lgas ~ "Highest offence counts",
`Local Government Area` %in% rate_lgas ~ "Highest offence rates",
TRUE ~ NA_character_
)
) %>%
filter(!is.na(chart_group)) %>%
mutate(
hover_text = paste0(
"LGA: ", `Local Government Area`,
"<br>Recorded offences: ",
scales::comma(`Offence Count`),
"<br>Rate per 100,000 population: ",
scales::comma(round(`Rate per 100,000 population`))
),
`Local Government Area` = reorder(
`Local Government Area`,
`Rate per 100,000 population`
)
)
# Draw the comparison chart
chart2 <- ggplot(
chart2_data,
aes(
x = `Rate per 100,000 population`,
y = `Local Government Area`,
colour = chart_group,
text = hover_text
)
) +
geom_point(size = 3.5, alpha = 0.9) +
scale_x_continuous(
labels = scales::label_comma()
) +
scale_colour_manual(
values = c(
"Highest offence counts" = "#276FBF",
"Highest offence rates" = "#E07A5F",
"Highest counts and rates" = "#2A9D8F"
)
) +
labs(
title = "The busiest LGAs are not always the highest-rate LGAs",
subtitle = "Ten highest LGAs by count and rate, Victoria, 2026",
x = "Recorded offences per 100,000 population",
y = NULL,
colour = NULL,
caption = paste(
"Source: Crime Statistics Agency Victoria, Table 01.",
"Rates provide population context but do not directly measure individual risk."
)
) +
theme_minimal(base_size = 12) +
theme(
plot.title = element_text(face = "bold", size = 14),
plot.subtitle = element_text(size = 11),
axis.text.y = element_text(size = 9),
panel.grid.minor = element_blank(),
legend.position = "bottom"
)
ggplotly(chart2, tooltip = "text")
Comparing 2017 with 2026 reveals contrasting changes across offence divisions. Justice-procedure offences recorded the largest percentage increase, followed by crimes against the person, while public-order and security offences declined. These differences show that the overall trend in recorded offences does not affect every offence category equally. However, changes in recorded offences alone cannot establish changes in underlying criminal behaviour.
# Prepare labels and hover information
chart3_data <- division_comparison %>%
mutate(
division_label = sub("^[A-F] ", "", `Offence Division`),
direction = if_else(percentage_change >= 0, "Increase", "Decrease"),
hover_text = paste0(
"Offence division: ", `Offence Division`,
"<br>2017: ", scales::comma(year_2017),
"<br>2026: ", scales::comma(year_2026),
"<br>Change: ", scales::comma(change),
"<br>Percentage change: ",
round(percentage_change, 1), "%"
)
) %>%
arrange(percentage_change) %>% mutate(division_label = factor(division_label, levels = division_label))
# Draw the diverging bar chart
chart3 <- ggplot(
chart3_data,
aes(
x = percentage_change,
y = division_label,
fill = direction,
text = hover_text
)
) +
geom_col(width = 0.7) +
geom_vline(
xintercept = 0,
colour = "grey40",
linewidth = 0.5
) +
scale_fill_manual(
values = c(
"Increase" = "#276FBF",
"Decrease" = "#E07A5F"
)
) +
scale_x_continuous(
labels = function(x) paste0(x, "%")
) +
labs(
title = "Justice-procedure offences rose fastest between 2017 and 2026",
subtitle = "Percentage change in recorded offences across six divisions",
x = "Change in recorded offences (%)",
y = NULL,
fill = NULL,
caption = paste(
"Source: Crime Statistics Agency Victoria, Table 02.",
"Changes describe recorded offences, not underlying crime prevalence."
)
) +
theme_minimal(base_size = 12) +
theme(
plot.title = element_text(face = "bold", size = 14),
plot.subtitle = element_text(size = 11),
axis.text.y = element_text(size = 10),
panel.grid.minor = element_blank(),
legend.position = "bottom"
)
ggplotly(chart3, tooltip = "text")
Recorded breaches of family violence orders increased from 38,500 in 2017 to 60,938 in 2026, an overall rise of approximately 58.3%. The count reached a peak of 63,310 in 2025 before declining by approximately 3.7% in 2026. Although the long-term trend is upward, the figures measure recorded offences rather than unique victims or the full prevalence of family violence, and they do not explain the reasons behind the changes.
# Prepare the annual figures and hover information
chart4_data <- family_violence_trend %>%
mutate(
hover_text = paste0(
"Year: ", Year,
"<br>Recorded offences: ",
scales::comma(recorded_offences),
ifelse(
is.na(percentage_change),
"",
paste0(
"<br>Change from previous year: ",
round(percentage_change, 1),
"%"
)
)
)
)
# Calculate the overall change from 2017 to 2026
start_count <- chart4_data %>% filter(Year == 2017) %>% pull(recorded_offences)
end_count <- chart4_data %>% filter(Year == 2026) %>% pull(recorded_offences)
overall_change <- (end_count - start_count) / start_count * 100
# Create the trend chart
chart4 <- ggplot(
chart4_data,
aes(
x = Year,
y = recorded_offences,
group = 1,
text = hover_text
)
) +
geom_line(
colour = "#276FBF",
linewidth = 1.1
) +
geom_point(
colour = "#276FBF",
size = 2.8
) +
geom_point(
data = filter(chart4_data, Year %in% c(2017, 2025, 2026)),
colour = "#E07A5F",
size = 3.8
) +
annotate(
"label",
x = 2018.5,
y = 67000,
label = paste0(
"+", round(overall_change, 1),
"% since 2017"
),
size = 3.5,
fontface = "bold",
fill = "white",
label.size = 0
) +
annotate(
"label",
x = 2025.8,
y = 57000,
label = "-3.75% from 2025",
size = 3.2,
fontface = "bold",
fill = "white",
label.size = 0
) +
scale_x_continuous(
breaks = 2017:2026,
expand = expansion(mult = c(0.03, 0.12))
) +
scale_y_continuous(
labels = scales::label_comma(),
limits = c(25000, 70000)
) +
labs(
title = "Recorded breaches of family violence orders rose over the decade",
subtitle = "Annual recorded offences in Victoria, 2017–2026",
x = "Year",
y = "Recorded offences",
caption = paste(
"Source: Crime Statistics Agency Victoria, Table 02.",
"Vertical axis starts at 25,000. Recorded offences are not a count of unique victims."
)
) +
theme_minimal(base_size = 12) +
theme(
plot.title = element_text(face = "bold", size = 14),
plot.subtitle = element_text(size = 11),
axis.title = element_text(face = "bold"),
panel.grid.minor = element_blank(),
plot.margin = margin(15, 35, 15, 15)
)
ggplotly(chart4, tooltip = "text") %>% layout(margin = list(t = 100, r = 70, b = 80, l = 80))
The increase in recorded breaches of family violence orders was not evenly distributed across local government areas. Casey recorded the largest absolute increase, with 1,992 more recorded breaches in 2026 than in 2017, followed by Greater Geelong with an increase of 1,676. These figures highlight differences in the number of recorded breaches across locations, but they do not account for population size or establish why the changes occurred.
# Prepare the ten LGAs with the largest absolute increases
chart5_data <- top_increases %>%
mutate(
lga_label = reorder(
`Local Government Area`,
change
),
hover_text = paste0(
"LGA: ", `Local Government Area`,
"<br>2017 recorded breaches: ",
scales::comma(year_2017),
"<br>2026 recorded breaches: ",
scales::comma(year_2026),
"<br>Absolute change: +",
scales::comma(change),
"<br>Percentage change: ",
round(percentage_change, 1), "%"
)
)
# Create the horizontal bar chart
chart5 <- ggplot(
chart5_data,
aes(
x = change,
y = lga_label,
text = hover_text
)
) +
geom_col(
fill = "#276FBF",
width = 0.7
) +
geom_text(
aes(label = paste0("+", scales::comma(change))),
hjust = -0.12,
size = 3.5
) +
scale_x_continuous(
labels = scales::label_comma(),
expand = expansion(mult = c(0, 0.18))
) +
labs(
title = "Casey recorded the largest increase in breaches",
subtitle = "Ten LGAs with the largest absolute change, 2017–2026",
x = "Change in recorded breaches",
y = NULL,
caption = paste(
"Source: Crime Statistics Agency Victoria, Table 02.",
"Ranked by absolute change, not population-adjusted rate."
)
) +
theme_minimal(base_size = 12) +
theme(
plot.title = element_text(face = "bold", size = 14),
plot.subtitle = element_text(size = 11),
axis.text.y = element_text(size = 10),
panel.grid.minor = element_blank(),
plot.margin = margin(15, 25, 15, 15)
)
ggplotly(chart5, tooltip = "text")
Recorded offences across Victoria fluctuated between 2017 and 2026, reaching their lowest annual total in 2022 before rising to a peak in 2025. The comparison of local government areas showed that the highest offence counts do not necessarily correspond to the highest population-adjusted rates. Across offence divisions, justice-procedure offences recorded the largest percentage increase between 2017 and 2026. Recorded breaches of family violence orders also increased over the decade, although the total declined slightly in 2026 after peaking in 2025. Together, these findings demonstrate why recorded offence counts, population-adjusted rates and offence categories should be examined separately.
This analysis uses recorded offence data and therefore does not measure all criminal activity, unique victims or the underlying prevalence of crime. Population-adjusted rates provide context but do not account for every difference between communities. Comparisons between 2017 and 2026 describe changes in recorded figures but do not establish their causes. The analysis focuses on selected tables and offence categories, so its findings should not be generalised to every aspect of public safety in Victoria. The latest reporting year may also be subject to future revisions by the data publisher.
Crime Statistics Agency Victoria. (2026). Data tables: Recorded offences by local government area, year ending June 2026 [Data set]. Victorian Government Data Directory. https://discover.data.vic.gov.au/dataset/data-tables-recorded-offences