Initial Climate Mood Analysis project

This commit is contained in:
Justin Fisher
2026-06-11 14:50:33 -04:00
commit 18e099ce84
34 changed files with 18807 additions and 0 deletions
@@ -0,0 +1,163 @@
#!/usr/bin/env python3
"""
Rebuild county representative-point GHI summaries from cached NSRDB raw CSV responses.
This does not call the NSRDB API. It reads data/nrel/representative_point_csv/*.csv, computes:
avgSolarGhiKwhM2Day = sum(hourly GHI) / 1000 / 365
and writes a county-keyed CSV compatible with build_county_climate_data.py.
"""
from __future__ import annotations
import argparse
import csv
from pathlib import Path
DEFAULT_POINTS_CSV = Path("data/nrel/county_representative_points.csv")
DEFAULT_REPRESENTATIVE_POINT_CSV_DIR = Path("data/nrel/representative_point_csv")
DEFAULT_OUTPUT_CSV = Path("data/nrel/county_representative_point_ghi_summary.csv")
def read_county_points(points_csv: Path) -> list[dict[str, str]]:
"""Read county representative points keyed by county FIPS."""
with points_csv.open(newline="", encoding="utf-8") as handle:
return list(csv.DictReader(handle))
def extract_ghi_values(csv_path: Path) -> list[float]:
"""Extract numeric hourly GHI values from a cached NSRDB CSV response."""
with csv_path.open(newline="", encoding="utf-8-sig") as handle:
reader = csv.reader(handle)
ghi_index: int | None = None
values: list[float] = []
for row in reader:
if ghi_index is None:
if {"Year", "Month", "Day", "Hour", "Minute", "GHI"}.issubset(set(row)):
ghi_index = row.index("GHI")
continue
if len(row) <= ghi_index or not row[ghi_index].strip():
continue
values.append(float(row[ghi_index]))
if ghi_index is None:
raise ValueError(f"Could not find NSRDB data header with a GHI column in {csv_path}.")
if not values:
raise ValueError(f"No GHI values were found in {csv_path}.")
return values
def select_raw_csv(representative_point_csv_dir: Path, county_fips: str) -> Path | None:
"""Return the cached raw CSV for a county, preferring polar data when both exist."""
matches = sorted(representative_point_csv_dir.glob(f"{county_fips}_*_ghi.csv"))
if not matches:
return None
polar_matches = [path for path in matches if "_polar-tmy_" in path.name]
return polar_matches[0] if polar_matches else matches[0]
def source_label(raw_csv: Path) -> str:
"""Return a readable source label from the cached filename."""
if "_polar-tmy_" in raw_csv.name:
return "NSRDB Polar TMY PSM v4 representative point"
if "_goes-tmy_" in raw_csv.name:
return "NSRDB GOES TMY PSM v4 representative point"
return "NSRDB TMY representative point"
def summarize_county(point: dict[str, str], raw_csv: Path) -> dict[str, str]:
"""Summarize one county raw CSV into the app solar metric."""
ghi_values = extract_ghi_values(raw_csv)
avg_daily_ghi = sum(ghi_values) / 1000 / 365
return {
"county_fips": point["county_fips"],
"county_name": point["county_name"],
"state_fips": point["state_fips"],
"state_abbr": point["state_abbr"],
"lat": point["lat"],
"lon": point["lon"],
"avgSolarGhiKwhM2Day": f"{avg_daily_ghi:.3f}",
"ghi_rows": str(len(ghi_values)),
"ghi_min": f"{min(ghi_values):.1f}",
"ghi_max": f"{max(ghi_values):.1f}",
"source": source_label(raw_csv),
"raw_csv": str(raw_csv),
}
def write_summary_csv(rows: list[dict[str, str]], output_csv: Path) -> None:
"""Write rebuilt county solar summaries."""
output_csv.parent.mkdir(parents=True, exist_ok=True)
with output_csv.open("w", newline="", encoding="utf-8") as handle:
writer = csv.DictWriter(
handle,
fieldnames=[
"county_fips",
"county_name",
"state_fips",
"state_abbr",
"lat",
"lon",
"avgSolarGhiKwhM2Day",
"ghi_rows",
"ghi_min",
"ghi_max",
"source",
"raw_csv",
],
)
writer.writeheader()
writer.writerows(rows)
def parse_args() -> argparse.Namespace:
"""Parse command-line arguments."""
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--points-csv", type=Path, default=DEFAULT_POINTS_CSV)
parser.add_argument(
"--representative-point-csv-dir",
dest="representative_point_csv_dir",
type=Path,
default=DEFAULT_REPRESENTATIVE_POINT_CSV_DIR,
)
parser.add_argument("--output-csv", type=Path, default=DEFAULT_OUTPUT_CSV)
return parser.parse_args()
def main() -> None:
"""Run the cached raw CSV summarizer."""
args = parse_args()
rows: list[dict[str, str]] = []
missing: list[str] = []
failed: list[str] = []
for point in read_county_points(args.points_csv):
county_fips = point["county_fips"]
raw_csv = select_raw_csv(args.representative_point_csv_dir, county_fips)
if raw_csv is None:
missing.append(county_fips)
continue
try:
rows.append(summarize_county(point, raw_csv))
except (OSError, ValueError) as exc:
failed.append(f"{county_fips}: {exc}")
write_summary_csv(rows, args.output_csv)
print(f"Wrote {len(rows)} county representative-point solar summaries to {args.output_csv}")
if missing:
print(f"Missing raw CSVs for {len(missing)} counties: {', '.join(missing[:20])}")
if failed:
print(f"Failed to summarize {len(failed)} counties:")
for message in failed[:20]:
print(f" {message}")
if __name__ == "__main__":
main()