Synopsis

This analysis examines the U.S. National Oceanic and Atmospheric Administration (NOAA) Storm Database to determine which types of severe weather event are most harmful to population health and which carry the greatest economic consequences across the United States. Population health impact is measured as the combined number of fatalities and injuries; economic impact is measured as the sum of reported property and crop damage, after applying the damage exponent codes stored in the database. The raw event type field contains close to a thousand free-text variants, so near-duplicate labels were collapsed into a smaller set of consistent categories before aggregation. Events were then ranked by total impact and the ten largest contributors in each domain were plotted. Tornadoes emerge as by far the most harmful event type for population health, causing more combined casualties than the next several categories together. Flooding produces the largest economic losses, followed by hurricanes and tornadoes. A small number of extreme damage records exert a strong influence on the economic ranking, and this is discussed as a limitation.

Data Processing

Load packages

library(dplyr)
library(ggplot2)
library(knitr)

Download and load the data

The NOAA Storm Database is read directly from the original compressed StormData.csv.bz2 archive. The file is only downloaded if it is not already present in the working directory, so that repeated knitting does not re-fetch roughly 47 MB each time. No preprocessing is carried out outside R.

if (!file.exists("StormData.csv.bz2")) {
  download.file(
    "https://d396qusza40orc.cloudfront.net/repdata%2Fdata%2FStormData.csv.bz2",
    destfile = "StormData.csv.bz2",
    method   = "auto",
    mode     = "wb"
  )
}

storm_data <- read.csv("StormData.csv.bz2", stringsAsFactors = FALSE)

dim(storm_data)
## [1] 902297     37

Inspect the variables used

Only seven of the 37 variables are required for this analysis: the event type, the two casualty counts, and the two damage amounts with their accompanying exponent codes.

str(storm_data[, c("EVTYPE", "FATALITIES", "INJURIES",
                   "PROPDMG", "PROPDMGEXP", "CROPDMG", "CROPDMGEXP")])
## 'data.frame':    902297 obs. of  7 variables:
##  $ EVTYPE    : chr  "TORNADO" "TORNADO" "TORNADO" "TORNADO" ...
##  $ FATALITIES: num  0 0 0 0 0 0 0 0 1 0 ...
##  $ INJURIES  : num  15 0 2 2 2 6 1 0 14 0 ...
##  $ PROPDMG   : num  25 2.5 25 2.5 2.5 2.5 2.5 2.5 25 25 ...
##  $ PROPDMGEXP: chr  "K" "K" "K" "K" ...
##  $ CROPDMG   : num  0 0 0 0 0 0 0 0 0 0 ...
##  $ CROPDMGEXP: chr  "" "" "" "" ...
length(unique(storm_data$EVTYPE))
## [1] 985

Clean the event type variable

EVTYPE is free text and contains a large number of near-duplicate labels. Left untreated, this splits single phenomena classifications across several rows and distorts the ranking: for example TSTM WIND and THUNDERSTORM WIND are recorded separately, as are HEAT and EXCESSIVE HEAT, and HURRICANE and HURRICANE/TYPHOON.This apply approaches introduced in this and other course modules.

The function below normalises case and whitespace, then assigns each record to the first matching category in an ordered list of rules. Ordering matters: more specific patterns are tested before more general ones, so that FLASH FLOOD is not subsequently absorbed by the broader FLOOD rule. Any record matching no rule keeps its original (uppercased) label.

clean_evtype <- function(x) {

  ev  <- gsub("[[:space:]]+", " ", toupper(trimws(x)))
  out <- rep(NA_character_, length(ev))

  rule <- function(out, pattern, label) {
    hit <- is.na(out) & grepl(pattern, ev)
    out[hit] <- label
    out
  }

  # Most specific patterns first
  out <- rule(out, "FLASH FLOOD",                          "FLASH FLOOD")
  out <- rule(out, "STORM SURGE|COASTAL FLOOD|TIDAL FLOOD", "STORM SURGE/COASTAL FLOOD")
  out <- rule(out, "TSTM|THUNDERSTORM",                    "THUNDERSTORM WIND")
  out <- rule(out, "HURRICANE|TYPHOON",                    "HURRICANE/TYPHOON")
  out <- rule(out, "TROPICAL STORM",                       "TROPICAL STORM")
  out <- rule(out, "TORNADO|TORNDAO",                      "TORNADO")
  out <- rule(out, "HEAT|WARM|HIGH TEMPERATURE",           "EXCESSIVE HEAT")
  out <- rule(out, "COLD|WIND CHILL|LOW TEMPERATURE|HYPOTHERMIA", "EXTREME COLD")
  out <- rule(out, "FROST|FREEZE",                         "FROST/FREEZE")
  out <- rule(out, "BLIZZARD|SNOW|WINTER|ICE STORM|WINTRY|FREEZING RAIN|SLEET", "WINTER WEATHER")
  out <- rule(out, "FLOOD|FLD",                            "FLOOD")
  out <- rule(out, "LIGHTNING|LIGHTING|LIGNTNING",         "LIGHTNING")
  out <- rule(out, "HAIL",                                 "HAIL")
  out <- rule(out, "RIP CURRENT",                          "RIP CURRENT")
  out <- rule(out, "FIRE",                                 "WILDFIRE")
  out <- rule(out, "DROUGHT",                              "DROUGHT")
  out <- rule(out, "HIGH WIND|STRONG WIND|GUSTY WIND",     "HIGH WIND")
  out <- rule(out, "FOG",                                  "FOG")
  out <- rule(out, "AVALANCE|AVALANCHE",                   "AVALANCHE")
  out <- rule(out, "HEAVY RAIN|EXCESSIVE RAIN|RAINFALL",   "HEAVY RAIN")

  out[is.na(out)] <- ev[is.na(out)]
  out
}

storm_data$EVENT <- clean_evtype(storm_data$EVTYPE)

length(unique(storm_data$EVENT))
## [1] 307

Aggregate population health impact

Fatalities and injuries are summed for each cleaned event category and combined into a single measure of total harm.

health_data <- storm_data %>%
  group_by(EVENT) %>%
  summarise(
    Fatalities = sum(FATALITIES, na.rm = TRUE),
    Injuries   = sum(INJURIES,   na.rm = TRUE),
    TotalHarm  = Fatalities + Injuries,
    .groups    = "drop"
  ) %>%
  arrange(desc(TotalHarm))

top_health <- head(health_data, 10)

kable(top_health,
      format.args = list(big.mark = ","),
      caption = "Table 1. Ten event types with the greatest population health impact.")
Table 1. Ten event types with the greatest population health impact.
EVENT Fatalities Injuries TotalHarm
TORNADO 5,636 91,407 97,043
EXCESSIVE HEAT 3,178 9,243 12,421
THUNDERSTORM WIND 754 9,544 10,298
FLOOD 512 6,873 7,385
WINTER WEATHER 632 5,952 6,584
LIGHTNING 817 5,231 6,048
FLASH FLOOD 1,035 1,802 2,837
HIGH WIND 421 1,796 2,217
WILDFIRE 90 1,608 1,698
HURRICANE/TYPHOON 135 1,333 1,468

Convert damage amounts to dollars

Damage amounts are split across two columns. PROPDMG and CROPDMG hold the number, and PROPDMGEXP and CROPDMGEXP hold a letter or digit saying what scale that number is on. A value of 25 with a code of K means 25 thousand dollars, so the two have to be combined before anything can be added up.

sort(unique(storm_data$PROPDMGEXP))
##  [1] ""  "-" "?" "+" "0" "1" "2" "3" "4" "5" "6" "7" "8" "B" "h" "H" "K" "m" "M"
sort(unique(storm_data$CROPDMGEXP))
## [1] ""  "?" "0" "2" "B" "k" "K" "m" "M"

A few records carry codes that aren’t documented anywhere — blanks, -, + and ? — and these hold so little damage between them that treating them as a multiplier of one makes no practical difference to the totals.

mult_map <- c("H" = 1e2, "K" = 1e3, "M" = 1e6, "B" = 1e9,
              "0" = 1,   "1" = 1e1, "2" = 1e2, "3" = 1e3, "4" = 1e4,
              "5" = 1e5, "6" = 1e6, "7" = 1e7, "8" = 1e8, "9" = 1e9)

get_multiplier <- function(x) {
  m <- unname(mult_map[toupper(trimws(as.character(x)))])
  m[is.na(m)] <- 1
  m
}

storm_data$PROP_DAMAGE <- storm_data$PROPDMG * get_multiplier(storm_data$PROPDMGEXP)
storm_data$CROP_DAMAGE <- storm_data$CROPDMG * get_multiplier(storm_data$CROPDMGEXP)

# Validation: no missing values introduced, and magnitudes are plausible
sum(is.na(storm_data$PROP_DAMAGE))
## [1] 0
sum(is.na(storm_data$CROP_DAMAGE))
## [1] 0
summary(storm_data$PROP_DAMAGE)
##      Min.   1st Qu.    Median      Mean   3rd Qu.      Max. 
## 0.000e+00 0.000e+00 0.000e+00 4.746e+05 5.000e+02 1.150e+11
summary(storm_data$CROP_DAMAGE)
##      Min.   1st Qu.    Median      Mean   3rd Qu.      Max. 
## 0.000e+00 0.000e+00 0.000e+00 5.442e+04 0.000e+00 5.000e+09

Aggregate economic impact

economic_data <- storm_data %>%
  group_by(EVENT) %>%
  summarise(
    PropertyDamage = sum(PROP_DAMAGE, na.rm = TRUE),
    CropDamage     = sum(CROP_DAMAGE, na.rm = TRUE),
    TotalDamage    = PropertyDamage + CropDamage,
    .groups        = "drop"
  ) %>%
  arrange(desc(TotalDamage))

top_economic <- head(economic_data, 10)

kable(
  top_economic %>%
    mutate(across(PropertyDamage:TotalDamage, ~ round(.x / 1e9, 2))),
 col.names = c("Event", "Property damage (bn USD)", "Crop damage (bn USD)", "Total damage (bn USD)"),
  caption = "Table 2. Ten event types with the greatest economic impact, in billions of dollars."
)
Table 2. Ten event types with the greatest economic impact, in billions of dollars.
Event Property damage (bn USD) Crop damage (bn USD) Total damage (bn USD)
FLOOD 150.24 10.86 161.10
HURRICANE/TYPHOON 85.36 5.52 90.87
TORNADO 57.00 0.41 57.42
STORM SURGE/COASTAL FLOOD 48.40 0.00 48.40
FLASH FLOOD 17.59 1.53 19.12
HAIL 15.98 3.05 19.02
WINTER WEATHER 12.42 5.32 17.74
DROUGHT 1.05 13.97 15.02
THUNDERSTORM WIND 12.78 1.27 14.06
WILDFIRE 8.50 0.40 8.90

Results

Population health impact

Across the full record, TORNADO is the event type most harmful to population health, accounting for 97,043 combined fatalities and injuries — more than the next 9 categories put together. Once the near-duplicate labels are collapsed, excessive heat and thunderstorm wind rank second and third; both are substantially larger than they appear in the uncleaned data, where each is split across two or more event codes. Fatalities and injuries behave differently: tornadoes and thunderstorm wind generate very high injury counts relative to deaths, whereas excessive heat has a markedly higher fatality share.

ggplot(top_health,
       aes(x = reorder(EVENT, TotalHarm), y = TotalHarm)) +
  geom_col(fill = "steelblue") +
  coord_flip() +
  scale_y_continuous(labels = function(x) format(x, big.mark = ",")) +
  labs(
    title = "Top 10 weather events by impact on population health",
    subtitle = "Combined fatalities and injuries, NOAA Storm Database 1950-2011",
    x = "Event type",
    y = "Total fatalities and injuries"
  ) +
  theme_minimal()
Figure 1. Ten weather event types with the greatest combined fatalities and injuries, 1950-2011.
Figure 1. Ten weather event types with the greatest combined fatalities and injuries, 1950-2011.

Economic consequences

FLOOD produces the greatest overall economic loss, at approximately $161.1 billion in combined property and crop damage. Hurricanes and typhoons, tornadoes and storm surge follow. The composition of the loss varies considerably by event type: flooding, hurricanes and tornadoes are overwhelmingly property-damage events, whereas drought is almost entirely a crop-damage event and would not appear in a property-only ranking.

ggplot(top_economic,
       aes(x = reorder(EVENT, TotalDamage), y = TotalDamage / 1e9)) +
  geom_col(fill = "darkgreen") +
  coord_flip() +
  labs(
    title = "Top 10 weather events by economic damage",
    subtitle = "Property and crop damage combined, NOAA Storm Database 1950-2011",
    x = "Event type",
    y = "Total damage (billions of dollars)"
  ) +
  theme_minimal()
Figure 2. Ten weather event types with the greatest total property and crop damage, 1950-2011.
Figure 2. Ten weather event types with the greatest total property and crop damage, 1950-2011.

Limitations

Three caveats are made in relation to the results above.

First, one record makes a lot of difference to the economic ranking. The biggest property damage entry in the whole database is a Napa County flood in 2006, recorded as $115 billion. The remarks field for that record describes much smaller losses, and the exponent code looks like a typo: B where M was meant. That single row is $115bn of the $150bn flood total. Correct it and flooding drops to around $46bn, which puts hurricanes and typhoons top instead. I have left it in so the analysis matches the database as published, but flooding only comes first because of it.

storm_data %>%
  filter(PROP_DAMAGE == max(PROP_DAMAGE)) %>%
  select(BGN_DATE, STATE, EVTYPE, PROPDMG, PROPDMGEXP, PROP_DAMAGE)
##           BGN_DATE STATE EVTYPE PROPDMG PROPDMGEXP PROP_DAMAGE
## 1 1/1/2006 0:00:00    CA  FLOOD     115          B    1.15e+11

Second, recording practice changes over time. Only tornadoes were recorded in the earliest years of the database, with the full range of event types captured from 1996 onwards. Totals accumulated over the whole period therefore overstate tornadoes relative to categories that entered the record later.

Third, damage figures are nominal and have not been adjusted for inflation, so losses from recent decades carry disproportionate weight in a series running from 1950 to 2011.