Weekly incident counts bounce around even when nothing is happening. Before you write "surge," you need to know how much bounce is normal. The tool for that is the standard deviation: the typical distance between a week and the average week. A week within about two standard deviations of the mean is ordinary weather. Beyond that, it might be climate — or a story.
The shaded band is the mean ± 2 standard deviations, computed from complete Monday–Sunday weeks. Red points poke above it. Click a red point to interrogate it like a reporter would.
Check the "semester weeks only" box. Winter break weeks are near-empty, so including them drags the mean down and stretches the standard deviation — making ordinary semester weeks look like spikes. Computed from semester weeks alone, the band is tighter and honest. The lesson generalizes: an average is only meaningful if you can say what population it describes. "The average week" at a university is two different things in October and in January.
For your story: a flagged week is a lead, not a conclusion. Before writing, check: Was it the first week of a semester? A home football weekend? One location generating many reports (see the clusters on Trends)? A change in how UMPD logs incidents? "Statistically unusual" means look here, not print this.
library(tidyverse)
library(lubridate)
activity <- read_csv("https://raw.githubusercontent.com/dwillis/umpd-logs/master/data/all-police-activity.csv")
weekly <- activity |>
mutate(week = floor_date(as_date(`Date Occurred`), "week", week_start = 1)) |>
filter(week >= ymd("2015-01-05")) |>
count(week, name = "incidents") |>
complete(week = seq(min(week), max(week), by = "week"),
fill = list(incidents = 0)) |>
filter(week < floor_date(today(), "week", week_start = 1)) # drop the partial week
band <- weekly |>
summarize(m = mean(incidents), s = sd(incidents)) # sd() uses n-1: the sample SD
weekly |>
mutate(z = (incidents - band$m) / band$s) |>
filter(z > 2) |>
arrange(desc(z))