364 lines
18 KiB
Markdown
364 lines
18 KiB
Markdown
# County Climate Data Sources and Mapping
|
|
|
|
This project now supports county polygons and county-level filter records keyed by 5-digit county FIPS.
|
|
|
|
## Launching the app
|
|
|
|
The browser blocks `fetch("data/climate-data.csv")` when `index.html` is opened directly as a `file://` URL, so run the site through a local HTTP server:
|
|
|
|
```powershell
|
|
.\serve.ps1
|
|
```
|
|
|
|
Then open [http://localhost:8000/](http://localhost:8000/). This keeps the app CSV-only while allowing the map and filters to load normally.
|
|
|
|
## Source 1: Koppen-Geiger classes (`koppenZone`)
|
|
|
|
- Dataset: Beck et al. updated 1-km Koppen-Geiger climate classes (historical + future windows)
|
|
- Landing page: [https://www.gloh2o.org/koppen/](https://www.gloh2o.org/koppen/)
|
|
- Primary paper for updated release: [https://www.nature.com/articles/s41597-023-02549-6](https://www.nature.com/articles/s41597-023-02549-6)
|
|
- Coverage: 1901-2099 (use historical 1991-2020 layer for this project to align with NOAA baselines)
|
|
- License shown on dataset page: CC BY 4.0
|
|
|
|
## Source 2: NOAA 1991-2020 gridded normals (`avgTempF`, `annualPrecipIn`, `seasonalityIndex`, previous `extremeDays`)
|
|
|
|
- Main product page: [https://www.ncei.noaa.gov/products/land-based-station/us-climate-normals](https://www.ncei.noaa.gov/products/land-based-station/us-climate-normals)
|
|
- Monthly gridded normals readme: [https://www.ncei.noaa.gov/sites/default/files/2022-04/Readme_Monthly_Gridded_Normals.pdf](https://www.ncei.noaa.gov/sites/default/files/2022-04/Readme_Monthly_Gridded_Normals.pdf)
|
|
- Daily gridded normals readme: [https://www.ncei.noaa.gov/sites/default/files/2022-09/Readme%20for%20Daily%20Gridded%20Normals%201991-2020.pdf](https://www.ncei.noaa.gov/sites/default/files/2022-09/Readme%20for%20Daily%20Gridded%20Normals%201991-2020.pdf)
|
|
- Daily gridded normals documentation (includes units in C/mm): [https://www.ncei.noaa.gov/sites/default/files/2022-09/Documentation_Daily_Gridded_Normals%20V1.0.pdf](https://www.ncei.noaa.gov/sites/default/files/2022-09/Documentation_Daily_Gridded_Normals%20V1.0.pdf)
|
|
|
|
The ETL script uses:
|
|
|
|
- Monthly `tavg` normals (C)
|
|
- Monthly `prcp` normals (mm)
|
|
- Daily `tmax` normals (C)
|
|
- Daily `tmin` normals (C)
|
|
|
|
If you have monthly nClimGrid history files (for example `nclimgrid_tavg.nc`) rather than 12-slice normals files:
|
|
|
|
- The script now computes a 1991-2020 monthly climatology from the time series automatically.
|
|
- `extremeDays` can run in `auto` mode, which uses a documented monthly proxy when daily grids are not provided.
|
|
|
|
## Source 3: County polygons / FIPS join geometry
|
|
|
|
- Current app geometry file: `data/geojson-counties-fips.json`
|
|
- Original Plotly county geometry source: [https://raw.githubusercontent.com/plotly/datasets/master/geojson-counties-fips.json](https://raw.githubusercontent.com/plotly/datasets/master/geojson-counties-fips.json)
|
|
- Official county geometry reference (Census TIGER/Line): [https://www.census.gov/geographies/mapping-files/time-series/geo/tiger-line-file.html](https://www.census.gov/geographies/mapping-files/time-series/geo/tiger-line-file.html)
|
|
|
|
## Source 4: Solar resource (`avgSolarGhiKwhM2Day`)
|
|
|
|
- Recommended dataset: NREL National Solar Radiation Database (NSRDB)
|
|
- Data/API page: [https://developer.nrel.gov/docs/solar/nsrdb/](https://developer.nrel.gov/docs/solar/nsrdb/)
|
|
- Maps/geospatial data page: [https://www.nrel.gov/gis/solar-resource-maps](https://www.nrel.gov/gis/solar-resource-maps)
|
|
- Fallback point API option: NASA POWER `ALLSKY_SFC_SW_DWN` (surface shortwave downwelling radiation), [https://power.larc.nasa.gov/docs/tutorials/service-data-request/api/](https://power.larc.nasa.gov/docs/tutorials/service-data-request/api/)
|
|
|
|
The app metric is designed for annual average daily global horizontal irradiance (GHI), in `kWh/m2/day`.
|
|
For county means, use a gridded annual GHI raster and pass it to the generator with `--solar-ghi-raster`.
|
|
|
|
### First test: NSRDB county representative points
|
|
|
|
For a lightweight first pass, sample each county at one interior representative point instead of requesting full county polygons.
|
|
This creates a point CSV:
|
|
|
|
```powershell
|
|
.venv\Scripts\python.exe scripts\build_county_representative_points.py
|
|
```
|
|
|
|
Then fetch a small NSRDB GOES TMY test batch.
|
|
If `--email` or `--api-key` are omitted, the script prompts you for them:
|
|
|
|
```powershell
|
|
.venv\Scripts\python.exe scripts\fetch_nsrdb_representative_point_ghi.py --limit 10
|
|
```
|
|
|
|
The fetch script uses GOES TMY first.
|
|
For high-latitude points, it can automatically retry the NSRDB Polar TMY endpoint when GOES reports no data.
|
|
If a county FIPS appears in `data/nrel/county_representative_point_ghi_error_log.csv` from an earlier run, that county tries the Polar endpoint first on the next run.
|
|
Disable this behavior with `--no-polar-fallback`.
|
|
|
|
Outputs:
|
|
|
|
- `data/nrel/county_representative_points.csv`: county FIPS, name, state, latitude, and longitude.
|
|
- `data/nrel/representative_point_csv/`: cached raw NSRDB CSV responses by county FIPS.
|
|
- `data/nrel/county_representative_point_ghi_summary.csv`: summarized `avgSolarGhiKwhM2Day` values.
|
|
|
|
The calculation is:
|
|
|
|
```text
|
|
avgSolarGhiKwhM2Day = sum(hourly GHI) / 1000 / 365
|
|
```
|
|
|
|
If the raw cache is complete but the summary CSV only contains the last fetched batch, rebuild the summary from cached files without calling the API:
|
|
|
|
```powershell
|
|
.venv\Scripts\python.exe scripts\rebuild_nsrdb_representative_point_ghi_summary.py
|
|
```
|
|
|
|
This point-based workflow is easier to validate and resume than full polygon downloads, but it is an approximation of county sunlight rather than an area-weighted county mean.
|
|
|
|
### First cloud-cover pass: NSRDB representative points
|
|
|
|
For a fast cloud-cover input layer, fetch representative-point NSRDB CSVs with observed GHI, Clearsky GHI, and Cloud Type:
|
|
|
|
```powershell
|
|
.venv\Scripts\python.exe scripts\fetch_nsrdb_representative_point_cloud_metrics.py --limit 10
|
|
```
|
|
|
|
Once the first batch looks right, fetch every county:
|
|
|
|
```powershell
|
|
.venv\Scripts\python.exe scripts\fetch_nsrdb_representative_point_cloud_metrics.py --all
|
|
```
|
|
|
|
Outputs:
|
|
|
|
- `data/nrel/representative_point_cloud_csv/`: cached raw NSRDB CSV responses with `ghi,clearsky_ghi,cloud_type`.
|
|
- `data/nrel/county_representative_point_cloud_summary.csv`: summarized cloud-cover proxy fields.
|
|
- `data/nrel/county_representative_point_cloud_error_log.csv`: failed county requests with redacted API context.
|
|
|
|
The primary calculation uses daylight rows where Clearsky GHI is at least 50 W/m2:
|
|
|
|
```text
|
|
cloudinessIndexPct = 1 - mean(clamped(GHI / Clearsky GHI, 0, 1))
|
|
```
|
|
|
|
The same summary also stores daylight row counts, observed-to-clear-sky ratio, and broad Cloud Type frequency buckets. This is faster than the polygon archive workflow, but it remains a representative-point county approximation.
|
|
|
|
Apply the representative-point cloudiness metric to the browser app CSV:
|
|
|
|
```powershell
|
|
.venv\Scripts\python.exe scripts\apply_nsrdb_cloud_metric_to_climate_data.py
|
|
```
|
|
|
|
The current cloud summary covers the 3,143 county representative points and leaves Puerto Rico rows blank in `data/climate-data.csv`.
|
|
|
|
### County-average target: NSRDB polygon cloud archive requests
|
|
|
|
For a less noisy county-level cloudiness layer, submit county polygons to the NSRDB archive workflow with `ghi,clearsky_ghi,cloud_type`. This uses the same polygon tiling and pacing logic as the GHI archive workflow, but writes separate cloud manifests, response JSON files, archives, and summaries.
|
|
|
|
Start with a dry run:
|
|
|
|
```powershell
|
|
.venv\Scripts\python.exe scripts\request_nsrdb_county_polygon_cloud_archives.py `
|
|
--dry-run `
|
|
--skip-site-count `
|
|
--limit 5
|
|
```
|
|
|
|
Then submit a small real batch:
|
|
|
|
```powershell
|
|
.venv\Scripts\python.exe scripts\request_nsrdb_county_polygon_cloud_archives.py `
|
|
--limit 5
|
|
```
|
|
|
|
Continue in resumable chunks:
|
|
|
|
```powershell
|
|
.venv\Scripts\python.exe scripts\request_nsrdb_county_polygon_cloud_archives.py `
|
|
--start 5 `
|
|
--limit 100
|
|
```
|
|
|
|
Download completed archives:
|
|
|
|
```powershell
|
|
.venv\Scripts\python.exe scripts\download_nsrdb_county_polygon_cloud_archives.py
|
|
```
|
|
|
|
Summarize downloaded archives into area-weighted county cloudiness:
|
|
|
|
```powershell
|
|
.venv\Scripts\python.exe scripts\summarize_nsrdb_county_polygon_cloud_archives.py --reuse-existing-output
|
|
```
|
|
|
|
Outputs:
|
|
|
|
- `data/nrel/county_polygon_cloud_request_manifest.csv`: submitted cloud archive requests.
|
|
- `data/nrel/county_polygon_cloud_error_log.csv`: cloud archive request errors.
|
|
- `data/nrel/polygon_cloud_request_responses/`: raw NSRDB cloud acknowledgement JSON files.
|
|
- `data/nrel/polygon_cloud_archives/`: downloaded cloud ZIP archives.
|
|
- `data/nrel/county_polygon_cloud_summary.csv`: area-weighted county cloudiness summaries.
|
|
|
|
Then update the browser app CSV. Polygon area-weighted values are used first when `data/nrel/county_polygon_cloud_summary.csv` exists; representative-point values remain the fallback:
|
|
|
|
```powershell
|
|
.venv\Scripts\python.exe scripts\apply_nsrdb_cloud_metric_to_climate_data.py
|
|
```
|
|
|
|
### County-average target: NSRDB polygon archive requests
|
|
|
|
For the final county-average solar layer, submit county polygons to the NSRDB archive workflow instead of sampling one representative point.
|
|
The polygon request returns a generated archive URL per county. Download those archives, then summarize them into a county polygon GHI CSV that can replace the representative-point solar values where available.
|
|
|
|
Start with a small dry run that does not touch the API:
|
|
|
|
```powershell
|
|
.venv\Scripts\python.exe scripts\request_nsrdb_county_polygon_ghi_archives.py `
|
|
--dry-run `
|
|
--skip-site-count `
|
|
--limit 5
|
|
```
|
|
|
|
Then submit a small real batch:
|
|
|
|
```powershell
|
|
.venv\Scripts\python.exe scripts\request_nsrdb_county_polygon_ghi_archives.py `
|
|
--limit 5
|
|
```
|
|
|
|
Once the first batch looks right, continue in resumable chunks:
|
|
|
|
```powershell
|
|
.venv\Scripts\python.exe scripts\request_nsrdb_county_polygon_ghi_archives.py `
|
|
--start 5 `
|
|
--limit 100
|
|
```
|
|
|
|
Or request every county from the current start point:
|
|
|
|
```powershell
|
|
.venv\Scripts\python.exe scripts\request_nsrdb_county_polygon_ghi_archives.py `
|
|
--all
|
|
```
|
|
|
|
Outputs:
|
|
|
|
- `data/nrel/county_polygon_ghi_request_manifest.csv`: one row per submitted county, including NSRDB profile, site count, request weight, response JSON path, and `download_url`.
|
|
- `data/nrel/county_polygon_ghi_error_log.csv`: counties that need retrying or tiling.
|
|
- `data/nrel/polygon_request_responses/`: raw API acknowledgement JSON files.
|
|
- `data/nrel/polygon_archives/`: downloaded county or tiled county ZIP archives.
|
|
- `data/nrel/county_polygon_ghi_summary.csv`: summarized polygon archive GHI, including `avgSolarGhiKwhM2Day`.
|
|
|
|
Download completed GHI archives:
|
|
|
|
```powershell
|
|
.venv\Scripts\python.exe scripts\download_nsrdb_county_polygon_ghi_archives.py
|
|
```
|
|
|
|
The script uses POST requests because county polygon WKT can be long.
|
|
It checks NSRDB site count by default and skips counties whose estimated request weight exceeds the API maximum.
|
|
Default pacing is one request every 2.1 seconds, matching the NSRDB archive limit.
|
|
Use `--include-puerto-rico` if you want to test Puerto Rico polygons too.
|
|
|
|
After the archives are downloaded, rebuild or resume the polygon summary:
|
|
|
|
```powershell
|
|
.venv\Scripts\python.exe scripts\summarize_nsrdb_county_polygon_archives.py --reuse-existing-output
|
|
```
|
|
|
|
Then update the app CSV. Polygon archive GHI is used first; representative-point GHI remains as a fallback for counties without polygon archive data:
|
|
|
|
```powershell
|
|
.venv\Scripts\python.exe scripts\apply_locally_extreme_metric_to_climate_data.py
|
|
```
|
|
|
|
## Metric definitions in generated output
|
|
|
|
- `koppenZone`: majority class within county polygon from Koppen raster.
|
|
- `avgTempF`: mean of 12 monthly county mean temperatures, converted C -> F.
|
|
- `annualPrecipIn`: sum of 12 monthly county mean precipitation totals, converted mm -> inches.
|
|
- `seasonalityIndex`: coefficient of variation of monthly precipitation totals, scaled to 0-100.
|
|
- `wettestPrecipMonth`: month with the highest 1991-2020 county mean precipitation total.
|
|
- `driestPrecipMonth`: month with the lowest 1991-2020 county mean precipitation total.
|
|
- previous `extremeDays` / `oldExtremeDays`: count of daily-normal or monthly-proxy days where county mean `tmax >= 95F` or `tmin <= 32F` (thresholds configurable in script). This is preserved only as an audit column after the NOAA nClimGrid-Daily metrics are applied.
|
|
- `avgSolarGhiKwhM2Day`: county mean annual average daily GHI, in `kWh/m2/day`. The current app CSV uses NSRDB polygon archive area-weighted values where available, with representative-point values kept as fallback.
|
|
- `cloudinessIndexPct`: representative-point NSRDB daylight cloudiness proxy derived from observed GHI divided by Clearsky GHI. Higher values mean observed irradiance is lower relative to modeled clear-sky irradiance. Despite the legacy field name, values are stored on a 0-1 scale.
|
|
|
|
## Run the generator
|
|
|
|
Install dependencies:
|
|
|
|
```powershell
|
|
pip install -r scripts/requirements_county_etl.txt
|
|
```
|
|
|
|
Run:
|
|
|
|
```powershell
|
|
python scripts/build_county_climate_data.py `
|
|
--counties-geojson data/geojson-counties-fips.json `
|
|
--koppen-raster data/koppen_geiger_tif/1991_2020/koppen_geiger_0p00833333.tif `
|
|
--koppen-legend data/koppen_geiger_tif/legend.txt `
|
|
--monthly-tavg-nc data/noaa/nclimgrid/nclimgrid_tavg.nc `
|
|
--monthly-prcp-nc data/noaa/nclimgrid/nclimgrid_prcp.nc `
|
|
--daily-tmax-nc data/noaa/nclimgrid/nclimgrid_tmax.nc `
|
|
--daily-tmin-nc data/noaa/nclimgrid/nclimgrid_tmin.nc `
|
|
--climatology-start-year 1991 `
|
|
--climatology-end-year 2020 `
|
|
--extreme-days-mode auto `
|
|
--solar-ghi-csv data/nrel/county_polygon_ghi_summary.csv `
|
|
--out data/climate-data.csv
|
|
```
|
|
|
|
Notes:
|
|
|
|
- This computes all counties in your geometry file, not just the sample records.
|
|
- For counties outside CONUS coverage in NOAA gridded files, fallback values are applied by the script when no valid grid values intersect.
|
|
- For physically-based daily `extremeDays`, provide true daily grids and set `--extreme-days-mode require-daily`.
|
|
- If `--counties-geojson` does not exist locally, the script will try to download the county GeoJSON automatically from the Plotly URL above and cache it at that path.
|
|
- `--solar-ghi-csv` is optional and can load county-keyed solar summaries into `avgSolarGhiKwhM2Day`.
|
|
- `--solar-ghi-raster` is optional and takes precedence over `--solar-ghi-csv`. If you have a gridded annual GHI raster, add `--solar-ghi-raster path/to/annual_ghi_kwh_m2_day.tif` for a true county-area raster mean.
|
|
|
|
## Source 5: NOAA nClimGrid-Daily county area averages (`locallyExtremeDays`)
|
|
|
|
- Main product page: [https://www.ncei.noaa.gov/products/land-based-station/nclimgrid-daily](https://www.ncei.noaa.gov/products/land-based-station/nclimgrid-daily)
|
|
- County/monthly area averages root: [https://www.ncei.noaa.gov/data/nclimgrid-daily/access/averages/](https://www.ncei.noaa.gov/data/nclimgrid-daily/access/averages/)
|
|
- User guide: [https://www.ncei.noaa.gov/data/nclimgrid-daily/doc/nclimgrid-daily_v1-0-0_user-guide.pdf](https://www.ncei.noaa.gov/data/nclimgrid-daily/doc/nclimgrid-daily_v1-0-0_user-guide.pdf)
|
|
- NCEI state-code to FIPS crosswalk: [https://www.ncei.noaa.gov/data/nclimgrid-daily/doc/us-state-codes_ncei-to-fips.csv](https://www.ncei.noaa.gov/data/nclimgrid-daily/doc/us-state-codes_ncei-to-fips.csv)
|
|
- Census boundary change notes: [https://www.census.gov/programs-surveys/geography/technical-documentation/boundary-change-notes.html](https://www.census.gov/programs-surveys/geography/technical-documentation/boundary-change-notes.html)
|
|
- Census county changes: [https://www.census.gov/programs-surveys/geography/technical-documentation/county-changes.html](https://www.census.gov/programs-surveys/geography/technical-documentation/county-changes.html)
|
|
|
|
The locally extreme metric uses county-specific 1991-2020 thresholds:
|
|
|
|
```text
|
|
p95_tmax_c = 95th percentile of county-average daily Tmax
|
|
p05_tmin_c = 5th percentile of county-average daily Tmin
|
|
locallyExtremeDays = count(Tmax >= p95_tmax_c OR Tmin <= p05_tmin_c)
|
|
```
|
|
|
|
The same daily county Tmax/Tmin records now also produce an absolute climate-bucket metric:
|
|
|
|
```text
|
|
absoluteExtremeDays = count(Tmax >= 95F OR Tmin <= 0F)
|
|
```
|
|
|
|
The heat threshold follows the EPA extreme-heat example of days at or above 95F.
|
|
The cold threshold is intentionally stricter than the older 32F freeze bucket because NWS cold guidance treats freezing as a freeze-warning/crop threshold and describes human extreme cold as region-dependent and often wind-chill based. The current NOAA cache does not include wind, so `Tmin <= 0F` is used as a configurable air-temperature proxy.
|
|
|
|
Run from the local NOAA CSV cache:
|
|
|
|
```powershell
|
|
.venv\Scripts\python.exe scripts\build_county_locally_extreme_data.py --skip-download
|
|
```
|
|
|
|
Outputs:
|
|
|
|
- `data/noaa/county_locally_extreme_thresholds.csv`
|
|
- `data/noaa/county_locally_extreme_days.csv`
|
|
- `data/noaa/county_locally_extreme_days_comparison.csv`
|
|
|
|
Apply the locally extreme average to the browser app CSV:
|
|
|
|
```powershell
|
|
.venv\Scripts\python.exe scripts\apply_locally_extreme_metric_to_climate_data.py
|
|
```
|
|
|
|
This keeps `data/climate-data.csv` compatible with the existing app by writing the locally extreme average into the active `extremeDays` column, while preserving the earlier proxy metric in `oldExtremeDays`. It also adds detail/audit columns:
|
|
|
|
- `locallyExtremeDays`
|
|
- `locallyExtremeHotDays`
|
|
- `locallyExtremeColdDays`
|
|
- `absoluteExtremeDays`
|
|
- `oldExtremeDays`
|
|
- `locallyExtremeAnalysisYears`
|
|
- `locallyExtremeSourceFips`
|
|
- `locallyExtremeFipsAdjustment`
|
|
|
|
FIPS/geography-vintage policy:
|
|
|
|
- Use current NOAA/Census county-equivalent FIPS for the locally extreme outputs.
|
|
- Correct NOAA's District of Columbia county row from source region code `18511` to Census FIPS `11001`.
|
|
- Apply only one-to-one old app comparison concordances, currently `46113 -> 46102` for Shannon/Oglala Lakota and `51515 -> 51019` for Bedford city/Bedford County.
|
|
- Do not guess split or many-to-one geography changes without a Census relationship file; leave those rows flagged in the comparison output.
|