A statistics classroom built on real UMPD data, updated daily. See the code

Moving averages: seeing the trend through the noise

Raw daily counts jump around: a loud Saturday, a dead Tuesday. A moving average replaces each day with the mean of the last k days, smoothing the jitter so real change becomes visible. It's the same technique every COVID dashboard used ("7-day average of new cases") — because reporting artifacts and day-of-week rhythms would otherwise bury the story.

Daily incidents, smoothed

Gray dots are actual daily counts; the red line is the trailing moving average. Drag the slider: a small window follows every bump, a large one flattens even real shifts. Choosing the window is a judgment call — which is exactly why you should always say what it is.

Find the pandemic

Switch the view to all time and look at spring 2020. Campus emptied out in March 2020 and incident counts collapsed — the whole year logged about 900 incidents against roughly 2,000 in a normal year. On the raw daily dots that's hard to see; on a 30-day average it's unmistakable. That's the point of smoothing: one quiet day means nothing, ninety of them are history happening.

For your story: never write "incidents doubled yesterday" from one day's count. Compare smoothed values, and name the window: "the 30-day average rose from A to B."

Reproduce it in R

library(tidyverse)
library(lubridate)
library(zoo)        # install.packages("zoo") -- for rollmeanr()

activity <- read_csv("https://raw.githubusercontent.com/dwillis/umpd-logs/master/data/all-police-activity.csv")

daily <- activity |>
  mutate(day = as_date(`Date Occurred`)) |>
  filter(day >= ymd("2015-01-01")) |>
  count(day, name = "incidents") |>
  complete(day = seq(min(day), max(day), by = "day"),
           fill = list(incidents = 0))

daily |>
  mutate(avg7  = rollmeanr(incidents, 7,  fill = NA),
         avg30 = rollmeanr(incidents, 30, fill = NA)) |>
  ggplot(aes(day)) +
  geom_point(aes(y = incidents), color = "gray80", size = 0.4) +
  geom_line(aes(y = avg30), color = "firebrick") +
  labs(y = "incidents per day (30-day average)")