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"))
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 an interactive Leaflet map which plots the crash locations via latitude 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 technical reasons. Differences in population, economies, and historical records likely contribute to the differences in recorded crashes between continents and regions.

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

Total Fatalities by Year

This visualization looks at the number of fatalities on a year-to-year basis. I printed the 15 years with the highest fatalities recorded, with the top 3 all occurring between 1943-45 during the midst of WW2. A trend of decreasing fatalities can also be observed. There were approximately 310 million worldwide passengers in 1970, which shot up to 4.7 billion in 2024, without close to a corresponding increase in fatalities. So, it speaks to the improvements in technology and safety among other reasons that the fatality numbers are significantly less in the modern era when comparing those two time periods.

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

The first question I thought of when looking at this data was whether certain brands of aircraft (inspired by the negative press around Boeing) had more recorded crashes historically. After examining the aircraft types and finding over 1,000 different entries, I simplified my data to compare aircraft manufacturers instead. The results surprised me, as despite the number of aircraft and flights dramatically increasing over time, the manufacturers with the most recorded crashes were not necessarily the most dominant brands of the past 50 years. Instead, many of the manufacturers with the most crashes were companies that produced large numbers of aircraft around the time of the Second World War, including Douglas, De Havilland, and Beechcraft. This is interesting when compared to my earlier graphs, which showed some of the highest annual crash and fatality totals during WW2. Additionally, a large percentage of crashes involving these manufacturers were attributed to human factors or unknown causes. Their high numbers of recorded crashes could also be related to the widespread use and long operating histories of their aircraft, rather than the aircraft necessarily being more dangerous. Ultimately this graph is useful for also showing which brands historically have had more crashes, but it doesn’t mean that brands with more recorded crashes are less safe as we do not know the scale of flights

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

This visualization shows the fatality rate of crew members and passengers based on the number of people on the plane and across decades. I wanted to see if the number of people aboard the plane a noticeable impact on the fatality rate. I also only counted crashes with both a crew member and a passenger when determining the percentage of people who died in recorded crashes for a even comparison. The percentages varied from decade to decade and occupancy group to occupancy group but generally appeared to decline over time. I also noticed that smaller aircraft, particularly those with 1–10 occupants, often had higher fatality rates than larger aircraft. There were also gaps noticeable in the earlier decades for some of the larger occupancy groups. This could be related to larger aircraft not existing, being less available, and/or gaps in recording data.

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

Crashes by Cause & Year

This visualization examines how the number of crashes from different causes has changed over time. Since the second world war, Human factors have been the most known cause, followed by technical failures. A notable observation from this graph can be seen in the spike observed from 1939 until 1945, which lines up with WW2. This is especially interesting when looking at crashes attributed to terrorism, hijacking, or sabotage, which increased massively during this period. 10 years prior, it had the least attributed casualties. Another interesting observation is how the number of recorded crashes has generally decreased in more recent decades, despite an exponential increase in aviation activity. Crashes attributed to unknown causes were not represented in this visualization since they could not be associated with a specific cause.

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

Fatal/Nonfatal Accidents by Phase

In my last visualization I wanted to explore how the phase of flight that the crash occurred impacted whether the accident resulted in a fatality or not. I found that crashes occurring during ground operations had the lowest percentage involving fatalities(16.5%). This was almost three times higher for Landing/Approach (47.5%) , while Crashes occurring while the Plane was in the flight phase involved at least one fatality 70.8% of the time. Given the higher speed/altitude and distance from assistance this makes sense, but the disparity was surprising. It is worth noting this is also the longest phase of the flight, which likely plays a large role in explaining those numbers.

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

In conclusion, this dataset and its visualizations showed many patterns relating to aviation crashes throughout history. The crash and fatality numbers were heavily influenced by the records during WW2. Human factors were consistently the leading known cause of plane crashes, while accidents occurring during the flight phase had the highest percentage involving fatalities. Both recorded crashes and fatalities have generally decreased as time has moved on, despite the significant increase in the number of flights and passengers. This gives an interesting look into how the characteristics of recorded aviation crashes have changed reflecting improvements in aviation technology, safety and global stability.

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 = {} }