Streszczenie

Raport przedstawia analizę bazy danych NOAA Storm Database z lat 1950–2011, oceniając wpływ ekstremalnych zjawisk pogodowych na zdrowie populacji oraz gospodarkę Stanów Zjednoczonych. W ramach przetwarzania danych przeprowadzono doglębną standaryzację kategorii zjawisk oraz przekształcono różne nieuporządkowane mnożniki finansowe w celu uzyskania rzeczywistych wartości strat. Analiza wpływu na zdrowie ludzi wykazała, że w ujęciu całkowitym tornada stanowią największe zagrożenie, generując największą liczbę rannych i ofiar. Z kolei ocena średnich skutków pojedynczego zdarzenia ujawnia, że najbardziej zgubne w momencie wystąpienia są ekstremalne upały oraz huragany. W aspekcie gospodarczym największe straty całkowite powodują powodzie oraz huragany, niszcząc przede wszystkim infrastrukturę na kwoty setek miliardów dolarów. Zupełnie inną specyfiką charakteryzują się susze, które z kolei stanowią największe strukturalne zagrożenie dla rolnictwa i upraw. Analiza potwierdza, że pojedyncze uderzenie huraganu generuje najwyższe średnie straty majątkowe.

Wyniki te wyraźnie wskazują na konieczność dywersyfikacji strategii zarządzania kryzysowego. Wymagane jest oddzielne planowanie zasobów ratunkowych dla częstych groźnych tornad oraz tworzenie dodatkowych licznych funduszy rezerwowych na rzadsze, lecz niszczycielskie uderzenia huraganów i długotrwałe susze.

Wczytanie i selekcja

W pierwszym kroku analizy wczytujemy oryginalny plik data_StormData.csv.bz2. Ponieważ zbiór zawiera blisko milion obserwacji i mógłby przeciążyć pamięć, staramy się zoptymalizawać pewne szczegóły ułatwiające pracę ze zbiorem danych. Używamy, na przykład włączamy cache = TRUE dla bloku kodu, co przyspieszy ponowne uruchomienie raportu (jak wskazano w instrukcji). Kluczowym działaniem jest natychmiastowe ograniczenie liczby kolumn. Z całej bazy wybraliśmy tylko osiem, które, wg. mnie, są niezbędne do odpowiedzi na dwa pytania badawcze, a mianowicie: datę zdarzenia, typ zjawiska, liczbę ofiar i rannych, wartość strat w majątku i uprawach wraz z ich mnożnikami. Dzięki temu oszczędzamy dużo pamięci, zachowując jednak możliwość przygotowania właściwych odpowiedzi na pytania. Załadowaliśmy również kilka wspomagających bibliotek do wizualizacji takich, jak ggplot2, dplyr i gridExtra.

library(dplyr)
library(readr)
library(lubridate)
library(ggplot2)
library(tidyr)
library(grid)
library(gridExtra) # dla peneli wykresów
storm_data_raw <- read_csv("data_StormData.csv.bz2") %>%
  select(
    BGN_DATE,    # Data zdarzenia
    EVTYPE,      # Typ zjawiska
    FATALITIES,  # Liczba ofiar
    INJURIES,    # Liczba rannych
    PROPDMG,     # Wartość strat majątku
    PROPDMGEXP,  # Mnożnik strat majątku (różne)
    CROPDMG,     # Wartość strat upraw
    CROPDMGEXP   # Mnożnik strat upraw (różne)
  )

str(storm_data_raw)
## tibble [902,297 × 8] (S3: tbl_df/tbl/data.frame)
##  $ BGN_DATE  : chr [1:902297] "4/18/1950 0:00:00" "4/18/1950 0:00:00" "2/20/1951 0:00:00" "6/8/1951 0:00:00" ...
##  $ EVTYPE    : chr [1:902297] "TORNADO" "TORNADO" "TORNADO" "TORNADO" ...
##  $ FATALITIES: num [1:902297] 0 0 0 0 0 0 0 0 1 0 ...
##  $ INJURIES  : num [1:902297] 15 0 2 2 2 6 1 0 14 0 ...
##  $ PROPDMG   : num [1:902297] 25 2.5 25 2.5 2.5 2.5 2.5 2.5 25 25 ...
##  $ PROPDMGEXP: chr [1:902297] "K" "K" "K" "K" ...
##  $ CROPDMG   : num [1:902297] 0 0 0 0 0 0 0 0 0 0 ...
##  $ CROPDMGEXP: chr [1:902297] NA NA NA NA ...

Przetwarzanie danych. Standaryzacja typów zjawisk

W danych surowych kolumna EVTYPE (czyli event type, także - typ zjawiska) zawiera ponad 900 unikalnych wartości, niemniej jednak wiele z nich to w rzeczywistości zjawiska identyczne lub prawie podobne zapisane z błędami, różną wielkością liter, zbędnymi spacjami lub skrótami (np. wszlkie formy slów “flood” lub “lightning”). Bez standaryzacji analiza skutków byłaby mocno zafałszowana. Różne odmiany opisu tornada czy pożarów lasu znajdowałyby się każdy w osobnej kategorii. Zamieniliśmy więc wszystkie nazwy na małe litery i usunęliśmy spacje funkcją stringr::str_trim, aby to naprawić. Następnie zastosowaliśmy instrukcję case_when z wyrażeniami regularnymi, która przypisuje każdy sposób opisywania zdarzenia do jednej z kilkunastu głównych grup. Dlatego np. wszystkie wpisy zawierające „flood”, „fld”, „stream”, „water” lub „urban” trafiły do udenoliconej kategorii flood. W ten sposób skorygowaliśmy nawet najcięższe przypadki literówek i same egzotyczne określenia zjawisk. Skrót tstm z kolei został przepisany do grupy wind / thunderstorm / hail razem z wiatrem i gradem.

Po czyszczeniu liczba unikalnych kategorii spadła do 35. Dzięki temu następujące agregacje strat i ofiar będą dokładne, a wykresy, z założenia, o wiele czytelniejsze. Na końcu, zrobiliśmy posortowaną listę wszystkich nowych kategorii do sprawdzenia, czy żadne istotne zjawisko nie zostało pominięte lub błędnie zaklasyfikowane (raczej bardziej dla siebie).

storm_data_clean <- storm_data_raw %>%
  mutate(
    evtype_lower = tolower(stringr::str_trim(EVTYPE)), # Małe litery, usunięcie spacji
    # Główna agregacja, wyrażenia regularne
EVTYPE = case_when(
      stringr::str_detect(evtype_lower, "flood|fld|stream|water|urban") ~ "flood",
      stringr::str_detect(evtype_lower, "wind|thunderstorm|tstm|hail|wnd|microburst|downburst|turbulence") ~ "wind / thunderstorm / hail",
      stringr::str_detect(evtype_lower, "tornado|torndao|spout|funnel|wall cloud|gustnado") ~ "tornado",
      stringr::str_detect(evtype_lower, "hurricane|typhoon|tropical|floyd") ~ "hurricane / tropical storm",
      stringr::str_detect(evtype_lower, "heat|warm|hot|hyperthermia|high temp|record high|record temp") ~ "heat",
      stringr::str_detect(evtype_lower, "cold|freeze|ice|snow|blizzard|winter|sleet|frost|chill|cool|glaze|hypothermia|wintry|mixed") ~ "winter / cold",
      stringr::str_detect(evtype_lower, "rain|precipitation|wet|shower") ~ "heavy rain",
      stringr::str_detect(evtype_lower, "lightn|lighti|ligtn") ~ "lightning",
      stringr::str_detect(evtype_lower, "dry|drought|driest") ~ "drought",
      stringr::str_detect(evtype_lower, "fire|wildfire|red flag") ~ "wildfire",
      stringr::str_detect(evtype_lower, "surge|tide|wave|surf|seas|current|swell|seiche|tsunami|drowning|marine|coastal|beach") ~ "storm surge / coastal event",
      stringr::str_detect(evtype_lower, "slide|slump") ~ "landslide / mudslide",
      stringr::str_detect(evtype_lower, "avalanc") ~ "avalanche",
      stringr::str_detect(evtype_lower, "dust") ~ "dust storm",
      stringr::str_detect(evtype_lower, "fog|smoke") ~ "fog / smoke",
      stringr::str_detect(evtype_lower, "volcanic|vog") ~ "volcanic activity",
      stringr::str_detect(evtype_lower, "summary|none|other|\\?|apache|northern|metro|no severe|monthly") ~ "ignored / unknown",
      
      TRUE ~ evtype_lower # Zabezpieczenie dla ewentualnych braków
    )
  ) %>%
  select(-evtype_lower)

# Sprawdzenie unikalnych kategorii przed i po czyszczeniu
length(unique(storm_data_raw$EVTYPE))
## [1] 977
length(unique(storm_data_clean$EVTYPE))
## [1] 35
View(data.frame(Typy = sort(unique(storm_data_clean$EVTYPE))))

Przetwarzanie danych. Standaryzacja strat ekonomicznych

W oryginalnym datasecie straty materialne i rolnicze zapisano w nietypowej formie. Są kolumny z wartością bazową ( np. PROPDMG, CROPDMG), a są kolumny z mnożnikami (PROPDMGEXP i CROPDMGEXP odpowiednio). Najprawdopodobniej w kolumnach mnożników litery K, M, B – oznaczają tysiące, miliony i biliardy dolarów, czyli inaczej precyzują skale zdarzenia. Co ciekawe, w kolumnach tych mogą się pojawić też błędne znaki, takie jak ?, +, - lub puste wartości. Bez poprawnej interpretacji te rekordy zostałyby źle ujęte w analizie. Dylemat tutaj polega na tym, iż jeśli strata 5 w kolumnie PROPDM z mnożnikiem M to 5 milionów dolarów (jasny przypadek), ale jak należy potraktować ten sam wpis z symbolem + - nie jest to koniecznie jasne.

Na tym etapie niemniej najpierw zamieniliśmy mnożniki K, M, B na odpowiadające im wartości liczbowe (10³, 10⁶ czy 10⁹). Wszystkie pozostałe znaki (a także ich brak) potraktowaliśmy jako mnożnik równy 1, innymi słowy zdecydowaliśmy się nie zmieniać w tym niejasnym przypadku wartości bazowej. Następnie utworzyliśmy nowe kolumny PropDamage i CropDamage, po prostu przemnożywszy wartości bazowe. Na koniec zsumowaliśmy nowe znaczenia w kolumnie TotalDamage, która swoją drogą reprezentuje całkowite straty ekonomiczne zdarzenia.

Po tej transformacji usunęliśmy oryginalne kolumny z wartościami bazowymi i mnożnikami (bo nie są już potrzebne). Sprawdziliśmy strukturę finalnego zbioru oraz podstawowe statystyki dla ofiar, rannych i strat. Po tych wszystkich przekształceniach każdy rekord ma w końcu mieć jednoznacznie określone straty w walucie, co pozwola na klarowne porównanie wpływu różnych typów zjawisk na gospodarkę.


storm_data_final <- storm_data_clean %>%
  mutate(
    # Dla majątku
    prop_mult = case_when(
      toupper(PROPDMGEXP) == "K" ~ 1000,
      toupper(PROPDMGEXP) == "M" ~ 1000000,
      toupper(PROPDMGEXP) == "B" ~ 1000000000,
      TRUE ~ 1
    ),
    # Dla upraw
    crop_mult = case_when(
      toupper(CROPDMGEXP) == "K" ~ 1000,
      toupper(CROPDMGEXP) == "M" ~ 1000000,
      toupper(CROPDMGEXP) == "B" ~ 1000000000,
      TRUE ~ 1
    ),
    PropDamage = PROPDMG * prop_mult,
    CropDamage = CROPDMG * crop_mult,
    TotalDamage = PropDamage + CropDamage # Całkowity koszt
  ) %>%
  # Usunięcie zbędnych kolumn mnożników
  select(-PROPDMG, -PROPDMGEXP, -CROPDMG, -CROPDMGEXP, -prop_mult, -crop_mult)

str(storm_data_final)
## tibble [902,297 × 7] (S3: tbl_df/tbl/data.frame)
##  $ BGN_DATE   : chr [1:902297] "4/18/1950 0:00:00" "4/18/1950 0:00:00" "2/20/1951 0:00:00" "6/8/1951 0:00:00" ...
##  $ EVTYPE     : chr [1:902297] "tornado" "tornado" "tornado" "tornado" ...
##  $ FATALITIES : num [1:902297] 0 0 0 0 0 0 0 0 1 0 ...
##  $ INJURIES   : num [1:902297] 15 0 2 2 2 6 1 0 14 0 ...
##  $ PropDamage : num [1:902297] 25000 2500 25000 2500 2500 2500 2500 2500 25000 25000 ...
##  $ CropDamage : num [1:902297] 0 0 0 0 0 0 0 0 0 0 ...
##  $ TotalDamage: num [1:902297] 25000 2500 25000 2500 2500 2500 2500 2500 25000 25000 ...
summary(storm_data_final %>% 
          select(FATALITIES, INJURIES, PropDamage, CropDamage, TotalDamage))
##    FATALITIES           INJURIES           PropDamage       
##  Min.   :  0.00000   Min.   :   0.0000   Min.   :0.000e+00  
##  1st Qu.:  0.00000   1st Qu.:   0.0000   1st Qu.:0.000e+00  
##  Median :  0.00000   Median :   0.0000   Median :0.000e+00  
##  Mean   :  0.01678   Mean   :   0.1557   Mean   :4.736e+05  
##  3rd Qu.:  0.00000   3rd Qu.:   0.0000   3rd Qu.:5.000e+02  
##  Max.   :583.00000   Max.   :1700.0000   Max.   :1.150e+11  
##    CropDamage         TotalDamage      
##  Min.   :0.000e+00   Min.   :0.00e+00  
##  1st Qu.:0.000e+00   1st Qu.:0.00e+00  
##  Median :0.000e+00   Median :0.00e+00  
##  Mean   :5.442e+04   Mean   :5.28e+05  
##  3rd Qu.:0.000e+00   3rd Qu.:1.00e+03  
##  Max.   :5.000e+09   Max.   :1.15e+11

Wyniki. Wpływ zjawisk pogodowych na zdrowie ludzi

Przypominając, pierwsze pytanie badawcze prosi zindentyfikować zjawiska pogodowe, stanowiące największe zagrożenie dla zdrowia i życia ludzi w USA. W surowym zbiorze danych skutki te są rozbite na wypadki kończące śmiercią (FATALITIES) oraz rannych (INJURIES). Aby spojrzeć na ten problem kompleksowo, trzeba wziąć pod uwagę pewne ew. nieścisłości. Zjawiska występujące bardzo często mogą generować ogromne straty łączne, podczas gdy zdarzenia rzadsze mogą być znacznie bardziej zgubne w skali 1 przypadku. Podzieliliśmy analizę więc na dwie perspektywy (analogicznie do PKB): całkowite obciążenie oraz średnią dotkliwość każdego zjawiska.

Zsumowaliśmy najpierw liczbę ofiar i rannych dla poszczególnych typów zjawisk, aby wyłonić dziesiątkę najbardziej zauważalnych zdarzeń. Z drugiej strony do obliczenia średniej podzieliliśmy łączną liczbę poszkodowanych przez ogólną liczbę wystąpień danego zjawiska. Odfiltrowaliśmy również zjawiska, które wystąpiły w bazie mniej niż 50 razy. Zrobiliśmy to celowo, aby uniknąć błędów statystycznych wywołanych odizolowanymi anomaliami, które mogłyby zaburzyć wiarygodność. Następnie przekształciliśmy zbiory tak, by ułatwić zestawienie dwóch typów na jednym wykresie.

Z pierwszej części panelu na wykresie A jasno wynika, że w ujęciu absolutnym liderem pod względem statystyk są tornada. Generują one rzeczywiście najwięcej rannych, daleko w tyle zostawiając wszelkie inne zjawiska. Z kolei analiza średnich na wykresie B dostarcza nieco innej perspektywy bardziej lokalnej w pewnym ujęciu. Okazuje się, że właśnie ekstremalne upały oraz huragany charakteryzują się najwyższym wskaźnikiem poszkodowanych w skali jednego zdazrenia. Co więcej, upały wyróżniają się na tle innych wyjątkowo wysokim odsetkiem ofiar co do liczby rannych. Po tych przekształceniach można stwierdzić, iż ocena ryzyka w gruncie zależy od tego, czy zmierzamy się z częstym masowym zjawiskiem, czy z rzadszym, lecz wysoce zgubnym uderzeniem.

# Całkowity wpływ na zdrowie
health_totals <- storm_data_final %>%
  group_by(EVTYPE) %>%
  summarise(
    Fatalities = sum(FATALITIES),
    Injuries = sum(INJURIES),
    Total_Harm = Fatalities + Injuries
  ) %>%
  arrange(desc(Total_Harm)) %>%
  head(10) %>%
  pivot_longer(cols = c(Fatalities, Injuries), names_to = "Impact_Type", values_to = "Count")

# Średni wpływ na zdrowie
health_averages <- storm_data_final %>%
  group_by(EVTYPE) %>%
  summarise(
    Event_Count = n(),
    Fatalities = sum(FATALITIES) / Event_Count,
    Injuries = sum(INJURIES) / Event_Count,
    Avg_Harm = Fatalities + Injuries
  ) %>%
  filter(Event_Count > 50) %>% # Filtracja rzadkich zjawisk
  arrange(desc(Avg_Harm)) %>%
  head(10) %>%
  pivot_longer(cols = c(Fatalities, Injuries), names_to = "Impact_Type", values_to = "Average_Count")

health_colors <- c("Fatalities" = "#d73027", "Injuries" = "#fdae61")

# Wykres A
plot_A <- ggplot(health_totals, aes(x = reorder(EVTYPE, Count, FUN = sum), y = Count, fill = Impact_Type)) +
  geom_col() +
  coord_flip() +
  scale_fill_manual(values = health_colors) +
  theme_minimal() +
  labs(
    title = "A: Całkowita liczba poszkodowanych",
    x = "",
    y = "Liczba osób",
    fill = "Typ"
  ) +
  theme(legend.position = "bottom")

# Wykres B
plot_B <- ggplot(health_averages, aes(x = reorder(EVTYPE, Average_Count, FUN = sum), y = Average_Count, fill = Impact_Type)) +
  geom_col() +
  coord_flip() +
  scale_fill_manual(values = health_colors) +
  theme_minimal() +
  labs(
    title = "B: Średnia liczba poszkodowanych na 1 zdarzenie",
    x = "",
    y = "Średnia liczba osób",
    fill = "Typ"
  ) +
  theme(legend.position = "bottom")

# Połączenie w jeden panel
grid.arrange(plot_A, plot_B, ncol = 2, top = textGrob("Wpływ ekstremalnych zjawisk pogodowych w USA (wymiar ludzki)", gp = gpar(fontsize = 16, fontface = "bold")))

Wyniki. Wpływ zjawisk pogodowych na straty ekonomiczne

Po drugie, staraliśmy się odpowiedzieć na pytanie, które zjawiska pogodowe powodują największe straty finansowe. W oryginalnym zbiorze zniszczenia zostały rozdzielone na szkody materialne dotyczące infrastruktury oraz szkody dla sektoru rolniczego. Przy badaniu tego aspektu napotykamy na analogiczny podwójny sposób ujęcia tego problemu. Katastrofy masowe, np. podtopienia, kumulują ogromny koszt w strategicznej (zawierającej lata lub nawet dekady), podczas gdy zjawiska o charakterze nagłym, jak np. huragany, potrafią zmarnować budżety już w wymiarze operacyjnym (dni lub tygodni). Dlatego znów dzielimy wizualizację na dwie nieco odmienne perspektywy: strukturę całkowitych kosztów wyrażoną w miliardach oraz strukturę średnich strat przypadających na pojedynczy wydarzenie.

Zaczynamy od grupowania danych, aby zsumować straty w majątku oraz w uprawach dla dziesięciu najbardziej kosztownych typów zdarzeń w ujęciu bezwzględnym. Jednocześnie w ramach wyznaczania uszczerbku pojedynczego uderzenia, obliczyliśmy go średni koszt poprzez podzielenie sumy zniszczeń przez ogólną liczbę zarejestrowanych zdarzeń. Dalej także odfiltrowaliśmy zjawiska o częstotliwości mniejszej niż 50 razy, co pozwoliło odrzucić błędy rejestracyjne i skrajne anomalie. Na koniec obie podgrupy danych na wszelki wypadek przekształciliśmy do formatu długiego (np. econ_long_avg), co z kolei pomogło stworzyć bardziej informacyjne wykresy, które w obrębie każdego słupka oddzielają straty infrastrukturalne od rolniczych (generalnie mniej więcej tak samo jak robiliśmy to w części 4).

Wykres A wskazuje jednoznacznie, że w wymiarze absolutnym największe obciążenie budżetowe generują powodzie, zbliżające się do granicy 180 miliardów dolarów. Na drugim miejscu są huragany. W obu tych przypadkach zniszczenia dotyczą infrastruktury publicznej i prywatnej. Zupełnie inny profil posiadają natomiast susze (drought), które choć w ogólnym rankingu zajmują dalszą pozycję, wykazują niemal stuprocentową dominację w stratach sektora rolniczego. Dodatkowe wnioski dostarcza analiza średnich na wykresie B. Generalnie jeden huragan powoduje straty materialne przekraczające 80 milionów dolarów. To dużo więcej niż w przypadku powodzi, których jest wprawdzie więcej, lecz każda pojedyncza powódź sprawia naprawdę mniejsze szkody. I znów, zarządzanie funduszami rezerwowymi faktycznie wymaga elastyczności, polegającej np. na równoległym budowaniu miejskich programów przeciwko powodziom oraz oddzielnych funduszy stabilizacyjnych dla rolnictwa po klęskach typu suszy.

# Struktura całkowitych strat
econ_long <- storm_data_final %>%
  group_by(EVTYPE) %>%
  summarise(Property = sum(PropDamage, na.rm = TRUE)/1e9, Crop = sum(CropDamage, na.rm = TRUE)/1e9, Total = Property + Crop) %>%
  arrange(desc(Total)) %>% head(10) %>%
  pivot_longer(cols = c(Property, Crop), names_to = "Damage_Type", values_to = "Amount")

# Średnie straty na 1 zdarzenie
econ_averages <- storm_data_final %>%
  group_by(EVTYPE) %>%
  summarise(
    Event_Count = n(),
    Property = sum(PropDamage, na.rm = TRUE) / Event_Count / 1e6, 
    Crop = sum(CropDamage, na.rm = TRUE) / Event_Count / 1e6,
    Total_Avg = sum(TotalDamage, na.rm = TRUE) / Event_Count / 1e6 
  ) %>%
  filter(Event_Count > 50) %>% 
  arrange(desc(Total_Avg)) %>%
  head(10)
econ_long_avg <- econ_averages %>%
  pivot_longer(cols = c(Property, Crop), names_to = "Damage_Type", values_to = "Amount")

econ_colors <- c("Property" = "#2c7bb6", "Crop" = "#abdda4")

# Wykres A
plot_A <- ggplot(econ_long, aes(x = reorder(EVTYPE, Total), y = Amount, fill = Damage_Type)) +
  geom_col() + coord_flip() + theme_minimal() +
  scale_fill_manual(values = econ_colors, labels = c("Uprawy", "Majątek")) +
  labs(title = "A: Struktura strat (Mld USD)", x = "", y = "Mld USD", fill = "") +
  theme(legend.position = "bottom")

# Wykres B
plot_B <- ggplot(econ_long_avg, aes(x = reorder(EVTYPE, Total_Avg), y = Amount, fill = Damage_Type)) +
  geom_col() + coord_flip() + scale_fill_manual(values = econ_colors, labels = c("Uprawy", "Majątek")) + theme_minimal() +
  labs(title = "B: Struktura strat na 1 zdarzenie (Mln USD)", x = "", y = "Mln USD", fill = "") +
  theme(legend.position = "bottom")

# Połączenie w jeden panel
grid.arrange(plot_A, plot_B, ncol = 2, top = textGrob("Wpływ ekstremalnych zjawisk w USA (wymiar ekonomiczny)", gp = gpar(fontsize = 16, fontface = "bold")))