Initial Climate Mood Analysis project
This commit is contained in:
@@ -0,0 +1,172 @@
|
||||
#!/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()
|
||||
Reference in New Issue
Block a user