Introduction & Datset

This report displays my visualizations based around aviation accident data. The dataset contains 27,598 accident records spanning from 1918 through August of 2026. It includes columns on crash locations, fatalities, cause of crash, time, flight phase, and operator. I use these records to display several visuals that show characteristics about the history of recorded aviation crashes since the dawn of the airplane. While this dataset may not be 100% complete with every aviation crash, it allows us examine the commonalities between crashes. Because this dataset only contains Crash data and not flight records without an incident, we can not determine overall saftey patterns but several atterns still developed amoung the flight crashes.

setwd('/Users/brad/Documents/MacFiles/R Studio/R_Data_Files')

library(dplyr)
library(ggplot2)
library(lubridate)
library(leaflet)
library(scales)
library(plotly)
library(RColorBrewer)

df = read.csv("planecrashdata.csv", na.strings=c("", "NA"))

Exploratory Steps

ColsToDrop = c("registration", "survivors", "crash_site",
  "schedule", "msn", "yom", "flight_type",
  "other_fatalities", "circumstances",
  "image_url", "hex_code", "external_id")
df = df[, !names(df) %in% ColsToDrop]

df$crash_cause[is.na(df$crash_cause)] = "Unknown"


df$Date = ymd(substring(df$timestamp, 1, 10))
df$year = year(df$Date)
df$month = month(df$Date, label=TRUE, abbr=TRUE)
df$decade = floor(df$year/10)*10

head(df[,c("timestamp", "Date", "year", "month", "decade")])
summary(df$year)

summary(df$year)
summary(df$total_fatalities)
summary(df$crew_on_board)
summary(df$passengers_on_board)

colSums(is.na(df))

count(df, crash_cause, sort=TRUE)
count(df, flight_phase, sort=TRUE)

Visualizations

Plane Crash Locations

My first visualization is a interactive Leaflet map which plots the crash locations via lattitude and longitude on a global map. 18,475 Of the 27,598 accidents in the dataset had coordinates available for mapping, with the highest concentration in the US and Europe. The color difference represents the determined cause of the crash. We can see that from the crashes with available geolocation data, the most common cause was Human Actions/cause. Followed by unknown(“NA” in the dataset) then tecnical reasons. Ultimately less populated and developed areas will have less recorded traffic which heavily contributes to the difference in crash numbers between contidents and reigions.

map_df = df %>%
  filter(!is.na(latitude), !is.na(longitude),
  latitude >= -90, latitude <= 90,
  longitude >= -180, longitude <= 180)

map_df$cause_group = map_df$crash_cause
#count(map_df, cause_group, sort=TRUE)
knitr::kable(count(map_df, cause_group, sort=TRUE),
             col.names=c("Crash Cause", "Number of Accidents"),
             caption="Recorded Plane Crashes by Cause")
Recorded Plane Crashes by Cause
Crash Cause Number of Accidents
Human factor 6618
Unknown 5810
Technical failure 3894
Weather 1079
Terrorism act, Hijacking, Sabotage 671
Other causes 403
cause_colors = colorFactor(
  palette=c("red", "orange", "blue", "purple", "green", "gray"),
  domain=c("Human factor", "Technical failure", "Weather",
  "Terrorism act, Hijacking, Sabotage", "Other causes", "Unknown"))

map_df$popup = paste("Aircraft:", map_df$aircraft,
  "<br>Date:", map_df$Date,
  "<br>Location:", map_df$crash_location,
  "<br>Cause:", map_df$cause_group,
  "<br>Fatalities:", map_df$total_fatalities)

plt1 = leaflet() %>%
  addTiles() %>%
  setView(lng=0, lat=20, zoom=2) %>%
  addCircleMarkers(
  lng=map_df$longitude,
  lat=map_df$latitude,
  radius=5,
  color=cause_colors(map_df$cause_group),
  fillColor=cause_colors(map_df$cause_group),
  opacity=0.7,
  fillOpacity=0.6,
  stroke=FALSE,
  popup=map_df$popup,
  clusterOptions=markerClusterOptions()
  ) %>%
  addLegend(
  position="bottomright",
  colors=c("red", "orange", "blue", "purple", "green", "gray"),
  labels=c("Human factor", "Technical failure", "Weather",
  "Terrorism act, Hijacking, Sabotage", "Other causes", "Unknown"),
  opacity=1,
  title="Crash Cause"
  )
plt1

Crashes by Cause & Year

cause_df = df %>%
  filter(crash_cause != "Unknown")

cause_df$crash_cause[cause_df$crash_cause == "Terrorism act, Hijacking, Sabotage"] = "Terror/Hij/Sab"

cause_counts = cause_df %>%
  group_by(year, crash_cause) %>%
  summarise(n=n(), .groups="keep") %>%
  data.frame()

knitr::kable(count(cause_df, crash_cause),
  col.names=c("Crash Cause", "Number of Accidents"),
  caption="Total Recorded Accidents by Classified Cause")
Total Recorded Accidents by Classified Cause
Crash Cause Number of Accidents
Human factor 9582
Other causes 603
Technical failure 5975
Terror/Hij/Sab 1131
Weather 1444
plt2 = ggplot(cause_counts, aes(x=year, y=n, color=crash_cause)) +
  geom_line(linewidth=1.3) +
  geom_point(data=subset(cause_counts, year %% 10 == 0),
  size=2.5, shape=21, fill="white") +
  scale_x_continuous(breaks=seq(1920, 2030, 10)) +
  labs(title="Plane Crashes by Cause and Year",
  x="Year",
  y="Number of Crashes",
  color="Crash Cause") +
  scale_color_brewer(palette="Set2") +
  theme_minimal()
plt2

Total Fatalities by Year

fatality_counts = df %>%
  group_by(year) %>%
  summarise(fatalities=sum(total_fatalities, na.rm=TRUE), .groups="keep") %>%
  data.frame()

#summary(fatality_counts$fatalities)

plt3 = ggplot(fatality_counts, aes(x=year, y=fatalities)) +
  geom_line(color="red", linewidth=1.3) +
  geom_point(data=subset(fatality_counts, year %% 10 == 0),
  size=2.5, shape=21, fill="white", color="red") +
  scale_x_continuous(breaks=seq(1920, 2030, 10)) +
  labs(title="Total Aviation Fatalities by Year",
  x="Year",
  y="Total Fatalities") +
  theme_minimal()

top_fatal_years = fatality_counts %>%
  arrange(desc(fatalities)) %>%
  head(15)

knitr::kable(top_fatal_years,
  col.names=c("Year", "Total Fatalities"),
  caption="Top 15 Years by Recorded Aviation Fatalities")
Top 15 Years by Recorded Aviation Fatalities
Year Total Fatalities
1945 4555
1944 4416
1943 3876
1972 3305
1985 2928
1973 2868
1996 2758
1974 2668
1942 2647
1969 2568
1989 2470
1977 2424
1962 2385
1968 2375
1979 2330
plt3_top = plt3 +
  geom_point(data=top_fatal_years, aes(x=year, y=fatalities),
  color="black", fill="gold", shape=21, size=3.5)

plt3_labeled = plt3_top +
  geom_text(data=top_fatal_years, aes(x=year, y=fatalities, label=year),
  color="black", size=3, vjust=-1)
plt3_labeled

Manufacturers by Crash Cause

manufacturer_df = df %>%
  filter(!is.na(aircraft))

manufacturer_df$manufacturer = sub(" .*", "", manufacturer_df$aircraft)
manufacturer_counts = count(manufacturer_df, manufacturer, sort=TRUE)

top_manufacturers = head(manufacturer_counts, 10)

knitr::kable(top_manufacturers,
  col.names=c("Aircraft Manufacturer", "Number of Accidents"),
  caption="Top 10 Aircraft Manufacturers by Recorded Accidents")
Top 10 Aircraft Manufacturers by Recorded Accidents
Aircraft Manufacturer Number of Accidents
Douglas 3737
De 2213
Cessna 1804
Beechcraft 1488
Lockheed 1432
Avro 1349
Boeing 1332
Piper 1085
PZL-Mielec 840
Antonov 815
manufacturer_causes = manufacturer_df %>%
  filter(manufacturer %in% top_manufacturers$manufacturer) %>%
  group_by(manufacturer, crash_cause) %>%
  summarise(n=n(), .groups="keep") %>%
  data.frame()

plt4 = ggplot(manufacturer_causes, aes(x=reorder(manufacturer, n, sum), y=n, fill=crash_cause)) +
  geom_bar(stat="identity") +
  coord_flip() +
  labs(title="Top 10 Aircraft Manufacturers by Crash Cause",
  x="Aircraft Manufacturer",
  y="Number of Crashes",
  fill="Crash Cause") +
  scale_fill_brewer(palette="Set2") +
  theme_minimal()
plt4

Fatality Rates by Occupancy & Decade

fatality_df = df %>%
  filter(!is.na(crew_on_board), !is.na(crew_fatalities),
  !is.na(passengers_on_board), !is.na(passengers_fatalities),
  crew_on_board > 0, passengers_on_board > 0,
  crew_fatalities >= 0, passengers_fatalities >= 0,
  crew_fatalities <= crew_on_board,
  passengers_fatalities <= passengers_on_board)

fatality_df$occupancy = fatality_df$crew_on_board + fatality_df$passengers_on_board

fatality_df$occupancy_group = ifelse(fatality_df$occupancy <= 10, "1-10",
  ifelse(fatality_df$occupancy <= 50, "11-50",
  ifelse(fatality_df$occupancy <= 100, "51-100", "101+")))

fatality_df$occupancy_group = factor(fatality_df$occupancy_group,
  levels=c("1-10", "11-50", "51-100", "101+"))

occupancy_counts = fatality_df %>%
  group_by(decade, occupancy_group) %>%
  summarise(crew_on_board=sum(crew_on_board),
  crew_fatalities=sum(crew_fatalities),
  passengers_on_board=sum(passengers_on_board),
  passengers_fatalities=sum(passengers_fatalities),
  .groups="keep") %>%
  data.frame()

crew_rates = data.frame(decade=occupancy_counts$decade,
  occupancy_group=occupancy_counts$occupancy_group,
  type="Crew",
  rate=100*occupancy_counts$crew_fatalities/occupancy_counts$crew_on_board)

passenger_rates = data.frame(decade=occupancy_counts$decade,
  occupancy_group=occupancy_counts$occupancy_group,
  type="Passengers",
  rate=100*occupancy_counts$passengers_fatalities/occupancy_counts$passengers_on_board)

rate_df = rbind(crew_rates, passenger_rates)

rate_df$panel = paste(rate_df$type, rate_df$occupancy_group)
rate_df$panel = factor(rate_df$panel,
  levels=c("Crew 1-10", "Crew 11-50", "Crew 51-100", "Crew 101+",
  "Passengers 1-10", "Passengers 11-50",
  "Passengers 51-100", "Passengers 101+"))

plt5 = ggplot(rate_df, aes(x=factor(decade), y=rate, fill=type)) +
  geom_bar(stat="identity") +
  facet_wrap(~panel, ncol=4, nrow=2) +
  scale_x_discrete(breaks=as.character(seq(1920, 2020, 20))) +
  scale_fill_manual(values=c("Crew"="orange", "Passengers"="forestgreen")) +
  labs(title="Crew and Passenger Fatality Rates by Occupancy and Decade",
  x="Decade",
  y="Fatality Rate (%)") +
  theme_light() +
  theme(plot.title=element_text(hjust=0.5),
  axis.text.x=element_text(angle=45, hjust=1, size=9),
  strip.text=element_text(size=10),
  legend.position="none")
plt5

Fatal/Nonfatal Accidents by Phase

phase_df = df

phase_df$phase_group = ifelse(is.na(phase_df$flight_phase), "Unknown",
  ifelse(phase_df$flight_phase %in% c("Takeoff (climb)", "Take off", "Initial climb"), "Takeoff/Climb",
  ifelse(phase_df$flight_phase %in% c("Landing (descent or approach)", "Landing", "Approach"), "Landing/Approach",
  ifelse(phase_df$flight_phase %in% c("Flight", "En route", "Manoeuvring",
  "Manoeuvring (airshow, firefighting, ag.ops.)"), "Flight",
  ifelse(phase_df$flight_phase %in% c("Taxiing", "Taxi", "Parking", "Standing", "Pushback / towing"), "Ground",
  "Unknown")))))

phase_df = phase_df %>%
  filter(!is.na(total_fatalities))

phase_df$fatal_status = ifelse(phase_df$total_fatalities > 0, "Fatal", "Nonfatal")

phase_counts = phase_df %>%
  group_by(phase_group, fatal_status) %>%
  summarise(n=length(fatal_status), .groups="keep") %>%
  data.frame()

phase_counts$fatal_status = factor(phase_counts$fatal_status,
  levels=c("Fatal", "Nonfatal"))

plt6 = plot_ly(textposition="inside", labels=~fatal_status, values=~n,
  marker=list(colors=c("firebrick", "steelblue"))) %>%
  add_pie(data=phase_counts[phase_counts$phase_group == "Takeoff/Climb",],
  name="Takeoff/Climb", title="Takeoff/Climb", domain=list(row=0, column=0)) %>%
  add_pie(data=phase_counts[phase_counts$phase_group == "Flight",],
  name="Flight", title="Flight", domain=list(row=0, column=1)) %>%
  add_pie(data=phase_counts[phase_counts$phase_group == "Landing/Approach",],
  name="Landing/Approach", title="Landing/Approach", domain=list(row=0, column=2)) %>%
  add_pie(data=phase_counts[phase_counts$phase_group == "Ground",],
  name="Ground", title="Ground", domain=list(row=1, column=0)) %>%
  add_pie(data=phase_counts[phase_counts$phase_group == "Unknown",],
  name="Unknown", title="Unknown", domain=list(row=1, column=1)) %>%
  layout(title="Fatal vs Nonfatal Accidents by Flight Phase", showlegend=TRUE,
  grid=list(rows=2, columns=3))

plt6

Wrap up / Conclusion

Source

I found this open data on Huggingface Citation: @misc{planecrashdata2026, author = {Mauro Pellonara}, title = {PlaneCrashData: Global Aviation Safety and Accident Database (1918-Present)}, year = {2026}, publisher = {Hugging Face}, howpublished = {} }