Synopsis

  1. 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.

  2. 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.

  3. 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.

Data Processing

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.

1. Load Packages

library(dplyr)
library(ggplot2)
library(readr)
library(scales)
library(stringdist)
library(stringr)

2. Download and read the storm data

## 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)

3. Clean and standardize event types

##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
    )
  )

4. Map the event types to 48 official event categories from NOAA. We are ensuring that top 95% events map to official categories as it was difficult to map all events as there is a big tail end

# 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
  1. Calculate the Property Damage and Crop damage by converting the exponents into multiplier and then multiplying the multiplier with the damage.

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
  )

Results

1. Health impact

  • Health impact = fatalities + injuries
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()

2. Economic impact

  • Economic impact = property damage + crop damage (scaled using multipliers)
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()

Conclusions

  1. The Tornadoes are the most harmful to the population far exceeding any other event type
  2. The Flood causes the most harm to economy followed by Hurricane/Typhoon, Tornado and Storm Surge/Tide.