# ===================== DATA PREPARATION =====================
not_lga <- c("Total", "Justice Institutions and Immigration Facilities",
"Unincorporated Vic")
# --- Table 01: incidents and rate per 100,000 by LGA and year ---
t01 <- read_excel(f_inc, sheet = "Table 01") %>%
rename(LGA = `Local Government Area`, Incidents = `Incidents Recorded`,
Rate = `Rate per 100,000 population`) %>%
mutate(LGA = str_trim(LGA)) %>% # LGA names carry a leading space
filter(!LGA %in% not_lga) # keep the 79 real councils
# Victoria = all 79 LGAs combined. Population is back-calculated from
# incidents / rate so that the Victorian rate is weighted correctly.
vic_rate <- t01 %>%
mutate(Pop = Incidents / Rate * 1e5) %>%
group_by(Year) %>%
summarise(Incidents = sum(Incidents), Rate = sum(Incidents) / sum(Pop) * 1e5)
wyn_rate <- t01 %>% filter(LGA == "Wyndham")
# --- Table 03: incidents by suburb and offence (Wyndham only) ---
t03_wyn <- read_excel(f_inc, sheet = "Table 03") %>%
rename(LGA = `Local Government Area`, Suburb = `Suburb/Town Name`,
Incidents = `Incidents Recorded`) %>%
mutate(LGA = str_trim(LGA)) %>%
filter(LGA == "Wyndham")
# --- Table 04: incidents by location type (Wyndham only) ---
t04_wyn <- read_excel(f_inc, sheet = "Table 04") %>%
rename(LGA = `Local Government Area`, Incidents = `Incidents Recorded`) %>%
mutate(LGA = str_trim(LGA)) %>%
filter(LGA == "Wyndham")
# --- Table 05: investigation (charge) status, all LGAs ---
t05 <- read_excel(f_inc, sheet = "Table 05") %>%
rename(LGA = `Local Government Area`, Status = `Charge Status`,
Incidents = `Incidents Recorded`) %>%
mutate(LGA = str_trim(LGA)) %>%
filter(!LGA %in% not_lga)
# --- Alleged offender file, Table 01: offender incidents per 100,000 ---
off01 <- read_excel(f_off, sheet = "Table 01") %>%
select(1:6) %>% # drop empty trailing columns
rename(LGA = `Local Government Area`, OffRate = `Rate per 100,000 population`) %>%
mutate(LGA = str_trim(LGA)) %>%
filter(LGA == "Wyndham")
# ---- Numbers used in the text (calculated, never typed in) ----
yr_first <- min(t01$Year); yr_last <- max(t01$Year)
v_now <- vic_rate$Rate[vic_rate$Year == yr_last]
v_prev <- vic_rate$Rate[vic_rate$Year == yr_last - 1]
w_now <- wyn_rate$Rate[wyn_rate$Year == yr_last]
w_2022 <- wyn_rate$Rate[wyn_rate$Year == 2022]
w_rank <- t01 %>% filter(Year == yr_last) %>% arrange(desc(Rate)) %>%
mutate(r = row_number()) %>% filter(LGA == "Wyndham") %>% pull(r)
n_lga <- n_distinct(t01$LGA)
sub_tot <- t03_wyn %>% group_by(Suburb, Year) %>%
summarise(Incidents = sum(Incidents), .groups = "drop")
tar_first <- sub_tot$Incidents[sub_tot$Suburb == "Tarneit" & sub_tot$Year == yr_first]
tar_last <- sub_tot$Incidents[sub_tot$Suburb == "Tarneit" & sub_tot$Year == yr_last]
theft <- t03_wyn %>% filter(`Offence Subdivision` == "B40 Theft")
tar_car <- theft %>% filter(Suburb == "Tarneit",
`Offence Subgroup` %in% c("B41 Motor vehicle theft",
"B42 Steal from a motor vehicle")) %>%
group_by(Year) %>% summarise(n = sum(Incidents))
car_first <- tar_car$n[tar_car$Year == yr_first]; car_last <- tar_car$n[tar_car$Year == yr_last]
status_share <- function(d) d %>% group_by(Year, Status) %>%
summarise(Incidents = sum(Incidents), .groups = "drop_last") %>%
mutate(Share = Incidents / sum(Incidents)) %>% ungroup()
wyn_status <- status_share(filter(t05, LGA == "Wyndham"))
vic_status <- status_share(t05)
get_share <- function(d, y, s) d$Share[d$Year == y & d$Status == s]
off_first <- off01$OffRate[off01$Year == yr_first]; off_last <- off01$OffRate[off01$Year == yr_last]
pct <- function(x) percent(x, accuracy = 1)
num <- function(x) comma(x, accuracy = 1)
I live in Tarneit, in the City of Wyndham, in Melbourne’s west. On 28 October 2026, the day I was finishing this report, offenders drove a stolen Toyota RAV4 through the entrance of a shopping centre on Wickford Road, close to where I live, and rammed the doors of a jewellery store at about 2:45am. At the time of writing, police were still looking for the people responsible (WyndhamTV, 2026). It left me with a simple question: how do people keep committing crime in a country that feels this safe and this well policed?
That one night touches three things I later found in the data: stolen cars, crime at shopping centres, and crimes where no one is caught.
This report follows that question from the whole state down to my suburb, using official Victorian crime statistics. Every chart is interactive. Hover over it for exact values, and use the controls under each chart to explore.
In the year to June 2026, Victoria recorded 6,464 criminal incidents per 100,000 people, down from 6,784 a year earlier. Wyndham looks even better. Its rate of 5,127 is below the state rate and ranks 46 out of 79 councils, where 1 is the highest rate.
lga_plot <- t01 %>%
mutate(hover = paste0("<b>", LGA, "</b><br>Year ending June ", Year,
"<br>Rate: ", num(Rate), " per 100,000",
"<br>Incidents: ", num(Incidents)))
lga_sd <- SharedData$new(lga_plot, key = ~LGA, group = "Type a council name")
end_lab <- function(d, txt, col, yshift) list(
x = yr_last, y = d$Rate[d$Year == yr_last], text = txt, xanchor = "left",
showarrow = FALSE, font = list(color = col, size = 14), xshift = 6, yshift = yshift)
p1 <- plot_ly(lga_sd, x = ~Year, y = ~Rate, text = ~hover, hoverinfo = "text",
height = 520) %>%
add_lines(split = ~LGA, line = list(color = col_grey, width = 1),
showlegend = FALSE) %>%
add_lines(data = vic_rate, x = ~Year, y = ~Rate, inherit = FALSE,
line = list(color = col_vic, width = 3.5), showlegend = FALSE,
hoverinfo = "text",
text = ~paste0("<b>Victoria (79 councils)</b><br>Year ending June ", Year,
"<br>Rate: ", num(Rate), " per 100,000")) %>%
add_lines(data = wyn_rate, x = ~Year, y = ~Rate, inherit = FALSE,
line = list(color = col_focus, width = 3.5), showlegend = FALSE,
hoverinfo = "text",
text = ~paste0("<b>Wyndham</b><br>Year ending June ", Year,
"<br>Rate: ", num(Rate), " per 100,000",
"<br>Incidents: ", num(Incidents))) %>%
layout(
title = list(text = paste0("<b>Wyndham's crime rate sits below Victoria's</b><br>",
"<sup>Criminal incidents per 100,000 people; grey lines are the other ",
n_lga - 1, " councils</sup>"), x = 0.02, font = list(size = 17)),
xaxis = list(title = ax_src("Year ending June", "Table 01"), tickvals = yr_first:yr_last,
range = c(yr_first - 0.2, yr_last + 1.1), tickfont = list(size = 13)),
yaxis = list(title = "Incidents per 100,000 people", tickformat = ",", rangemode = "tozero",
tickfont = list(size = 13)),
annotations = list(
end_lab(vic_rate, "<b>Victoria</b>", col_vic, 10),
end_lab(wyn_rate, "<b>Wyndham</b>", col_focus, -10)),
margin = list(t = 80, b = 90)) %>%
highlight(on = "plotly_click", off = "plotly_doubleclick", selectize = TRUE,
persistent = FALSE, color = col_pick, opacityDim = 0.6) %>%
config(displaylogo = FALSE)
p1
How to use: type your own council into the search box, or click any grey line, to highlight it in blue. Double-click to clear. The top grey line is the City of Melbourne. Its rate is high partly because many people visit, but few people live there.
A low council rate hides what is happening street by street. In Tarneit, recorded incidents rose from 1,369 in 2017 to 3,708 in 2026, an increase of 171%. Tarneit now has more incidents than Hoppers Crossing and is second only to Werribee.
dumb <- sub_tot %>%
filter(Year %in% c(yr_first, yr_last)) %>%
pivot_wider(names_from = Year, values_from = Incidents, names_prefix = "y") %>%
rename(first = 2, last = 3) %>%
replace_na(list(first = 0, last = 0)) %>%
filter(last >= 100) %>% # suburbs with 100+ incidents
mutate(change = ifelse(first > 0, last / first - 1, NA),
is_tar = Suburb == "Tarneit",
Suburb = reorder(Suburb, last),
# suburbs with under 20 incidents in the first year are new estates: % change is meaningless
lab = ifelse(first < 20, " new estate", paste0(" ", ifelse(change >= 0, "+", ""), pct(change))),
hover = paste0("<b>", Suburb, "</b><br>", yr_first, ": ", num(first),
"<br>", yr_last, ": ", num(last), "<br>Change: ", lab))
p2 <- plot_ly(dumb, y = ~Suburb, height = 560) %>%
add_segments(data = filter(dumb, !is_tar), x = ~first, xend = ~last, yend = ~Suburb,
showlegend = FALSE, line = list(color = "#BBBBBB", width = 3), hoverinfo = "none") %>%
add_segments(data = filter(dumb, is_tar), x = ~first, xend = ~last, yend = ~Suburb,
showlegend = FALSE, line = list(color = col_focus, width = 4), hoverinfo = "none") %>%
add_markers(data = dumb, x = ~first, name = paste("Year ending June", yr_first),
marker = list(color = "#9A9A9A", size = 11),
text = ~hover, hoverinfo = "text") %>%
add_markers(data = dumb, x = ~last, name = paste("Year ending June", yr_last),
marker = list(color = ~ifelse(is_tar, col_focus, col_vic), size = 12),
text = ~hover, hoverinfo = "text") %>%
add_text(data = dumb, x = ~pmax(first, last), text = ~lab, textposition = "middle right",
textfont = list(size = 12, color = ~ifelse(is_tar, col_focus, "#444444")),
showlegend = FALSE, hoverinfo = "none") %>%
layout(
title = list(text = paste0("<b>Tarneit's incidents grew faster than any other large Wyndham suburb</b><br>",
"<sup>Recorded criminal incidents by suburb, ", yr_first, " (grey) vs ",
yr_last, " (dark); suburbs with 100+ incidents; label = % change</sup>"),
x = 0.02, font = list(size = 17)),
xaxis = list(title = ax_src("Criminal incidents recorded",
"Table 03"),
tickformat = ",", range = c(0, max(dumb$last) * 1.15), tickfont = list(size = 13)),
yaxis = list(title = "", tickfont = list(size = 13)),
legend = list(orientation = "h", x = 0, y = 1.0, yanchor = "bottom", font = list(size = 12)),
margin = list(t = 110, b = 90, l = 130)) %>%
config(displaylogo = FALSE)
p2
How to use: hover over a dot for exact counts. Click a year in the legend to hide it.
A note on fairness: these suburb figures are counts, not rates. Tarneit is one of Melbourne’s fastest-growing suburbs, so part of this rise is simply more people. Even so, Wyndham’s rate per person rose from 3,951 in 2022 to 5,127 in 2026 (+30%). More people does not explain all of it.
Most of Tarneit’s growth comes from theft. Car-related theft (stealing a car, or stealing from one) went from 306 incidents in 2017 to 863 in 2026, making it the largest type of theft in Tarneit today.
sub_pick <- c("Tarneit", "Werribee", "Hoppers Crossing", "Point Cook",
"Truganina", "Wyndham Vale")
grp_cols <- c("Steal from a motor vehicle" = "#D55E00", "Motor vehicle theft" = "#E69F00",
"Other theft" = "#0072B2", "Steal from a retail store" = "#009E73",
"Theft of a bicycle" = "#CC79A7", "Receiving or handling stolen goods" = "#7F7F7F")
theft_plot <- theft %>%
filter(Suburb %in% sub_pick, `Offence Subgroup` != "B46 Fare evasion") %>%
mutate(Group = str_remove(`Offence Subgroup`, "^B\\d+ ")) %>%
group_by(Suburb, Group, Year) %>% summarise(n = sum(Incidents), .groups = "drop") %>%
complete(Suburb, Group, Year = yr_first:yr_last, fill = list(n = 0)) %>% # missing = 0
mutate(Group = factor(Group, levels = names(grp_cols)))
p3 <- plot_ly(height = 520)
for (s in sub_pick) for (g in names(grp_cols)) {
d <- filter(theft_plot, Suburb == s, Group == g)
p3 <- add_lines(p3, data = d, x = ~Year, y = ~n, name = g, legendgroup = g,
visible = (s == "Tarneit"), showlegend = TRUE,
line = list(color = grp_cols[[g]],
width = ifelse(grepl("motor", g), 3.5, 2)),
text = ~paste0("<b>", s, "</b><br>", g, "<br>Year ending June ", Year,
"<br>Incidents: ", num(n)),
hoverinfo = "text")
}
n_g <- length(grp_cols)
buttons <- lapply(sub_pick, function(s) list(
label = s, method = "update",
args = list(list(visible = rep(sub_pick == s, each = n_g)),
list(title = list(text = paste0("<b>Theft in ", s, ", by type</b><br>",
"<sup>Recorded theft incidents, year ending June</sup>"),
x = 0.02, font = list(size = 17))))))
p3 <- p3 %>% layout(
title = list(text = "<b>Theft in Tarneit, by type</b><br><sup>Recorded theft incidents, year ending June</sup>",
x = 0.02, font = list(size = 17)),
updatemenus = list(list(type = "dropdown", active = 0, buttons = buttons,
x = 1, xanchor = "right", y = 1.17, yanchor = "top")),
xaxis = list(title = ax_src("Year ending June", "Table 03 (B40 Theft)"), dtick = 1,
tickfont = list(size = 13)),
yaxis = list(title = "Theft incidents recorded", rangemode = "tozero", tickformat = ",",
tickfont = list(size = 13)),
legend = list(title = list(text = "<b>Type of theft</b>"), font = list(size = 12)),
margin = list(t = 90, b = 90)) %>%
config(displaylogo = FALSE)
p3
How to use: pick another suburb from the dropdown (top right) to compare it with Tarneit. Click a theft type in the legend to hide or show it.
Across Wyndham, the house is still the most common place where an incident is recorded. But the fastest growth is in public, everyday places: streets and footpaths, service stations, car parks and train stations.
loc <- t04_wyn %>%
mutate(Place = str_remove(`Location Group`, "^\\d+ "),
Type = str_remove(`Location Division`, "^\\d+ ")) %>%
filter(Year %in% c(yr_first, yr_last)) %>%
group_by(Place, Type, Year) %>% summarise(n = sum(Incidents), .groups = "drop") %>%
pivot_wider(names_from = Year, values_from = n, names_prefix = "y", values_fill = 0) %>%
rename(first = 3, last = 4) %>%
slice_max(last, n = 12) %>% # 12 most common places
mutate(Place = reorder(Place, last),
change = last / first - 1,
hover = paste0("<b>", Place, "</b> (", Type, ")<br>", yr_first, ": ", num(first),
"<br>", yr_last, ": ", num(last),
"<br>Change: ", ifelse(change >= 0, "+", ""), pct(change)))
type_cols <- c("Residential" = "#0072B2", "Community" = "#E69F00", "Other" = "#009E73")
loc$Type <- factor(loc$Type, levels = names(type_cols))
p4 <- plot_ly(loc, y = ~Place, height = 560) %>%
add_bars(x = ~last, color = ~Type, colors = type_cols, text = ~hover,
hoverinfo = "text", textposition = "none", orientation = "h") %>%
add_markers(x = ~first, name = paste(yr_first, "level"), text = ~hover, hoverinfo = "text",
marker = list(symbol = "line-ns-open", size = 18, color = col_vic,
line = list(width = 3, color = col_vic))) %>%
layout(
title = list(text = paste0("<b>Public places are catching up with homes</b><br>",
"<sup>Wyndham criminal incidents by place, ", yr_last,
" (bars) vs ", yr_first, " (black tick); 12 most common places</sup>"),
x = 0.02, font = list(size = 17)),
xaxis = list(title = ax_src("Criminal incidents recorded",
"Table 04"),
tickformat = ",", tickfont = list(size = 13)),
yaxis = list(title = "", tickfont = list(size = 13)),
legend = list(title = list(text = "<b>Type of place</b>"), font = list(size = 12),
traceorder = "normal"),
margin = list(t = 80, b = 90, l = 170)) %>%
config(displaylogo = FALSE)
p4
How to use: hover over a bar to see the growth since 2017. Click a place type in the legend to isolate it.
This is the answer to my question. In 2026, 63% of Wyndham’s incidents were unsolved, compared with 55% across Victoria and 46% in Wyndham in 2022. Charges were laid in only 26% of incidents, down from 38% in 2017. The second CSA dataset points the same way. Wyndham’s alleged offender rate barely moved, from 1,778 to 1,825 per 100,000 people (Crime Statistics Agency Victoria, 2026b), while incidents kept climbing. More crime is being recorded, but the rate of people being caught has not kept up.
lga_status <- t05 %>% group_by(LGA) %>% group_modify(~ status_share(.x)) %>% ungroup() %>%
mutate(Status = factor(Status, levels = c("Unsolved", "Charges laid", "No charges laid")),
hover = paste0("<b>", LGA, "</b><br>", Status, ", ", Year, ": ", pct(Share)))
focus_status <- bind_rows(mutate(wyn_status, Area = "Wyndham"),
mutate(vic_status, Area = "Victoria")) %>%
mutate(Status = factor(Status, levels = levels(lga_status$Status)),
Area = factor(Area, levels = c("Wyndham", "Victoria")),
hover = paste0("<b>", Area, "</b><br>", Status, ", ", Year, ": ", pct(Share)))
st_sd <- SharedData$new(lga_status, key = ~LGA, group = "Type a council name (chart 5)")
g5 <- ggplot() +
geom_line(data = st_sd, aes(Year, Share, group = LGA, text = hover),
colour = col_grey, linewidth = 0.3) +
geom_line(data = focus_status, aes(Year, Share, colour = Area, group = Area, text = hover),
linewidth = 1.4) +
facet_wrap(~ Status, nrow = 1) +
scale_colour_manual(values = c(Wyndham = col_focus, Victoria = col_vic), name = NULL) +
scale_y_continuous(labels = percent_format(accuracy = 1), limits = c(0, 0.85)) +
scale_x_continuous(breaks = c(2018, 2022, 2026)) +
labs(x = "Year ending June", y = "Share of all criminal incidents") +
theme_minimal(base_size = 13) +
theme(strip.text = element_text(face = "bold", size = 13), panel.grid.minor = element_blank(),
legend.position = "top")
p5 <- ggplotly(g5, tooltip = "text", height = 520) %>%
layout(
title = list(text = paste0("<b>In Wyndham, almost two in three incidents now go unsolved</b><br>",
"<sup>Investigation status 30 days after recording; grey lines are all ",
n_lga, " councils</sup>"), x = 0.02, font = list(size = 17)),
legend = list(orientation = "h", x = 0.02, y = 1.02, font = list(size = 13)),
annotations = list(list(x = 0.5, y = -0.3, xref = "paper", yref = "paper", showarrow = FALSE,
xanchor = "center", text = paste0(src_inc, ", Table 05."),
font = list(size = 11, color = "#555555"))),
margin = list(t = 110, b = 130)) %>%
highlight(on = "plotly_click", off = "plotly_doubleclick", selectize = TRUE,
persistent = FALSE, color = col_pick) %>%
config(displaylogo = FALSE)
p5
How to use: type a council name, or click a grey line, to highlight that council in blue in all three panels at once. Double-click to clear.
The data showed me something I did not expect. Victoria looks relatively safe on average, and Wyndham’s crime rate per person is lower than the state average. However, Tarneit is one of the fast-growing suburbs in Wyndham, with more people choosing to live here. This made me think about why people choose a place to live, and safety is an important part of that decision. Seeing crime increase alongside the suburb’s growth surprised me, because if people do not feel safe, it could affect the community and the area’s future growth as well.
A recent incident in Tarneit made this concern more real. On 28 October 2026, offenders used a stolen Toyota RAV4 to ram the entrance of a shopping centre on Wickford Road and break into a jewellery store (WyndhamTV, 2026). It reminds us that car theft is not always an isolated incident and can be connected to more serious crimes. Taking care of our belongings and being responsible about security matters.
For residents like me, a few practical steps can help:
I believe awareness is the key. Residents should not hesitate to report concerns, and police should respond seriously and promptly. This can help people feel more confident that support is available, while making offenders aware that criminal activity will not simply go unnoticed. Individual precautions alone cannot solve crime; a safer community needs both responsible residents and effective police support.
Data sources: Crime Statistics Agency Victoria (2026a, 2026b), using the year ending June 2026 release, covering July 2016 to June 2026. Both datasets were downloaded from the Victorian Government open data portal under a Creative Commons Attribution 4.0 licence. No other datasets were used.
Data preparation: All data preparation and calculations were completed in R, with code available under each “Code” button.
readxl.Limitations: Recorded crime includes only incidents reported to or detected by police, so some crimes may not appear in the data. Charts 2 and 3 show suburb-level counts rather than rates because suburb population figures are not included in these data tables. Investigation status is recorded 30 days after an incident is recorded, so some incidents classified as unsolved may be resolved later (Crime Statistics Agency Victoria, n.d.). The Tarneit ram-raid incident is included as local context, not as a finding from the datasets analysed.
Allaire, J. J., Xie, Y., Dervieux, C., McPherson, J., Luraschi, J., Ushey, K., Atkins, A., Wickham, H., Cheng, J., Chang, W., & Iannone, R. (2023). rmarkdown: Dynamic documents for R (Version 2.25) [R package]. https://CRAN.R-project.org/package=rmarkdown
Baglin, J. (2026). Data visualisation: From theory to practice. RMIT University. https://data-visualisation.stem.melbourne/
Cheng, J., & Sievert, C. (2023). crosstalk: Inter-widget interactivity for HTML widgets (Version 1.2.1) [R package]. https://CRAN.R-project.org/package=crosstalk
Crime Statistics Agency Victoria. (n.d.). Glossary and data dictionary. Retrieved October 28, 2026, from https://www.crimestatistics.vic.gov.au/about-the-data/glossary-and-data-dictionary
Crime Statistics Agency Victoria. (2026a). Criminal incident by LGA – Year ending June 2026 [Data set]. Victorian Government Data Directory. https://discover.data.vic.gov.au/dataset/criminal-incident/resource/c5750abf-4440-4e86-8bf0-11abd04741a9
Crime Statistics Agency Victoria. (2026b). Alleged offender incidents by LGA – Year ending June 2026 [Data set]. Victorian Government Data Directory. https://discover.data.vic.gov.au/dataset/data-tables-alleged-offender-incidents/resource/b36fb8cb-be44-4ceb-9adc-3a75aa6e4724
R Core Team. (2024). R: A language and environment for statistical computing [Computer software]. R Foundation for Statistical Computing. https://www.R-project.org/
Sievert, C. (2020). Interactive web-based data visualization with R, plotly, and shiny. Chapman and Hall/CRC. https://plotly-r.com
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. (2023). readxl: Read Excel files (Version 1.4.3) [R package]. https://CRAN.R-project.org/package=readxl
WyndhamTV. (2026, October 28). Police investigate Tarneit jewellery store ram raid. https://www.wyndhamtv.com.au/police-investigate-tarneit-jewellery-store-ram-raid/
I used Generative AI to help me understand the assignment requirements and clarify aspects of the R programming operations used in my work. I also used it for basic grammar, spelling, and language checks to improve the clarity of my writing. I reviewed the suggestions and take responsibility for verifying their accuracy and for the final submitted work.