← Back to Project 1

NOAA Storm Events Data Downloader & Merger

Python script that pulls seven years of NOAA Storm Events files straight from NCEI, decompresses them in memory and merges them into one dataset for the Project 1 models.

# =============================================================================
# Title:       NOAA Storm Events Data Downloader & Merger
# Author:      Hali Sarah Parsons
# Date:        2026-09-22
# Modified By: Claude (portable output path, corrected download comment)
# Description: Downloads NOAA Storm Events detail CSV files for 2020-2026
#              directly from NCEI, decompresses them, and merges into one
#              clean dataset for use in DSC 450 Project 1 extreme weather
#              severity prediction model.
# =============================================================================

import os
import gzip
import requests
import pandas as pd
from io import BytesIO

# Output directory: the user's own Downloads folder. Built from the home
# directory so the script runs on any machine, not just the author's.
OUTPUT_DIR = os.path.join(os.path.expanduser("~"), "Downloads")
OUTPUT_FILE = os.path.join(OUTPUT_DIR, "noaa_storm_events_2020_2026.csv")

# Base URL for NOAA NCEI Storm Events bulk CSV files
BASE_URL = "https://www.ncei.noaa.gov/pub/data/swdi/stormevents/csvfiles/"

# File names for each year — pulled directly from the NOAA directory listing
# Format: StormEvents_details-ftp_v1.0_d{YEAR}_c{COMPILED_DATE}.csv.gz
FILES = {
    2020: "StormEvents_details-ftp_v1.0_d2020_c20260323.csv.gz",
    2021: "StormEvents_details-ftp_v1.0_d2021_c20260323.csv.gz",
    2022: "StormEvents_details-ftp_v1.0_d2022_c20260625.csv.gz",
    2023: "StormEvents_details-ftp_v1.0_d2023_c20260323.csv.gz",
    2024: "StormEvents_details-ftp_v1.0_d2024_c20260728.csv.gz",
    2025: "StormEvents_details-ftp_v1.0_d2025_c20260819.csv.gz",
    2026: "StormEvents_details-ftp_v1.0_d2026_c20260918.csv.gz",
}

def download_and_read(year, filename):
    """
    Downloads a gzipped CSV from NOAA, decompresses it in memory,
    and returns it as a DataFrame. Avoids saving intermediate files
    to disk — only the final merged CSV is saved.
    """
    url = BASE_URL + filename
    print(f"  Downloading {year}... ", end="", flush=True)

    try:
        # Each year's file is downloaded whole into memory; the 60-second
        # timeout keeps a stalled connection from hanging the whole run.
        response = requests.get(url, timeout=60)
        response.raise_for_status()

        # Decompress gzip in memory and read directly into pandas
        with gzip.open(BytesIO(response.content)) as f:
            df = pd.read_csv(f, low_memory=False)

        print(f"{len(df):,} records")
        return df

    except requests.exceptions.RequestException as e:
        # If a year fails, skip it and continue — don't abort the whole run
        print(f"FAILED ({e})")
        return None

def main():
    os.makedirs(OUTPUT_DIR, exist_ok=True)

    all_frames = []

    print("Downloading NOAA Storm Events data (2020-2026)...\n")

    for year, filename in FILES.items():
        df = download_and_read(year, filename)
        if df is not None:
            # Tag each row with its source year for traceability
            df["SOURCE_YEAR"] = year
            all_frames.append(df)

    if not all_frames:
        print("\nNo data downloaded. Check your internet connection.")
        return

    # Merge all years into one DataFrame
    print("\nMerging all years...")
    merged = pd.concat(all_frames, ignore_index=True)
    print(f"Total records: {len(merged):,}")
    print(f"Columns: {list(merged.columns)}")

    # Save merged CSV to Downloads
    merged.to_csv(OUTPUT_FILE, index=False)
    print(f"\nSaved to: {OUTPUT_FILE}")

if __name__ == "__main__":
    main()