library(tidyverse)
library(plotly)
library(kableExtra)
library(here)
library(leaflet)
library(lubridate)
library(icons)
library(htmlwidgets)
# get cleaned, merged data for WA
load(file = here("Data", "Clean", params$rdafile))

# Data prep (common)
source(here("Analyses", "Common", "waAnalysesPrep.R"))

# for geocoded LDs
with(homicides, write.csv(cbind(feID,latitude,longitude),
                          here("Data", "Clean",
                               "geocodes2015.csv")))

Introduction

This report tracks the trends in people killed by police in WA since 2015, a period that spans before and after a series of legislative reforms intended to reduce police violence and increase accountability for misconduct. A brief overview of these changes is given in the Legislative History section of this report.

The TOC on the left can be used to navigate through the sections of the report. There are interactive maps with links to media reports, breakdowns by demographics, geography, cause of death, agency and time to provide some insight into the patterns and trends, and a list of all persons killed to acknowledge the lives lost and provide a starting point for those seeking more information on specific cases. Information on the data sources and limitations can be found in the Data sources section. The data and scripts used for this report are available on GitHub, access details can be found in the Public Access section.

MOST RECENT DATA UPDATE (2025-10-15):

Before the 2021 legislative reforms, one out of every 6 persons killed in Washington state was killed during an encounter with law enforcement. (On average, about 250 homicide deaths were recorded each year in WA State from 2015-2020, with about 40 killed during encounters with police. You can view and download the state’s official death data here).

In 2021, the year many reforms passed, the number of people killed by police in Washington dropped by 60%, the largest decline seen in any state. More information on the temporal trends can be found in the section on “Trends over time”.


To find out more about what we do or get involved, please email us at Next Steps Washington


Interactive Map

You can click the numbered circles to reach the individual map pointers for each person killed by police.

  • Hovering over the pointer brings up the name of the person killed and agency of the officer who killed them;

  • Clicking the pointer will bring up a url to a news article on the case (if available).

popup_text <- homicides %>%
  mutate(url_click_new = paste0("\"", url_click, "\"")) %>%
  select(url_click_new)

map <- leaflet(data = homicides, 
               width = "100%") %>% 
  addTiles() %>%
  addMarkers( ~ longitude,
              ~ latitude,
              popup = ~ popup_text$url_click_new,
              label = ~ as.character(paste(name, "by", agency)),
              clusterOptions = markerClusterOptions()
              )
map

Breakdowns

Race

Table

See the “Discussion” tab for more information on how race is ascertained and why the number of missing cases is so high.

tab <- homicides %>%
  mutate(race = case_match(race,
    "API" ~ "Asian/Pacific Islander",
    "BAA" ~ "Black/African American",
    "HL" ~ "Hispanic/Latinx",
    "NAA" ~ "Native American/Indigenous",
    "WEA" ~ "White/European American",
    .default = race),
    race = fct_relevel(race,  "Unknown", after = Inf)
  ) %>%
group_by(race) %>%
  summarize(Number = n(),
            Percent = round(100*Number/nrow(homicides), 1)
  ) %>%
  bind_rows(data.frame(race="Total", 
                       Number = sum(.$Number), 
                       Percent = sum(.$Percent))) %>%
  rename(Race = race) 

tab %>%
  kable(caption = "Breakdown by Race") %>%
  kable_styling(bootstrap_options = c("striped","hover")) %>%
  row_spec(row=dim(tab)[1], bold = T) %>%
  add_footnote(label = "Percents may not sum to 100 due to rounding",
               notation = "symbol")
Breakdown by Race
Race Number Percent
Asian/Pacific Islander 29 6.9
Black/African American 47 11.2
Hispanic/Latinx 61 14.5
Native American/Indigenous 15 3.6
White/European American 189 44.9
Unknown 80 19.0
Total 421 100.1
* Percents may not sum to 100 due to rounding

Plots

homicides %>%
  mutate(race = case_match(race,
    "API" ~ "Asian/Pacific Islander",
    "BAA" ~ "Black/African American",
    "HL" ~ "Hispanic/Latinx",
    "NAA" ~ "Native American/Indigenous",
    "WEA" ~ "White/European American",
    .default = race),
    race = fct_relevel(race,  "Unknown", after = Inf)
  ) %>%
  count(race) %>%
  mutate(perc = n / nrow(homicides)) %>%
  
  ggplot(aes(x=race, 
             y = perc, 
             label = n)) +
  geom_bar(stat="identity", fill="blue", alpha=.5) +
  geom_text(aes(y = perc), size = 3, nudge_y = .025) +
  
  theme(axis.text.x = element_text(size=5.5)) +
  
  scale_y_continuous(labels = scales::percent_format(acc=1)) +
  
  labs(title = "Fatalities by Race",
       caption = paste0("WA State since ", 
                        params$from, 
                        "; y-axis=pct, bar label=count")) +
         xlab("Reported Race") +
         ylab("Percent of Total")

homicides %>%
  mutate(raceb = case_when(race == "WEA" ~ "White",
                           race == "Unknown" ~ "Unknown",
                           TRUE ~ "BIPOC"),
         raceb = fct_relevel(raceb, "Unknown", 
                             after = Inf)) %>%
  count(raceb) %>%
  mutate(perc = n / nrow(homicides)) %>%
  ggplot(aes(x=raceb, 
             y = perc, 
             label = n)) +
  geom_text(aes(y = perc), size = 3, nudge_y = .025) +
  geom_bar(stat="identity", fill="blue", alpha=.5) +
  
  scale_y_continuous(labels = scales::percent_format(acc=1)) +
  
  labs(title = "Fatalities by Race",
       caption = paste0("WA State since ", 
                        params$from, 
                        "; y-axis=pct, bar label=count")) +
  xlab("Reported Race") +
  ylab("Percent of Total")


Discussion

Racial disparities in the risk of being killed by police are one of the most important factors driving the public demand for police accountability and reform. For that reason it is important to understand how these numbers can, and cannot be used.

TL;DR While there are many uncertainties in the data that make it difficult/impossible to know the exact numbers, there are still some conclusions we can draw with confidence.


How is race identified?

All crowd-sourced datasets rely primarily on media reports to find cases, and these media reports rarely include information on the race of the person killed. Each of the projects that provide data for this report has made additional efforts made to identify race.

  • MPV

The MPV project makes extensive additional efforts to identify the race of the person killed. From their data methodology page:

Our data has been meticulously sourced from official police use of force data collection programs in states like California, Texas and Virginia, combined with nationwide data from The Gun Violence Archive and the Fatal Encounters database, two impartial crowdsourced databases. We’ve also done extensive original research to further improve the quality and completeness of the data; searching social media, obituaries, criminal records databases, police reports and other sources to identify the race of 90 percent of all victims in the database.

  • Fatal Encounters

The FE project used a statistical imputation model to predict race for the missing cases. A brief description of the methodology is online here. They were able to impute just over half of the missing cases with reasonable confidence, and we use these imputations for the FE data in this report.

  • WA local

The local project conducts a search for coroner/ME reports, obituaries, multiple media reports and social media channels for information that can be used to assign the race of the person killed.

How often is race missing?

We use a hierarchical assignment of race for the data in this report: MPV data when available, WA local data for recent cases not included in MPV, and Fatal Encounters (FE) with imputation for cases excluded from both MPV and WA local data.

tab <- homicides %>%
#  mutate(Race = if_else(race == "Unknown", "Missing", "Available")) %>%
  mutate(race = factor(race),
         Race = fct_relevel(race, "Unknown", after = Inf)) %>%
  group_by(Race, source) %>%
  count()  %>%
  
  pivot_wider(names_from = Race, values_from = n) %>%
  ungroup() %>%

  add_row(source = "Total", summarise(., across(where(is.numeric), sum))) %>%

  rowwise() %>%
  mutate(Total = sum(across(API:Unknown)),
         "Unknown Pct" = round(100*Unknown/Total, 1))


tab %>%
  kable(caption = "Breakdown by Source of Information") %>%
  kable_styling(bootstrap_options = c("striped","hover")) %>%
  row_spec(which(tab$source == "Total"), bold=T) %>%
  column_spec(which(names(tab)=="Total"), bold=T) %>%
  column_spec(which(names(tab)=="Unknown Pct"), italic=T) %>%
  add_footnote(label = "API = Asian and Pacific Islander; BAA = Black/African American; HL = Hispanic/Latino; NAA = Native American/Alaskan native; WEA = White/European American",
               notation = "symbol")
Breakdown by Source of Information
source API BAA HL NAA WEA Unknown Total Unknown Pct
FE 5 4 3 2 29 7 50 14.0
MPV 13 26 30 11 98 24 202 11.9
WA 11 17 28 2 62 49 169 29.0
Total 29 47 61 15 189 80 421 19.0
* API = Asian and Pacific Islander; BAA = Black/African American; HL = Hispanic/Latino; NAA = Native American/Alaskan native; WEA = White/European American

Bottom line: The unknown race cases make it impossible to say exactly how many people killed by police are in each racial group.


We are reporting the raw counts in this report, not per capita rates

Breaking the total count down by race, the largest single group of persons killed by police are identified as White/European-American: 45%

So, does this mean that we can say: “There aren’t any racial disparities in persons killed by police?”.

No. This raw count can not be used to assess racial disparities, because the word disparity implies the risk is “disproportionate” – that is, higher (or lower) than proportional. Proportional is a comparative term: it compares the proportion of fatalities by race, to the proportion of the population by race. If the two proportions are the same, then we can say there are no disparities. In this case, if 45% of the population is White, then we can say that their risk of being killed by police is proportional to their share of the population.

Bottom line: The raw counts can not be used to identify racial disparities.


So why don’t we calculate standardized per capita rates?

Because the classification of cases by race, in the MPV and Fatal Encounters datasets, is not consistent with the classification of race in the US Census population data that are needed to calculate per capita rates.

The calculation of per capita rates takes the ratio of the fatality count (in the numerator) to the population count (in the denominator). To break this down by race we need a consistent measure of race to use for the numerator and denominator for all groups. And we don’t have that.

  • About 5% of people in WA state report two or more races in the US Census. This multiple-race classification does not exist in the datasets on persons killed by police.

  • Hispanic/Latinx is an ethnicity that crosses several racial groups, primarily White, Black and Native American. In the US Census data, race is measured separately from ethnicity, so you can see these overlaps. But in the MPV and Fatal Encountersdatasets, “Hispanic” is coded as a racial group, rather than as a separate ethnicity classification.

For more information on how the Census codes race and ethnicity:

U.S. Decennial Census Measurement of Race and Ethnicity Across the Decades: 1790–2020

Bottom line: The mismatch between racial categories coded in the FE and WaPo data, and the categories used in the US Census makes it impossible to calculate per capita rates accurately for all groups.


What we can say about disparities in WA State

The population of Washington State overwhelmingly identifies as White – 79% in 2020 source: WA State OFM. If we exclude Hispanics (which the Census treats as ethnicity, not race), non-Hispanic Whites comprise 70% of the WA population source: Statistical Atlas of the US. By contrast only 45% of the persons killed by police are identified as non-Hispanic White.

So there is a racial disparity in the risk of being killed by police:

The risk for non-Hispanic Whites is disproportionately low, because their share of fatalities is lower than their population share.

The “risk ratio” is a common way to combine this information into a single number that is easy to understand: the ratio of the fatality share to the population share. When the two shares are the same, the risk ratio equals 1. When it is less than one, this means the share of fatalities is lower than the population share; the risks are disproportionately low. When the risk ratio is greater than one it means the share of fatalities is larger than the population share; the risks are disproportionately high.

This can be summarized as a percentage: 100*(risk ratio-1), which tells you the percent lower (or higher) the risk ratio is than expected if the risk was proportional to the share of population.

In WA State:

  • For non-Hispanic whites this value is 100*( 45% / 70% -1) = -0.36 – their risk of being killed by police is 36% lower than expected.

  • For Blacks/African Americans this value is 100*( 11% / 4% -1) = 2.1 – their risk of being killed by police is 210% higher than expected.

Note that the number of “unknown race” cases after imputation is not enough to change the direction of these disparities. Even if we assume that all of the “unknown race” cases are White, their share of incidents would still be below their share in the population.

So, even though we can’t be certain of the exact value of the risk ratio, we can say with some confidence that there are racial disparities in the risk of being killed by police, and that these disparities indicate that non-Hispanic Whites are less likely than other racial groups to be killed by law enforcement officers.

Bottom line: while the largest single group of persons killed by police is identified as White, the risk ratio for this group, which adjusts for their share of the population, shows they are disproportionately less likely to be killed than other groups.


Perceived vs. self-identified race and implicit bias

The race classification in the FE and WaPo data most likely represent the subject’s race as reported by law enforcement. This may be the officer’s perception, or it may be informed by previous interactions and self-reports, we have no way of knowing. In addition, if our goal was to use these data as evidence for (or against) the effects of systemic implicit bias in the police use of deadly force, we would need to control for other variables that may influence these patterns, like the perceived and actual weapon status of the person killed, the circumstances that precipitated the incidents, the presence or absence of other persons at risk in the incidents, and the detailed population composition of the jurisdiction.

Bottom line: We can’t use these data to answer the question of the officer’s intention, or systemic implicit bias.


Cause of death

The table shows all of the causes that appear in the dataset. The plot aggregates these into three categories, as the first two (shot and vehicle-related) account for the large majority of cases. Vehicle-related refers to deaths caused by vehicle crashes or getting run over by a car. Typically these occur in the context of vehicle pursuits/chases, but sometimes they are the result of fatal accidents caused by on-duty officers.

Plot

homicides %>%
  mutate(
    cod_3 = factor(
      case_when(
        cod == "Gunshot" ~ "shot", 
        cod == "Vehicle" ~ "vehicle-related",
        TRUE ~ "other"),
      levels = c("shot", "vehicle-related", "other"))
  ) %>%
  count(cod_3) %>%
  mutate(perc = n / nrow(homicides)) %>%
  ggplot(aes(x=cod_3, 
             y = perc, 
             label = n)) +
  geom_text(aes(y = perc), size = 3, nudge_y = .025) +
  geom_bar(stat="identity", fill="blue", alpha=.5) +
  labs(title = "Fatalities by Cause of Death",
       caption = paste0("WA State since ", 
                        params$from, 
                        "; y-axis=pct, bar label=count")) +
  xlab("Cause of Death") +
  ylab("Percent of Total")

Table

tab <- homicides %>%
  group_by(cod) %>%
  summarize(Number = n(),
            Percent = round(100*Number/nrow(homicides), 1)
  ) %>%
  arrange(desc(Number)) %>%
  bind_rows(data.frame(cod ="Total", 
                       Number = sum(.$Number), 
                       Percent = sum(.$Percent))) 

tab %>%
  kable(caption = "Breakdown by Cause of Death",
        col.names = c("Cause of Death", "Number", "Percent")) %>%
  kable_styling(bootstrap_options = c("striped","hover")) %>%
  row_spec(row=dim(tab)[1], bold = T)  %>%
  add_footnote(label = "Percents may not sum to 100 due to rounding",
               notation = "symbol")
Breakdown by Cause of Death
Cause of Death Number Percent
Gunshot 306 72.7
Vehicle 79 18.8
Taser 12 2.9
Asphyxiated/Restrained 10 2.4
Drowned 4 1.0
Other 4 1.0
Burned/Smoke inhalation 2 0.5
Drowning 2 0.5
Beaten 1 0.2
Medical emergency 1 0.2
Total 421 100.2
* Percents may not sum to 100 due to rounding

Alleged weapon

The source of this information is typically the initial press release or report to the media from the involved law enforcement agency; it is not always possible to get independent verification. So it should be interpreted as the alleged weapon status of the person who was killed.

Plot

homicides %>%
  count(weapon) %>%
  mutate(perc = n / nrow(homicides)) %>%
  ggplot(aes(reorder(weapon, perc),
             y = perc, 
             label = n)) +
  geom_text(aes(y = perc), size = 3, nudge_y = .025) +
  geom_bar(stat="identity", fill="blue", alpha=.5) +
  labs(title = "Fatalities by Alleged Victim Weapon",
       caption = paste0("WA State since ", 
                        params$from, 
                        "; y-axis=pct, bar label=count")) +
  xlab("Alleged Weapon Used by Victim") +
  ylab("Percent of Total")

Bodycam

This information comes from the MPV dataset, so is not available for the data from other sources. These additional cases are listed as “Unknown” below.

homicides %>%
  mutate(bodycam = if_else(bodycam != "None", "Yes", "No"),
         bodycam = tidyr::replace_na(bodycam, "Unknown"),
         bodycam = fct_relevel(bodycam, "Unknown", 
                             after = Inf)) %>%
  group_by(bodycam) %>%
  summarize(Number = n(),
            Percent = round(100*Number/nrow(homicides), 1)
  ) %>%
  bind_rows(data.frame(bodycam ="Total", 
                       Number = sum(.$Number), 
                       Percent = sum(.$Percent))) %>%
  kable(caption = "Breakdown by Bodycam",
        col.names = c("Bodycam", "Number", "Percent")) %>%
  kable_styling(bootstrap_options = c("striped","hover")) %>%
  row_spec(row=4, bold = T)  %>%
  add_footnote(label = "Percents may not sum to 100 due to rounding",
               notation = "symbol")
Breakdown by Bodycam
Bodycam Number Percent
No 234 55.6
Yes 79 18.8
Unknown 108 25.7
Total 421 100.1
* Percents may not sum to 100 due to rounding

County

Washington State has 39 Counties. Since 2015 there have been people killed by police in 29 of them.

Plot

homicides %>%
  group_by(county) %>%
  summarize(n = n(),
            perc = n / nrow(homicides)) %>%
  ggplot(aes(reorder(county, perc),
             y = perc, 
             label = n)) +
  geom_text(aes(y = perc), size = 3, nudge_y = .005) +
  geom_bar(stat="identity", fill="blue", alpha=.5) +
  
  labs(title = "County",
       caption = paste0("WA State since ", 
                        params$from, 
                        "; x-axis=pct, bar label=count")) +
  xlab("County where fatal encounter happened") +
  ylab("Percent of Total") +
  
  scale_y_continuous(labels = scales::percent_format(acc=1)) +
  
  coord_flip()

Table

homicides %>%
  group_by(county) %>%
  summarize(Number = n(),
            Percent = round(100*Number/nrow(homicides), 1)
  ) %>%
  DT::datatable(rownames = F,
                caption = "Breakdown by County")

Agency involved

  • The plot only shows agencies with 3 or more fatalities since 2015. Since there are more than 100 agencies with fatalities, the plot text would become impossible to read if all of them were included. Incidents with more than one agency involved are grouped together and labeled “Multiple Agencies”.

  • The table includes all agencies, all fatalities, and identifies the full list of agencies when there is more than one involved in the incident(s).

Plot

homicides %>%
  mutate(agencyR = ifelse(grepl(",", agency), "Multiple agencies", agency)) %>%
  group_by(agencyR) %>%
  summarize(n = n(),
            perc = n / nrow(homicides)) %>%
  filter(n > 2) %>%
  
  ggplot(aes(reorder(agencyR, perc),
             y = perc, 
             label = n)) +
  geom_text(aes(y = perc), size = 2, nudge_y = .003) +
  geom_bar(stat="identity", fill="blue", alpha=.5) +
  
  labs(title = "Agency responsible (agencies with > 2 fatalities only)",
       caption = paste0("WA State since ", 
                        params$from, 
                        "; x-axis=pct of all incidents, bar label=count")) +
  xlab("") +
  ylab("Percent of Total") +
  
  # this works with the flipped axis id
  theme(axis.text.y = element_text(size=6)) +
  
  scale_y_continuous(labels = scales::percent_format(acc=1)) +
  
  coord_flip()

Table

homicides %>%
  group_by(agency) %>%
  summarize(Number = n(),
            Percent = round(100*Number/nrow(homicides), 1)
  ) %>%
  DT::datatable(rownames = F,
                caption = "Breakdown of Fatalities by Agency of Involved Officer",
                filter = 'top',
                escape = FALSE, 
                extensions = 'Buttons', 
                options = list(
                  dom = 'Bfrtip',
                  buttons = c('copy', 'csv', 'excel', 'pdf', 'print')) )

Legislative Districts

This information is obtained by geocoding the latitude and longitude data on the geocodio platform.

Washington State has 49 legislative districts. Since 2015 there have been people killed by police in 44 of them.

Given the number of districts involved, we only plot those with more than one fatality since 2015 for legibility .

The table includes all districts and all incidents.

Plot

homicides %>%
  group_by(WA_District) %>%
  summarize(n = n(),
            perc = n / nrow(homicides)) %>%
  filter(n > 1) %>%
  
  ggplot(aes(reorder(WA_District, perc),
             y = perc, 
             label = n)) +
  geom_text(aes(y = perc), size = 2, nudge_y = .003) +
  geom_bar(stat="identity", fill="blue", alpha=.5) +
  
  labs(title = "WA Legislative District (LDs with > 1 fatality only)",
       caption = paste0("WA State since ", 
                        params$from, 
                        "; x-axis=pct of all incidents, bar label=count")) +
  xlab("") +
  ylab("Percent of Total") +
  
  # this works with the flipped axis id
  theme(axis.text.y = element_text(size=6)) +
  
  scale_y_continuous(labels = scales::percent_format(acc=1)) +
  
  coord_flip()

Table

homicides %>%
  group_by(WA_District) %>%
  summarize(Number = n(),
            Percent = round(100*Number/nrow(homicides), 1)
  ) %>%
  DT::datatable(rownames = F,
                caption = "Breakdown of Fatalities by WA Legislative District",
                filter = 'top',
                escape = FALSE, 
                extensions = 'Buttons', 
                options = list(
                  dom = 'Bfrtip',
                  buttons = c('copy', 'csv', 'excel', 'pdf', 'print')) )

Online information availability

This takes the form of a single url to a news article that is available online. There are often multiple news articles available online, and they may report the conflicting details of the event, as well as conflicting perspectives on the justifiability of the use of lethal force. It provides definitive evidence of a fatality, but the information contained in the article should be treated as a place to start research, not as the definitive description of the event.

Clickable urls are available in this report in the Interactive Map and Say their names sections. When clicked, they will take you to the article.

tab <- homicides %>%
  mutate(url_info = case_when(url_info == "" ~ "No",
                               is.na(url_info) ~ "No",
                               TRUE ~ "Yes")) %>%
  group_by(url_info) %>%
  summarize(Number = n(),
            Percent = round(100*Number/nrow(homicides), 1)
  ) %>%
  bind_rows(data.frame(url_info ="Total", 
                       Number = sum(.$Number), 
                       Percent = sum(.$Percent))) 

tab %>%
  kable(caption = "URL for media reference",
        col.names = c("Availability", "Number", "Percent")) %>%
  kable_styling(bootstrap_options = c("striped","hover")) %>%
  row_spec(row=dim(tab)[1], bold = T) %>%
  add_footnote(label = "Percents may not sum to 100 due to rounding",
               notation = "symbol")
URL for media reference
Availability Number Percent
Yes 421 100
Total 421 100
* Percents may not sum to 100 due to rounding

Trend over time

There are two sets of plots here. The first focuses on the monthly totals, and how they have changed over time. The second focuses on the cumulative totals as the year progresses, and how this has changed over time.

# index restricted to first/last complete month
thisyr <- as.character(max(index$year))
prevyrs1 <- paste0(min(index$year), "-", max(index$year)-1)
prevyrs2 <- paste0(min(index$year), "-", last_complete_yr-1)

# left_join to index imposes same restriction
df_by_month <- homicides %>%
  group_by(year, month) %>%
  summarize(count = n()) %>%
  left_join(index, ., by = c("year", "mon"="month")) %>%
  mutate(count = tidyr::replace_na(count, 0),
         mo = match(mon, month.abb),
         mon = factor(mon, levels = month.abb),
         this.yr = factor(ifelse(year==max(year),
                                 thisyr,
                                 prevyrs1),
                          levels = c(prevyrs1, thisyr)),
         last.compl.yr = factor(ifelse(year==last_complete_yr,
                                 as.character(last_complete_yr),
                                 prevyrs2),
                          levels = c(prevyrs2, as.character(last_complete_yr))))

yrs <- as.numeric(lubridate::ymd(
  paste0(seq(min(index$year), max(index$year), 1), 
         "-", start_mo, "-01")))

Monthly totals

Trend

  • The points show the monthly totals, the grey line shows the smoothed average trend in monthly fatalities over time, and the grey shaded ribbon shows the 95% confidence interval around the trend line.

  • We only plot months once the data are complete, so the plot will lag 1-2 months behind the current date.

  • This is an interactive plot: You can hover over points to get the exact values for the number of cases for that month and year.

# we will only plot after current month has finished

p <- ggplot(df_by_month, aes(x = index.date, 
                     y = count))  +
  geom_point(size=1.5, shape=21, stroke=0.25,
             aes(fill = factor(year),
                 text = paste0(mon, ": ", count))) +
  geom_smooth(span = params$smooth_span, col="grey")  +
  
  # year boundaries
  geom_vline(xintercept = yrs, col="white", alpha=.5) +
  
  geom_hline(yintercept = 0, col="grey") +
  
  labs(title = paste("Monthly fatalities:",
                     month.abb[start_mo], start_yr, " - ",
                     month.abb[last_complete_mo], max(index$year)),
       x = "Month/Year",
       y = "Number",
       caption = paste("WA since", params$startyr),
       fill = "Year") +
  
  ylim(c(-2, NA)) + # min needs to be < smoother lower se
  
  scale_x_continuous(
    breaks = df_by_month$index.date[seq(1, nrow(df_by_month), 6)],
    label = df_by_month$mon.yr[seq(1, nrow(df_by_month), 6)]
    )  +
  
  scale_y_continuous(breaks = c(0, 3, 6, 9, 12)) +
  
  theme(axis.text.x = element_text(angle = 90, vjust = 0.5)) +
  theme(panel.grid.major = element_blank(),
        panel.grid.minor = element_blank())

ggplotly(p, tooltip = "text") %>% 
  style(hoverinfo = "y", traces = 9) # use plotly_json(p) to verify

With key dates

This plot highlights key dates for legislation and legal actions related to police practices and accountability.

  • The \(\color{blue}{\textsf{blue lines}}\) are legislative/regulatory actions (940 passes, 940 WACs, and effective dates of legislation from 2021 forward). Solid lines are reform policies aimed at reducing police violence; dashed lines are partial rollbacks of these reforms.

  • The \(\color{red}{\textsf{red lines}}\) are legal actions (Ellis investigation taken over by AGO, ex-Officer Nelson charged in Jesse Sarey case, Officers Burbank Collins and Rankine charged in Ellis case). Solid lines are actions taken to hold officers accountable; dashed lines are acquittals and exonerations.

  • This is an interactive plot: You can hover over points to get the number of cases for that month, and hovering at the top or bottom of the vertical lines will identify the action.

p <- ggplot(df_by_month, 
            aes(x = index.date, y = count))  +
  geom_point(size=1.5, shape=21, stroke=0.25,
             aes(text = paste0(mon, ": ", count))) +
  geom_smooth(span = params$smooth_span, col="grey")  +
  
  # year boundaries
  geom_vline(xintercept = yrs, col="white", alpha=.5) +


  # reline y-axis in case yr[1] = start_date 
  geom_vline(aes(xintercept = as.numeric(start_date),
                 text = ""),
             col="grey", alpha=.8) +
  
  # key dates for prosecution
  geom_vline(aes(xintercept = as.numeric(date.ellis.ago),
                 text = "Ellis case taken over by AGO"),
             col="red", alpha=.8) +
  geom_vline(aes(xintercept = as.numeric(date.sarey.charged),
                 text = "Sarey case officer charged"),
             col="red", alpha=.8) +
  geom_vline(aes(xintercept = as.numeric(date.ellis.charged),
                 text = "Ellis case officers charged"),
             col="red", alpha=.8) +
  geom_vline(aes(xintercept = as.numeric(date.ellis.acquit),
                 text = "Ellis case officers acquitted"),
             col="red", lty=2, alpha=.8) +
    geom_vline(aes(xintercept = as.numeric(date.nelson.convicted),
                 text = "ex-Officer Nelson convicted of murder"),
             col="red", alpha=.8) +

  
  # key dates for legislation
  geom_vline(aes(xintercept = as.numeric(date.940.pass), 
                 text = "940 passes"), 
             col="blue", alpha=.8) +
  geom_vline(aes(xintercept = as.numeric(date.940.WAC), 
                 text = "940 WACs take effect"), 
             col="blue", alpha=.8) +
  geom_vline(aes(xintercept = as.numeric(date.2021.law), 
                 text = "2021 laws"), 
             col="blue", alpha=.8) +
  geom_vline(aes(xintercept = as.numeric(date.2022.law), 
                 text = "2022 laws"), 
             col="blue", lty=2, alpha=.8) +
  geom_vline(aes(xintercept = as.numeric(date.2023.law), 
                 text = "2023 laws"), 
             col="blue", lty=2, alpha=.8) +

  # zero axis
  geom_hline(yintercept = 0, col="grey") +
  
  labs(title = paste("Monthly fatalities with key dates:",
                     month.abb[start_mo], start_yr, " - ",
                     month.abb[last_complete_mo], max(index$year)),
       x = "Month/Year",
       y = "Number",
       caption = paste("WA since", params$startyr),
       fill = "Year") +
  
  ylim(c(-2, NA)) + # min needs to be < smoother lower se
  
  scale_x_continuous(
    breaks = df_by_month$index.date[seq(1, nrow(df_by_month), 6)], 
    label = df_by_month$mon.yr[seq(1, nrow(df_by_month), 6)])  +
  
  scale_y_continuous(breaks = c(0, 3, 6, 9)) +
  
  theme(axis.text.x = element_text(angle = 90, vjust = 0.5)) +
  theme(panel.grid.major = element_blank(),
        panel.grid.minor = element_blank())

ggplotly(p, tooltip = "text") %>% 
  style(hoverinfo = "y", traces = 2)

This year vs. previous years

  • The points show the monthly totals for each year. They are color coded to highlight the current year.

  • The blue line shows the moving average for the monthly fatality rate for the 2015-2020 period, and the grey shaded ribbon shows the 95% confidence band around that average.

  • The black line shows the moving average for the monthly fatality rate for the current year. When this line lies outside the grey ribbon for the blue line, this suggests the difference between this year and previous years is larger than normal variation would predict.

  • We only plot months once the data are complete, so the plot will lag 1-2 months behind the current date.

  • This is an interactive plot: You can hover over points to get the month and year for each point.

# we will only plot after current month has finished

# make sure the factor levels match
this.yr.color = c("lightsteelblue", "black")
names(this.yr.color) <- levels(df_by_month$this.yr)
this.yr.alpha = c(0.5, 1)
names(this.yr.alpha) <- levels(df_by_month$this.yr)

p <- ggplot(data = df_by_month,
            aes(x = mo, y = count)) +
  
  geom_jitter(aes(col = this.yr,
                  alpha = this.yr,
                  text = mon.yr),
              width = 0, height = .1) +
  scale_color_manual(name="Year", 
                     values=this.yr.color) +
  scale_alpha_manual(values=this.yr.alpha) +
  guides(alpha = "none") +
  
  geom_smooth(data = df_by_month %>% filter(year != max(year)),
              color = "lightsteelblue") +
  geom_smooth(data = df_by_month %>% filter(year == max(year)), se=F,
              color = "black") +
  
  scale_x_continuous(breaks = seq(1,12,1), 
                     label = month.abb) +
  
  labs(title = paste0("WA since ", params$startyr, 
                     ":  Monthly totals with moving average"),
       caption = paste("WA since", params$startyr)) +
  xlab("Month") +
  ylab("Number of people killed")


ggplotly(p, tooltip = "text") %>% 
  style(hoverinfo = "y", traces = 3)

Last complete year vs. previous years

  • The points show the monthly totals for each year. They are color coded to highlight the current year.

  • The blue line shows the moving average for the monthly fatality rate for the 2015-2023 period, and the grey shaded ribbon shows the 95% confidence band around that average.

  • The black line shows the moving average for the monthly fatality rate for 2024, the last complete year. When this line lies outside the grey ribbon for the blue line, this suggests the difference between this year and previous years is larger than normal variation would predict.

  • This is an interactive plot: You can hover over points to get the month and year for each point.

# make sure the factor levels match
last.compl.yr.color = c("lightsteelblue", "black")
names(last.compl.yr.color) <- levels(df_by_month$last.compl.yr)
last.compl.yr.alpha = c(0.5, 1)
names(last.compl.yr.alpha) <- levels(df_by_month$last.compl.yr)

p <- ggplot(data = df_by_month %>% filter(year <= last_complete_yr),
            aes(x = mo, y = count)) +
  
  geom_jitter(aes(col = last.compl.yr,
                  alpha = last.compl.yr,
                  text = mon.yr),
              width = 0, height = .1) +
  scale_color_manual(name="Year", 
                     values=last.compl.yr.color) +
  scale_alpha_manual(values=last.compl.yr.alpha) +
  guides(alpha = "none") +
  
  geom_smooth(data = df_by_month %>% filter(year != last_complete_yr),
              color = "lightsteelblue") +
  geom_smooth(data = df_by_month %>% filter(year == last_complete_yr), se=F,
              color = "black") +
  
  scale_x_continuous(breaks = seq(1,12,1), 
                     label = month.abb) +
  
  labs(title = paste0("WA since ", params$startyr, 
                     ":  Monthly totals with moving average"),
       caption = paste("WA since", params$startyr)) +
  xlab("Month") +
  ylab("Number of people killed")


ggplotly(p, tooltip = "text")  %>% 
  style(hoverinfo = "y", traces = 3) # use plotly_json(p) to verify

Annual totals

Cumulative by month

  • The lines show the cumulative total fatalities by month as the year progresses, for each year.

  • We only plot months once the data are complete, so the plot will lag 1-2 months behind the current date.

# plotly doesn't handle the multiple legend properly so we don't use it here

# set up color palette for previous years:
n.yrs <- max(index$year) - params$startyr # num yrs excl this yr
paletteFunc <- colorRampPalette(c('blue', 'yellow', 'red'))
my.colors <- paletteFunc(n.yrs)


df <- df_by_month %>%
  group_by(year) %>%
  mutate(cumulative = cumsum(count),
         mon = factor(mon, levels=month.abb))

# Plot prev yrs separately and specify 2 aes (color, ltyp) to get 2 legends

ggplot(data= df %>% filter(year != max(df$year)),
       aes(x = mon, 
           y = cumulative, 
           color = factor(year),
           group = year)) +
  
  geom_line(size = 1, alpha = .5) +
  geom_point(aes(text = paste('Total =', cumulative, '<br>', 
                              mon, '<br>', year)),
                 size=1, alpha = .6) +
  scale_color_manual(values = my.colors) +

  geom_line(data = df %>% filter(year == max(df$year)),
            aes(x = mon, 
                y = cumulative,
                linetype = factor(year),
                text = paste('Total =', cumulative, '<br>', 
                             mon, '<br>', year)),
            color = "black", size = 2, alpha = 0.7) +
  scale_linetype_manual(name = "This Yr", values = c("solid")) +
  
  labs(title = paste0("WA since ", params$startyr,
                     ":  Cumulative fatalities by Month and Year"),
       x = "Month", 
       y = "Number of people killed",
       caption = paste("WA since", params$startyr),
       color = "Previous Yrs") +
  
  guides(color = guide_legend(order = 1),
         linetype = guide_legend(order = 2))

#ggplotly(p, tooltip = "text")

By year

Calendar year

graphdf.calyr <- homicides %>%
  filter(year >= params$startyr) %>%
  group_by(Year = year) %>%
  summarize(Num = n())

p <- ggplot(graphdf.calyr,
       aes(x = Year, y = Num, 
           fill = Num,
           text = Num)) +
  
  geom_bar(stat = "identity") + 

  scale_fill_gradient(low = "seashell", high = "firebrick",
                      guide = "none") +
  
  scale_x_continuous(breaks = unique(graphdf.calyr$Year)) +
  
  annotate("text", 
           x = graphdf.calyr$Year[nrow(graphdf.calyr)], 
           y = graphdf.calyr$Num[nrow(graphdf.calyr)] + 1, 
           size = 3, label = "YTD") +
  
  labs(title = "People killed by police in WA by Year",
       x = "Calendar year",
       y = "Number killed")

ggplotly(p, tooltip = "text")

Legislative year

We start with the first complete legislative year.

graphdf.legyr <- homicides %>%
  filter(date > as.Date(params$startlegyr)) %>%
  group_by(leg.year) %>%
  summarize(Num = n())

p <- ggplot(graphdf.legyr,
       aes(x = leg.year, y = Num, 
           fill = Num,
           text = Num)) +
  
  geom_bar(stat = "identity") + 

  scale_fill_gradient(low = "seashell", high = "firebrick",
                      guide = "none") +
  
  scale_x_discrete(labels = function(x) 
    stringr::str_wrap(graphdf.legyr$leg.year, width = 8)) +
  
  theme(axis.text.x = element_text(size = 5)) +
  
  annotate("text", 
           x = graphdf.legyr$leg.year[nrow(graphdf.legyr)], 
           y = graphdf.legyr$Num[nrow(graphdf.legyr)] + 1, 
           size = 3, label = "YTD") +
  
  labs(title = "People killed by police in WA by Legislative Year",
       x = "Legislative year",
       y = "Number killed")

ggplotly(p, tooltip = "text")

By cause of death

Here we plot the cumulative totals each year, broken down to show the two leading causes of death.

  • The bars represent totals for each year, divided by cause of death: gunshot, vehicle pursuit and all other causes. A case is classified as a vehicle pursuit death if the cause of death was vehicular accident during an active or recently terminated vehicle pursuit.

  • Note that the current year only reflects cases for the year to date (October 20 2025), so is not strictly comparable with the previous years.

  • This is an interactive plot: You can hover over bar segments to get the exact values for that cause of death in that year.

Calendar year

graphdf.calyr <-  homicides %>%
  filter(year >= params$startyr) %>%
  mutate(
    cod_3 = factor(
      case_when(
        cod == "Gunshot" ~ "shot", 
        grepl("Active|Terminated", vpursuit) ~ "vehicle pursuit",
        TRUE ~ "other"),
      levels = c("shot", "vehicle pursuit", "other")),
    year = as.character(year)) %>%
  group_by(year, cod_3) %>%
  summarize(n = n()) %>%
  mutate(percent = round(100*n / sum(n), 1))
  
p <- ggplot(graphdf.calyr,
            aes(x = year, y = n,
             fill = cod_3, 
             text = paste0("N=", n, 
                           ", (", round(percent),"%)"))
         ) +
  
  geom_bar(stat="identity", alpha=.5,
           position = position_stack(reverse=T)) +
  
  annotate("text", 
           x = graphdf.calyr$year[nrow(graphdf.calyr)], 
           y = sum(graphdf.calyr$n[(nrow(graphdf.calyr)-2):nrow(graphdf.calyr)]) + 1, 
           size = 3, label = "YTD") +
  
  scale_fill_manual(values = c("cadetblue","darkorange3", "goldenrod")) +
  
  labs(title = paste0("Fatalities by Cause of Death and Year"),
       caption = paste("WA since", params$startyr),
       x = "Year",
       y = "Number",
       fill = "Cause of\nDeath")

ggplotly(p, tooltip = "text")

Legislative year

We start with the first complete legislative year.

graphdf.legyr <-  homicides %>%
  filter(date > as.Date(params$startlegyr)) %>%
  mutate(
    cod_3 = factor(
      case_when(
        cod == "Gunshot" ~ "shot", 
        grepl("Active|Terminated", vpursuit) ~ "vehicle pursuit",
        TRUE ~ "other"),
      levels = c("shot", "vehicle pursuit", "other")),
    year = as.character(year)) %>%
  group_by(leg.year, cod_3) %>%
  summarize(n = n()) %>%
  mutate(percent = round(100*n / sum(n), 1))
  
p <- ggplot(graphdf.legyr,
            aes(x = leg.year, y = n,
                fill = cod_3, 
                text = paste0("N=", n, 
                              ", (", round(percent),"%)"))
            ) +
  
  geom_bar(stat="identity", alpha=.5,
           position = position_stack(reverse=T)) +
  
  annotate("text", 
           x = graphdf.legyr$leg.year[nrow(graphdf.legyr)], 
           y = sum(graphdf.legyr$n[(nrow(graphdf.legyr)-2):nrow(graphdf.legyr)]) + 1, 
           size = 3, label = "YTD") +
  
  scale_fill_manual(values = c("cadetblue","darkorange3", "goldenrod")) +
  
  scale_x_discrete(labels = function(x) 
    stringr::str_wrap(unique(graphdf.legyr$leg.year), width = 8)) +
  
  theme(axis.text.x = element_text(size = 5)) +
  
  labs(title = paste0("Fatalities by Cause of Death and Legislative Year"),
       caption = paste("WA since", params$startyr),
       x = "Legislative Year",
       y = "Number",
       fill = "Cause of\nDeath")

ggplotly(p, tooltip = "text")

WA vs. other states

Here we report the annual totals up through the last complete year – 2024 – with data from the MPV project only, since it provides the only consistent data series. This limits the cases to those meeting the MPV inclusion criteria (excludes suicides and most pursuit related fatalities).

Compared to all other states

Population adjusted rate

The number of people shot is represented here as a population-adjusted rate per million residents, which makes it possible to compare states with different sized populations. The raw numbers are given on the next tab.

p <- ggplot(state.rates %>% filter(grepl("Washington|Other", state)), 
       aes(x=factor(year), y=rate, 
           group=state, fill=state,
           text =paste("rate: ", round(rate, 1)))) + 
  geom_col() + #coord_flip() +
  facet_wrap(~state) +
  
  theme(axis.text.x = element_text(size = 6),
        axis.text.y = element_text(size = 8)) +
  
  labs(title = "Rate of persons killed by police: MPV data",
       x="Year", y="Rate per million")

ggplotly(p, tooltip = "text")

Number shot and annual percent change

kable(mpv_tab,
      caption = "Persons killed by police: MPV data",
      col.names = c("Year", rep(c("All other states", "WA"), 2)),
      digits = c(0,0,0,1,1),
      align = c("l",rep("r", 4))) %>%
  
  kable_styling(bootstrap_options = c("striped","hover"),
                full_width = F) %>%
  add_header_above(c("", "Number" = 2, "Percent Change" = 2 ))
Persons killed by police: MPV data
Number
Percent Change
Year All other states WA All other states WA
2015 1080 19 NA NA
2016 1038 27 -3.89% 42.1%
2017 1051 40 1.25% 48.1%
2018 1111 27 5.71% -32.5%
2019 1059 40 -4.68% 48.1%
2020 1126 35 6.33% -12.5%
2021 1134 14 0.71% -60.0%
2022 1163 41 2.56% 192.9%
2023 1226 22 5.42% -46.3%
2024 1236 33 0.82% 50.0%

State rankings 2024

Here we report the MPV data by state for the most recent complete year: 2024.

Number killed

  • States are ranked by the the number of persons killed in 2024.

  • The bar for WA State is highlighted with a black border.

  • These are interactive plots: You can hover over bars to get the exact values for the number of persons shot in 2024.

p <- ggplot(data = states.all %>% filter(!grepl("United|Other", state)),
            aes(x=shot2024, y=reorder(state, shot2024), 
                fill = shot2024,
                color = factor(ifelse(state=="Washington", 1, 0)),
                text=paste(state, 
                           "total:", shot2024))) +
              
  geom_col() +
    
  scale_color_manual(values = c("grey", "black")) +
  scale_fill_gradient2(
    low = "blue", 
    mid = "white", 
    high = "red", 
    midpoint = median(states.all$shot2024)) +
  #scale_x_continuous(labels = scales::percent) +
  
  theme(axis.text.y=element_text(size=rel(.7)), legend.position = "none") +
  labs(title = paste("State ranking: Number of persons shot by police in", last_complete_yr),
       x="Number shot",
       y="State")

ggplotly(p, tooltip = "text")

Population-adjusted rate

  • States are ranked by the population adjusted rate of persons killws in 2024: the rate per million state residents.

  • The bar for WA State is highlighted with a black border.

  • These are interactive plots: You can hover over bars to get the exact values for the rate of persons shot in 2024.

p <- ggplot(data = states.all %>% filter(!grepl("United|Other", state)),
            aes(x=rateshot2024, y=reorder(state, rateshot2024), 
                fill = rateshot2024,
                color = factor(ifelse(state=="Washington", 1, 0)),
                text=paste(state, 
                           "total:", round(rateshot2024, 1)))
            ) +
              
  geom_col() +
    
  scale_color_manual(values = c("grey", "black")) +
  scale_fill_gradient2(
    low = "blue", 
    mid = "white", 
    high = "red", 
    midpoint = median(states.all$rateshot2024)) +
  #scale_x_continuous(labels = scales::percent) +
  
  theme(axis.text.y=element_text(size=rel(.7)), legend.position = "none") +
  labs(title = paste("State ranking: Population adjusted rate of persons shot by police in", last_complete_yr),
       x="Rate per million residents",
       y="State")

ggplotly(p, tooltip = "text")

Change from previous year

  • States are ranked by the percent change in the number of persons killed from 2023 to 2024.

  • The bar for WA State is highlighted with a black border.

  • These are interactive plots: You can hover over bars to get the exact values for the number of persons shot in 2024 year, and the percent change from the previous year.

df <- states.all %>% 
  filter(!grepl("United|Other", state)) %>%
  select(state, shot2023:shot2024) %>%
  mutate(pct.chg = shot2024/shot2023 -1)

p <- ggplot(data = df,
            aes(x=pct.chg, y=reorder(state, pct.chg), 
                fill = pct.chg,
                color = factor(ifelse(state=="Washington", 1, 0)),
                text=paste0(state, 
                           paste("<br>", "number shot in ", last_complete_yr, ": ", shot2024),
                           paste("<br>", "change from last year: ", scales::percent(pct.chg, acc = 1))))
            ) +
              
  geom_col() +
    
  scale_color_manual(values = c("grey", "black")) +
  scale_fill_gradient2(
    low = "blue", 
    mid = "white", 
    high = "red", 
    midpoint = 0) +
  scale_x_continuous(labels = scales::percent) +
  
  theme(axis.text.y=element_text(size=rel(.7)), legend.position = "none") +
  labs(title = paste("State ranking: Percent change in number of persons shot by police",
                     last_complete_yr-1, "-", last_complete_yr),
       x="Percent change",
       y="State")

ggplotly(p, tooltip = "text")

Say their names

Name known

Of the 421 persons killed by police, 407 of the victim’s names are known at this time. In the table below “WA-LD” refers to the Washington state legislative district.

NOTE: There are 2 tabs above, one for the names we do know, the other for the names we don’t yet know. Selecting a tab will display the associated table.

homicides %>%
  filter(name != "Unknown") %>%
  select(name, date, age=age, race, county, `WA-LD`=WA_District, 
         agency, link = url_click) %>%
  arrange(desc(date)) %>%
  DT::datatable(rownames = F,
                caption = paste("The Names We Know:  as of", scrape_date),
                filter = 'top',
                escape = FALSE, 
                extensions = 'Buttons', 
                options = list(
                  dom = 'Bfrtip',
                  buttons = c('copy', 'csv', 'excel', 'pdf', 'print')) )

Name Unknown

The remaining 14 of the victim’s names are not known at this time. In the table below “WA-LD” refers to the Washington state legislative district.

homicides %>%
  filter(name == "Unknown") %>%
  select(name, date, age=age, race, county, `WA-LD`=WA_District, 
         agency, link = url_click) %>%
  arrange(desc(date)) %>%
  DT::datatable(rownames = F,
                caption = paste("The Names We Don't Know:  as of", scrape_date),
                filter = 'top',
                escape = FALSE, 
                extensions = 'Buttons', 
                options = list(
                  dom = 'Bfrtip',
                  buttons = c('copy', 'csv', 'excel', 'pdf', 'print')) )

Appendices

Legislative history

Police accountability and reform has been an active topic in the Washington state legislature since 2018. Changes have been achieved via both citizen initiative and traditional bills.

One of the first major legislative reforms was Initiative 940. I-940 established de-escalation training requirements, independent investigations of all homicides by police and changed the criminal accountability standard from malice to reasonable officer. It passed with over 60% of the statewide vote in the 2018 November general election, becoming WA state law on December 6, 2018. Certain provisions took effect later: amendments by HB 1064 took effect February 4, 2019, and the first Washington Administrative Code (WAC) requirements took effect January 6, 2020. The WAC is reviewed and updated annually.

In the 2021 legislative session (Jan - April) several bills were passed on a range of issues relating to police reform.

  • HB 1054 Established limits on the use of police tactics like neck restraints, canines, tear gas and pursuits, prohibited the use of military equipment, and required police to be identifiable.

  • HB 1088 Required agencies to forward Brady impeachment information for their officers to the local county prosecutor, request Brady information from prosecutors during the hiring process, and report the hiring of officers with prior potential impeachment disclosure to the prosecutor.

  • HB 1267 Established a statewide Office of Independent Investigations to investigate all incidents of police use of deadly force.

  • HB 1089 Established state audits of all independent investigations of police use of deadly force.

  • HB 1310 Established requirements for deescalation and restrictions on the use of force and deadly force.

  • SB 5051 Provided oversight and decertification authority for the WA Criminal Justice Training Commission.

  • SB 5259 Established a statewide police use of force data collection program.

Other important bills passed in 2021 include SB 5066 which established a duty for officers to intervene when their colleagues use excessive force, and HB 1140 which requires that juveniles be provided access to attorneys.

The year these reforms passed the number of people killed by police in Washington dropped by 60%, the largest decline seen in any state. More information on the temporal trends can be found in the section on “Trend over time”.

Some changes have been made to the 2021 reforms in subsequent sessions:

  • In 2022 HB 2037 modified the policy on use of force during Terry Stops allowing force to be used to prevent a person from fleeing.

  • In 2023 SB 5352 modified the policy on vehicular pursuits, reducing the standard of evidence from probable cause to reasonable suspicion, adding some offenses to the list of pursuit-eligible offenses, and relaxing the supervisory authorization requirements.

  • In 2024 Initiative 2113 further modified the statewide pursuit policy, eliminating the specific list of offenses that restricted pursuits to violent felonies, and allowing them for any suspected criminal offense.


Data sources

Where the data come from

This report is updated about once each week, pulling, cleaning and merging data from three sources.

The primary data source is MPV, but due to coverage limitations in the MPV project we augment their data with historical data from Fatal Encounters and a locally updated dataset for WA with coverage criteria equivalent to Fatal Encounters. Cases in the WA local dataset are linked to their MPV records when applicable, and all records in the final dataset include identifiers for linking to their records in the legacy datasets from Fatal Encounters and the Washington Post (see the following sections for more details).

The breakdown of the data sources is shown below. All cases covered by MPV are listed in that column, the additional cases contributed from FE (2015-21) and WA local (2022+) are shown in the other two columns.

tab <- wa_2015 %>% 
  mutate(Source = factor(
    case_when(
      !is.na(mpvID) ~ "MPV",
      !is.na(feID) ~ "FE",
      !is.na(waID) ~ "WA",
      TRUE ~ "other"),
    levels = c("MPV", "FE", "WA"))
  ) %>%
  count(Source, Year = as.character(year)) %>% 
  pivot_wider(names_from=Source, values_from=n) %>% 
  mutate(across(where(is.numeric), ~replace_na(.,0))) %>%
  add_row(Year = "Total", summarise(., across(where(is.numeric), sum)))
  
tab %>%
  kableExtra::kbl(caption = "Breakdown of fatalities by data source and year") %>%
  kable_styling(bootstrap_options = c("striped", "hover")) %>%
  add_header_above(header = c(" " = 1, "Data Source *" = 3)) %>%
  row_spec(which(tab$Year == "Total"), bold=T) %>%
  add_footnote(label = "Most records in MPV and WA can be found in multiple sources, see below for more details on the overlap",
               notation = "symbol")
Breakdown of fatalities by data source and year
Data Source *
Year MPV FE WA
2015 19 1 1
2016 27 8 1
2017 40 12 1
2018 27 13 1
2019 40 15 1
2020 35 20 1
2021 14 15 0
2022 41 0 12
2023 22 0 12
2024 33 0 18
2025 15 0 17
Total 313 84 65
* Most records in MPV and WA can be found in multiple sources, see below for more details on the overlap

Mapping Police Violence

Mapping Police Violence (MPV) aims to establish a comprehensive national open-source dataset of homicides by police, on and off duty. It includes fatal shootings and fatalities caused by tasers, physical beatings, restraint holds, chemical spray, police dog attacks, less-lethal force and vehicular homicides. Vehicular homicides are included in MPV only when the fatalities are directly caused by a police action: intentionally running someone over, intentional collisions and vehicle disabling during a pursuit that leads to a fatal accident. These criteria result in the exclusion of most vehicle pursuit-related fatalities.

MPV is the only national project that is still actively updating. It has incorporated historical data from two legacy projects:

MPV merged the incidents from these two datasets to prevent duplication, removed incidents that did not meet their inclusion criteria, added eligible incidents that were missed by both projects, kept all of the information from both datasets on the included incidents, and is now augmenting all incidents (back through 2013) with additional information.

Fatal Encounters

Fatal Encounters (FE) is a legacy project that sought to establish a comprehensive national open-source dataset of all deaths during encounters with police. This project was active through December 31, 2021, includes records going back to 2000, and produced the largest compilation of persons killed during encounters with police that is publicly available: 31,641 fatalities at the time the project ended. It covers deaths by all causes during a police encounter, including suicides, accidents, killings by off-duty police and vehicular pursuit related fatalities. The vehicular pursuit coverage in FE is broader than MPV, as it includes all fatalities from accidents during a pursuit, so it captures bystanders killed by the fleeing subject.

  • FE historical data, for cases consistent with the MPV inclusion criteria, are incorporated by the MPV project into their database.

  • We recover FE cases that were excluded by MPV and include them in the WA local data. They are used in this report, subject to the exclusions noted in a later section.

WA Local data

For WA State only we maintain a dataset that uses the same coverage criteria as Fatal Encounters. In this dataset we

  • update fatalities if they are known to us before they appear in MPV,

  • supplement the MPV data with new fatalities from police vehicular pursuits and other encounters by replicating the google alerts and searches used by Fatal Encounters, and

  • occasionally add additional historical incidents found through searches of local records (e.g., Medical Examiner reports, Inquest reports, etc.).

Washington Post

The Washington Post Fatal Force (WaPo) dataset is a legacy project that sought to create a national database of fatal shootings by on-duty police. It was actively updating from January 1, 2015 through December 31, 2024. There were 10,429 fatalities recorded at the time the project ended.

  • WaPo historical data has been incorporated by the MPV project into their database.

For all of our data sources, it is worth noting that:

  • None include deaths in custody after booking.

  • All sources may be missing incidents.

A comparison of the cases found in each dataset is presented in the next section of this report.

There has typically been a 1-2 week lag in MPV updates, but it can sometimes be longer. The date shown in the “most recent data update” tab above is an indication of the current lag. As of this report, the last data updates were:

  • October 15 2025 for Mapping Police Violence
  • October 15 2025 for the WA local data

This report is restricted to the cases that can be classified as homicides by police and vehicular homicides during police pursuits, it excludes cases where the cause of death during the police encounter is ruled a suicide and other medical emergencies that result in death while in custody (see the next section for details).


Dataset features

All of the data sources we use are open-source, “unofficial” datasets. Their data are not drawn from mandatory government reporting on deaths caused by law enforcement officers because there is no official national data program designed for this purpose that is currently functioning.

The datasets are based instead on cases known/discovered by the project teams, primarily through online searches. You can find more details about the methodology used by each project on their websites: Fatal Encounters, Washington Post, and Mapping Police Violence.

Open-source vs. Official

In theory, deaths caused by police should also be captured by several official data collection programs:

Studies have repeatedly shown, however, that these official programs consistently fail to identify about half of the people killed by police. The most recent study to have replicated this finding was conducted by the Institute for Health Metrics and Evaluation in 2021, and published in the Lancet. You can access the study here, and less technical summaries can be found in the news media. Other studies show similar rates of undercounting in the Arrest Related Death and Supplementary Homicide Reporting systems, one example is here.

The one exception to this is the National Violent Deaths Reporting system, where a study from 2015 found that it captured almost all of the people shot and killed by police in the 27 states that were participating in the program at the time. But there has been no update since the state coverage reached all 50 states in 2018, and there has been no assessment of its coverage of deaths due to causes other than gunshot. Perhaps most importantly, the study simply showed that this dataset had comparable coverage to the open-source datasets, not that it was superior.

Research has also shown that the methodology used by the open-source data sets is effective:

“Findings indicate that the open-source methodology alone identifies the majority of law enforcement homicides, but agency surveys aid in identifying deaths by other causes (e.g., accidents, suicides, and natural causes).” BJS 2019


At this point, it is widely acknowleged that the open-source datasets have the most complete coverage of these cases. That is why we use them here.


The other key benefit to the Fatal Encounters / Mapping Police Violence data in particular is that every case includes a link to documentation that verifies the case and serves as a starting place for additional research. This allows us to identify the law enforcement agency(ies) involved, provides more context and sometimes reveals conflicting narratives as to the events. All of this helps to remind us that each case is a person, not just a statistic.

That said, it is important to keep in mind that these open-source datasets, too, are not perfect. They will miss cases that have not generated a digital signature that can be found using their search methodologies. So it would be accurate to say that our open-source data provide a lower bound estimate of the number of people killed by police; the true value is likely higher, but it can not be lower.

Dataset differences

Since MPV has become the one national project still actively updating, they have incorporated the historical data from FE and WaPo into their database: they merged the incidents that met their inclusion criteria and preserved almost all of the data fields from each project. Each record in the MPV data has three ID variables indicating where the case can be found: all cases have an MPV ID, and some cases also have FE and/or WaPo IDs. The presence or absence of the FE and WaPo IDs reflects differences in inclusion criteria (e.g., the WaPo data is restricted to fatal shootings), MPV’s identification of additional historical cases over time, and the cessation of data collection for FE and WaPo.

MPV excludes most of the deaths that occur during police vehicular pursuits. As can be seen in this report, police pursuits are the second leading cause of death in police-civilian encounters; the exclusion of pursuit-related fatalities results in an incomplete assessment of the death toll from police activities. You can find more details on pursuit fatalities here in WA State in this report.

We recover the pursuit related fatalities by pulling these from the FE dataset for 2015-2021, and tracking the post 2021 incidents in the WA local data (for WA State only). Like MPV, the WA local data includes ID fields that allow its cases to be matched to the other datasets: MPV, FE and WaPo.

The coverage of each dataset can be assessed by these ID tags: each record in our final dataset includes the set of ID tags that show whether it was included in the source datasets, and if so, the unique ID in that dataset. That coverage, by year, is shown below. The “Total Cases” column in the table is the total cases in that year, so the number of cases in each of the dataset ID columns gives a sense of what fraction of that year’s cases were covered by that dataset.

2015 is 462. These are classified as follows:

tab <- homicides %>%
  group_by(Year = as.character(year)) %>%
  summarize(FE.ID = sum(!is.na(feID)),
            MPV.ID = sum(!is.na(mpvID)),
            WaPo.ID = sum(!is.na(wapoID)),
            WaLocal.ID = sum(!is.na(waID)),
            Total.Cases = n()
  ) %>%
  mutate(Percent = round(100*Total.Cases/sum(Total.Cases), 1)) %>%

  add_row(Year = "Total", summarise(., across(where(is.numeric), sum)))

tab %>%
  kable(caption = "Dataset Coverage by Year") %>%
  kable_styling(bootstrap_options = c("striped","hover")) %>%
  add_header_above(header = c(" " = 1, "Data Source" = 4, "Yearly totals"=2)) %>%
  row_spec(dim(tab)[1], bold = T)  %>%
  column_spec(which(names(tab)=="Total.Cases"), bold=T) %>%
  column_spec(which(names(tab)=="Percent"), italic=T) %>%

  add_footnote(label = "Percents may not sum to 100 due to rounding",
               notation = "symbol")
Dataset Coverage by Year
Data Source
Yearly totals
Year FE.ID MPV.ID WaPo.ID WaLocal.ID Total.Cases Percent
2015 19 19 16 1 20 4.8
2016 31 27 26 1 32 7.6
2017 49 40 38 1 50 11.9
2018 35 27 23 1 36 8.6
2019 49 40 36 1 50 11.9
2020 49 35 31 1 50 11.9
2021 20 14 13 0 20 4.8
2022 0 41 38 52 52 12.4
2023 0 22 22 33 33 7.8
2024 0 33 32 48 48 11.4
2025 0 15 0 30 30 7.1
Total 252 313 275 169 421 100.2
* Percents may not sum to 100 due to rounding

Exclusions

Cases officially reported as suicides and “Medical Emergencies” are excluded from the analyses in this report.

The Fatal Encounters data (and our extension of their data for WA State) includes some cases classified as “suicides” that occur during an encounter with police. These are not cases of “suicide by cop”, where the victim appears to have provoked the police in order to be killed. They are cases where the victim shoots or otherwise kills themself. We have excluded these from the analyses in this report, because the report focuses on persons killed by police.

However, we acknowledge that these cases warrant further assessment. In many of these cases, officers did shoot at the suspect, but missed or failed to kill them. In addition the classification as “suicide” is not definitive, it is typically based on the narrative provided by law enforcement and some level of investigation by the local coroner or medical examiner. Finally, even if these are suicides, it is possible that the outcome might have been different if the case had been handled differently, for example, by calling in a crisis intervention team and deescalating effectively.

The excluded case counts and fraction of total cases by year is shown in the table below.

tab <- wa_2015 %>%
  group_by(year, homicide) %>%
  summarize(Number = n()) %>%
  pivot_wider(id_cols = year,
              names_from = homicide,
              names_prefix = "N",
              values_from = Number) %>%
  replace_na(list(N0=0)) %>%
  mutate(Percent = round(100*N0/(N0 + N1), 1),
         year = as.character(year)) %>%
  select(year, Number=N0, Percent) %>%
  bind_rows(data.frame(year ="Total", 
                       Number = sum(.$Number), 
                       Percent = round(100*sum(.$Number)/nrow(wa_2015), 1)))

tab %>%
  kable(caption = "Cases labeled 'Suicide' by year",
        col.names = c("Type", "Number", "Percent of all cases")) %>%
  kable_styling(bootstrap_options = c("striped","hover"),
                full_width = F) %>%
  row_spec(dim(tab)[1], bold = T)  %>%
  add_footnote(label = "These cases were excluded from analyses in this report",
               notation = "symbol")
Cases labeled ‘Suicide’ by year
Type Number Percent of all cases
2015 1 4.8
2016 4 11.1
2017 3 5.7
2018 5 12.2
2019 6 10.7
2020 6 10.7
2021 9 31.0
2022 1 1.9
2023 1 2.9
2024 3 5.9
2025 2 6.2
Total 41 8.9
* These cases were excluded from analyses in this report

There are also a couple of cases where the cause of death is coded as “Medical emergency”. We exclude these cases from this report as well, as they do not meet the definition of “homicide.” But we note that in all of these cases, there was substantial interaction with the police in the period preceding the death, and the person died in police custody. The cases in WA involved lengthy vehicular pursuits or physical restraints, and in the national level dataset, many of these cases involved the victim being restrained and injected with Ketamine by emergency responders.


What is a homicide?

The deadly force incidents in this report are homicides. A homicide is simply defined as the killing of one person by another. In the context of this report it refers to any encounter with law enforcement officers that results in a fatality. Homicides normally result in a criminal investigation or inquest, but the word does not imply a crime has been committed.

  • The word homicide means only that the death was caused in some way by the officer.

  • It does not not mean the officer’s actions that led to the death were justified, or that they were unjustified.

The law then defines different types of homicides, in order to distinguish levels of intention and criminal culpability. In the U.S., these types and definitions are broadly similar across states, but do vary somewhat in the the how many different types are specified, and what names are used for each type. The classifications for WA State can be found in 9A.32 RCW

The definitions below are taken from a useful online summary that uses simple language, based on California State laws.

Homicide
Homicide is the killing of one person by another. This is a broad term that includes both legal and illegal killings. For example, a soldier may kill another soldier in battle, but that is not a crime. The situation in which the killing happened determines whether it is a crime.

  • Murder is the illegal and intentional killing of another person. Under California Penal Code Section 187, for example, murder is defined as one person killing another person with malice aforethought. Malice is defined as the knowledge and intention or desire to do evil. Malice aforethought is found when one person kills another person with the intention to do so.

    In California, for example, a defendant may be charged with first-degree, second-degree, capital or felony murder.

    • First-degree murder is the most serious and includes capital murder – first-degree murder with “special circumstances” that make the crime even more egregious. These cases can be punishable by life in prison without the possibility of parole, or death.

    • Second-degree murder is murder without premeditation, but with intent that is typically rooted in pre-existing circumstances. The penalty for second-degree murder may be up to 15 years to life in prison in California.

    • Felony murder is a subset of first-degree murder and is charged when a person is killed during the commission of a felony, such as a robbery or rape.

  • Manslaughter is the illegal killing of another person without premeditation, and in some cases without the intent to kill. These cases are treated as less severe crimes than murder. Manslaughter can also be categorized as voluntary or involuntary.

    • Voluntary manslaughter occurs when a person kills another without premeditation, typically in the heat of passion. The provocation must be such that a reasonable person under the same circumstances would have acted the same way. Penalties for voluntary manslaughter include up to 11 years in prison in California.

    • Involuntary manslaughter is when a person is killed by actions that involve a wanton disregard for life by another. Involuntary manslaughter is committed without premeditation and without the true intent to kill, but the death of another person still occurs as a result. Penalties for involuntary manslaughter include up to four years in prison in California.

    • Vehicular manslaughter occurs when a person dies in a car accident due to another driver’s gross negligence or even simple negligence, in certain circumstances.


Public Access

This report follows the principles of transparent, reproducible science. All of the scripts, data and software are open source and freely available to the public, online.


The scripts needed to scrape the data and reproduce this report are posted on our public GitHub Repository.


The software used to produce this report comes from the open-source R project. You can view the code in the report itself by clicking the Code buttons in the right margin, or you can download all of the scripts from the repository.

  • If you find a bug in our code, please let us know by posting it as an issue in the repository.

  • If you have a question about the methodology, or the data, or the topic in general, please start a thread on the repository discussions page (note: you’ll need a GitHub account – they’re free, just follow the instructions at the link).

  • If you want to get involved, please email us at Next Steps Washington


: : : :