# DATA PREPARATION
# Source 1: Crime Statistics Agency (CSA) Victoria, year ending June 2026,
#           downloaded from DataVic into a local "data/" folder.
# Source 2: Australian Bureau of Statistics (ABS) LGA feature service,
#           requested directly from the ABS ArcGIS REST API (see Chart 4).


f_state <- "data/Data_Tables_Recorded_Offences_Visualisation_Year_Ending_June_2026.xlsx"
f_lga   <- "data/Data_Tables_LGA_Recorded_Offences_Year_Ending_June_2026.xlsx"

# CSA sheets have empty trailing columns and some names with leading spaces
read_csa <- function(path, sheet) {
  read_excel(path, sheet = sheet) |>
    select(where(~ !all(is.na(.x)))) |>
    mutate(across(where(is.character), str_squish))
}

state_offences <- read_csa(f_state, "Table 01")  # offence subgroup x year
state_location <- read_csa(f_state, "Table 02")  # offence subgroup x location x year
lga_offences   <- read_csa(f_lga,   "Table 02")  # LGA x offence subgroup x year

theft_labels <- c(
  "B41 Motor vehicle theft"                = "Car stolen",
  "B42 Steal from a motor vehicle"         = "Theft from a car",
  "B43 Steal from a retail store"          = "Shoplifting",
  "B44 Theft of a bicycle"                 = "Bicycle theft",
  "B45 Receiving or handling stolen goods" = "Handling stolen goods",
  "B46 Fare evasion"                       = "Fare evasion",
  "B49 Other theft"                        = "Other theft"
)

#  Chart 1: theft subgroups over ten years 
theft_trend <- state_offences |>
  filter(str_starts(`Offence Subdivision`, "B40")) |>
  transmute(year     = Year,
            subgroup = recode(`Offence Subgroup`, !!!theft_labels),
            count    = `Offence Count`,
            rate     = `Rate per 100,000 population`) |>
  group_by(subgroup) |>
  mutate(index_2019 = 100 * count / count[year == 2019]) |>  # 2019 = pre-COVID
  ungroup() |>
  arrange(subgroup, year)

#  Chart 2: three ways of counting "retail theft" 
is_retail <- function(x) str_detect(x, "^33 ")   # Location Subdivision "33 Retail"

definitions <- bind_rows(
  state_offences |>
    filter(`Offence Subgroup` == "B43 Steal from a retail store") |>
    transmute(year = Year, count = `Offence Count`,
              measure = "Shoplifting offences, any location"),
  state_location |>
    filter(`Offence Subgroup` == "B43 Steal from a retail store",
           is_retail(`Location Subdivision`)) |>
    group_by(year = Year) |>
    summarise(count = sum(`Offence Count`), .groups = "drop") |>
    mutate(measure = "Shoplifting offences at retail locations"),
  state_location |>
    filter(str_starts(`Offence Subdivision`, "B40"),
           is_retail(`Location Subdivision`)) |>
    group_by(year = Year) |>
    summarise(count = sum(`Offence Count`), .groups = "drop") |>
    mutate(measure = "All theft offences at retail locations")
) |>
  arrange(measure, year)

# Integrity check: reproduce the two figures published in September 2026
def_2026 <- definitions |> filter(year == 2026)
stopifnot(def_2026$count[def_2026$measure == "Shoplifting offences, any location"] == 43124,
          def_2026$count[def_2026$measure == "Shoplifting offences at retail locations"] == 37398)

#  Chart 3: where theft happens (2019 vs 2026) 
theft_places <- state_location |>
  filter(`Offence Subgroup` %in% c("B43 Steal from a retail store", "B49 Other theft"),
         is_retail(`Location Subdivision`) | `Location Group` == "381 Licensed Premises",
         Year %in% c(2019, 2026)) |>
  transmute(year     = Year,
            subgroup = recode(`Offence Subgroup`, !!!theft_labels),
            place    = str_remove(`Location Group`, "^\\d+ "),
            count    = `Offence Count`) |>
  complete(year, subgroup, place, fill = list(count = 0)) |>
  pivot_wider(names_from = year, values_from = count, names_prefix = "y") |>
  filter(y2026 >= 500 | y2019 >= 500) |>  # keep meaningful categories
  mutate(change = 100 * (y2026 / y2019 - 1),
         label  = paste0(place, " (", tolower(subgroup), ")"),
         label  = factor(label, levels = label[order(y2026)]))

#  Charts 4 & 5: shoplifting by LGA 
lga_shoplifting <- lga_offences |>
  filter(`Offence Subgroup` == "B43 Steal from a retail store") |>
  group_by(year = Year, lga = `Local Government Area`) |>
  summarise(count = sum(`Offence Count`),
            rate  = first(`LGA Rate per 100,000 population`),
            .groups = "drop") |>
  complete(year, lga, fill = list(count = 0, rate = 0))

# Greater Melbourne's 31 metropolitan councils (all others are regional)
metro_lgas <- c("Banyule", "Bayside", "Boroondara", "Brimbank", "Cardinia", "Casey",
                "Darebin", "Frankston", "Glen Eira", "Greater Dandenong", "Hobsons Bay",
                "Hume", "Kingston", "Knox", "Manningham", "Maribyrnong", "Maroondah",
                "Melbourne", "Melton", "Merri-bek", "Monash", "Moonee Valley",
                "Mornington Peninsula", "Nillumbik", "Port Phillip", "Stonnington",
                "Whitehorse", "Whittlesea", "Wyndham", "Yarra", "Yarra Ranges")

# Handy figures for the narrative text
shop      <- theft_trend |> filter(subgroup == "Shoplifting")
shop_2026 <- shop$count[shop$year == 2026]
shop_2019 <- shop$count[shop$year == 2019]
car_from  <- theft_trend |> filter(subgroup == "Theft from a car", year == 2026)
servo     <- theft_places |> filter(place == "Service Station", subgroup == "Other theft")
super     <- theft_places |> filter(place == "Supermarket", subgroup == "Shoplifting")
licensed  <- theft_places |> filter(place == "Licensed Premises", subgroup == "Shoplifting")
fmt <- function(x) comma(round(x))
# ABS data: boundaries + socio-economic indicators via REST API 
# Victorian LGA codes start with "2". Geometry is simplified server-side
# (maxAllowableOffset) to keep the published page light.
abs_url <- paste0(
  "https://geo.abs.gov.au/arcgis/rest/services/Hosted/LGA_ABS_SEIFACS_2024_Apr/",
  "FeatureServer/0/query?",
  "where=lga_code_2021%20LIKE%20%272%25%27",
  "&outFields=lga_code_2021,lga_name_2021,erp_p_20,irsd_score,irsd_decile_au,blade_bus_12",
  "&outSR=4326&maxAllowableOffset=0.002&f=geojson"
)

abs_lga <- st_read(abs_url, quiet = TRUE)

# Make LGA names match CSA naming before joining:
# drop "(Vic.)" suffixes, apply the 2022 Moreland -> Merri-bek renaming,
# and compare names ignoring case, spaces and hyphens.
name_key <- function(x) tolower(str_remove_all(x, "[^A-Za-z]"))

abs_lga <- abs_lga |>
  mutate(lga_clean = str_remove(lga_name_2021, "\\s*\\(Vic\\.?\\)"),
         lga_clean = recode(lga_clean, "Moreland" = "Merri-bek"),
         key       = name_key(lga_clean),
         pop       = erp_p_20,
         retail_per_1000 = 1000 * blade_bus_12 / erp_p_20)

lga_shoplifting <- lga_shoplifting |> mutate(key = name_key(lga))

unmatched <- setdiff(unique(lga_shoplifting$key), abs_lga$key)
if (length(unmatched) > 0) warning("CSA LGAs not matched to ABS: ", paste(unmatched, collapse = ", "))

lga_profile <- abs_lga |>
  st_drop_geometry() |>
  select(key, pop, irsd_score, irsd_decile_au, retail_per_1000)

The headline

In September 2026, two numbers for the same crime hit the news. Victoria Police reported 43,124 shoplifting offences in the year to June. Retail industry groups reported a “record” 37,398. Both came from the same Crime Statistics Agency data.

This report tests the headlines against the data. It asks how much theft is really increasing, how the counting choices shape what we hear, and where in Victoria (and in which kinds of shops) things are actually being stolen.

1. Shoplifting is the theft that kept climbing

pal1 <- c("Shoplifting"           = "#D55E00",
          "Theft from a car"      = "#0072B2",
          "Car stolen"            = "#56B4E9",
          "Other theft"           = "#7F7F7F",
          "Handling stolen goods" = "#CC79A7",
          "Bicycle theft"         = "#009E73")

measures <- list(
  count      = list(label = "Offences",                 axis = "Recorded offences",               fmt = ",.0f"),
  rate       = list(label = "Rate per 100,000",         axis = "Offences per 100,000 Victorians", fmt = ",.0f"),
  index_2019 = list(label = "Change since 2019",        axis = "Index (2019 = 100)",              fmt = ".0f")
)

fig1 <- plot_ly()
for (s in names(pal1)) {
  d  <- theft_trend |> filter(subgroup == s)
  hi <- s == "Shoplifting"
  fig1 <- fig1 |>
    add_trace(x = d$year, y = d$count, type = "scatter", mode = "lines+markers",
              name = s,
              line   = list(color = pal1[[s]], width = if (hi) 4 else 2),
              marker = list(color = pal1[[s]], size  = if (hi) 8 else 5),
              opacity = if (hi) 1 else 0.75,
              hovertemplate = paste0("<b>", s, "</b><br>Year ending June %{x}<br>",
                                     "%{y:,.0f} offences<extra></extra>"))
}

# Annotation that follows the shoplifting line for each measure
shop_note <- function(m) {
  list(x = 2026, y = shop[[m]][shop$year == 2026], xref = "x", yref = "y",
       text = "<b>Shoplifting</b><br>highest in a decade", showarrow = TRUE,
       arrowhead = 2, ax = -70, ay = -45, font = list(color = "#D55E00"))
}

covid_note <- list(x = 2021, y = 1, xref = "x", yref = "paper", yanchor = "bottom",
                   text = "COVID-19 restrictions", showarrow = FALSE,
                   font = list(size = 11, color = "#555555"))

buttons1 <- lapply(names(measures), function(m) {
  ys <- lapply(names(pal1), function(s) theft_trend[[m]][theft_trend$subgroup == s])
  ht <- lapply(names(pal1), function(s)
    paste0("<b>", s, "</b><br>Year ending June %{x}<br>%{y:", measures[[m]]$fmt, "}",
           if (m == "count") " offences" else if (m == "rate") " per 100,000" else " (2019 = 100)",
           "<extra></extra>"))
  list(method = "update", label = measures[[m]]$label,
       args = list(list(y = ys, hovertemplate = ht),
                   list(yaxis = list(title = measures[[m]]$axis, rangemode = "tozero"),
                        annotations = list(shop_note(m), covid_note))))
})

fig1 |>
  layout(
    title = list(text = "<b>Car-related theft fell in 2026. Shoplifting didn't.</b>",
                 x = 0.02),
    xaxis = list(title = "Year ending June", dtick = 1),
    yaxis = list(title = "Recorded offences", rangemode = "tozero"),
    hovermode = "closest",
    legend = list(orientation = "h", y = -0.2),
    shapes = list(list(type = "rect", xref = "x", yref = "paper",
                       x0 = 2019.5, x1 = 2022.5, y0 = 0, y1 = 1,
                       fillcolor = "#BBBBBB", opacity = 0.18, line = list(width = 0))),
    annotations = list(shop_note("count"), covid_note),
    updatemenus = list(list(type = "buttons", direction = "right",
                            x = 0, y = 1.13, xanchor = "left", buttons = buttons1)),
    margin = list(t = 90)
  ) |>
  config(displaylogo = FALSE)

Use the buttons to switch between raw counts, population-adjusted rates and change since 2019. Click legend items to hide or show theft types.

Data source: Crime Statistics Agency (2026b), Recorded offences – Year ending Jun 2026, Table 01.

Theft from cars is still far more common (77,348 offences in 2026), but it fell from its 2025 peak, as did car theft itself. Shoplifting kept rising and now sits 76% above its pre-COVID level, more than any other theft type. The “Change since 2019” view makes this easiest to see.

2. Two numbers, one crime

pal2 <- c("Shoplifting offences at retail locations" = "#E69F00",
          "Shoplifting offences, any location"       = "#D55E00",
          "All theft offences at retail locations"   = "#56B4E9")

who_uses <- c("Shoplifting offences at retail locations" = "Figure used by retail industry groups",
              "Shoplifting offences, any location"       = "Figure used by Victoria Police",
              "All theft offences at retail locations"   = "Adds 'other theft' at shops and service stations")

fig2 <- plot_ly()
for (i in seq_along(pal2)) {
  m <- names(pal2)[i]
  d <- definitions |> filter(measure == m)
  fig2 <- fig2 |>
    add_trace(x = d$year, y = d$count, type = "scatter", mode = "lines+markers",
              name = m, line = list(color = pal2[[m]], width = 3),
              marker = list(color = pal2[[m]], size = 6),
              fill = if (i == 1) "none" else "tonexty",
              fillcolor = toRGB(pal2[[m]], alpha = 0.12),
              hovertemplate = paste0("<b>", m, "</b><br>", who_uses[[m]],
                                     "<br>Year ending June %{x}: %{y:,.0f}<extra></extra>"))
}

end_labels <- lapply(names(pal2), function(m) {
  v <- def_2026$count[def_2026$measure == m]
  list(x = 2026, y = v, xref = "x", yref = "y", xanchor = "left", showarrow = FALSE,
       text = paste0("  <b>", comma(v), "</b><br>  ", who_uses[[m]]),
       font = list(color = pal2[[m]], size = 11), align = "left")
})

fig2 |>
  layout(
    title = list(text = "<b>The same data can tell you 37,398, 43,124 or 62,123</b>", x = 0.02),
    xaxis = list(title = "Year ending June", tickvals = 2017:2026, range = c(2016.6, 2028.8)),
    yaxis = list(title = "Recorded offences", rangemode = "tozero"),
    hovermode = "x unified",
    showlegend = FALSE,
    annotations = end_labels,
    margin = list(r = 20)
  ) |>
  config(displaylogo = FALSE)

Hover over any year to compare all three measures.

Data source: Crime Statistics Agency (2026b), Recorded offences – Year ending Jun 2026, Tables 01 and 02.

The two figures differ because they count different things. The industry number only includes shoplifting offences recorded at a shop. The police number includes shoplifting offences wherever they were recorded, so a shoplifter caught in a car park still counts. Neither one picks up the thousands of thefts at shops that police file under “other theft.” Add those in and the 2026 total climbs to 62,123. None of these numbers is wrong. Each one answers a different question, so it pays to check which one a headline is using.

There’s one more thing to keep in mind. Victoria Police puts part of the recent rise down to new online systems that make it easier for retailers and petrol stations to report theft (Victoria Police, 2026). So some of the increase may reflect more thefts being reported, not more thefts happening.

3. Where it’s actually being stolen from

col3 <- c("Shoplifting" = "#D55E00", "Other theft" = "#0072B2")

fig3 <- plot_ly(theft_places, height = 560) |>
  add_segments(x = ~y2019, xend = ~y2026, y = ~label, yend = ~label,
               line = list(color = "#BBBBBB", width = 3),
               hoverinfo = "none", showlegend = FALSE) |>
  add_markers(x = ~y2019, y = ~label, name = "2019 (pre-COVID)",
              marker = list(color = "white", size = 10,
                            line = list(color = "#555555", width = 2)),
              hovertemplate = "<b>%{y}</b><br>2019: %{x:,.0f}<extra></extra>")

for (s in names(col3)) {
  d <- theft_places |> filter(subgroup == s)
  fig3 <- fig3 |>
    add_markers(data = d, x = ~y2026, y = ~label, name = paste0("2026 (", tolower(s), ")"),
                marker = list(color = col3[[s]], size = 12),
                customdata = ~round(change),
                hovertemplate = paste0("<b>%{y}</b><br>2026: %{x:,.0f}",
                                       "<br>Change since 2019: %{customdata:+}%<extra></extra>"))
}

fig3 |>
  layout(
    title = list(text = "<b>Supermarkets doubled, licensed premises more than tripled. Service stations top the list.</b>",
                 x = 0.02, font = list(size = 15)),
    xaxis = list(title = "Recorded offences (year ending June)", rangemode = "tozero",
                 tickformat = ","),
    yaxis = list(title = ""),
    legend = list(orientation = "h", y = -0.2),
    margin = list(l = 10, t = 70)
  ) |>
  config(displaylogo = FALSE)

Each line joins 2019 (hollow circle) to 2026 (filled circle). Hover to see the percentage change, and click legend items to isolate shoplifting or other theft.

Data source: Crime Statistics Agency (2026b), Recorded offences – Year ending Jun 2026, Table 02.

The largest single category isn’t shoplifting at all. It’s “other theft” at service stations (12,712 offences), which most likely reflects fuel drive-offs. The data doesn’t name the item stolen, but petrol is the obvious candidate at a service station. Among shoplifting locations, supermarkets rose 106% and licensed premises (which can include bottle shops) rose 239% since 2019. That growth points to everyday goods and alcohol rather than luxury items: jewellers barely register.

4. It isn’t just an inner-city problem

map_data <- abs_lga |>
  left_join(lga_shoplifting |> filter(year == 2026) |> select(key, lga, count_26 = count, rate_26 = rate),
            by = "key") |>
  left_join(lga_shoplifting |> filter(year == 2019) |> select(key, count_19 = count, rate_19 = rate),
            by = "key") |>
  filter(!is.na(lga)) |>
  mutate(change = ifelse(count_19 > 0, 100 * (count_26 / count_19 - 1), NA),
         type   = ifelse(lga %in% metro_lgas, "Metropolitan", "Regional"))

# Same fixed bins for both years so the two layers are directly comparable;
# the open top bin stops inner-Melbourne from washing out the rest of the state
rate_bins <- c(0, 200, 400, 600, 800, 1000, 1400, Inf)
pal_rate  <- colorBin("YlOrRd", domain = NULL, bins = rate_bins, na.color = "#DDDDDD")
pal_chg   <- colorBin(c("#2166AC", "#92C5DE", "#FDDBC7", "#F4A582", "#D6604D", "#67001F"),
                      domain = NULL, bins = c(-Inf, -25, 0, 25, 50, 100, Inf), na.color = "#DDDDDD")

popup <- with(map_data, paste0(
  "<b>", lga, "</b> (", type, ")<br>",
  "Shoplifting offences 2026: ", comma(count_26), "<br>",
  "Rate per 100,000: ", comma(round(rate_26)), " (2019: ", comma(round(rate_19)), ")<br>",
  "Change in offences since 2019: ", ifelse(is.na(change), "n/a", paste0(sprintf("%+.0f", change), "%")), "<br>",
  "Disadvantage decile (1 = most disadvantaged): ", irsd_decile_au, "<br>",
  "Retail businesses per 1,000 residents: ", round(retail_per_1000, 1)))

hover <- lapply(paste0("<b>", map_data$lga, "</b><br>",
                       comma(round(map_data$rate_26)), " per 100,000 (2026)"), htmltools::HTML)

leaflet(map_data, height = 560) |>
  addProviderTiles(providers$CartoDB.Positron) |>
  addPolygons(group = "Rate 2026", fillColor = ~pal_rate(rate_26), fillOpacity = 0.8,
              color = "white", weight = 1, popup = popup, label = hover,
              highlightOptions = highlightOptions(weight = 3, color = "#333333", bringToFront = TRUE)) |>
  addPolygons(group = "Rate 2019", fillColor = ~pal_rate(rate_19), fillOpacity = 0.8,
              color = "white", weight = 1, popup = popup, label = hover,
              highlightOptions = highlightOptions(weight = 3, color = "#333333", bringToFront = TRUE)) |>
  addPolygons(group = "Change since 2019", fillColor = ~pal_chg(change), fillOpacity = 0.8,
              color = "white", weight = 1, popup = popup, label = hover,
              highlightOptions = highlightOptions(weight = 3, color = "#333333", bringToFront = TRUE)) |>
  addLegend(colors = pal_rate(c(100, 300, 500, 700, 900, 1200, 2000)),
            labels = c("Under 200", "200 to 400", "400 to 600", "600 to 800",
                       "800 to 1,000", "1,000 to 1,400", "Over 1,400"),
            position = "bottomright", opacity = 0.8,
            title = "Rate layers:<br>shoplifting per<br>100,000 people") |>
  addLegend(colors = pal_chg(c(-50, -10, 10, 40, 75, 150)),
            labels = c("Fell more than 25%", "Fell up to 25%", "Rose up to 25%",
                       "Rose 25% to 50%", "Rose 50% to 100%", "More than doubled"),
            position = "bottomleft", opacity = 0.8,
            title = "Change layer:<br>offences since 2019") |>
  addLayersControl(baseGroups = c("Rate 2026", "Rate 2019", "Change since 2019"),
                   options = layersControlOptions(collapsed = FALSE)) |>
  setView(lng = 144.9, lat = -37.0, zoom = 7)

Switch layers at the top right, hover for a quick read, and click a council for its full profile. Zoom in to see inner Melbourne.

Data sources: Crime Statistics Agency (2026a), Recorded offences by LGA – Year ending Jun 2026, Table 02; Australian Bureau of Statistics (2024), LGA boundaries and indicators.

top_regional <- map_data |> st_drop_geometry() |>
  filter(type == "Regional") |> arrange(desc(rate_26)) |> slice_head(n = 3)
metro_rates  <- map_data |> st_drop_geometry() |> filter(type == "Metropolitan") |> pull(rate_26)
n_metro_below <- sum(metro_rates < min(top_regional$rate_26))
list_and <- function(x) paste0(paste(head(x, -1), collapse = ", "), " and ", tail(x, 1))

Inner Melbourne councils have the highest rates, as expected for areas with many shops and visitors but relatively few residents. The surprise is regional Victoria: Greater Shepparton, Latrobe and Campaspe all have shoplifting rates above 987 per 100,000. That’s higher than 25 of Melbourne’s 31 metropolitan councils.

5. Does shoplifting follow disadvantage, or the shops?

scatter_data <- lga_shoplifting |>
  inner_join(lga_profile, by = "key") |>
  mutate(type = ifelse(lga %in% metro_lgas, "Metropolitan", "Regional"),
         hover = paste0("<b>", lga, "</b> (", type, ")<br>",
                        "Shoplifting rate: ", comma(round(rate)), " per 100,000<br>",
                        "Disadvantage score (IRSD): ", round(irsd_score), "<br>",
                        "Retail businesses per 1,000: ", round(retail_per_1000, 1)))

plot_ly(scatter_data, x = ~irsd_score, y = ~rate, size = ~pop, sizes = c(10, 60),
        color = ~type, colors = c("Metropolitan" = "#0072B2", "Regional" = "#E69F00"),
        frame = ~year, ids = ~lga, text = ~hover, hoverinfo = "text",
        type = "scatter", mode = "markers",
        marker = list(opacity = 0.75, line = list(color = "white", width = 1))) |>
  layout(
    title = list(text = "<b>Shoplifting rate vs socio-economic disadvantage, by council</b>",
                 x = 0.02),
    xaxis = list(title = "&#8592; more disadvantaged      IRSD score      less disadvantaged &#8594;"),
    yaxis = list(title = "Shoplifting offences per 100,000 people",
                 range = c(0, max(scatter_data$rate) * 1.05)),
    legend = list(title = list(text = "<b>Council type</b>"))
  ) |>
  animation_opts(frame = 900, transition = 600, redraw = FALSE) |>
  animation_slider(currentvalue = list(prefix = "Year ending June ", font = list(size = 14)),
                   pad = list(t = 70)) |>
  animation_button(pad = list(t = 80)) |>
  config(displaylogo = FALSE)

Press play to watch councils move from 2017 to 2026. Bubble size shows population. Hover for details and click the legend to compare metropolitan and regional councils.

Data sources: Crime Statistics Agency (2026a), Recorded offences by LGA – Year ending Jun 2026, Table 02; Australian Bureau of Statistics (2024), IRSD scores, population and retail business counts.

s26 <- scatter_data |> filter(year == 2026)
r_irsd   <- cor(s26$rate, s26$irsd_score,      method = "spearman")
r_retail <- cor(s26$rate, s26$retail_per_1000, method = "spearman")
strength <- function(r) cut(abs(r), c(-Inf, 0.2, 0.4, 0.6, Inf),
                            labels = c("very weak", "weak", "moderate", "strong"))
direction <- function(r) ifelse(r < 0, "negative", "positive")

# The interpretation adapts to whichever relationship is stronger in the data
interpretation <- if (r_retail > 0.2 && abs(r_retail) >= abs(r_irsd)) {
  "Shop density is the stronger predictor. Rates are calculated per resident, but shoplifting happens where the shops are, so councils that act as regional shopping hubs score high whatever their residents' circumstances."
} else if (r_irsd < -0.2) {
  "More disadvantaged councils tend to record higher shoplifting rates, but the scatter is wide. Councils that act as shopping hubs for a wider region stand out whatever their disadvantage score."
} else {
  "Neither measure explains the pattern on its own. Shoplifting rates vary widely between councils with similar levels of disadvantage and similar numbers of shops."
}

In 2026, the rank correlation between a council’s shoplifting rate and its disadvantage score was -0.03 (very weak, negative). The correlation with retail businesses per resident was 0.19 (very weak, positive). Neither measure explains the pattern on its own. Shoplifting rates vary widely between councils with similar levels of disadvantage and similar numbers of shops.

What the data says

  • Shoplifting really has risen. It is up 76% on 2019, the largest rise of any theft type, and it kept growing in 2026 while car-related theft fell. Better reporting may explain part of the increase.
  • The headline number is a choice. The same dataset gives 37,398, 43,124 or 62,123 depending on what is counted and where.
  • What is being stolen is ordinary. The growth is in groceries, alcohol and fuel, not luxury goods.
  • It isn’t only a city story. Several regional cities have higher shoplifting rates than most metropolitan councils.

References

Australian Bureau of Statistics. (2024). LGA_ABS_SEIFACS_2024_Apr: Selected social and economic data by Local Government Area [Data set, ArcGIS feature service]. https://geo.abs.gov.au/arcgis/rest/services/Hosted/LGA_ABS_SEIFACS_2024_Apr/FeatureServer/0

Crime Statistics Agency. (2026a). Recorded offences by LGA – Year ending Jun 2026 [Data set]. DataVic. https://discover.data.vic.gov.au/dataset/data-tables-recorded-offences/resource/06bb24c8-8f62-4144-a746-99ef5492e4f9

Crime Statistics Agency. (2026b). Recorded offences – Year ending Jun 2026 [Data set]. DataVic. https://discover.data.vic.gov.au/dataset/data-tables-recorded-offences/resource/35098a5b-d906-446f-a7fb-eaf07270ce45

SmartCompany. (2026). 102 thefts a day: Victorian retailers battle record-high shoplifting. https://www.smartcompany.com.au/?p=346167

Victoria Police. (2026, September 24). Release of Crime Statistics Agency data – year ending June 2026 [Media release]. https://www.police.vic.gov.au/release-crime-statistics-agency-data-year-ending-june-2026

Anthropic. (2026). Claude (Opus 5.5) [Large language model]. https://claude.ai

Cheng, J., Schloerke, B., Karambelkar, B., & Xie, Y. (2024). leaflet: Create interactive web maps with the JavaScript ‘Leaflet’ library [R package]. https://rstudio.github.io/leaflet/

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. 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., François, R., Henry, L., Müller, K., & Vaughan, D. (2023). dplyr: A grammar of data manipulation [R package]. https://dplyr.tidyverse.org

Acknowledgements

I acknowledge the use of Claude/ grammarly to check grammar, correct errors, and improve the formatting of this assignment. I entered sections of my own completed draft with prompts such as “Check this paragraph for grammar and spelling errors” and “Fix the formatting of this section so headings and spacing are consistent.” The output was then reviewed by me, and I accepted or rejected each suggested correction individually. No ideas, analysis, data interpretation, or written content were generated by Ai, all content in this submission is my own.

Data notes

Figures are for financial years ending 30 June. “Shoplifting” is CSA offence subgroup B43 Steal from a retail store. “Retail locations” is CSA location subdivision 33 Retail. Licensed premises (location group 381) are classified by CSA as recreational, but they are shown in Chart 3 because they include bottle shops. LGA rates are CSA’s published rates per 100,000 residents. The ABS indicators use 2021 LGA boundaries. Merri-bek appears as “Moreland” in the ABS data and was renamed before joining.