Skip to content

Serve weather data from GHCNh through a generic load_data api - #103

Merged
travis-recurve merged 14 commits into
masterfrom
feature/ghcnh-migration
Jul 22, 2026
Merged

Serve weather data from GHCNh through a generic load_data api#103
travis-recurve merged 14 commits into
masterfrom
feature/ghcnh-migration

Conversation

@travis-recurve

Copy link
Copy Markdown
Contributor

Your checklist for this pull request

Please review the guidelines for contributing to this repository.

  • Make sure you are requesting to pull a feature/bugfix branch (right side). Don't request your master!
  • Make sure tests pass and coverage has not fallen (177 tests, fully offline; coverage 99%).
  • Update the CHANGELOG.md to describe your changes in a bulleted list under the "Development" section at the top of the changelog.
  • Code style: PEP 008. (Not blackened, per maintainer preference.)
  • New functions and classes have numpy-style docstrings. The sphinx docs are retired in this pull request; documentation moves to opendsm.energy.
  • All git commits have the "Signed-off-by" message for the Developer Certificate of Origin.

Description

Serves observed weather data from NOAA GHCNh, resolving #100. ISD and GSOD stopped receiving data 2025-08-27; GHCNh serves the same stations through the same NCEI access api (endpoint identified by @k-wolfe99 in #100) with history back decades and current data. Based on #102; the commits unique to this pull request start at "Delete dead FTP fetch path".

Because this is a breaking release, the api is consolidated rather than aliased:

  • load_data(usaf_id, start, end, frequency, variables) returns a DataFrame and a warnings list, replacing the load_isd_*/load_gsod_* family; daily values are means of hourly values. GHCNh variables beyond temperature (dew point, relative humidity, wind speed, ...) are available through variables
  • ISDStation is renamed WeatherStation, with ghcn_id, ghcn_first_year, and ghcn_last_year attributes
  • Caching stores under new keys, so ISD-era cache entries are never served; the deleted ISD/GSOD code paths, filename helpers, and CLI commands are removed rather than deprecated

Station registry:

  • Every station maps to its GHCNh id (ICAO join → USW000{wban} pattern → nearest neighbor within 5 km, with a 50 km sanity guard that caught one ICAO reuse). 349 stations with no GHCNh counterpart are removed; 99.8% of high-quality stations map
  • One longstanding registry error surfaced by the mapping is corrected: the isd-history longitude for 725374 (Ann Arbor Municipal, KARB) is off by 4 degrees, placing the station in Lake Michigan
  • Quality ratings are computed from the GHCNh inventory with the same thresholds as before; ratings can also be computed for the period being requested (rank_stations(..., rating_period=...), get_station_quality), rating the five calendar years ending two years after the request's last date
  • ghcn_first_year/ghcn_last_year record each station's data era; about a fifth of the registry, nearly all low quality, has no observations after 2023

Validation:

  • scripts/validate_ghcnh_against_isd.py compares both sources through the library's pipeline over a stratified sample of 210 stations (every state and territory, all quality tiers) × 4 years: over 572 comparable station-years, the median hourly deviation is 0.0001 °C and the largest annual-mean deviation is 0.14 °C. GHCNh hourly coverage matches or exceeds ISD's at the median

@k-wolfe99

k-wolfe99 commented Jul 16, 2026

Copy link
Copy Markdown

Good thinking on using a coordinate distance search to map GHCNh stations to ISD equivalents when they don't match by ICAO, WBAN, or USAF identifiers. I've been working on a tool to pull data from the GHCNh into SkySpark, and I found a station search endpoint on the NOAA search service API that lets you search for weather stations within a bounding box of coordinates. It returns the weather parameters available at each site and their coverage over the date span you provide in the request. I've found it useful for searching for weather stations and getting live data availability. Here's an example search around SFO:

https://www.ncei.noaa.gov/access/services/search/v1/data?dataset=global-historical-climatology-network-hourly&startDate=2022-01-01&endDate=2100-12-31&bbox=38.070,-122.946,37.172,-121.812&limit=10000

You folks seem to have a pretty robust process for evaluating data quality and ranking stations, so probably not a useful addition here. I was just getting tired of depending on NOAA to update their station inventory file, which hasn't been updated since February. It also provides per-parameter coverage which was important to me.

@travis-recurve

Copy link
Copy Markdown
Contributor Author

Thanks, that's a useful find. For the id mapping itself we're in good shape: the ICAO/WBAN/coordinate mapping in this PR was cross-validated against isd-history coordinates and an overlap comparison of served data, so the search endpoint would mostly confirm what the packaged registry already encodes.

Where it looks genuinely valuable is the per-station-year dataTypes listing in its responses. The registry's ghcn_inventory table carries total monthly observation counts per station, with no per-variable dimension, so station quality ratings and ranking are effectively temperature-anchored, and a request for something like variables=("wind_speed",) can return an all-NaN column from a station that simply never reports it. Presence-by-variable-by-year from this endpoint is exactly the information needed to close that: it could ship in a future registry update and feed station selection for non-temperature variables.

@k-wolfe99

Copy link
Copy Markdown

Yep, exactly my thoughts. You've already validated the mapping by cross comparison of data during overlapping coverage dates so no need to re-invent the wheel there, that analysis was thoroughly convincing. As you mentioned, only potential area of added value from this endpoint is for improving the detail of station quality ratings.

Great work so far!

Base automatically changed from feature/modernize-and-test-harness to master July 21, 2026 17:00
Comment thread eeweather/stations.py
return window_start, window_end


def _quality_from_minimum(minimum):

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same calculation is written out separately in 3 spots: here, get_station_qualities(), and _compute_station_quality_from_ghcnh() in database.py

should consolidate them or at least share constants if it's an optimization issue

- fetch_isd_raw_temp_data_old / fetch_gsod_raw_temp_data_old, the NOAA FTP
  connection proxy, and their testing mocks and packaged sample files
- ftp.ncei.noaa.gov no longer serves these files; nothing referenced them

Signed-off-by: Travis Sikes <124101151+travis-recurve@users.noreply.github.com>
- ghcn_id and ghcn_map_method columns on isd_station_metadata, built by
  ICAO join (nearest candidate within 50 km), USW000{wban} id pattern
  (within 50 km), then nearest GHCNh station within 5 km
- 349 of 4845 stations have no GHCNh counterpart and are removed from the
  packaged database (3 high / 7 medium / 339 low quality)
- rebuild pipeline downloads the GHCNh station list and applies the same
  mapping; isd-history/isd-inventory downloads move from dead ftp to https
- ranking/summary snapshots and metadata pins updated for the reduced
  registry

Signed-off-by: Travis Sikes <124101151+travis-recurve@users.noreply.github.com>
Replaces the ISD/GSOD fetch and load stack. ISD and GSOD stopped receiving
data 2025-08-27; GHCNh serves the same stations with history back decades
and current data.

- eeweather/sources/ghcnh.py fetches multi-variable hourly frames from the
  NCEI access api (endpoint identified by k-wolfe99 in #100)
- load_data(usaf_id, start, end, frequency, variables) returns a DataFrame
  and warnings list, replacing the load_isd_*/load_gsod_* family; daily
  values are means of hourly values
- ISDStation renamed WeatherStation with a load_data method; ghcn_id
  attribute added
- caching stores all fetched variables per station-year under new
  ghcnh-hourly-* keys; refreshes request the union of requested and cached
  variables; ISD-era cache entries never match again
- ISDDataNotAvailableError/GSODDataNotAvailableError collapse to
  DataNotAvailableError; inspect-isd-filenames CLI commands deleted
- fix WeatherStation.deserialize_tmy3_hourly_temp_data delegating to the
  ISD deserializer
- tests migrated to GHCNh fixtures; pins from captured payloads including
  truncation (723826, decommissioned 2013-11-04), internal gap (720193
  2019), no-observation year (722874 2025), and a cross-source test pinning
  2007 means against the ISD-era values

Signed-off-by: Travis Sikes <124101151+travis-recurve@users.noreply.github.com>
- docs/ tree deleted; documentation home is opendsm.energy
- README documents the load_data api and GHCNh source
- scripts/validate_ghcnh_against_isd.py reproduces the ISD overlap
  validation (38 station-years: median hourly deviation 0.0001 C, max
  annual-mean deviation 0.05 C, GHCNh coverage >= ISD)

Signed-off-by: Travis Sikes <124101151+travis-recurve@users.noreply.github.com>
…ected coordinates

- cross-source ISD pin test removed; overlap validation (38 station-years,
  median hourly deviation 0.0001 C) is the consistency evidence
- isd-history longitude for 725374 is off by 4 degrees (places the station
  in Lake Michigan); corrected against the physical site and GHCNh, which
  restores its icao mapping to USW00094889. Correction applied in the
  rebuild pipeline via ISD_COORDINATE_CORRECTIONS
- registry: 4497 stations, all GHCNh-mapped

Signed-off-by: Travis Sikes <124101151+travis-recurve@users.noreply.github.com>
- up to 2 high, 1 medium, 1 low quality stations per state and territory:
  210 stations, 840 station-years over 2016/2019/2022/2024
- per-station-year results written to validation_results.csv

Signed-off-by: Travis Sikes <124101151+travis-recurve@users.noreply.github.com>
- immediate retries land inside the api's intermittent 5xx bursts; retries
  now wait 5s then 10s
- port retry, parsing, empty-year, and duplicate-timestamp tests to the
  ghcnh source module
- validation script skips failed station-years and writes results
  incrementally

Signed-off-by: Travis Sikes <124101151+travis-recurve@users.noreply.github.com>
- ghcn_first_year/ghcn_last_year columns from the GHCNh inventory, on the
  packaged database and the rebuild pipeline; exposed on WeatherStation
  and its json
- 892 of 4497 stations (846 low, 27 medium, 19 high quality) have no
  observations after 2023; era columns let ranking and callers filter by
  data availability instead of discovering empty fetches

Signed-off-by: Travis Sikes <124101151+travis-recurve@users.noreply.github.com>
- same thresholds as the ISD-inventory rating (every month of the last 5
  full years over 600 observations = high, over 360 = medium), applied to
  GHCNh monthly counts; the ISD-inventory rating is deleted
- tiers move for about a fifth of stations: 1858 high / 393 medium /
  2246 low; 332 stations undercounted by the retired ISD inventory
  upgrade to high, 164 recently-dead stations drop out of high
- ranking pins and snapshots updated for the new tiers

Signed-off-by: Travis Sikes <124101151+travis-recurve@users.noreply.github.com>
- get_station_quality(usaf_id, start, end) and get_station_qualities
  (whole-registry, one query) rate stations from GHCNh monthly counts
  over the five calendar years ending two years after the request's last
  date, sliding back to end no later than the last full year; every month
  over 600 observations is high, over 360 medium, less or absent is low
- rank_stations(..., rating_period=(start, end)) filters and reports
  quality for the request's era; stations dead today rate high in their
  active years
- ghcn_inventory table (monthly counts per registry station-year) ships
  in the packaged database and the rebuild pipeline
- a rating_period ranking costs about 70 ms warm; the default path is
  unchanged

Signed-off-by: Travis Sikes <124101151+travis-recurve@users.noreply.github.com>
Signed-off-by: Travis Sikes <124101151+travis-recurve@users.noreply.github.com>
- test fixture CSVs move from eeweather/resources to tests/fixtures and
  the shipped testing module folds into tests/conftest.py; nothing
  outside the test suite consumed either
- tmy3-stations.html (rebuild-time primary source; its NREL page no
  longer exists) moves to scripts/data
- the tutorial notebook taught the removed api; documentation lives at
  opendsm.energy

Signed-off-by: Travis Sikes <124101151+travis-recurve@users.noreply.github.com>
- lives in utils.py; single-station get_ghcn_id remains in stations

Signed-off-by: Travis Sikes <124101151+travis-recurve@users.noreply.github.com>
Signed-off-by: Travis Sikes <124101151+travis-recurve@users.noreply.github.com>
@travis-recurve
travis-recurve force-pushed the feature/ghcnh-migration branch from a725d05 to 71fd9d8 Compare July 22, 2026 22:38
@travis-recurve
travis-recurve merged commit 700fd3c into master Jul 22, 2026
17 checks passed
@travis-recurve
travis-recurve deleted the feature/ghcnh-migration branch July 22, 2026 22:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants