Files
Climate-Mood-Analysis/scripts/apply_precipitation_month_metrics_to_climate_data.py
T
2026-06-11 14:50:33 -04:00

173 lines
6.2 KiB
Python

#!/usr/bin/env python3
"""
Add wettest/driest precipitation month metrics to data/climate-data.csv.
The metrics use the existing monthly NOAA nClimGrid precipitation file. They
are categorical month labels derived from 1991-2020 county-area means.
"""
from __future__ import annotations
import argparse
import csv
from pathlib import Path
import numpy as np
import xarray as xr
from build_county_climate_data import (
MONTH_NAMES,
_as_monthly_climatology,
_extract_grid_2d,
_load_counties,
_select_data_var,
_zonal_mean,
)
REPO_ROOT = Path(__file__).resolve().parents[1]
DEFAULT_CLIMATE_DATA = REPO_ROOT / "data" / "climate-data.csv"
DEFAULT_COUNTIES_GEOJSON = REPO_ROOT / "data" / "geojson-counties-fips.json"
DEFAULT_MONTHLY_PRCP_NC = REPO_ROOT / "data" / "noaa" / "nclimgrid" / "nclimgrid_prcp.nc"
PRECIP_MONTH_FIELDS = ["wettestPrecipMonth", "driestPrecipMonth"]
def normalize_fips(value: object) -> str:
"""Return a 5-digit county FIPS string from mixed text or numeric input."""
digits = "".join(ch for ch in str(value).strip() if ch.isdigit())
return digits.zfill(5)[-5:] if digits else ""
def read_csv_rows(path: Path) -> tuple[list[str], list[dict]]:
"""Read a CSV while preserving the source field order."""
with path.open("r", encoding="utf-8-sig", newline="") as csv_file:
reader = csv.DictReader(csv_file)
if reader.fieldnames is None:
raise ValueError(f"{path} has no CSV header.")
return list(reader.fieldnames), list(reader)
def fieldnames_with_precip_months(original_fields: list[str]) -> list[str]:
"""Place the new fields next to the existing precipitation metrics."""
fields = [field for field in original_fields if field not in PRECIP_MONTH_FIELDS]
insert_after = "seasonalityIndex"
insert_at = fields.index(insert_after) + 1 if insert_after in fields else len(fields)
return fields[:insert_at] + PRECIP_MONTH_FIELDS + fields[insert_at:]
def build_precip_month_lookup(
*,
counties_geojson: Path,
monthly_prcp_nc: Path,
climatology_start_year: int,
climatology_end_year: int,
) -> dict[str, tuple[str, str]]:
"""Calculate wettest and driest precipitation month for each county."""
counties = _load_counties(counties_geojson)
monthly_prcp = xr.open_dataset(monthly_prcp_nc, decode_times=True)
try:
prcp_var = _select_data_var(monthly_prcp, "mlyprcp_norm")
prcp_monthly = _as_monthly_climatology(
monthly_prcp[prcp_var],
start_year=climatology_start_year,
end_year=climatology_end_year,
)
prcp_by_month: list[list[float]] = []
for month in range(12):
prcp_arr, prcp_transform = _extract_grid_2d(prcp_monthly.isel(time=month))
prcp_by_month.append(_zonal_mean(prcp_arr, prcp_transform, counties))
finally:
monthly_prcp.close()
lookup: dict[str, tuple[str, str]] = {}
for idx, row in counties.iterrows():
county_fips = str(row["county_fips"])
prcp_months_mm = np.array([prcp_by_month[m][idx] for m in range(12)], dtype=np.float64)
if not np.any(np.isfinite(prcp_months_mm)):
continue
wettest_month = MONTH_NAMES[int(np.nanargmax(prcp_months_mm))]
driest_month = MONTH_NAMES[int(np.nanargmin(prcp_months_mm))]
lookup[county_fips] = (wettest_month, driest_month)
return lookup
def apply_precipitation_month_metrics(
*,
climate_data: Path,
counties_geojson: Path,
monthly_prcp_nc: Path,
out: Path,
climatology_start_year: int,
climatology_end_year: int,
) -> None:
"""Merge precipitation month categories into the app climate CSV."""
original_fields, rows = read_csv_rows(climate_data)
lookup = build_precip_month_lookup(
counties_geojson=counties_geojson,
monthly_prcp_nc=monthly_prcp_nc,
climatology_start_year=climatology_start_year,
climatology_end_year=climatology_end_year,
)
updated_count = 0
missing_count = 0
for row in rows:
county_fips = normalize_fips(row.get("countyFips", ""))
values = lookup.get(county_fips)
if values:
row["wettestPrecipMonth"], row["driestPrecipMonth"] = values
updated_count += 1
else:
row["wettestPrecipMonth"] = ""
row["driestPrecipMonth"] = ""
missing_count += 1
source = (row.get("source") or "").strip() or "unknown-source"
tag = f"precip-month-extremes=noaa-nclimgrid-monthly-{climatology_start_year}-{climatology_end_year}"
if tag not in source:
row["source"] = f"{source} + {tag}"
fieldnames = fieldnames_with_precip_months(original_fields)
with out.open("w", encoding="utf-8", newline="") as csv_file:
writer = csv.DictWriter(csv_file, fieldnames=fieldnames, extrasaction="ignore")
writer.writeheader()
writer.writerows(rows)
print(f"Wrote {len(rows)} rows to {out}")
print(f"Updated precipitation month metrics for {updated_count} rows.")
print(f"Left precipitation month metrics blank for {missing_count} rows.")
def parse_args() -> argparse.Namespace:
"""Parse command-line paths for the precipitation month update."""
parser = argparse.ArgumentParser(
description="Add wettestPrecipMonth and driestPrecipMonth to climate-data.csv."
)
parser.add_argument("--climate-data", type=Path, default=DEFAULT_CLIMATE_DATA)
parser.add_argument("--counties-geojson", type=Path, default=DEFAULT_COUNTIES_GEOJSON)
parser.add_argument("--monthly-prcp-nc", type=Path, default=DEFAULT_MONTHLY_PRCP_NC)
parser.add_argument("--out", type=Path, default=DEFAULT_CLIMATE_DATA)
parser.add_argument("--climatology-start-year", type=int, default=1991)
parser.add_argument("--climatology-end-year", type=int, default=2020)
return parser.parse_args()
def main() -> None:
"""Run the CSV update."""
args = parse_args()
apply_precipitation_month_metrics(
climate_data=args.climate_data,
counties_geojson=args.counties_geojson,
monthly_prcp_nc=args.monthly_prcp_nc,
out=args.out,
climatology_start_year=args.climatology_start_year,
climatology_end_year=args.climatology_end_year,
)
if __name__ == "__main__":
main()