Refine climate metrics and data pipeline

This commit is contained in:
2026-07-24 22:55:14 -04:00
parent 016f7386d8
commit 4e2d878e5b
32 changed files with 3557 additions and 3347 deletions
+19 -21
View File
@@ -13,7 +13,7 @@ Metrics produced per county:
- wettestPrecipMonth: month with the highest 1991-2020 county mean precipitation
- driestPrecipMonth: month with the lowest 1991-2020 county mean precipitation
- extremeDays: count of normal-days with Tmax >= hot threshold or Tmin <= freeze threshold
- avgSolarGhiKwhM2Day: annual average daily global horizontal irradiance (GHI), when a solar raster or representative-point CSV is provided
- meanDailyGlobalHorizontalRadiationKwhM2Day: mean daily global horizontal radiation (GHI), when a solar raster or representative-point CSV is provided
This script is intended for offline generation of complete county records.
"""
@@ -22,15 +22,15 @@ from __future__ import annotations
import argparse
import csv
import json
from pathlib import Path
from typing import Dict, List, Tuple
import geopandas as gpd
import numpy as np
import rasterio
from rasterio.features import geometry_mask
import xarray as xr
from affine import Affine
from rasterio.features import geometry_mask
DEFAULT_COUNTIES_GEOJSON_URL = "https://raw.githubusercontent.com/plotly/datasets/master/geojson-counties-fips.json"
@@ -261,7 +261,7 @@ def _select_data_var(dataset: xr.Dataset, preferred: str) -> str:
def _load_solar_ghi_csv(solar_ghi_csv: Path, counties: gpd.GeoDataFrame) -> List[float]:
"""Load county-keyed average daily GHI values from a representative-point or area-average CSV."""
"""Load county-keyed mean daily GHI values from a representative-point or area-average CSV."""
with solar_ghi_csv.open(newline="", encoding="utf-8") as handle:
reader = csv.DictReader(handle)
if reader.fieldnames is None:
@@ -276,22 +276,22 @@ def _load_solar_ghi_csv(solar_ghi_csv: Path, counties: gpd.GeoDataFrame) -> List
f"Solar GHI CSV at {solar_ghi_csv} must include county_fips or countyFips."
)
if "avgSolarGhiKwhM2Day" not in reader.fieldnames:
if "meanDailyGlobalHorizontalRadiationKwhM2Day" not in reader.fieldnames:
raise ValueError(
f"Solar GHI CSV at {solar_ghi_csv} must include avgSolarGhiKwhM2Day."
f"Solar GHI CSV at {solar_ghi_csv} must include meanDailyGlobalHorizontalRadiationKwhM2Day."
)
solar_by_fips: Dict[str, float] = {}
for row in reader:
county_fips = _normalize_fips(row.get(fips_field, ""), 5)
raw_value = str(row.get("avgSolarGhiKwhM2Day", "")).strip()
raw_value = str(row.get("meanDailyGlobalHorizontalRadiationKwhM2Day", "")).strip()
if not county_fips or not raw_value:
continue
try:
solar_by_fips[county_fips] = float(raw_value)
except ValueError as exc:
raise ValueError(
f"Invalid avgSolarGhiKwhM2Day value for county {county_fips}: {raw_value}"
f"Invalid meanDailyGlobalHorizontalRadiationKwhM2Day value for county {county_fips}: {raw_value}"
) from exc
return [
@@ -357,7 +357,7 @@ def _infer_time_resolution_days(data_array: xr.DataArray) -> float:
return float(np.median(deltas))
def _extract_grid_2d(data_array: xr.DataArray) -> Tuple[np.ndarray, "Affine"]:
def _extract_grid_2d(data_array: xr.DataArray) -> Tuple[np.ndarray, Affine]:
"""Convert a lat/lon slice to a raster array and transform."""
# Expected shape for 2D arrays: lat, lon
# Build affine from center coordinates.
@@ -376,8 +376,6 @@ def _extract_grid_2d(data_array: xr.DataArray) -> Tuple[np.ndarray, "Affine"]:
x_res = abs(lon[1] - lon[0])
y_res = abs(lat[0] - lat[1])
from affine import Affine
top_left_x = lon.min() - (x_res / 2.0)
top_left_y = lat.max() + (y_res / 2.0)
transform = Affine.translation(top_left_x, top_left_y) * Affine.scale(x_res, -y_res)
@@ -477,7 +475,7 @@ def _compute_extreme_days(
day_tmax = _zonal_mean(tmax_arr, tmax_transform, counties)
day_tmin = _zonal_mean(tmin_arr, tmin_transform, counties)
for idx, (mx, mn) in enumerate(zip(day_tmax, day_tmin)):
for idx, (mx, mn) in enumerate(zip(day_tmax, day_tmin, strict=True)):
if np.isnan(mx) or np.isnan(mn):
continue
if mx >= hot_threshold_c or mn <= freeze_threshold_c:
@@ -509,7 +507,7 @@ def _compute_extreme_days_monthly_proxy(
month_tmax = _zonal_mean(tmax_arr, tmax_transform, counties)
month_tmin = _zonal_mean(tmin_arr, tmin_transform, counties)
for idx, (mx, mn) in enumerate(zip(month_tmax, month_tmin)):
for idx, (mx, mn) in enumerate(zip(month_tmax, month_tmin, strict=True)):
if np.isnan(mx) or np.isnan(mn):
continue
if mx >= hot_threshold_c or mn <= freeze_threshold_c:
@@ -634,9 +632,9 @@ def build_county_records(
solar_source_tag = "solar-ghi-representative-point"
else:
if solar_ghi_raster is not None:
print(f"Solar GHI raster not found at {solar_ghi_raster}; leaving avgSolarGhiKwhM2Day blank.")
print(f"Solar GHI raster not found at {solar_ghi_raster}; leaving meanDailyGlobalHorizontalRadiationKwhM2Day blank.")
if solar_ghi_csv is not None:
print(f"Solar GHI CSV not found at {solar_ghi_csv}; leaving avgSolarGhiKwhM2Day blank.")
print(f"Solar GHI CSV not found at {solar_ghi_csv}; leaving meanDailyGlobalHorizontalRadiationKwhM2Day blank.")
solar_ghi_kwh_m2_day = [float("nan")] * len(counties)
solar_source_tag = "no-solar-ghi-source"
@@ -699,14 +697,14 @@ def build_county_records(
if not use_missing_extreme
else None
)
avg_solar_ghi_value = (
mean_daily_global_horizontal_radiation_value = (
round(float(solar_ghi_kwh_m2_day[idx]), 2)
if np.isfinite(solar_ghi_kwh_m2_day[idx])
else None
)
source_suffix = " + missing-noaa-numeric" if used_any_missing_numeric else ""
solar_source_suffix = "" if avg_solar_ghi_value is not None else " + missing-solar-ghi"
solar_source_suffix = "" if mean_daily_global_horizontal_radiation_value is not None else " + missing-solar-ghi"
record = {
"countyName": county_name,
@@ -718,7 +716,7 @@ def build_county_records(
"wettestPrecipMonth": wettest_precip_month,
"driestPrecipMonth": driest_precip_month,
"extremeDays": extreme_days_value,
"avgSolarGhiKwhM2Day": avg_solar_ghi_value,
"meanDailyGlobalHorizontalRadiationKwhM2Day": mean_daily_global_horizontal_radiation_value,
"source": (
"kg-beck2023 + noaa-nclimgrid-1991-2020 "
f"({extreme_days_source_tag}) + {solar_source_tag}{source_suffix}{solar_source_suffix}"
@@ -751,7 +749,7 @@ def write_csv(records: Dict[str, dict], out_file: Path) -> None:
"wettestPrecipMonth",
"driestPrecipMonth",
"extremeDays",
"avgSolarGhiKwhM2Day",
"meanDailyGlobalHorizontalRadiationKwhM2Day",
"source",
]
@@ -818,14 +816,14 @@ def parse_args() -> argparse.Namespace:
"--solar-ghi-raster",
type=Path,
default=None,
help="Optional raster of annual average daily GHI in kWh/m2/day for avgSolarGhiKwhM2Day.",
help="Optional raster of mean daily GHI in kWh/m2/day for meanDailyGlobalHorizontalRadiationKwhM2Day.",
)
parser.add_argument(
"--solar-ghi-csv",
type=Path,
default=None,
help=(
"Optional county CSV with avgSolarGhiKwhM2Day. Used as a representative-point fallback "
"Optional county CSV with meanDailyGlobalHorizontalRadiationKwhM2Day. Used as a representative-point fallback "
"when --solar-ghi-raster is not supplied."
),
)