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-outputs", params$rdafile))

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

# for geocoded LDs
with(homicides, write.csv(cbind(feID,latitude,longitude),
                          here("data-outputs",
                               "geocodes2015.csv")))

# restrict to end of current year: 2023"

homicides <- homicides %>% filter(year < 2024)

Introduction

Washington State has enacted a series of statewide police accountability reforms since 2018. A brief overview of these changes is given in the Legislative History section. This report tracks the trend in people killed by police in WA since 2015, before and after those reforms.

The TOC on the left can be used to navigate through the sections of this report. There are 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.

This is the end of year report for 2023

Before the 2021 legislative reforms, one out of every 6 homicides in Washington state was a person 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.


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).

map <- leaflet(data = homicides, #%>% filter(year==2023), 
               width = "100%") %>% 
  addTiles() %>%
  addMarkers( ~ longitude,
              ~ latitude,
              popup = ~ url_click,
              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 22 6.6
Black/African American 41 12.3
Hispanic/Latinx 48 14.4
Native American/Indigenous 13 3.9
White/European American 173 51.8
Unknown 37 11.1
Total 334 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.


Many case reports are missing data on race

Over 25% of the cases in the original data do not have information that explicitly identifies the race of the person killed. Both the Fatal Encounters project and the Washington Post rely primarily on media reports (and some public records requests) to find these cases, so they are limited by the information provided in those sources, and if race is missing in one dataset it is typically missing in the other. The Fatal Encounters team used a statistical imputation model to try to predict race for the missing cases. A brief description of the methodology is online here. They are able to impute just over half of the missing cases with reasonable confidence, and we include these imputations in the breakdowns we report.

We use a hierarchical assignment of race for the data in this report: Fatal Encounters (FE) with imputation if available, Washington Post (WaPo) otherwise. Discrepancies in race assignment between FE and WaPo are rare, but when they happen we independently research and are either assign race as confirmed, or assign as FE if confirmation is not available. The resulting source distribution is shown below.

tab <- homicides %>%
  mutate(race.source = fct_relevel(race.source, "Race not known", after = Inf)) %>%
  group_by(race.source) %>%
  summarize(Number = n(),
            Percent = round(100*Number/nrow(homicides), 1)
  ) %>%
  bind_rows(data.frame(race.source ="Total", 
                       Number = sum(.$Number), 
                       Percent = sum(.$Percent))) %>%
  rename(`Race Variable Source` = race.source)

tab %>%
  kable(caption = "Breakdown by Source of Information") %>%
  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 Source of Information
Race Variable Source Number Percent
FE 239 71.6
FE imputed 43 12.9
WaPo 15 4.5
Race not known 37 11.1
Total 334 100.1
* Percents may not sum to 100 due to rounding

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: 52%

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 52% 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 both the Fatal Encounters and WaPo 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 (though the Washington Post is trying to correct that going forward).

  • 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 Fatal Encounters and WaPo datasets, “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 52% 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*( 52% / 70% -1) = -0.26 – their risk of being killed by police is 26% lower than expected.

  • For Blacks/African Americans this value is 100*( 12% / 4% -1) = 2.41 – their risk of being killed by police is 241% 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 Fatal Encounters 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 253 75.7
Vehicle 52 15.6
Taser 12 3.6
Asphyxiated/Restrained 8 2.4
Drowned 4 1.2
Burned/Smoke inhalation 2 0.6
Other 2 0.6
Beaten 1 0.3
Total 334 100.0
* 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 has not been independently verified. It should be interpreted as the alleged weapon status of the person who was killed.

The data come from the Post dataset, so this is not available for the additional cases in Fatal Encounters. The additional cases in Fatal Encounters are included in the “Unknown” category in the plot and table below.

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 Post dataset, so is missing for the additional cases in Fatal Encounters. These additional cases are listed as “Unknown” below.

homicides %>%
  mutate(bodycam = ifelse(bodycam, "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 212 63.5
Yes 30 9.0
Unknown 92 27.5
Total 334 100.0
* Percents may not sum to 100 due to rounding

County

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/PD involved

This information comes from Fatal Encounters.

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

Online information availability

This information comes from Fatal Encounters. It 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.

The clickable urls are available in this report in the Interactive Map and Say their names sections. They should be treated as a place to start research, not as the definitive description of the event.

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 news article in Fatal Encounters",
        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 news article in Fatal Encounters
Availability Number Percent
Yes 334 100
Total 334 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=3, 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=3, alpha=.8) +
  geom_vline(aes(xintercept = as.numeric(date.2023.law), 
                 text = "2023 laws"), 
             col="blue", lty=3, 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-2022 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 2023, 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.5, alpha = .7) +
  geom_point(aes(text = paste('Total =', cumulative, '<br>', 
                              mon, '<br>', year)),
                 size=1, alpha = 1) +
  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)) +
  
  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 (January 08 2024), 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)) +
  
  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

Annual totals and change

Cumulative annual totals up to the last complete year, 2023, with percentage change from 2022 to 2023.

Here we report the results for the Washington Post data only. Recall that Washington Post data only include persons shot and killed by police, while Fatal Encounters includes all persons who are killed by police (including deaths due to asphyxiation, tasers, vehicle chases, etc).

Washington Post data

kable(wapo_tab,
      caption = "Persons shot by police in WaPo: WA vs. all other states",
      col.names = c("Year", "All Other States", "WA")) %>%
  kable_styling(bootstrap_options = c("striped","hover"),
                full_width = F) %>%
  row_spec(nrow(fe_tab), bold = T)  #%>%
Persons shot by police in WaPo: WA vs. all other states
Year All Other States WA
2015 979 16
2016 932 26
2017 945 38
2018 969 23
2019 960 37
2020 988 31
2021 1035 13
2022 1057 39
2023 1132 21
Pct change 2022 to 2023 7 -46
  # add_footnote(label = "YTD counts:  Jan - Nov of each year",
  #              notation = "symbol")

State rankings

Here we report the results from the Fatal Encounters project (through 2021, when it stopped reporting) and the Washington Post data (through the most recent complete year) separately. Recall that Washington Post data only include persons shot and killed by police, while Fatal Encounters includes all persons who are killed by police (including deaths due to asphyxiation, tasers, vehicle chases, etc).

  • States are ranked by the percent change in persons killed from 2022 to 2023.

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

  • Since percentage changes in numbers smaller than 10 tend to have high volatility from year to year, we show the plot using both states with average annual counts of 10 or more, and all states.

  • These are interactive plots: You can hover over bars to get the exact values for the percent change and average annual number of persons killed from 2015 - 2022.

# use fe_data (not merged) b/c we need all US cases

fe_dat <- fe_data %>%
  filter(homicide==1 & year >= start_yr & year <= last_complete_yr) %>%
  group_by(year, st) %>%
  summarize(count = n()) %>%
  pivot_wider(id_cols = st,
              names_from = year,
              names_prefix = "cases_",
              values_from = count) %>%
  replace(is.na(.), 0)
  
  if (params$from == "2015") {
    fe_dat <- fe_dat %>%
      rowwise() %>%
      mutate(
        average = mean(c_across(cases_2015:cases_2020)),
        pct.chg_2016 = cases_2016/cases_2015 - 1,
        pct.chg_2017 = cases_2017/cases_2016 - 1,
        pct.chg_2018 = cases_2018/cases_2017 - 1,
        pct.chg_2019 = cases_2019/cases_2018 - 1,
        pct.chg_2020 = cases_2020/cases_2019 - 1,
        pct.chg_2021 = cases_2021/cases_2020 - 1)
  } else {
    fe_dat <- fe_dat %>%
      rowwise() %>%
      mutate(
        average = mean(c_across(cases_2019:cases_2020)),
        pct.chg_2020 = cases_2020/cases_2019 - 1,
        pct.chg_2021 = cases_2021/cases_2020 - 1)
  }

wapo_dat <- wapo_data %>%
  filter(year >= start_yr & year <= last_complete_yr) %>%
  group_by(year, st) %>%
  summarize(count = n()) %>%
  pivot_wider(id_cols = st,
              names_from = year,
              names_prefix = "cases_",
              values_from = count) %>%
  replace(is.na(.), 0) 

  
  if (params$from == "2015") {
    wapo_dat <- wapo_dat %>%
      rowwise() %>%
      mutate(average = mean(c_across(cases_2015:cases_2020)),
             pct.chg_2016 = cases_2016/cases_2015 - 1,
             pct.chg_2017 = cases_2017/cases_2016 - 1,
             pct.chg_2018 = cases_2018/cases_2017 - 1,
             pct.chg_2019 = cases_2019/cases_2018 - 1,
             pct.chg_2020 = cases_2020/cases_2019 - 1,
             pct.chg_2021 = cases_2021/cases_2020 - 1)
  } else {
    wapo_dat <- wapo_dat %>%
      rowwise() %>%
      mutate(average = mean(c_across(cases_2019:cases_2020)),
             pct.chg_2020 = cases_2020/cases_2019 - 1,
             pct.chg_2021 = cases_2021/cases_2020 - 1)
  }

FE Data

Reminder that FE stopped reporting in 2021, so this comparison is for the last 2 years of reporting: 2021 vs. 2020.

States with 10+ cases
# For now, just through 2021

p <- ggplot(data = fe_dat %>% 
              filter(average >= 10 & !is.infinite(pct.chg_2021)),
            aes(x=pct.chg_2021, y=reorder(st, pct.chg_2021), 
                fill = pct.chg_2021,
                col=factor(ifelse(st=="WA", 1, 0)),
                text=paste(st, 
                           "change:", scales::percent(pct.chg_2021, acc=1),
                           "avg:", round(average)))) +
              
  geom_bar(stat="identity") +
    
  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(0.6)), legend.position = "none") +
  labs(title = paste("State ranking: % Change from 2020 to 2021 in persons killed by police"),
       x="Percent change in persons killed",
       y="State")

ggplotly(p, tooltip = "text")
All states
p <- ggplot(data = fe_dat %>% filter(!is.infinite(pct.chg_2021)),
            aes(x=pct.chg_2021, y=reorder(st, pct.chg_2021), 
                fill = pct.chg_2021,
                col=factor(ifelse(st=="WA", 1, 0)),
                text=paste(st, 
                           "change:", scales::percent(pct.chg_2021, acc=1),
                           "avg:", round(average)))) +
              
  geom_bar(stat="identity") +
    
  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(.5)), legend.position = "none") +
  labs(title = paste("State ranking: % Change from 2020 to 2021 in persons killed by police"),
       x="Percent change in persons killed",
       y="State")

ggplotly(p, tooltip = "text")
Table

The table includes all states. Here you can clearly see the impact of small case numbers on the percentage change measure.

fe_dat %>%
  select(st, cases_2020, cases_2021, pct.chg_2021) %>%
  arrange(desc(pct.chg_2021)) %>%
  DT::datatable(rownames = F,
                caption = "Fatal Encounters data: People killed by police in 2020 & 2021",
                colnames = c("State", "2020 cases", "2021 cases", "Percentage change"),
                filter = 'top') %>%
  DT::formatPercentage("pct.chg_2021", 1)

WaPo Data

States with 10+ cases
p <- ggplot(data = wapo_dat %>% 
              filter(average >= 10 & !is.infinite(pct.chg_2021)),
            aes(x=pct.chg_2021, y=reorder(st, pct.chg_2021), 
                fill = pct.chg_2021,
                col=factor(ifelse(st=="WA", 1, 0)),
                text=paste(st, 
                           "change:", scales::percent(pct.chg_2021, acc=1),
                           "avg:", round(average)))) +
              
  geom_bar(stat="identity") +
    
  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(.6)), legend.position = "none") +
  labs(title = paste("State ranking: % Change from",
                     last_complete_yr-1, "to", last_complete_yr, "in persons killed by police"),
       x="Percent change in persons killed",
       y="State")

ggplotly(p, tooltip = "text")
All states
p <- ggplot(data = wapo_dat %>% filter(!is.infinite(pct.chg_2021)),
            aes(x=pct.chg_2021, y=reorder(st, pct.chg_2021), 
                fill = pct.chg_2021,
                col=factor(ifelse(st=="WA", 1, 0)),
                text=paste(st, 
                           "change:", scales::percent(pct.chg_2021, acc=1),
                           "avg:", round(average)))) +
              
  geom_bar(stat="identity") +
    
  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(.5)), legend.position = "none") +
  labs(title = paste("State ranking: % Change from",
                     last_complete_yr-1, "to", last_complete_yr, "in persons killed by police"),
       x="Percent change in persons killed",
       y="State")

ggplotly(p, tooltip = "text")
Table

The table includes all states. Here you can clearly see the impact of small case numbers on the percentage change measure.

wapo_dat %>%
  select(st, cases_2020, cases_2021, pct.chg_2021) %>%
  arrange(desc(pct.chg_2021)) %>%
  DT::datatable(rownames = F,
                caption = "WaPo data: People shot by police in 2020 & 2021",
                colnames = c("State", "2020 cases", "2021 cases", "Percentage change"),
                filter = 'top') %>%
  DT::formatPercentage("pct.chg_2021", 1)

Say their names

Name known

Of the 334 persons killed by police, 327 of the victim’s names are known at this time.

homicides %>%
  filter(name != "Unknown") %>%
  select(name, date, age=age, county, agency, url = 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 7 of the victim’s names are not known at this time.

homicides %>%
  filter(name == "Unknown") %>%
  select(name, date, age=age, county, agency, url = 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')) )

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:

Additional bills and initiatives are in process for the 2024 session. ____

Data sources

Where the data come from

This report is updated about once each week, pulling, cleaning and merging data from two online sources: Fatal Encounters (https://fatalencounters.org/), and the Washington Post (https://www.washingtonpost.com/graphics/investigations/police-shootings-database/).

  • Fatal Encounters includes all deaths during encounters with police;

  • Washington Post only includes fatal shootings by police.

  • Neither dataset includes deaths in custody after booking.

  • Both datasets may be missing incidents.

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

There is typically a 1-2 week lag in the Fatal Encounters and WaPo datasets, 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:

  • December 31 2021 for the Fatal Encounters project (FE has paused updating)
  • December 31 2023 for the Washington Post

While the Fatal Encounters project has paused, we are keeping track of these encounters for WA State by hand. The most recent known incident was on December 10 2023.

This report is restricted to the cases that can be classified as homicides by police and vehicular homicides during police pursuits, it excludes cases identified in the Fatal Encounters dataset as suicides and medical emergencies (see the next section for details).


Dataset features

Both the Fatal Encounters project and the Washington Post 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. The datasets are based instead on cases known/discovered by the two project teams, primarily through online searches. You can find more details about the methodology used by each project on their websites: Fatal Encounters and Washington Post.

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. This is why we use them here.


The other key benefit to the Fatal Encounters 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

Fatal Encounters includes more cases than the Washington Post, because the Post dataset is restricted to fatal shootings.


Among the cases missing from the Washington Post data are Manny Ellis (in WA) and George Floyd (at the national level), because their deaths were caused by asphyxiation, not gunshots.


The Washington Post also excludes 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, so their inclusion in Fatal Encounters is important. You can find more details on pursuit fatalities here in WA State in this report.

The total number of cases in the Fatal Encounters dataset for WA State since 2015 is 371. These are classified as follows:

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

tab %>%
  kable(caption = "All Fatal Encounters dataset cases, broken down by type",
        col.names = c("Type", "Number", "Percent")) %>%
  kable_styling(bootstrap_options = c("striped","hover")) %>%
  row_spec(dim(tab)[1], bold = T)  %>%
  add_footnote(label = "Percents may not sum to 100 due to rounding",
               notation = "symbol")
All Fatal Encounters dataset cases, broken down by type
Type Number Percent
Deadly force 240 64.7
Active pursuit 34 9.2
Suicide 32 8.6
Less lethal force 22 5.9
Involved pursuit 17 4.6
Unintended 8 2.2
Mix of pursuit-related cases 7 1.9
Attempted stop 5 1.3
Vehicle accident 4 1.1
Medical emergency 1 0.3
NA 1 0.3
Total 371 100.1
* Percents may not sum to 100 due to rounding

Suicides and “Medical Emergencies” excluded

The Fatal Encounters data 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 shot by police. 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. The classification as “suicide” is not definitive, it is typically based on the narrative provided by law enforcement. And 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.

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

tab <- merged_data %>%
  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(merged_data), 1)))

tab %>%
  kable(caption = "Cases labeled 'Suicide' in Fatal Encounters dataset 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’ in Fatal Encounters dataset by year
Type Number Percent of all cases
2015 1 4.8
2016 4 11.4
2017 2 3.9
2018 5 12.5
2019 6 11.1
2020 6 10.7
2021 9 31.0
2022 3 5.8
2023 1 3.0
Total 37 10.0
* 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 both involved a lengthy vehicular pursuit, and in the national level dataset, many of these cases involved the victim being restrained and injected with Ketamine by emergency responders.


Homicide case comparison

Restricting the Fatal Encounters dataset to the homicide cases, it still has 28% more cases than found in the Post dataset.

tab <- homicides %>%
  mutate(in.wapo = ifelse(is.na(wapoID), 
                          "In FE only", 
                          "In both FE and WaPo")) %>%
  group_by(in.wapo) %>%
  summarize(Number = n(),
            Percent = round(100*Number/nrow(homicides), 1)
  ) %>%
  bind_rows(data.frame(in.wapo ="Total", 
                       Number = sum(.$Number), 
                       Percent = sum(.$Percent))) 

tab %>%
  kable(caption = "Cases found in each dataset",
        col.names = c("Case is:", "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")
Cases found in each dataset
Case is: Number Percent
In FE only 92 27.5
In both FE and WaPo 242 72.5
Total 334 100.0
* Percents may not sum to 100 due to rounding

Missing cases in WaPo

By cause of death

tab <- homicides %>%
  filter(is.na(wapoID)) %>%
  group_by(cod) %>%
  summarize(Number = n()
  ) %>%
  arrange(desc(Number)) %>%
  bind_rows(data.frame(cod ="Total", 
                       Number = sum(.$Number))) 

pct.shot.missing <- tab$Number[tab$cod=="Gunshot"]/tab$Number[tab$cod=="Total"]

Among the cases missing from the Post dataset, 11 (12%) are identified as fatal shootings in the Fatal Encounters dataset.

tab %>%
  kable(caption = "Cause of death for cases missing from WaPo dataset",
        col.names = c("Cause:", "Number")) %>%
  kable_styling(bootstrap_options = c("striped","hover")) %>%
  row_spec(row=c(which(tab$cod=="Gunshot"),
                 dim(tab)[1]), bold = T)
Cause of death for cases missing from WaPo dataset
Cause: Number
Vehicle 52
Taser 12
Gunshot 11
Asphyxiated/Restrained 8
Drowned 4
Burned/Smoke inhalation 2
Other 2
Beaten 1
Total 92

By name, for fatal shootings

Below is the list of cases identified as gunshot fatalities in Fatal Encounters that are missing from the Washington Post dataset.

homicides %>%
  filter(is.na(wapoID) & cod == "Gunshot") %>%
  select(name, date, city, county, agency, url_click) %>%
  DT::datatable(rownames = F,
                caption = "Gunshot cases missing from WaPo dataset",
                escape = FALSE)

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


: : : :