148 lines
5.2 KiB
Python
148 lines
5.2 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Merge NSRDB cloudiness metrics into climate-data.csv.
|
|
|
|
Adds:
|
|
- cloudinessIndexPct
|
|
|
|
Polygon area-weighted values are used first when available; representative-point
|
|
values remain the fallback.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import csv
|
|
from pathlib import Path
|
|
|
|
|
|
DEFAULT_CLIMATE_DATA = Path("data/climate-data.csv")
|
|
DEFAULT_POLYGON_CLOUD_SUMMARY = Path("data/nrel/county_polygon_cloud_summary.csv")
|
|
DEFAULT_REPRESENTATIVE_POINT_CLOUD_SUMMARY = Path("data/nrel/county_representative_point_cloud_summary.csv")
|
|
METRIC_FIELD = "cloudinessIndexPct"
|
|
POLYGON_SOURCE_TAG = "nsrdb-polygon-area-weighted-cloudiness-tmy"
|
|
REPRESENTATIVE_POINT_SOURCE_TAG = "nsrdb-representative-point-cloudiness-tmy"
|
|
CLOUD_SOURCE_TAGS = {
|
|
POLYGON_SOURCE_TAG,
|
|
REPRESENTATIVE_POINT_SOURCE_TAG,
|
|
}
|
|
|
|
|
|
def load_cloud_values(path: Path) -> dict[str, str]:
|
|
"""Read cloudiness values keyed by county FIPS."""
|
|
values: dict[str, str] = {}
|
|
with path.open("r", encoding="utf-8-sig", newline="") as handle:
|
|
reader = csv.DictReader(handle)
|
|
fieldnames = reader.fieldnames or []
|
|
required_fields = {"county_fips", METRIC_FIELD}
|
|
missing_fields = sorted(required_fields - set(fieldnames))
|
|
if missing_fields:
|
|
raise ValueError(f"{path} is missing fields: {', '.join(missing_fields)}")
|
|
|
|
for row in reader:
|
|
county_fips = (row.get("county_fips") or "").strip().zfill(5)
|
|
value = (row.get(METRIC_FIELD) or "").strip()
|
|
if county_fips and value:
|
|
values[county_fips] = value
|
|
return values
|
|
|
|
|
|
def append_source_tag(source: str, tag: str) -> str:
|
|
"""Append a source tag if it is not already present."""
|
|
parts = [
|
|
part.strip()
|
|
for part in source.split("+")
|
|
if part.strip() and part.strip() not in CLOUD_SOURCE_TAGS
|
|
]
|
|
if tag not in parts:
|
|
parts.append(tag)
|
|
return " + ".join(parts)
|
|
|
|
|
|
def ensure_field_after(fieldnames: list[str], field: str, after_field: str | None = None) -> None:
|
|
"""Insert a field into CSV field order if it is missing."""
|
|
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_metric(
|
|
climate_data: Path,
|
|
polygon_cloud_summary: Path,
|
|
representative_point_cloud_summary: Path,
|
|
) -> tuple[int, int, int, int]:
|
|
"""Merge cloudiness values into the app climate CSV."""
|
|
polygon_cloudiness_by_fips = (
|
|
load_cloud_values(polygon_cloud_summary)
|
|
if polygon_cloud_summary.exists()
|
|
else {}
|
|
)
|
|
representative_cloudiness_by_fips = load_cloud_values(representative_point_cloud_summary)
|
|
|
|
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, METRIC_FIELD, "avgSolarGhiKwhM2Day")
|
|
|
|
polygon_count = 0
|
|
representative_count = 0
|
|
missing_count = 0
|
|
for row in rows:
|
|
county_fips = (row.get("countyFips") or "").strip().zfill(5)
|
|
polygon_value = polygon_cloudiness_by_fips.get(county_fips, "")
|
|
representative_value = representative_cloudiness_by_fips.get(county_fips, "")
|
|
value = polygon_value or representative_value
|
|
row[METRIC_FIELD] = value
|
|
if polygon_value:
|
|
row["source"] = append_source_tag(row.get("source", ""), POLYGON_SOURCE_TAG)
|
|
polygon_count += 1
|
|
elif representative_value:
|
|
row["source"] = append_source_tag(row.get("source", ""), REPRESENTATIVE_POINT_SOURCE_TAG)
|
|
representative_count += 1
|
|
else:
|
|
missing_count += 1
|
|
|
|
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), polygon_count, representative_count, missing_count
|
|
|
|
|
|
def main() -> int:
|
|
parser = argparse.ArgumentParser(description="Merge NSRDB cloudiness metric into climate-data.csv.")
|
|
parser.add_argument("--climate-data", type=Path, default=DEFAULT_CLIMATE_DATA)
|
|
parser.add_argument("--polygon-cloud-summary", type=Path, default=DEFAULT_POLYGON_CLOUD_SUMMARY)
|
|
parser.add_argument(
|
|
"--representative-point-cloud-summary",
|
|
type=Path,
|
|
default=DEFAULT_REPRESENTATIVE_POINT_CLOUD_SUMMARY,
|
|
)
|
|
args = parser.parse_args()
|
|
|
|
row_count, polygon_count, representative_count, missing_count = merge_metric(
|
|
args.climate_data,
|
|
args.polygon_cloud_summary,
|
|
args.representative_point_cloud_summary,
|
|
)
|
|
print(f"Updated {args.climate_data} with {METRIC_FIELD}")
|
|
print(f"Rows: {row_count}")
|
|
print(f"Polygon area-weighted rows: {polygon_count}")
|
|
print(f"Representative-point fallback rows: {representative_count}")
|
|
print(f"Missing {METRIC_FIELD}: {missing_count}")
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|