173 lines
6.2 KiB
Python
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()
|