Project Purpose

This project explores patterns and causes of car crashes in Seattle using publicly available data. The goal was to uncover insights that could inform urban planning and safety policies.

Data Cleaning & Preparation

To start, I downloaded the car crash data from the Seattle Department of Transportation (SDOT) website, which provides comprehensive records of traffic accidents in the city. The dataset includes key details such as accident location, time, severity, and contributing factors, as well as other variables that could potentially influence accident rates.

After obtaining the raw data, I spent some time familiarizing myself with it and decoding the column/factor names. I then performed several data cleaning steps to prepare it for analysis.

The first step involved checking for missing values, as incomplete records can skew results. I used the tidyverse and dplyr packages to filter out any rows with missing or irrelevant data, ensuring that only valid entries were included in the analysis.

Next, I removed any unnecessary or repeated columns that did not contribute to the analysis. This included metadata such as case numbers, system links, or internal identifiers. By eliminating these columns, I was able to streamline the dataset and make it more manageable for further exploration.

crash_data <- crash_data %>%
  select(-c(STATUS, SE_ANNO_CAD_DATA, INCKEY, COLDETKEY, REPORTNO, EXCEPTRSNCODE,
            EXCEPTRSNDESC, INCDATE, SDOT_COLCODE, DIAGRAMLINK, REPORTLINK, SDOTCOLNUM,
            STCOLCODE, SEGLANEKEY, CROSSWALKKEY, SPDCASENO, Source.of.the.collision.report,
            SHAREDMICROMOBILITYCD, SHAREDMICROMOBILITYDESC))

I then renamed columns to make them more readable and meaningful. This step helped make the analysis and visualization process easier by reducing the need to constantly reference the original documentation or data dictionary.

crash_data <- crash_data %>%
  rename(
    address_type = ADDRTYPE,
    intersection_id = INTKEY,
    severity_code = SEVERITYCODE,
    severity = SEVERITYDESC,
    collision_type = COLLISIONTYPE,
    people_involved = PERSONCOUNT,
    pedestrians = PEDCOUNT,
    bicyclists = PEDCYLCOUNT,
    vehic_involved = VEHCOUNT,
    minor_injuries = INJURIES,
    serious_injuries = SERIOUSINJURIES,
    date = INCDTTM,
    junction_type = JUNCTIONTYPE,
    sdot_collision_desc = SDOT_COLDESC,
    inattention = INATTENTIONIND,
    under_influence = UNDERINFL,
    road_condition = ROADCOND,
    light_condition = LIGHTCOND,
    no_ped_rightofway = PEDROWNOTGRNT,
    hit_parked_car = HITPARKEDCAR,
    state_collision_desc = ST_COLDESC,
    source = Source.description,
    added_date = Added.date,
    modified_date = Modified.date,
    longitude_x = x,
    latitude_y = y
  )

To facilitate time-based analysis, I converted the crash date to a standard Date format. I also rearranged the columns so that the date appears at the beginning of the dataset.

crash_data$date <- as.Date(crash_data$date, format = "%m/%d/%Y")
crash_data <- crash_data %>%
  arrange(desc(date)) %>%
  select(date, everything())

Finally, I extracted the year from the date column and filtered the dataset to include only crashes from 2004 through 2024. This 20-year range provides a broad yet recent enough scope for trend analysis.

crash_data <- mutate(crash_data, year = lubridate::year(date))

crash_data_filtered <- crash_data %>%
  filter(year >= 2004 & year <= 2024)

By the end of the cleaning process, I had a well-structured dataset that was ready for exploration. All irrelevant or incorrect data was removed, unnecessary columns were eliminated, and the remaining variables were formatted in a way that made further analysis using dplyr and ggplot2 both easy and reliable.

Line Chart: Count of Car Crashes by Year (2004–2024)

One of the first visualizations I wanted to build was a line chart to examine trends in Seattle car crashes over time. Understanding whether crash incidents are increasing or decreasing annually can help provide useful insights for policy recommendations or safety campaigns.

Code & Visualization

library(ggplot2)
library(dplyr)
library(lubridate)

#add year column
crash_data <- mutate(crash_data, year = lubridate::year(date))

#filter for 2004–2024
crash_data_filtered <- crash_data %>%
  filter(year >= 2004 & year <= 2024)

#creates line graph
ggplot(crash_data_filtered, aes(x = year)) +
  geom_line(stat = "count", size = 1, color = "lightblue") +
  geom_point(stat = "count", color = "lightblue", size = 2) +
  ggtitle("Count of Car Crashes by Year (2004–2024)") +
  xlab("Year") +
  ylab("Count of Car Crashes") +
  scale_x_continuous(breaks = seq(2004, 2024, by = 2), expand = c(0, 0)) +
  theme(
    plot.title = element_text(size = 18, face = "bold"),
    axis.title.x = element_text(size = 14),
    axis.title.y = element_text(size = 14),
    axis.text = element_text(size = 12)
  )

Process

Before creating the plot, I made sure to extract the year from the date column using lubridate::year()and filter the dataset to only include data from 2004 through 2024 for a clean 20-year time frame.

Since each row in the dataset represents a single car crash, I used stat = "count" to let ggplot2 automatically count how many crashes occurred in each year.

For this visual I chose to use a line graph because line graphs are ideal for time series data. It allowed me to see how crash frequency changes over time and detect any patterns, spikes, or declines.

Lastly, I selected a light blue color to make the visualization look colorful but not too overwhelming and keep it easy to read. I also added both a line and point markers to clearly indicate each year’s count.

Heat Map

To better understand the spatial distribution of car crashes in Seattle, I created a heatmap to visualize where crashes were most densely concentrated. Heatmaps are effective for identifying hotspots in geographic data, allowing us to detect patterns that might not be obvious in raw tables or scatter plots.

Code and Visualization (map for 2010)

##creating base heat map

library(lubridate)
library(ggplot2)
library(ggmap)
library(leaflet)
library(leaflet.extras)
library(sf)


##creating new table with only columns and years we will be using
heatmap_data <- crash_data %>%
  mutate(date = as.Date(date),
         month = month(date),
         year = year(date)) %>%  
  filter(year >= 2004, year <= 2024) %>%  
  select(date, year, month, longitude = longitude_x, latitude = latitude_y) %>% 
  filter(!is.na(longitude), !is.na(latitude))

#create sf object and transform CRS
heatmap_data_sf <- st_as_sf(heatmap_data, coords = c("longitude", "latitude"), crs = 2285)
heatmap_data_latlon <- st_transform(heatmap_data_sf, crs = 4326)

heatmap_data_latlon <- cbind(
  st_coordinates(heatmap_data_latlon) %>% as.data.frame() %>% #extracts coordinates
    rename(longitude = X, latitude = Y),
  year = heatmap_data$year,
  month = heatmap_data$month
)

#filter map by year
heatmap_year_data <- heatmap_data_latlon %>%
  filter(year == 2010) #any year from 2004-2024

#plot heat map
leaflet(heatmap_year_data) %>%
  addProviderTiles(providers$CartoDB.Positron) %>%
  setView(lng = -122.3321, lat = 47.6062, zoom = 11.5) %>%
  addHeatmap(
    lng = ~longitude,
    lat = ~latitude,
    intensity = ~1,
    blur = 20,
    max = 0.1,
    radius = 15
  )

Process

I began by filtering the dataset to include only the years between 2004 and 2024. Then, I selected only the necessary columns: date, year, month, longitude, and latitude. I removed rows with missing geographic coordinates to ensure accurate mapping.

Since leaflet maps require coordinates in the WGS 84 system (EPSG:4326), I converted the crash data to an sf object and ensured it was using the correct coordinate reference system. I then extracted the latitude and longitude values to be used by leaflet.

For this visual, I made it interactive with a slide bar, however the one above is only showing the crashes from 2010, for example purposes.

The resulting heatmap shows areas with high concentrations of crashes, particularly in downtown Seattle and along major highways and intersections. The brighter and more intense the red color, the higher the density of crashes in that location. This kind of visualization can be helpful for city planners, traffic engineers, or policy makers to identify areas where safety interventions might be most needed.

This visualization complements the line graph I created above, which showed temporal trends in crash frequency. While the line graph revealed how crashes changed over time, this heatmap shows where they happened most often. Combining both time-based and location-based analysis provides a more complete picture of the crash data and supports deeper insights.

Bar Graphs

To explore the environmental factors that might influence car crashes, I created a set of bar graphs that break down crash counts by road condition, weather, and light condition. These variables help us understand the context in which many crashes occur, whether roads were wet or icy, visibility was poor, or weather was severe. Bar charts are a good choice for this type of categorical data since they clearly show the frequency of each condition and allow for easy comparisons across categories.

Code and Visualizations

#loads libraries for graphs
library(ggplot2)
library(dplyr)
library(scales)
library(ggthemes)



##Road condition distributiom
#filters out low value factors
crash_data_filtered <- crash_data[!crash_data$road_condition %in% c("", "Other", "Unknown", "Standing Water", "Sand/Mud/Dirt", "Oil"), ]

#counts number of each road condition and arranges them in descending order
road_condition_counts <- crash_data_filtered %>%
  count(road_condition) %>%
  arrange(desc(n))

#bar plot of road condition distribution (top conditions)
ggplot(road_condition_counts, aes(x = reorder(road_condition, -n), y = n)) +
  geom_bar(stat = "identity", fill = "burlywood4") +
  geom_text(aes(label = n), vjust = -0.5, size = 3) +  #adds counts above bars
  theme_minimal() +
  labs(title = "Distribution of Road Conditions",
       x = "Road Condition",
       y = "Count") +
  theme_economist(base_size = 15)

##weather distribution 
#filters out low value factors
crash_data_filtered_weather <- crash_data[!crash_data$WEATHER %in% c("", "Other", "Unknown", "Blowing Snow", "Severe Crosswind", "Blowing Sand/Dirt", "Partly Cloudy", "Sleet/Hail/Freezing Rain"), ]

#counts number of each road condition and arranges them
weather_counts <- crash_data_filtered_weather %>%
  count(WEATHER) %>%
  arrange(desc(n))

#bar plot of weather distribution
ggplot(weather_counts, aes(x = reorder(WEATHER, -n), y = n)) +
  geom_bar(stat = "identity", fill = "burlywood4") +
  geom_text(aes(label = n), vjust = -0.5, size = 3) +  #adds count above bars
  theme_minimal() +
  labs(title = "Distribution of Weather Conditions",
       x = "Weather Condition",
       y = "Count") +
  theme_economist(base_size = 15) +
  scale_y_continuous(labels = label_comma())

##light condition distribution 
#filters out low value factors
crash_data_filtered_light <- crash_data[!crash_data$light_condition %in% c("", "Other", "Unknown", "Dark - Unknown Lighting", "Dark - Street Lights Off"), ]

#counts number of each light condition and arranges them
light_counts <- crash_data_filtered_light %>%
  count(light_condition) %>%
  arrange(desc(n))

#plots bar graph of light consition ditribution
ggplot(light_counts, aes(x = reorder(light_condition, -n), y = n)) +
  geom_bar(stat = "identity", fill = "burlywood4") +
  geom_text(aes(label = n), vjust = -0.5, size = 3) +  #adds counts above bars
  theme_minimal() +
  labs(title = "Distribution of Light Conditions",
       x = "Light Condition",
       y = "Count") +
  theme_economist(base_size = 15) +
  scale_y_continuous(labels = label_comma())

Process

I created three separate plots, each focusing on a key environmental factor at the time of the crash. For each graph, I first filtered out less meaningful categories such as “Unknown” or “Other” to ensure that the visuals were clean.

Using dplyr::count() made it easy to tally up the number of crashes per category, and I sorted the results in descending order to make the most common conditions more visually prominent. I used geom_text() to label the bars with exact counts and chose the color pallet ‘burlywood4’ to keep the graphs visually appealing and cohesive across all three factors.

The first bar graph shows road conditions, with “Wet” and “Dry” roads being the most common crash settings; which makes sense given Seattle’s rainy climate. The weather distribution chart further supports this, with “Rain” and “Clear” being the top two conditions during crashes. Lastly, for light conditions, “Daylight” was by far the most frequent, followed by “Dark - Street Lights On”, indicating many crashes happen when visibility is still reasonably good.

These graphs provide insight into the types of environmental contexts where crashes are more likely to occur. This information could be valuable for bringing awareness to stakeholders or adjusting traffic infrastructure and signage in areas with known risks.

Interactive Shiny app and Research

To bring everything together, I created an interactive Shiny application that integrates all of the visualizations (line graph, heat map, and bar charts) into a single, user-friendly dashboard. This app allows users to explore Seattle car crash data dynamically by year, location, and environmental conditions. By adding in filters and controls, the app makes the analysis more accessible to policymakers, researchers, and community members who want to explore crash trends and patterns on their own.

In addition to building the app, I conducted extensive research to support the interpretations and insights shared throughout this project. I referenced local traffic safety reports and academic studies to make sure that any claims I made were backed by credible evidence. This helped me not only validate patterns I observed in the data but also better understand the broader social and environmental context surrounding crash incidents in Seattle.

By combining data visualization, interactivity, and evidence-based research, this project offers a comprehensive look at traffic safety in the city and can potentially guide efforts toward making Seattle’s roads safer.