Refine climate metrics and data pipeline
This commit is contained in:
@@ -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."
|
||||
),
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user