CAISO Generator Weather Map: CISO Plants and Recent Heat#
Authors: Claire Kim · Brian Nguyen ·
Created: 2026-07-31 ·
Tags: #Exploration #Weather #Geospatial
This notebook builds the interactive generator heat map from the EIA-860M generator inventory and recent Open-Meteo weather imputed onto plant coordinates.
Scope is California plants with balancing_authority == "CISO". EIA also carries a small number of CISO-tagged records outside California; California weather proxies are not a clean match for them, so they enter only as a sensitivity check.
Method#
Generator source:
data/opensource/eia860m_generators.parquet, pulled from EIA-860M.CAISO identifier: EIA uses
CISO, notCAISO, in thebalancing_authorityfield.Weather source: Open-Meteo archive API, no API key, recent hourly temperatures aggregated to daily HDD/CDD.
Imputation: each plant is assigned to the nearest configured representative Open-Meteo point by plant latitude/longitude.
Zone labels: the output keeps both CAISO market-zone labels (
NP15,SP15,ZP26) and NOAA/NCEI climate-division proxy labels so the load-zone/weather-zone mismatch remains visible.
import json
from pathlib import Path
import sys
import pandas as pd
import polars as pl
from IPython.display import display
PROJECT_ROOT = Path.cwd()
if PROJECT_ROOT.name == "notebooks":
PROJECT_ROOT = PROJECT_ROOT.parent
SRC_DIR = PROJECT_ROOT / "src"
if str(SRC_DIR) not in sys.path:
sys.path.insert(0, str(SRC_DIR))
from chart_theme import html_table
from generator_weather_map import (
DEFAULT_STATES,
build_generator_weather_map,
build_weather_zone_crosswalk,
ciso_inventory_scope_summary,
get_location_weights,
get_locations,
load_eia_generators,
)
DATA_DIR = PROJECT_ROOT / "data"
OUTPUT_DIR = PROJECT_ROOT / "_output"
PROCESSED_DIR = DATA_DIR / "processed"
pd.set_option("display.max_columns", 120)
pd.set_option("display.width", 180)
print(PROJECT_ROOT)
.
1. Confirm the Generator Scope#
CISO is EIA’s balancing-authority code for CAISO. The table counts what the California-only default keeps and drops.
generators = load_eia_generators(DATA_DIR)
scope_summary = ciso_inventory_scope_summary(generators, states=DEFAULT_STATES)
html_table(pl.from_pandas(scope_summary))
| scope | state | generator_rows | plants | nameplate_capacity_mw |
|---|---|---|---|---|
| excluded_out_of_scope_state | NM | 2 | 2 | 3,650.20 |
| excluded_out_of_scope_state | AZ | 31 | 14 | 2,904.20 |
| excluded_out_of_scope_state | NV | 31 | 10 | 1,756.40 |
| included | CA | 2,779 | 1,661 | 91,897.10 |
2. Load the Generator-Weather Data and Build the Map#
The imputed plant tables and the standalone HTML map are produced by the collab_generator_weather doit task (src/chart_caiso_generator_weather_map.py), which queries Open-Meteo only when the local recent-weather cache is missing or stale.
imputed_plants = pd.read_parquet(PROCESSED_DIR / "caiso_generator_weather_imputed.parquet")
daily_panel = pd.read_parquet(PROCESSED_DIR / "caiso_generator_weather_daily_imputed.parquet")
crosswalk = build_weather_zone_crosswalk(
get_locations("core"),
location_weights=get_location_weights("core"),
)
fig = build_generator_weather_map(imputed_plants, crosswalk=crosswalk)
print(f"Plant markers: {len(imputed_plants):,}")
print(f"Daily plant-weather rows: {len(daily_panel):,}")
Plant markers: 1,661
Daily plant-weather rows: 36,542
One row per plant marker, with the plant, zone, weather-proxy and recent-heat fields side by side.
preview_cols = [
"plant_id", "plant_name", "state", "county", "nameplate_capacity_mw",
"primary_technology_group", "region", "noaa_climate_division_proxy",
"nearest_weather_location", "distance_to_weather_proxy_km",
"recent_period_start", "recent_period_end", "recent_temperature_f_max",
"recent_cdd65_mean", "recent_cdd65_sum", "heat_stress_label",
]
html_table(pl.from_pandas(imputed_plants[preview_cols].head(10)))
| plant_id | plant_name | state | county | nameplate_capacity_mw | primary_technology_group | region | noaa_climate_division_proxy | nearest_weather_location | distance_to_weather_proxy_km | recent_period_start | recent_period_end | recent_temperature_f_max | recent_cdd65_mean | recent_cdd65_sum | heat_stress_label |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 6,099 | Diablo Canyon | CA | San Luis Obispo | 2,323.00 | Nuclear | ZP26 | San Joaquin Drainage | bakersfield | 167.67 | 2026-08-05 | 2026-08-26 | 111.50 | 24.92 | 548.15 | Extreme CDD |
| 260 | Dynegy Moss Landing Power Plant Hybrid | CA | Monterey | 1,848.00 | Natural gas | NP15 | Central Coast Drainage | san_jose | 60.02 | 2026-08-05 | 2026-08-26 | 88.30 | 4.24 | 93.32 | Warm CDD |
| 350 | Ormond Beach | CA | Ventura | 1,612.00 | Natural gas | SP15 | South Coast Drainage | los_angeles | 85.63 | 2026-08-05 | 2026-08-26 | 101.80 | 14.67 | 322.81 | Hot CDD |
| 286 | Geysers Unit 5-20 | CA | Sonoma | 1,236.00 | Geothermal | NP15 | Sacramento Drainage | sacramento | 110.71 | 2026-08-05 | 2026-08-26 | 103.50 | 11.76 | 258.70 | Hot CDD |
| 55,151 | La Paloma Generating Plant | CA | Kern | 1,156.00 | Natural gas | ZP26 | San Joaquin Drainage | bakersfield | 52.71 | 2026-08-05 | 2026-08-26 | 111.50 | 24.92 | 548.15 | Extreme CDD |
| 315 | AES Alamitos LLC | CA | Los Angeles | 1,115.00 | Natural gas | SP15 | South Coast Drainage | los_angeles | 34.16 | 2026-08-05 | 2026-08-26 | 101.80 | 14.67 | 322.81 | Hot CDD |
| 6,100 | Helms Pumped Storage | CA | Fresno | 1,053.00 | Hydro | ZP26 | San Joaquin Drainage | fresno | 79.71 | 2026-08-05 | 2026-08-26 | 109.50 | 23.46 | 516.17 | Extreme CDD |
| 358 | Mountainview Generating Station | CA | San Bernardino | 1,036.80 | Natural gas | SP15 | South Coast Drainage | los_angeles | 92.35 | 2026-08-05 | 2026-08-26 | 101.80 | 14.67 | 322.81 | Hot CDD |
| 64,210 | Bellefield Solar and Energy Storage Farm | CA | Kern | 1,000.00 | Battery | ZP26 | San Joaquin Drainage | bakersfield | 91.33 | 2026-08-05 | 2026-08-26 | 111.50 | 24.92 | 548.15 | Extreme CDD |
| 55,518 | High Desert Power Plant | CA | San Bernardino | 960.00 | Natural gas | SP15 | South Coast Drainage | los_angeles | 100.81 | 2026-08-05 | 2026-08-26 | 101.80 | 14.67 | 322.81 | Hot CDD |
capacity_by_region = (
imputed_plants.groupby(["region", "heat_stress_label"], observed=False)["nameplate_capacity_mw"]
.sum()
.reset_index()
.pivot(index="region", columns="heat_stress_label", values="nameplate_capacity_mw")
.fillna(0)
)
html_table(pl.from_pandas(capacity_by_region.reset_index()))
| region | Extreme CDD | Hot CDD | Warm CDD |
|---|---|---|---|
| NP15 | 0.00 | 13,024.40 | 7,040.50 |
| SP15 | 0.00 | 36,891.90 | 0.00 |
| ZP26 | 34,940.30 | 0.00 | 0.00 |
Central Valley / ZP26 carries the hottest exposure; coastal and northern plants are warm but less extreme. These are imputed proxy-point temperatures, not measured plant conditions.
3. Interactive Map#
Marker size is nameplate MW; marker color is recent mean daily CDD65 at the nearest Open-Meteo proxy. Hover or click a plant for its details.
Execution note: The figure is emitted twice - the Plotly mime type for live sessions, a self-contained CDN embed for static HTML exports. GitHub’s notebook preview runs neither.
fig.update_layout(height=780)
display(
{
# nbconvert and the site build drop the Plotly mime type, so the bundle
# also carries self-contained HTML; each front end picks the richest
# rendering it supports
"application/vnd.plotly.v1+json": json.loads(fig.to_json()),
"text/html": fig.to_html(full_html=False, include_plotlyjs="cdn"),
},
raw=True,
)
4. Daily Plant-Weather Panel#
One row per plant per day. A plant-level outage join needs a generator-level panel with plant IDs; the current aggregate outage file supports only a system-level join.
daily_cols = [
"date", "plant_id", "plant_name", "nameplate_capacity_mw",
"primary_technology_group", "region", "noaa_climate_division_proxy",
"nearest_weather_location", "temperature_f_mean", "temperature_f_max",
"cdd65", "hdd65", "total_cdh65", "total_hdh65",
]
html_table(pl.from_pandas(daily_panel[daily_cols].head(10)))
| date | plant_id | plant_name | nameplate_capacity_mw | primary_technology_group | region | noaa_climate_division_proxy | nearest_weather_location | temperature_f_mean | temperature_f_max | cdd65 | hdd65 | total_cdh65 | total_hdh65 |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 2026-08-05 | 34 | Rollins | 12.10 | Hydro | NP15 | Sacramento Drainage | sacramento | 75.20 | 98.60 | 10.20 | 0.000 | 223.10 | 9.000 |
| 2026-08-05 | 72 | Venice Hydro | 10.10 | Hydro | SP15 | South Coast Drainage | los_angeles | 74.03 | 90.80 | 9.03 | 0.000 | 189.70 | 0.000 |
| 2026-08-05 | 104 | J S Eastwood | 199.80 | Hydro | ZP26 | San Joaquin Drainage | fresno | 89.43 | 105.50 | 24.43 | 0.000 | 513.00 | 0.000 |
| 2026-08-05 | 180 | Volta 2 | 1.00 | Hydro | NP15 | Sacramento Drainage | sacramento | 75.20 | 98.60 | 10.20 | 0.000 | 223.10 | 9.000 |
| 2026-08-05 | 214 | Alta Powerhouse | 1.00 | Hydro | NP15 | Sacramento Drainage | sacramento | 75.20 | 98.60 | 10.20 | 0.000 | 223.10 | 9.000 |
| 2026-08-05 | 215 | Angels | 1.40 | Hydro | ZP26 | San Joaquin Drainage | merced | 83.32 | 101.50 | 18.32 | 0.000 | 384.80 | 0.000 |
| 2026-08-05 | 217 | Balch 1 | 31.00 | Hydro | ZP26 | San Joaquin Drainage | fresno | 89.43 | 105.50 | 24.43 | 0.000 | 513.00 | 0.000 |
| 2026-08-05 | 218 | Balch 2 | 97.20 | Hydro | ZP26 | San Joaquin Drainage | fresno | 89.43 | 105.50 | 24.43 | 0.000 | 513.00 | 0.000 |
| 2026-08-05 | 219 | Belden | 117.90 | Hydro | NP15 | Sacramento Drainage | sacramento | 75.20 | 98.60 | 10.20 | 0.000 | 223.10 | 9.000 |
| 2026-08-05 | 220 | Bucks Creek | 66.00 | Hydro | NP15 | Sacramento Drainage | sacramento | 75.20 | 98.60 | 10.20 | 0.000 | 223.10 | 9.000 |
Geography remains the caveat: plant coordinates, CAISO market hubs, load zones and NOAA weather proxies are related but not identical spatial layers.