# Cleaned file created by the data preparation script (crime_pipeline_from_scratch.R)
# Source: Crime Statistics Agency Victoria, recorded offences and alleged offender
# incidents by LGA (CC BY 4.0)
non_lga <- c("Total",
"Justice Institutions and Immigration Facilities",
"Unincorporated Vic")
lga_year <- read_csv("data/clean/lga_year.csv") |>
filter(!lga %in% non_lga) |>
mutate(across(c(year, n_offences, offence_rate), as.numeric))
# Data checks: stop knitting if the data is not as expected
stopifnot(
n_distinct(lga_year$lga) > 0,
nrow(count(lga_year, lga, year) |> filter(n > 1)) == 0
)
Crime statistics can reveal how recorded offences differ across communities and how these patterns change over time. However, the number of recorded offences alone does not provide a fair comparison between areas with different population sizes. This investigation uses data from the Crime Statistics Agency Victoria to examine recorded offences across local government areas (LGAs), focusing on offence rates, changes over time, offence composition, and differences between locations.
The analysis addresses three main questions: which LGAs have the highest recorded offence rates, how the types and volume of recorded offences have changed, and whether areas with higher recorded offence rates also tend to have higher alleged offender incident rates. Interactive visualisations are used to investigate these questions.
# Change in offence rate between the first and last year for each LGA
change_tbl <- lga_year |>
arrange(lga, year) |>
group_by(lga) |>
summarise(first_year = first(year),
last_year = last(year),
rate_first = first(offence_rate),
rate_last = last(offence_rate),
.groups = "drop") |>
mutate(abs_change = rate_last - rate_first,
pct_change = 100 * (rate_last - rate_first) / rate_first)
plot_data <- lga_year |>
left_join(change_tbl |> select(lga, first_year, abs_change, pct_change),
by = "lga") |>
mutate(label = paste0("<b>", lga, "</b>",
"<br>Year ending June ", year,
"<br>Rate per 100,000: ", round(offence_rate),
"<br>Offences: ", scales::comma(n_offences),
"<br>Change since ", first_year, ": ",
round(abs_change), " per 100,000"))
# Largest absolute increase and decrease in offence rate
#largest_increase <- change_tbl |>
# filter(is.finite(abs_change)) |>
# slice_max(abs_change, n = 1, with_ties = FALSE)
#largest_decrease <- change_tbl |>
# filter(is.finite(abs_change)) |>
# slice_min(abs_change, n = 1, with_ties = FALSE)
#largest_increase
#largest_decrease
shared <- SharedData$new(plot_data, key = ~lga, group = "lga_group")
line_plot <- plot_ly(shared, x = ~year, y = ~offence_rate,
split = ~lga,
type = "scatter", mode = "lines",
text = ~label, hoverinfo = "text",
line = list(color = "rgba(120,120,120,0.35)", width = 1),
showlegend = FALSE) |>
layout(xaxis = list(title = "Year ending June", dtick = 1),
yaxis = list(title = "Recorded offences per 100,000 population")) |>
highlight(on = "plotly_hover", off = "plotly_doubleclick",
color = "#d95f02", opacityDim = 0.25, selectize = FALSE)
bscols(
widths = c(3, 9),
list(
filter_select("lga_filter", "Pick one or more LGAs", shared, ~lga),
filter_slider("change_filter", "Change in rate per 100,000 since start",
shared, ~abs_change, round = 0)
),
line_plot
)
The trends show that recorded offence rates have not changed uniformly across Victoria. Between the first and last years shown, East Gippsland recorded the largest increase, rising by 5,282 offences per 100,000 residents, while Mansfield recorded the largest decrease, falling by 3,869 per 100,000. In the year ending June 2026, East Gippsland’s rate was approximately 13,647 per 100,000, compared with 3,291 in Mansfield. This contrast highlights the importance of examining changes over time rather than assuming all LGAs follow the same pattern. These figures describe recorded offences relative to resident populations and do not, by themselves, explain why the rates changed.
# Offence counts by LGA, year and offence subdivision (created by the data preparation script)
lga_sub <- read_csv("data/clean/lga_subdivision.csv") |>
filter(!lga %in% non_lga, !is.na(n_offences)) |>
mutate(year = as.numeric(year),
subdivision = str_remove(offence_subdivision, "^[A-Z][0-9]+\\s+"))
stopifnot(
nrow(count(lga_sub, lga, year, offence_subdivision) |> filter(n > 1)) == 0
)
# Share of each LGA's recorded offences by subdivision. complete() makes sure every
# LGA has a row for every subdivision in every year, which the animation needs.
composition <- lga_sub |>
group_by(lga, year, subdivision) |>
summarise(n_offences = sum(n_offences), .groups = "drop") |>
complete(lga, year, subdivision, fill = list(n_offences = 0)) |>
group_by(lga, year) |>
mutate(total = sum(n_offences),
share = if_else(total > 0, n_offences / total, 0)) |>
ungroup() |>
mutate(label = paste0("<b>", lga, "</b>",
"<br>", subdivision,
"<br>Year ending June ", year,
"<br>Offences: ", scales::comma(n_offences),
"<br>Share of LGA total: ", scales::percent(share, accuracy = 0.1)))
n_sub <- n_distinct(composition$subdivision)
# Most common offence subdivision in the latest year,
# based on the combined counts across included LGAs
#latest_composition <- composition |>
# filter(year == max(year)) |>
# group_by(subdivision) |>
# summarise(
# total_offences = sum(n_offences, na.rm = TRUE),
# .groups = "drop"
# ) |>
# mutate(share = total_offences / sum(total_offences)) |>
# arrange(desc(total_offences))
#head(latest_composition, 5)
plot_ly(composition,
x = ~lga, y = ~share, color = ~subdivision, frame = ~year,
colors = scales::viridis_pal()(n_sub),
type = "bar", text = ~label, hoverinfo = "text") |>
layout(barmode = "stack",
xaxis = list(title = "", tickangle = -60,
categoryorder = "array",
categoryarray = sort(unique(composition$lga))),
yaxis = list(title = "Share of recorded offences", tickformat = ".0%"),
legend = list(title = list(text = "Offence subdivision")),
margin = list(b = 120)) |>
animation_opts(frame = 600, transition = 0, redraw = TRUE) |>
animation_slider(currentvalue = list(prefix = "Year ending June "))
The offence-composition chart shows that the mix of recorded offences differs across LGAs. In the year ending June 2026, Theft was the most frequently recorded category across the included areas, accounting for 37% of the combined recorded offences. However, the proportions of individual offence categories vary between LGAs, showing that communities can have different offence profiles even within the same reporting period. Because this chart compares shares rather than absolute rates, it describes the composition of recorded offences, not which area has the highest overall offence rate.
# Statewide totals across the 79 LGAs. The totals exclude offences recorded at
# Justice institutions and immigration facilities, in Unincorporated Victoria, and
# where the location is unknown.
state_year <- lga_year |>
group_by(year) |>
summarise(n_offences = sum(n_offences, na.rm = TRUE), .groups = "drop") |>
mutate(pct_change = 100 * (n_offences / lag(n_offences) - 1),
label = paste0("Year ending June ", year,
"<br>Recorded offences: ", scales::comma(n_offences),
if_else(is.na(pct_change), "",
paste0("<br>Change on previous year: ",
sprintf("%+.1f", pct_change), "%"))))
# Identify the highest and lowest included annual totals
#state_year |>
# slice_max(n_offences, n = 1, with_ties = FALSE)
#state_year |>
# slice_min(n_offences, n = 1, with_ties = FALSE)
# Overall change between the first and last available years
#state_year |>
# summarise(
# first_year = first(year),
# last_year = last(year),
# first_count = first(n_offences),
# last_count = last(n_offences),
# absolute_change = last_count - first_count,
# percentage_change =
# 100 * (last_count / first_count - 1)
# )
plot_ly(state_year, x = ~year, y = ~n_offences,
type = "scatter", mode = "lines+markers",
text = ~label, hoverinfo = "text",
line = list(color = "#1b6ca8", width = 3),
marker = list(color = "#1b6ca8", size = 8)) |>
layout(xaxis = list(title = "Year ending June", dtick = 1),
yaxis = list(title = "Recorded offences (all LGAs)",
rangemode = "tozero", tickformat = ","))
The annual totals show a fluctuating pattern rather than a steady increase or decrease. Among the years displayed, the included LGA records reached a peak of 634,151 offences in the year ending June 2025, before falling by 2.2% to 620,140 in June 2026. The lowest annual total in this period was 467,080 in June 2022. Despite the latest annual decline, the total in 2026 was approximately 17.1% higher than the 529,748 offences recorded in 2017. This indicates that the latest decrease should be viewed within the longer-term trend. These totals sum the included LGA records and exclude specified locations; they should not be described as a complete statewide total.
latest <- max(lga_year$year)
lga_latest <- lga_year |>
filter(year == latest, !is.na(offence_rate)) |>
mutate(region = str_remove(police_region, "^[0-9]+\\s+"),
rank = rank(-offence_rate, ties.method = "first"),
label = paste0("<b>", lga, "</b>",
"<br>Region: ", region,
"<br>Rate per 100,000: ", round(offence_rate),
"<br>Offences: ", scales::comma(n_offences),
"<br>Rank: ", rank, " of ", n()))
# Five LGAs with the highest recorded offence rates
# lga_latest |>
# arrange(desc(offence_rate)) |>
# select(
# lga,
# police_region,
# offence_rate,
# n_offences,
# rank
# ) |>
# slice_head(n = 5)
shared_rank <- SharedData$new(lga_latest, key = ~lga, group = "rank_group")
rank_plot <- plot_ly(shared_rank, x = ~offence_rate, y = ~lga, color = ~region,
colors = "Dark2", type = "bar", orientation = "h",
text = ~label, hoverinfo = "text") |>
layout(xaxis = list(title = paste0("Recorded offences per
100,000 population, year ending June ", latest)),
yaxis = list(title = "", categoryorder = "total ascending"),
legend = list(title = list(text = "Police region")))
bscols(
widths = c(3, 9),
list(
filter_slider("rank_filter", "Rank range (1 = highest rate)", shared_rank, ~rank, round = 0),
filter_select("rank_lga", "Pick LGAs", shared_rank, ~lga)
),
rank_plot
)
In the year ending June 2026, Melbourne ranked first among the 79 included LGAs, with 44,694 recorded offences and a rate of approximately 22,860 per 100,000 residents. This illustrates why offence counts and population-adjusted rates should be considered together: a high count alone does not show how frequently offences are recorded relative to the resident population. Melbourne’s rate should also be interpreted cautiously because its large commuter, visitor and commercial populations may not be fully reflected in the resident-population denominator. A higher recorded rate should not automatically be interpreted as a definitive measure of personal risk.
This investigation examined recorded offences across Victorian local government areas using four interactive visualisations. The findings show that offence patterns differ substantially between locations and over time. Between the first and last years analysed, East Gippsland recorded an increase of 5,282 offences per 100,000 residents, while Mansfield recorded a decrease of 3,869 per 100,000. Theft was the most frequently recorded offence category in the year ending June 2026, accounting for 37% of combined offences across the included areas.
The overall number of recorded offences also fluctuated over the period. The included LGA records reached a peak of 634,151 in June 2025 before declining by 2.2% to 620,140 in June 2026. Despite this recent decline, the 2026 total remained approximately 17.1% higher than in 2017. In the latest year, Melbourne ranked first among the 79 included LGAs by offence rate, at approximately 22,860 per 100,000 residents.
Together, these findings demonstrate the importance of examining offence rates, counts and offence composition separately. However, recorded offences do not capture all criminal activity, and rates based on resident populations may be affected by commuters, visitors and other people who are present in an area. The results describe patterns in recorded administrative data and should not be treated as a definitive measure of personal risk or as an explanation of why crime patterns change.
This investigation uses data published by the Crime Statistics Agency Victoria covering recorded offences and alleged offender incidents across local government areas. The cleaned datasets were prepared using a reproducible R data-processing pipeline and loaded from local CSV files.
The analysis combines annual offence counts and offence rates with offence subdivision data to examine changes over time, differences between areas and the composition of recorded offences. The latest available year is used for the LGA ranking and the comparison between offence rates and alleged offender incident rates.
Records labelled as statewide totals, justice institutions and immigration facilities, and Unincorporated Victoria were excluded from the LGA analysis. Consequently, the statewide trend represents the sum of the included LGA records rather than necessarily representing every offence recorded across Victoria. The source data also has limitations, including exclusions for unknown locations and suppression of alleged offender breakdown counts of three or fewer.
Rates are expressed per 100,000 residents where supplied by the source data. Interactive charts were created with Plotly, while Crosstalk provides linked filtering and selection for relevant visualisations. All findings should be interpreted in the context of the source data’s definitions, exclusions and limitations.
ChatGPT was used for technical guidance on implementing interactive data visualisations in R using Plotly and Crosstalk, including assistance with filtering, hover information and chart functionality. The data analysis, interpretation of findings, visualisation design decisions and final conclusions were reviewed and completed by the author.
I acknowledge the Crime Statistics Agency Victoria as the source of the public crime data used in the investigation.
Crime Statistics Agency. (2026). Recorded offences by LGA - Year ending Jun 2026 [Data set]. State Government of Victoria. https://discover.data.vic.gov.au/dataset/data-tables-recorded-offences/resource/06bb24c8-8f62-4144-a746-99ef5492e4f9
Crime Statistics Agency. (2026). Alleged offender incidents by LGA [Data set]. State Government of Victoria. https://discover.data.vic.gov.au/dataset/data-tables-alleged-offender-incidents/resource/b36fb8cb-be44-4ceb-9adc-3a75aa6e4724