#!/usr/bin/env python3 """ Merge selected gridMET humidity and heat metrics into climate-data.csv. Currently adds: - avgSummerSpecificHumidityGKg - humidHeatDays - humidHeatSourceFips - humidHeatFipsAdjustment """ from __future__ import annotations import argparse import csv from pathlib import Path DEFAULT_CLIMATE_DATA = Path("data/climate-data.csv") DEFAULT_GRIDMET_HUMIDITY = Path("data/gridmet/county_gridmet_humidity.csv") METRIC_FIELDS = [ "avgSummerSpecificHumidityGKg", "humidHeatDays", "humidHeatSourceFips", "humidHeatFipsAdjustment", ] SOURCE_TAGS_BY_FIELD = { "avgSummerSpecificHumidityGKg": "gridmet-specific-humidity-1991-2020", "humidHeatDays": "heat-index-noaa-tmax-gridmet-rmin-1991-2020", } def load_gridmet_values(path: Path) -> dict[str, dict[str, str]]: values: dict[str, dict[str, str]] = {} with path.open("r", encoding="utf-8", newline="") as handle: reader = csv.DictReader(handle) fieldnames = reader.fieldnames or [] missing_fields = [field for field in METRIC_FIELDS if field not in fieldnames] if missing_fields: raise ValueError(f"{path} is missing fields: {', '.join(missing_fields)}") for row in reader: county_fips = (row.get("countyFips") or "").strip().zfill(5) if county_fips: values[county_fips] = { field: (row.get(field) or "").strip() for field in METRIC_FIELDS } return values def append_source_tag(source: str, tag: str) -> str: clean_source = source.strip() if tag in clean_source: return clean_source return f"{clean_source} + {tag}".strip(" +") def ensure_field_after(fieldnames: list[str], field: str, after_field: str | None = None) -> None: if field in fieldnames: return if after_field and after_field in fieldnames: fieldnames.insert(fieldnames.index(after_field) + 1, field) return insert_at = fieldnames.index("source") if "source" in fieldnames else len(fieldnames) fieldnames.insert(insert_at, field) def merge_metrics(climate_data: Path, humidity_csv: Path) -> tuple[int, dict[str, int]]: gridmet_by_fips = load_gridmet_values(humidity_csv) with climate_data.open("r", encoding="utf-8", newline="") as handle: reader = csv.DictReader(handle) fieldnames = list(reader.fieldnames or []) rows = list(reader) if "countyFips" not in fieldnames: raise ValueError(f"{climate_data} is missing countyFips") ensure_field_after(fieldnames, "avgSummerSpecificHumidityGKg") ensure_field_after(fieldnames, "humidHeatDays", "avgSummerSpecificHumidityGKg") ensure_field_after(fieldnames, "humidHeatSourceFips", "humidHeatDays") ensure_field_after(fieldnames, "humidHeatFipsAdjustment", "humidHeatSourceFips") missing_counts = {field: 0 for field in METRIC_FIELDS} for row in rows: county_fips = (row.get("countyFips") or "").strip().zfill(5) gridmet_values = gridmet_by_fips.get(county_fips, {}) for field in METRIC_FIELDS: value = gridmet_values.get(field, "") row[field] = value if not value: missing_counts[field] += 1 for field, tag in SOURCE_TAGS_BY_FIELD.items(): if row.get(field): row["source"] = append_source_tag(row.get("source", ""), tag) with climate_data.open("w", encoding="utf-8", newline="") as handle: writer = csv.DictWriter(handle, fieldnames=fieldnames) writer.writeheader() writer.writerows(rows) return len(rows), missing_counts def main() -> int: parser = argparse.ArgumentParser(description="Merge gridMET humidity and Heat Index metrics into climate-data.csv.") parser.add_argument("--climate-data", type=Path, default=DEFAULT_CLIMATE_DATA) parser.add_argument("--humidity-csv", type=Path, default=DEFAULT_GRIDMET_HUMIDITY) args = parser.parse_args() row_count, missing_counts = merge_metrics(args.climate_data, args.humidity_csv) print(f"Updated {args.climate_data} with {', '.join(METRIC_FIELDS)}") print(f"Rows: {row_count}") for field, missing_count in missing_counts.items(): print(f"Missing {field}: {missing_count}") return 0 if __name__ == "__main__": raise SystemExit(main())