knitr::opts_chunk$set(echo = TRUE)
library(dplyr)
library(ggplot2)
library(tidyr)
This analysis uses the NOAA Storm Database to identify which types of severe weather are most harmful to population health and which carry the greatest economic cost in the United States. Records before 1996 are excluded because NWS Directive 10-1605 expanded reporting to all 48 event types only in that year; earlier records contain almost exclusively tornado, hail, and thunderstorm wind events and would bias any comparison across event types. Damage figures were converted to dollars, and one 2006 flood record coded in billions rather than millions was corrected. The free-text event labels were consolidated into 21 categories using pattern matching, with unmatched records verified to account for less than 0.1% of casualties and damage. Tornadoes produce the most casualties overall, at roughly 22,000 killed or injured. Excessive heat is nonetheless the deadliest single event type, causing 2,037 fatalities compared with 1,513 for tornadoes. Hurricanes cause the greatest economic damage at approximately 87 billion, followed by storm surge and flooding. Drought is an outlier in kind rather than scale: it causes negligible property damage but is the largest single source of crop loss at $13.4 billion
First we load the data into R
storm <- read.csv("repdata_data_StormData.csv.bz2")
Next we subset the data in order to not work with so many unneeded columns:
storm <- storm %>% select(REFNUM, BGN_DATE, STATE, EVTYPE, FATALITIES, INJURIES, PROPDMG, PROPDMGEXP, CROPDMG, CROPDMGEXP)
Because we didn’t have all the events types pre-1996, I decided to only analyze based on data we have all event types for
storm$year <- as.integer(format(as.Date(storm$BGN_DATE, format = "%m/%d/%Y"), "%Y"))
storm <- storm %>% filter(year >= 1996)
nrow(storm)
## [1] 653530
Next we want to convert the damage numbers to dollars
Damage is recorded across two columns per category: a numeric
coefficient in PROPDMG and CROPDMG, and a
separate magnitude character in PROPDMGEXP and
CROPDMGEXP, where K, M, and B denote thousands, millions,
and billions. The coefficient on its own is not interpretable — a value
of 5 means five thousand dollars or five million depending on the
accompanying character — so the two must be combined before any totals
are computed.
Within the 1996 onward subset the magnitude columns are almost entirely clean:
table(storm$PROPDMGEXP)
##
## 0 B K M
## 276185 1 32 369938 7374
table(storm$CROPDMGEXP)
##
## B K M
## 373069 4 278686 1771
Only K, M, B, and blanks appear, apart from a single property record carrying a literal “0”, which is treated as tens and is too small to affect any total. Blank entries correspond to rows reporting no damage at all, so mapping them to zero discards nothing:
sum(storm$PROPDMGEXP == "" & storm$PROPDMG > 0)
## [1] 0
sum(storm$CROPDMGEXP == "" & storm$CROPDMG > 0)
## [1] 0
Both are zero, confirming that a blank magnitude always accompanies a zero coefficient.
The conversion below also handles H (hundreds), lower-case entries, and the undocumented symbols that appear in the pre-1996 portion of the database. None of those occur in the filtered data, but retaining the branches means the function gives correct results if applied to the full record.
exp_mult <- function(e) {
e <- toupper(trimws(as.character(e)))
case_when(
e == "B" ~ 1e9,
e == "M" ~ 1e6,
e == "K" ~ 1e3,
e == "H" ~ 1e2,
e %in% as.character(0:8) ~ 1e1,
TRUE ~ 0
)
}
storm <- storm %>%
mutate(propcost = PROPDMG * exp_mult(PROPDMGEXP),
cropcost = CROPDMG * exp_mult(CROPDMGEXP),
totalcost = propcost + cropcost)
With the damage converted to dollars, the next step is to remove records with no casualties and no damage.
impact <- storm %>%
filter(FATALITIES > 0 | INJURIES > 0 | totalcost > 0)
nrow(impact)
## [1] 201318
length(unique(impact$EVTYPE))
## [1] 222
Next we need to clean up the EVTYPE column, as the values aren’t consistent.
normalize <- function(x) {
x <- toupper(trimws(x))
x <- gsub("[^A-Z ]", " ", x)
x <- gsub("\\s+", " ", x)
trimws(x)
}
impact <- impact %>%
mutate(ev = normalize(EVTYPE),
event = case_when(
grepl("TORNADO|TORNDAO|FUNNEL|WATERSPOUT", ev) ~ "Tornado",
grepl("HURRICANE|TYPHOON", ev) ~ "Hurricane",
grepl("TROPICAL STORM|TROPICAL DEPRESSION",ev) ~ "Tropical Storm",
grepl("FLASH", ev) ~ "Flash Flood",
grepl("FLOOD|FLD|HIGH WATER|RISING WATER", ev) ~ "Flood",
grepl("TSTM|THUNDERSTORM|THUDERSTORM", ev) ~ "Thunderstorm Wind",
grepl("HAIL", ev) ~ "Hail",
grepl("LIGHTNING|LIGHTING|LIGNTNING", ev) ~ "Lightning",
grepl("HEAT|WARM|RECORD HIGH|HYPERTHERMIA",ev) ~ "Excessive Heat",
grepl("SNOW|BLIZZARD|ICE|ICY|SLEET|WINTER|WINTRY|GLAZE|FREEZING RAIN", ev) ~ "Winter Weather",
grepl("COLD|CHILL|FROST|FREEZE|HYPOTHERMIA", ev) ~ "Cold",
grepl("AVALANCH|AVALANCE", ev) ~ "Avalanche",
grepl("WIND", ev) ~ "High Wind",
grepl("FIRE", ev) ~ "Wildfire",
grepl("DROUGHT|DRY|DRIEST", ev) ~ "Drought",
grepl("STORM SURGE|SURGE", ev) ~ "Storm Surge",
grepl("HEAVY RAIN|^RAIN|PRECIPITATION", ev) ~ "Heavy Rain",
grepl("RIP CURRENT|SURF|SEAS|SWELL|TIDE|COASTAL|TSUNAMI|MARINE", ev) ~ "Surf & Coastal",
grepl("FOG", ev) ~ "Fog",
grepl("DUST", ev) ~ "Dust",
grepl("SLIDE|SLUMP|EROSION", ev) ~ "Landslide",
TRUE ~ "Other"
))
Then ran this to confirm unmatched rows were insignificant:
impact %>% summarise(across(c(FATALITIES, INJURIES, totalcost), ~ sum(.x[event == "Other"]) / sum(.x)))
## FATALITIES INJURIES totalcost
## 1 0.0006871278 0.0008451919 9.565891e-06
All three proportions are very low, confirming that events which matched no pattern contribute negligibly to casualties and damage and can be left grouped as “Other.”
I think one record was coded incorrectly. It is coded with a magnitude B, giving property damage of $115 billion, which is larger than every other flood in the database combined. The accompanying NWS narrative describes damage in the tens of millions, indicating M was intended. This shows the outlier:
impact %>% arrange(desc(propcost)) %>%
select(REFNUM, year, STATE, EVTYPE, PROPDMG, PROPDMGEXP, propcost) %>%
head(3)
## REFNUM year STATE EVTYPE PROPDMG PROPDMGEXP propcost
## 1 605943 2006 CA FLOOD 115.00 B 1.150e+11
## 2 577616 2005 LA STORM SURGE 31.30 B 3.130e+10
## 3 577615 2005 LA HURRICANE/TYPHOON 16.93 B 1.693e+10
And this corrects it:
impact <- impact %>%
mutate(propcost = ifelse(REFNUM == 605943, propcost / 1000, propcost),
totalcost = propcost + cropcost)
health <- impact %>%
group_by(event) %>%
summarise(fatalities = sum(FATALITIES),
injuries = sum(INJURIES),
casualties = fatalities + injuries) %>%
arrange(desc(casualties))
head(health, 10)
## # A tibble: 10 × 4
## event fatalities injuries casualties
## <chr> <dbl> <dbl> <dbl>
## 1 Tornado 1513 20670 22183
## 2 Excessive Heat 2037 7702 9739
## 3 Flood 453 6846 7299
## 4 Thunderstorm Wind 398 5164 5562
## 5 Lightning 651 4141 4792
## 6 Winter Weather 548 3569 4117
## 7 Flash Flood 887 1674 2561
## 8 High Wind 387 1508 1895
## 9 Surf & Coastal 735 888 1623
## 10 Wildfire 87 1458 1545
health %>%
slice_max(casualties, n = 10) %>%
pivot_longer(c(fatalities, injuries), names_to = "type", values_to = "n") %>%
ggplot(aes(reorder(event, n), n, fill = type)) +
geom_col(show.legend = FALSE) +
facet_wrap(~type, scales = "free_x") +
coord_flip() +
labs(x = NULL, y = "Persons affected",
title = "US storm casualties by event type, 1996-2011",
caption = "Note differing x-axis scales") +
theme_minimal()
Tornadoes account for the largest number of casualties overall, with roughly 22,000 people killed or injured between 1996 and 2011, which is more than twice the next category. The ranking changes if deaths alone are considered: excessive heat caused 2,037 fatalities against 1,513 for tornadoes, making it the single deadliest event type despite producing about a third as many injuries. Flooding, thunderstorm wind, and lightning are the next three largest groups, each contributing several thousand casualties.
econ <- impact %>%
group_by(event) %>%
summarise(property = sum(propcost),
crop = sum(cropcost),
total = property + crop) %>%
arrange(desc(total))
head(econ, 10)
## # A tibble: 10 × 4
## event property crop total
## <chr> <dbl> <dbl> <dbl>
## 1 Hurricane 81718889010 5350107800 87068996810
## 2 Storm Surge 47834724000 855000 47835579000
## 3 Flood 29658573760 5013161500 34671735260
## 4 Tornado 24622810010 283425010 24906235020
## 5 Hail 14595237420 2496822450 17092059870
## 6 Flash Flood 15222268910 1334901700 16557170610
## 7 Drought 1047833600 13367581000 14415414600
## 8 Thunderstorm Wind 7919453280 1016992600 8936445880
## 9 Tropical Storm 7644212550 677711000 8321923550
## 10 Wildfire 7760449500 402255130 8162704630
econ %>%
slice_max(total, n = 10) %>%
pivot_longer(c(property, crop), names_to = "type", values_to = "usd") %>%
ggplot(aes(reorder(event, usd), usd / 1e9, fill = type)) +
geom_col(show.legend = FALSE) +
facet_wrap(~type, scales = "free_x") +
coord_flip() +
labs(x = NULL, y = "Damage (billions USD)",
title = "US storm damage by event type, 1996-2011",
caption = "Note differing x-axis scales") +
theme_minimal()
Hurricanes are the costliest event type, at approximately 87 billion in combined property and crop damage, followed by storm surge at 47.8 billion and flooding at 34.7 billion. Because storm surge and tropical storms are themselves consequences of tropical cyclones, the true cost attributable to tropical systems is closer to 143 billion once those categories are combined. Drought is the clearest case for reporting property and crop damage separately: it ranks only seventh on total damage and causes almost no structural loss, yet at 13.4 billion it is by a wide margin the largest source of crop damage in the dataset.