Overview

Link to the Final Shiny App.

Background Information

The Washington Post has recorded fatal police shootings since 2015. They have published and now host a dataset containing information about those fatally shot (since 2015) and the situations leading up to the shootings. The links above will lead you to their dataset and The Washington Post’s nicely-done dashboard.

Objective

Using The Washington Post’s dataset, I wanted to make a simple Shiny App using R that helps visualize some of the fatal police shootings since 2015.

There are many, many different ways of visualizing the data, but I have chosen to keep it simple by visualizing each fatality as a red circle.

Packages Used

  • Shiny and Shiny Themes
  • Tidyverse
  • Leaflet (map)
  • Lubridate (for dates)

Summary of Version 1

The first version of the app is simple from a user and developer’s point of view. Users can adjust the date range to their liking, as well as filter the shootings by state or region. In the next version, there will be more graphs displayed and the map will share a color palette with these graphs. Once I started working on the color palettes, I decided to upload this simpler version just as a placeholder for the more informative app still being made.

In this version, the global.R file reads/merges the data from The Washington Post with the states/regions from the ‘datasets’ package. The ui.R file contains the date and state/region selectors, as well as calls the server.R file to render the map and graph, after filtering the dataset given the date/state filters.

Code

ui.R

# ui.R for Visualizing Police Shootings
# Simeon Paynter
# last updated: 6-10-2021

ui <- fluidPage(
    theme = shinytheme("yeti"),
    titlePanel(title="Fatal Police Shootings (since 1/1/2015)", windowTitle="Fatal Police Shootings"),
    h6("Data source: ", a(href = "https://www.washingtonpost.com/graphics/investigations/police-shootings-database/", "The Washington Post")),
    
    sidebarLayout(
        # Sidebar's position relative to the main panel
        position = "right",
        #sidebar
        sidebarPanel(
            #--------------#
            radioButtons(inputId = "Date_Picker", 
                         label = strong("Dates Displayed:"), 
                         choices = c("All" = "All",
                                     "Recent Amount of Time" = "Past_time",
                                     "Specific Year" = "Specif_Yr",
                                     "Custom Date Range" = "Custom_Dates"),
                         selected = "All"),
            # depending on how user wants to select date
            conditionalPanel(condition = "input.Date_Picker == 'Past_time'",
                             selectInput(inputId = "Past_x_time",
                                         label = strong("Amount of Time to display:"),
                                         choices = c("Past Month" = "Mo1",
                                                     "Past 3 Months" = "Mo3",
                                                     "Past 6 Months" = "Mo6",
                                                     "Past Year" = "Yr1"),
                                         selected = "Mo1")),
            
            conditionalPanel(condition = "input.Date_Picker == 'Specif_Yr'",
                             selectInput(inputId = "Spec_Yr",
                                         label = strong("Select a Year:"),
                                         choices = c(year:2015), 
                                         selected = year)),
            
            conditionalPanel(condition = "input.Date_Picker == 'Custom_Dates'",
                             dateRangeInput(inputId = "Date_Range",
                                            label = strong("Enter Specific Date Range: "),
                                            format = "M d, yyyy",
                                            start = "2015-01-01",
                                            end = today,
                                            min = "2015-01-01",
                                            max = today
                                            )),
            #---------------#
            radioButtons(inputId = "State_Region_filter",
                         label = strong("Filter by Region or State:"),
                         choices = c("All U.S." = "All",
                                     "State(s)" = "State",
                                     "Region(s)" = "Region"),
                         selected = "All"),
            
            # depending if user wants to filter the data for a specific state or region
            conditionalPanel(condition = "input.State_Region_filter == 'State'",
                             selectInput(inputId = "State_Selector",
                                         label = strong("Select State(s) to filter by: "),
                                         choices = c(state.abb),
                                         selected = "MN",
                                         multiple = TRUE),
                             h6("* Use backspace to remove state(s).")),
            
            conditionalPanel(condition = "input.State_Region_filter == 'Region'",
                             selectInput(inputId = "Region_Selector",
                                         label = strong("Select Region(s) to filter by: "),
                                         choices = c("Pacific",
                                                     "Mountain",
                                                     "West North Central",
                                                     "West South Central",
                                                     "East North Central",
                                                     "East South Central",
                                                     "Middle Atlantic",
                                                     "New England",
                                                     "South Atlantic"),
                                         selected = "Pacific",
                                         multiple = TRUE),
                             h6("* Use backspace to remove region(s)."))
        ),
        # main
        mainPanel(
            leafletOutput(outputId = "map"),
            plotOutput(outputId = "graph")
        )
    ),
    
    h6("App created By Simeon Paynter. Code can be found ", a(href = "https://github.com/stpaynter/Visualizing-Police-Shootings", "here"))
)

server.R

# server.R for Visualizing Police Shootings
# Simeon Paynter
# last updated: 6-10-2021

server <- function(input, output, session) {
    
    # Filter by date range
    filtered_by_date <- reactive({
        # All years
        if(input$Date_Picker == "All"){
            return(merged_df)
            
        # Past week, Past month, Past year, ...
        }else if(input$Date_Picker == "Past_time"){
            
            # from global.R, today is the last day available.
            end = today
            
            # using 'lubridate' package for easy subtraction of time
            if(input$Past_x_time == "Mo1"){
                start = today - months(1)
            }else if(input$Past_x_time == "Mo3"){
                start = today - months(3)
            }else if(input$Past_x_time == "Mo6"){
                start = today - months(6)
            }else if(input$Past_x_time == "Yr1"){
                start = today - years(1)
            }else{
                start = "2015-01-01"
            }
            
            temp_df <- merged_df %>% filter(date >= start, date <= end)
            return(temp_df)
               
        # Select a specific year
        }else if(input$Date_Picker == "Specif_Yr"){
            temp_df <- merged_df %>% filter(format(date, '%Y') == input$Spec_Yr)
            return(temp_df)
        
        # Fully-custom date range
        }else{
            start = input$Date_Range[1]
            end = input$Date_Range[2]
            
            temp_df <- merged_df %>% filter(date >= start, date <= end)
            return(temp_df)
        }
    })
    
    # State/ Region Selection
    filtered_by_state_region <- reactive({
        filtered_df <- filtered_by_date()
        
        if(input$State_Region_filter == "All"){
            return(filtered_df)
            
        }else if(input$State_Region_filter == "State"){
            if (length(input$State_Selector) < 1){
                temp_df <- c()
                return(temp_df)
            }else{
                temp_df <- filtered_df %>% filter(state %in% input$State_Selector)
                return(temp_df)
            }
            
        }else if(input$State_Region_filter == "Region"){
            if(length(input$Region_Selector) < 1){
                temp_df <- c()
                return(temp_df)
            }else{
                temp_df <- filtered_df %>% filter(region %in% input$Region_Selector)
                return(temp_df)
            }
        }
    })
    
    # Create Map
    output$map <- renderLeaflet({
        df <- filtered_by_state_region()
        
        if(!is.null(df)){
            name <- ifelse(is.na(df$name), "Name Unknown", df$name)
            age <- ifelse(is.na(df$age), "Age Unknown", df$age)
            gender <- ifelse(is.na(df$gender), "Unknown", df$gender)
            race <- ifelse(is.na(df$race), "Unknown", df$race)
            
            labels <- paste0(name, 
                             "<br>Age: ", age,
                             "<br>City: ", df$city, ", ", df$state,
                             "<br>Gender: ", gender,
                             "<br>Race: ", race,
                             "<br>Signs of Mental Illness: ", df$signs_of_mental_illness,
                             "<br>Armed: ", df$armed,
                             "<br>Date of Death: ", format(df$date, "%B %d, %Y")
            )

            leaflet(df) %>%
                addProviderTiles("CartoDB.Positron") %>%
                addCircleMarkers(lng = ~longitude,
                                 lat = ~latitude,
                                 radius = 5,
                                 fillOpacity = 0.25,
                                 color = "red",
                                 stroke = FALSE,
                                 label = lapply(labels, HTML))
            
        }else{ #if state/region is blank, a world map is displayed.
            leaflet(df) %>% addProviderTiles("CartoDB.Positron")
        }

    })
    
    # Create graph
    output$graph <- renderPlot({
        df <- filtered_by_state_region()

        if(!is.null(df)){
            min_date <- min(df$date) %>% format("%B %d, %Y")
            max_date <- max(df$date) %>% format("%B %d, %Y")
            
            df1 <- df %>% group_by(state, city) %>% summarise(count = n()) %>% arrange(desc(count)) %>% head(10)
                
            ggplot(df1, aes(x=reorder(city, count), y = count)) +
                    
            geom_bar(stat="identity", width=0.5, fill="red", alpha=0.5) +
                    
            labs(title = paste0("Cities Where Shooting Occurred"),
                 subtitle = paste(min_date, "to", max_date),
                 x = "City",
                 y = "# of Shootings") +
                    
            theme(plot.title = element_text(hjust=0.5),
                  plot.subtitle = element_text(hjust = 0.5)) + 
                    
            coord_flip()
        }
    })
}

global.R

# global.R for Visualizing Police Shootings
# Simeon Paynter
# last updated: 6-10-2021

# Libraries used
library(shiny)
library(shinythemes)
library(tidyverse)
library(lubridate)
library(leaflet)

# Today's Date, Year
today = Sys.Date()
year = format(today, '%Y')

# Read in data from Washington Post's GitHub, specify variable types
WP_data <- read_csv("https://github.com/washingtonpost/data-police-shootings/releases/download/v0.1/fatal-police-shootings-data.csv",
                    col_types = cols(
                      .default = col_character(),
                      id = col_skip(),
                      date = col_date(),
                      age = col_integer(),
                      signs_of_mental_illness = col_logical(),
                      body_camera = col_logical(),
                      longitude = col_double(),
                      latitude = col_double(),
                      is_geocoding_exact = col_skip()
                    ))

# Write-out gender and race variables
WP_data$gender <- sub("F", "Female", WP_data$gender)
WP_data$gender <- sub("M", "Male", WP_data$gender)
WP_data$race <- sub("A", "Asian", WP_data$race)
WP_data$race <- sub("H", "Hispanic", WP_data$race)
WP_data$race <- sub("N", "Native American", WP_data$race)
WP_data$race <- sub("O", "Other", WP_data$race)
WP_data$race <- sub("B", "Black (non-Hispanic)", WP_data$race)
WP_data$race <- sub("W", "White (non-Hispanic)", WP_data$race)

# Combine with state name from the 'datasets' package
states <- data.frame(state_name = state.name, 
                     state_abb = state.abb,
                     region = paste(state.division))
merged_df <- inner_join(WP_data, states, by = c("state" = "state_abb"))