Dam Storage Totals Too High: One Reservoir Counted Many Times
On this page
Problem statement
Add up the storage column of the National Inventory of Dams and the United States seems to hold about 1,450 million acre-feet behind its dams, more than the true figure by hundreds of millions. Storage belongs to a reservoir, but the inventory lists structures. A reservoir held back by a main dam, saddle dikes and a spillway dam appears once for each, and every row carries the whole reservoir's storage. Usually those rows share one NID id, but some reservoirs' structures have ids of their own. One natural lake is in the inventory too. This page finds the three kinds of repetition and builds a total that counts each reservoir once.
Quick answer
Keep one row per NID id, then one row per large reservoir (the same storage of 10,000 acre-feet or more in the same state), and leave out the Great Lake:
naive = nid.nidStorage.sum()
per_id = nid.sort_values("isAssociatedStructureId").drop_duplicates("nidId")
big = per_id.nidStorage >= 10_000
per_res = pd.concat([per_id[~big], per_id[big].drop_duplicates(["nidStorage", "state"])])
fixed = per_res[per_res.name != "Soo Locks"].nidStorage.sum()
print(f"summed over rows: {naive / 1e6:,.1f} million acre-feet; one per NID id: {per_id.nidStorage.sum() / 1e6:,.1f}; "
f"one per reservoir: {per_res.nidStorage.sum() / 1e6:,.1f}; without the Soo Locks: {fixed / 1e6:,.1f}")
summed over rows: 1,448.4 million acre-feet; one per NID id: 1,136.6; one per reservoir: 1,090.3; without the Soo Locks: 812.8
Summed over every row, the inventory holds 1,448.4 million acre-feet. Keeping one row per NID id removes 311.8 million, and one row per large reservoir another 46.3 million. Leaving out the Soo Locks' Great Lake figure removes 277.5 million more. The result, 812.8 million acre-feet or about 1,000 kmΒ³, is 56% of the naive sum.
Three kinds of repetition
1. Associated structures
weiss = nid[nid.nidId == "AL01415"][["name", "isAssociatedStructureId", "nidHeight", "nidStorage"]]
print(weiss.to_string(index=False))
print(f"rows marked as associated structures: {(nid.isAssociatedStructureId == 'Yes').sum():,}; ids with more than one row: {nid.nidId.duplicated().groupby(nid.nidId).any().sum()}")
name isAssociatedStructureId nidHeight nidStorage
Weiss - Highway 411 Dike Yes 90 306400.0
Weiss - Saddle Dike A Yes 10 306400.0
Weiss - Spillway Diversion Dam Yes 90 306400.0
Weiss - Saddle Dike B Yes 10 306400.0
Weiss - Saddle Dike C Yes 10 306400.0
Weiss - Main Dam No 86 306400.0
rows marked as associated structures: 1,010; ids with more than one row: 437
Weiss Lake in Alabama is one reservoir behind a main dam, a spillway diversion dam, a highway dike and three saddle dikes, six rows under one id, each with the full 306,400 acre-feet. 1,010 rows are flagged as associated structures, but the flag is not complete: the seven segments of the Herbert Hoover Dike around Lake Okeechobee share an id without it. Deduplicate on the id, not on the flag.
2. Structures with ids of their own
seg = per_id[big & per_id.duplicated(["nidStorage", "state"], keep=False)]
print(f"large-storage rows sharing storage and state with another id: {len(seg)}, in {seg.groupby(['nidStorage', 'state']).ngroups} groups")
print(seg.sort_values('nidStorage', ascending=False)[['name', 'state', 'nidStorage', 'riverName']].head(6).to_string(index=False))
large-storage rows sharing storage and state with another id: 489, in 182 groups
name state nidStorage riverName
Clemson Lower Diversion Dam South Carolina 3438700.0 Seneca River
Clemson Upper Diversion Dam South Carolina 3438700.0 Seneca River
Spirit Lake Bia 5 North Dakota 3170994.0 Sheyenne River -TR
Spirit Lake Bia 4 North North Dakota 3170994.0 Sheyenne River -TR
Spirit Lake St. Michael 1 North Dakota 2437497.0 Sheyenne River -TR
Spirit Lake Spring Lake North Dakota 2437497.0 Sheyenne River -TR
489 large-storage rows in 182 groups share their exact storage and state with a dam under another id. They include Quabbin Reservoir's Winsor Dam, Goodnough Dike and spillway in Massachusetts, the Glendo dam and dikes in Wyoming, and the two diversion dams of Lake Hartwell's Clemson project in South Carolina, each listed with the full reservoir. River names don't help here: Glendo's dikes are on the "North Platte River Offstream" and the dam on the North Platte River.
3. A natural lake
print(nid.nlargest(3, "nidStorage")[["name", "state", "primaryPurposeId", "nidHeight", "nidStorage"]].to_string(index=False))
name state primaryPurposeId nidHeight nidStorage
Soo Locks Michigan Navigation 17 277540000.0
Hoover Dam Nevada Hydroelectric 730 30237000.0
Garrison Dam - Williston Levee North Dakota Flood Risk Reduction 28 26000000.0
The largest storage figure belongs to a 17-foot navigation lock: the Soo Locks at the outlet of Lake Superior, a natural lake. A 28-foot levee shares Garrison Dam's 26 million acre-feet, listed as its associated structure.
Checking the rule
The data used on this page
The US Army Corps of Engineers' National Inventory of Dams (NID), the whole inventory as a GeoPackage from its API, downloaded on 19 October 2026 and saved on this site, because the inventory is updated as states and agencies send revisions. Heights are in feet, storage in acre-feet and areas in acres, as the NID gives them.
from pathlib import Path
import zipfile
import geopandas as gpd
import numpy as np
import pandas as pd
import requests
Path("data").mkdir(exist_ok=True)
# US Army Corps of Engineers, National Inventory of Dams: the whole inventory as a GeoPackage, and the Census
# Bureau's 2024 cartographic boundary counties. Saved 19 October 2026, because the inventory is updated continuously.
# Live: https://nid.sec.usace.army.mil/api/nation/gpkg
ARCHIVE = "usace-nid-2026-10-19.zip"
if not Path(f"data/{ARCHIVE}").exists():
Path(f"data/{ARCHIVE}").write_bytes(requests.get(f"https://data.spatialworkflow.io/{ARCHIVE}", timeout=900).content)
Z = zipfile.ZipFile(f"data/{ARCHIVE}")
if not Path("data/nid.gpkg").exists():
Z.extract("nid.gpkg", "data")
NUMERIC = ["damHeight", "hydraulicHeight", "structuralHeight", "nidHeight", "damLength", "volume", "yearCompleted",
"nidStorage", "maxStorage", "normalStorage", "surfaceArea", "drainageArea", "maxDischarge"]
nid = gpd.read_file("data/nid.gpkg")
nid[NUMERIC] = nid[NUMERIC].apply(pd.to_numeric, errors="coerce") # the GeoPackage stores them as text
print(f"National Inventory of Dams: {len(nid):,} rows, {nid.nidId.nunique():,} NID ids")
National Inventory of Dams: 92,626 rows, 91,840 NID ids
Example 1: what the reservoir rule drops, and why only large ones
dropped = per_id[~per_id.index.isin(per_res.index)]
print(f"rows dropped by the reservoir rule: {len(dropped):,}; storage they repeated: {dropped.nidStorage.sum() / 1e6:,.1f} million acre-feet")
small = per_id[~big]
print(f"small dams sharing storage and state with another id: {small.duplicated(['nidStorage', 'state'], keep=False).sum():,}; most common shared figure: {small.nidStorage.mode().iloc[0]:,.0f} acre-feet")
rows dropped by the reservoir rule: 307; storage they repeated: 46.3 million acre-feet
small dams sharing storage and state with another id: 66,537; most common shared figure: 50 acre-feet
66,537 small dams share their storage figure and state with another dam, mostly because small dams are given round figures such as 50 acre-feet. Merging them would delete thousands of real reservoirs. Among large reservoirs an exact match is rare by chance, so the rule only applies from 10,000 acre-feet up, where it drops 307 rows and 46.3 million acre-feet of repeated storage.
Example 2: storage by state, before and after
b = pd.DataFrame({"rows": nid.groupby("state").nidStorage.sum(), "reservoirs": per_res[per_res.name != "Soo Locks"].groupby("state").nidStorage.sum()}) / 1e6
b["overcount"] = b.rows / b.reservoirs - 1
print(b.sort_values("rows", ascending=False).head(6).round(2).to_string())
rows reservoirs overcount
state
Michigan 282.04 3.47 80.19
Texas 114.60 92.29 0.24
North Dakota 91.60 59.59 0.54
California 84.32 46.44 0.82
Florida 69.07 16.60 3.16
Oklahoma 63.52 37.22 0.71
By state the overcount varies enormously. Michigan's rows sum to 80 times its reservoir storage because of the Soo Locks, and Florida's to more than four times because of the Herbert Hoover Dike. Texas, whose structures mostly have one row each, is overcounted by 24%. A ranking of states by summed rows is a ranking of how their dams are recorded.
Internal links
- Ranking reservoirs β the ranking this total feeds
- Reading the inventory β rows, structures and ids
- Nameplate or summer capacity β another national total that depends on which rows and columns you sum
- Population totals larger than the whole β another double count