Storms and other severe weather events can cause both public health and economic problems for communities and municipalities. Many severe events can result in fatalities, injuries, and property damage, and preventing such outcomes to the extent possible is a key concern. This analysis explores the impact of severe weather events in USA using U.S. National Oceanic and Atmospheric Administration’s (NOAA) storm database from 1950 to 2011. This database tracks characteristics of major storms and weather events in the United States, including when and where they occur, as well as estimates of any fatalities, injuries, and property damage. The objective is to identify which events are most harmful to population health and which have the greater economic consequences.
Population health impact was assessed by combined number of fatalities and injuries associated with each event type. Economic impact was evaluated by combining Crop and Property Damage after appropriately scaling damage values using the exponents reported for each.
To ensure consistency, event types were cleaned and mapped to official NOAA event categories. The results show that certain event types account for a disproportionate share of both human and economic losses, highlighting the importance of targeted disaster preparedness and mitigation strategies.
The analysis uses the NOAA Storm Database, which contains records of major weather events in the United States from 1950 to 2011. The dataset includes information on event types, fatalities, injuries, and estimates of property and crop damages.
library(dplyr)
library(ggplot2)
library(readr)
library(scales)
library(stringdist)
library(stringr)
## Download the zip file
url <- "https://d396qusza40orc.cloudfront.net/repdata%2Fdata%2FStormData.csv.bz2"
tmp <- tempfile(fileext = ".csv.bz2")
download.file(url, tmp, mode = "wb") # mode="wb" is important on Windows
## Unzip the file and read csv into a data frame
df <- read.csv(bzfile(tmp, open = "rt"), header = TRUE)
##Remove the invalid values starting with Summary or Summary of
df <- df[!startsWith(df$EVTYPE, "Summary"), ] ##See how to add ignore case in this statement
## Basic cleaning of data
df$EVTYPE_CLEAN <- df$EVTYPE %>%
toupper() %>% # Uppercase everything
str_trim() %>% # Remove leading/trailing spaces
str_squish() %>% # Collapse multiple spaces
str_replace_all("[[:punct:]]", " ") %>% # Remove punctuation
str_squish() # Clean up again after punctuation removal
## handle plurals and common variations
df$EVTYPE_CLEAN <- df$EVTYPE_CLEAN %>%
str_replace("TSTM", "THUNDERSTORM") %>% # replace TSTM with Thunderstorm
str_replace("WND", "WIND") %>%
str_replace("WINDS?$", "WIND") %>% # WIND or WINDS → WIND
str_replace("FLOODS?$", "FLOOD") %>%
str_replace("STORMS?$", "STORM") %>%
str_replace("RAINS?$", "RAIN") %>%
str_replace("FIRES?$", "FIRE") %>%
str_replace("CURRENTS?$", "CURRENT")
# Remove some qualifiers
df$EVTYPE_CLEAN = df$EVTYPE_CLEAN %>%
str_replace_all("\\b(LOCALLY|LATE SEASON|EARLY SEASON|ISOLATED|SCATTERED|POSSIBLE|PROLONG|GUSTY|UNSEASONAL|SAHARAN)\\b", "") %>%
str_squish()
df <- df %>%
mutate(
EVTYPE_CLEAN = case_when(
str_detect(EVTYPE_CLEAN, "TSTM|THUNDERSTORM") ~ "THUNDERSTORM WIND",
str_detect(EVTYPE_CLEAN, "HURRICANE|TYPHOON") ~ "HURRICANE/TYPHOON",
str_detect(EVTYPE_CLEAN, "TORNADO|FUNNEL") ~ "TORNADO",
str_detect(EVTYPE_CLEAN, "FLASH FLOOD") ~ "FLASH FLOOD",
str_detect(EVTYPE_CLEAN, "FLOOD|RIVER FLOOD") ~ "FLOOD",
str_detect(EVTYPE_CLEAN, "EXCESSIVE HEAT") ~ "EXCESSIVE HEAT",
str_detect(EVTYPE_CLEAN, "\\bHEAT\\b") ~ "HEAT",
str_detect(EVTYPE_CLEAN, "COLD|WIND CHILL|LOW TEMPERATURE") ~ "COLD/WIND CHILL",
str_detect(EVTYPE_CLEAN, "WINTER STORM") ~ "WINTER STORM",
str_detect(EVTYPE_CLEAN, "BLIZZARD") ~ "BLIZZARD",
str_detect(EVTYPE_CLEAN, "HAIL") ~ "HAIL",
str_detect(EVTYPE_CLEAN, "DROUGHT") ~ "DROUGHT",
str_detect(EVTYPE_CLEAN, "WILD|FOREST FIRE") ~ "WILDFIRE",
str_detect(EVTYPE_CLEAN, "RIP CURRENT") ~ "RIP CURRENT",
str_detect(EVTYPE_CLEAN, "STORM SURGE") ~ "STORM SURGE/TIDE",
str_detect(EVTYPE_CLEAN, "HIGH WIND|STRONG WIND|WIND") ~ "HIGH WIND",
str_detect(EVTYPE_CLEAN, "SNOW|WINTER") ~ "WINTER WEATHER",
str_detect(EVTYPE_CLEAN, "WIND RAIN|WIND HVY RAIN") ~ "HIGH WIND",
str_detect(EVTYPE_CLEAN, "URBAN SML STREAM FLD") ~ "FLOOD",
str_detect(EVTYPE_CLEAN, "HEAVY SURF HIGH SURF") ~ "HIGH SURF",
TRUE ~ EVTYPE_CLEAN
)
)
# Official 48 NOAA/NWS Storm Data “Event Types”
official_events <- c(
"ASTRONOMICAL LOW TIDE",
"AVALANCHE",
"BLIZZARD",
"COASTAL FLOOD",
"COLD/WIND CHILL",
"DEBRIS FLOW",
"DENSE FOG",
"DENSE SMOKE",
"DROUGHT",
"DUST DEVIL",
"DUST STORM",
"EXCESSIVE HEAT",
"EXTREME COLD/WIND CHILL",
"FLASH FLOOD",
"FLOOD",
"FROST/FREEZE",
"FUNNEL CLOUD",
"FREEZING FOG",
"HAIL",
"HEAT",
"HEAVY RAIN",
"HEAVY SNOW",
"HIGH SURF",
"HIGH WIND",
"HURRICANE/TYPHOON",
"ICE STORM",
"LAKESHORE FLOOD",
"LAKE-EFFECT SNOW",
"LIGHTNING",
"MARINE HAIL",
"MARINE HIGH WIND",
"MARINE STRONG WIND",
"MARINE THUNDERSTORM WIND",
"RIP CURRENT",
"SEICHE",
"SLEET",
"STORM SURGE/TIDE",
"STRONG WIND",
"THUNDERSTORM WIND",
"TORNADO",
"TROPICAL DEPRESSION",
"TROPICAL STORM",
"TSUNAMI",
"VOLCANIC ASH",
"WATERSPOUT",
"WILDFIRE",
"WINTER STORM",
"WINTER WEATHER"
)
##fuzzy matching the remaining values to the official list
# Unique values
map <- df %>%
distinct(EVTYPE_CLEAN) %>%
mutate(
idx = amatch(EVTYPE_CLEAN, official_events, method = "jw", maxDist = 0.15),
EVTYPE_FINAL = ifelse(!is.na(idx), official_events[idx], EVTYPE_CLEAN)
)
# inspect suspicious mappings (optional)
map %>% count(EVTYPE_FINAL, sort = TRUE) %>% head(20)
## EVTYPE_FINAL n
## 1 LIGHTNING 5
## 2 TROPICAL STORM 5
## 3 EXCESSIVE HEAT 4
## 4 WATERSPOUT 4
## 5 HEAVY RAIN 3
## 6 VOLCANIC ASH 3
## 7 ASTRONOMICAL LOW TIDE 2
## 8 AVALANCHE 2
## 9 DUST DEVIL 2
## 10 DUST STORM 2
## 11 FLASH FLOOD 2
## 12 HEAVY SNOW 2
## 13 TORNADO 2
## 14 1
## 15 ABNORMAL WARMTH 1
## 16 ABNORMALLY DRY 1
## 17 ABNORMALLY WET 1
## 18 AGRICULTURAL FREEZE 1
## 19 APACHE COUNTY 1
## 20 BEACH EROSIN 1
# apply mapping
df1 <- df %>%
left_join(map %>% select(EVTYPE_CLEAN, EVTYPE_FINAL), by = "EVTYPE_CLEAN")
#Check the top event types and see if this covers 95% of event types from official event types
df1 %>% count(EVTYPE_FINAL, sort = TRUE) %>% mutate (pct = n/ sum(n)) %>% head(20)
## EVTYPE_FINAL n pct
## 1 THUNDERSTORM WIND 336808 0.373308210
## 2 HAIL 289281 0.320630663
## 3 TORNADO 67688 0.075023414
## 4 FLASH FLOOD 55668 0.061700795
## 5 FLOOD 30451 0.033751004
## 6 HIGH WIND 26569 0.029448308
## 7 WINTER WEATHER 25813 0.028610380
## 8 LIGHTNING 15762 0.017470143
## 9 HEAVY RAIN 11773 0.013048851
## 10 WINTER STORM 11441 0.012680872
## 11 WILDFIRE 4232 0.004690626
## 12 WATERSPOUT 3847 0.004263903
## 13 BLIZZARD 2743 0.003040262
## 14 COLD/WIND CHILL 2501 0.002772036
## 15 DROUGHT 2495 0.002765386
## 16 ICE STORM 2006 0.002223392
## 17 EXCESSIVE HEAT 1702 0.001886447
## 18 FROST/FREEZE 1344 0.001489651
## 19 DENSE FOG 1293 0.001433124
## 20 HIGH SURF 962 0.001066253
Convert exponent columns (PROPDMGEXP/CROPDMGEXP) into multipliers.
NOAA uses letters and digits to indicate magnitude; some rows contain
symbols such as +, -, ?, or
blanks. We convert these to a numeric multiplier.
# Convert the exponents into multipliers
exp_to_multiplier <- function(exp) {
exp <- str_to_lower(str_trim(as.character(exp)))
case_when(
exp == "" ~ 0, # empty means no exponent
exp == "h" ~ 1e2, # hundreds
exp == "k" ~ 1e3, # thousands
exp == "m" ~ 1e6, # millions
exp == "b" ~ 1e9, # billions
str_detect(exp, "^[0-8]$") ~ 10, # digit exponent
exp %in% c("+", "?") ~ 1, # ambiguous symbols; common choice is treat as 1
exp == "-" ~ 0,
TRUE ~ NA_real_ # anything unexpected -> NA so you can inspect
)
}
#Calculate the total damage
df1 <- df1 %>%
mutate(
prop_mult = exp_to_multiplier(PROPDMGEXP),
crop_mult = exp_to_multiplier(CROPDMGEXP),
prop_damage = PROPDMG * prop_mult,
crop_damage = CROPDMG * crop_mult,
total_damage = prop_damage + crop_damage
)
health_by_event <- df1 %>%
mutate(
FATALITIES = as.numeric(FATALITIES),
INJURIES = as.numeric(INJURIES)
) %>%
group_by(EVTYPE_FINAL) %>%
summarise(
fatalities = sum(FATALITIES, na.rm = TRUE),
injuries = sum(INJURIES, na.rm = TRUE),
health_impact = fatalities + injuries,
.groups = "drop"
)
top_fatalities <- health_by_event %>% arrange(desc(fatalities)) %>% slice_head(n = 10)
top_injuries <- health_by_event %>% arrange(desc(injuries)) %>% slice_head(n = 10)
top_combined <- health_by_event %>% arrange(desc(health_impact)) %>% slice_head(n = 10) %>% mutate(event = reorder(EVTYPE_FINAL, health_impact))
ggplot(top_combined, aes(x = event, y = health_impact)) +
geom_col(fill = "steelblue") +
coord_flip() +
scale_y_continuous(labels = comma) +
labs(
title = "Top 15 NOAA Storm Event Types by Population Health Impact",
subtitle = "Health Impact = Fatalities + Injuries",
x = "Event Type",
y = "Total Health Impact (Fatalities + Injuries)"
) +
theme_minimal()
damage_by_event <- df1 %>%
mutate(
total_damage
) %>%
group_by(EVTYPE_FINAL) %>%
summarise(
total_damage = sum(total_damage, na.rm = TRUE),
.groups = "drop"
)
top_economic_consequence <- damage_by_event %>% arrange(desc(total_damage)) %>% slice_head(n = 10) %>% mutate(event = reorder(EVTYPE_FINAL, total_damage))
ggplot(top_economic_consequence, aes(x = event, y = total_damage)) +
geom_col(fill = "steelblue") +
coord_flip() +
scale_y_continuous(labels = comma) +
labs(
title = "Top 15 NOAA Storm Event Types by Economic Impact",
subtitle = "Economic Impact = Property + Crop",
x = "Event Type",
y = "Total Economic Impact (Property + Crop)"
) +
theme_minimal()