Skip to content

Latest commit

 

History

2 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

wattprint

County-level accountability for the data-centre electricity cost fight — where every assumption is a toggle you can flip.

Three credible institutions have looked at whether data centres are raising electricity costs for everyone else, and reached what appear to be opposite conclusions. wattprint is a versioned ETL pipeline that reproduces both sides on one dataset, at county level, with the methodology in the open and every contestable choice exposed as a setting rather than buried in code.

Status: v0.1 — the county-year price panel, with full provenance. The two estimators land in v0.2 and v0.3. What is built works end to end and is validated against published figures; what is not built is marked as not built.


The dispute

Who Claim What it actually measures
Monitoring Analytics (PJM's Independent Market Monitor) Data centres drove $6.3B of ~$16.4B in 2028/29 capacity charges — 38%; $29.4B of $63.6B across four auctions Forward capacity procurement, 2025–29, on a scarce grid
E3 (commissioned by the Data Center Coalition, May 2026) No quantitative evidence of historical cross-subsidy; ~half the PJM price rise is load growth, half is market design and retirements Literature review + auction analysis
EPRI (Watten, Bistline & Blanford, June 2026) Each doubling of data-centre capacity → ~3.5% decrease in retail rates; rates would be ~6% higher absent the 2019–24 buildout Realised retail rates, 2015–24, on a surplus grid

The thesis

EPRI and the IMM are not contradicting each other. They are measuring different quantities in different regimes.

On a surplus grid, new load spreads fixed costs over more kWh and average rates fall. On a scarce grid, new load climbs a steep supply curve and procurement costs rise. Both can be true, in sequence, on the same system.

That reframing is cheap to assert and hard to demonstrate. wattprint aims to demonstrate it: estimate the rate effect on rolling windows, overlay the forward capacity result on the same axis, and then test it as a coefficient rather than a picture —

log(rate) = β₀·log₂(DC) + β₁·[log₂(DC) × reserve_margin] + county FE + year FE + ε

H₀: β₁ = 0. Rejecting it is the formal statement of "both findings are right, in different regimes." That is the most defensible contribution this repo can make, and it arrives in v0.3.

Getting the numbers attached to the right auction

Three of the most-quoted figures in this fight belong to three different PJM delivery years, and coverage routinely conflates them:

Delivery year Clearing price ($/MW-day) Announced
2024/2025 28.92 2023-07
2025/2026 269.92 2024-07-30
2026/2027 329.17 (at collar cap) 2025-07-22
2027/2028 333.44 (at collar cap) 2025-12-17
2028/2029 325.00 2026-07-14
  • $333.44/MW-day is the 2027/2028 auction, not 2025/2026.
  • The ~833% jump that started the fight is $28.92 → $269.92, 2024/25 → 2025/26.
  • ~5,250 MW of forecast load growth, ~5,100 MW of it data centres, is the 2027/2028 forecast versus 2026/2027.
  • $6.3B is the 2028/2029 auction — a different auction from the $333.44 one — and it is the IMM's figure, not PJM's. The two publicly disagree.

A trap worth knowing: 134,479 MW × $333.44 × 365 and 138,318 MW × $325.00 × 365 both come to roughly $16.4B, so sources citing "$16.4 billion" for different auctions look contradictory but are not.

This series is pinned as a test fixture (tests/fixtures/pjm_bra_clearing_prices.yaml), so a wrong attribution fails a test rather than shipping.


The thing you should know before quoting any number here

County-level retail electricity price does not exist in any dataset.

EIA-861 publishes revenue and sales at utility × state × sector. A separate table lists which counties each utility serves, with no weight attached. Every county rate in this repo — and in anyone else's — is constructed by choosing a weight and splitting the state total.

wattprint's response is not to hide that but to make it the primary control:

  • allocation.weight (population / customers / area / equal) is the headline toggle.
  • gold_allocation_sensitivity publishes the min/max/spread across every scheme for every county-year. No county rate is presented without its band.
  • Within one utility, every county gets an identical rate by construction, so the effective unit of variation is utility × state × year. Standard errors cluster at the utility, and any regression reports effective N beside raw N.

How much does the choice actually matter? Population vs land-area weighting, 30,983 county-years:

Identical >1% apart >10% apart Median diff p95 Max
15.4% 47.7% 2.3% 0.91% 7.05% 148%

About 1% for a typical county — with a real tail.


Does it reproduce anything?

Yes, and the gaps are explained rather than smoothed.

Rate construction vs EIA's published 2023 averages, before any county allocation. EIA reports energy and delivery service types describing the same electrons (identical MWh to the digit); naively summing all three understates the rate by ~13%.

Sector wattprint EIA published
Residential 16.01 ¢/kWh 15.98
Commercial 12.96 12.75
Industrial 8.21 8.06
All sectors 12.92 12.72

The county panel re-aggregates to 1.4–3.8% above EIA's published national average — the same direction every year. That offset is fully accounted for: the panel places 88.5–91.7% of national residential MWh into counties, and the unplaced remainder is 60.9% Retail Power Marketers and 11.2% Community Choice Aggregators — competitive suppliers who own no wires and therefore have no service territory. Their sales are ~75% energy-only and average 10.4 ¢/kWh against 14.3 ¢/kWh for placed sales.

So the offset is definitional: a wattprint county rate is a bundled, incumbent-utility rate. In restructured states it is not what a customer who shops actually pays. gold_panel_coverage publishes this per year.

Full detail: docs/REPLICATION_LOG.md.


Quick start

Requires Python 3.11+. No Docker, no orchestration tooling, no API keys — every v0.1 source is public and unauthenticated.

git clone https://github.com/Akasi132/wattprint
cd wattprint
python -m venv .venv
.venv\Scripts\activate            # Windows;  source .venv/bin/activate elsewhere
pip install -e ".[dev]"

wattprint doctor                  # is every source still where we think it is?
wattprint fetch --all             # ~6.6 MB, content-addressed, provenance logged
wattprint build --scenario baseline
wattprint show gold_county_price_panel

Then flip the main assumption and watch the answer move:

wattprint build --scenario allocation_area
wattprint diff baseline allocation_area

Trace any number back to the bytes it came from:

wattprint lineage gold_county_price_panel --scenario baseline

Scenarios

Each is a small readable diff against baseline via extends:.

Scenario What it is for
baseline Population-weighted residential rates, 2015–2024
allocation_area Land-area weighting — a deliberately poor proxy, to bound sensitivity
allocation_equal Every served county weighted equally — the null hypothesis of allocation
epri_replication EPRI's window and state-level clustering, for like-for-like comparison
imm_replication The IMM's forward-capacity framing
adversarial_e3 Tuned to make this repo's own likely conclusion look weakest

adversarial_e3 is not decoration. If wattprint looks built to reach a predetermined answer it is worthless as a referee, so the strongest version of the case against our result ships in the repo, runs in CI, and is displayed next to the baseline.

How "every transformation is versioned" is made real

Three mechanisms, all mechanical:

Content-addressed raw + committed ledger. Downloads land at data/raw/<source>/<sha256[:12]>/. Re-fetching unchanged bytes is a no-op; changed bytes land beside the old ones. data/raw/manifest.jsonl is committed — the bulk data is not, the ledger describing it is. This is how you learn PJM quietly reissued a report.

Deterministic stages. Every stage is a pure function of declared inputs and the scenario — no I/O, no clock, no network inside a stage body. Verified by hashing frame content (not Parquet bytes, which embed writer metadata).

Lineage beside every output. _lineage.json records input hashes, raw checksums, package versions, git SHA, and code_dirty — a result built from an uncommitted tree is labelled as such everywhere it is displayed.

The assumption boundary is the load-bearing design decision: bronze and silver contain zero assumptions; only gold depends on the scenario. Silver stores every allocation weight side by side as columns, so switching the primary toggle is a column selection rather than a full ETL rerun — gold rebuilds in under a second, which is what makes precomputing hundreds of scenarios for a static site feasible. A test asserts no non-gold stage reads the scenario, because a silver stage that did would keep serving a stale table after the assumption changed.

Data sources

Source License Role
PUDL (Catalyst Cooperative) CC-BY-4.0 EIA-861 service territories + retail sales. Already normalises county FIPS and carries population/area weights.
US Census Gazetteer Public domain Pins the county FIPS vintage
Epoch AI (v0.2) CC-BY ~75 largest AI data centres with capacity — a sample, not a census
PJM RPM (v0.3) PJM ToS Base Residual Auction results
IM3 Data Center Atlas (v0.3) ODbL OSM-derived siting. Share-alike, so opt-in only

Deliberately excluded, with reasons recorded in config/sources.yaml and enforced by a test: all FERC data (Form 1's taxonomy moved in March 2026 across ~116 poorly-normalised tables; Form 714 is balancing-authority grain; eLibrary has no public API), and the commercial data-centre directories whose terms prohibit scraping (Baxtel, datacenters.com, DataCenterMap, ringmast4r/Global-Data-Center-Map).

Licensing

Code is MIT. Data is not — see LICENSE-DATA.md. No bulk data is committed, which is a license boundary rather than a size preference: IM3 is ODbL share-alike, and committing ODbL-derived tables into an MIT repo would attempt to relicense them. License class propagates automatically through lineage (strictest input wins), and tests/test_licenses.py fails the build if data is ever tracked.

Roadmap

  • v0.1 ✅ County-year price panel, provenance, contracts, allocation sensitivity, coverage accounting
  • v0.2 Epoch AI siting → treatment; EPRI-style panel regression (Spec A state-level, Spec B county-level); self-contained HTML report
  • v0.3 PJM BRA parser + VRR reconstruction + capacity counterfactual swept over supply elasticity; the regime-interaction test; IM3 opt-in; GitHub Pages site with client-side toggles
  • v0.4+ Hourly PJM load; ERCOT; shift-share IV; EIA's data-centre survey once it exits pilot (after 2026-09-30)

Read before citing

  • docs/KNOWN_BIASES.md — every limitation that could change how you read a number. Test-enforced.
  • docs/REPLICATION_LOG.md — where our numbers land against published figures, including the gaps.
  • SPEC.md — the methodology, pre-registered before the pipeline was run.

Citing

Kasi, A. (2026). wattprint: county-level data center energy accountability. https://github.com/Akasi132/wattprint

Please cite the underlying data too — PUDL (CC-BY-4.0, DOI 10.5281/zenodo.3653158) and the US Census Bureau.

About

County-level accountability for the data center electricity cost fight - a versioned ETL where every assumption is a toggle. Reproduces both sides of the PJM/EPRI dispute on one dataset.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages