Recomputing NOAA's 1991–2020 U.S. Climate Normals from the GHCN-Daily observations they are built on, across 12 stations, as a Facetwork FFL workflow. September 2026.
A climate normal is the reference against which "hotter than average" is measured — in agriculture, energy demand forecasting, insurance pricing and building codes. NOAA publishes the normals and the daily observations they derive from, so unlike most published results this one can be checked rather than trusted.
The question is not "are the published normals correct". It is narrower and more useful: can an independent party, starting from the same archived observations, arrive at the same numbers? Where the answer is no, the gap measures how much of the published product depends on decisions that are documented in prose but not reproducible from the archive.
NOAA NCEI's U.S. Climate Normals are not an average of the daily archive. Their published Normals Calculation Methodology (2020) describes a substantially longer pipeline, and the two steps that matter here are both corrections applied before any averaging:
| Step | What it does | NCEI's own words |
|---|---|---|
| Time-of-observation adjustment | Corrects for when the observer read the thermometer. A station read at 5 pm double-counts hot afternoons in TMAX; one read at 7 am double-counts cold mornings in TMIN. | "a time of observation (TOB) adjustment that strives to make it as if the observations at a particular station had been taken at local midnight" |
| Homogenisation | Detects and corrects breaks from station moves, instrument changes and other shifts in practice, by pairwise comparison with neighbours (Menne & Williams 2009). | "GHCN-Daily … is not homogenized like the monthly temperature dataset … The standardization procedures account for both documented and undocumented station moves and other changes in observing practices." |
| Gap filling | Regression against nearby stations, for monthly temperature and precipitation, where suitable neighbours exist. | flagged R in the output |
| Rounding | Banker's rounding, computed in metric units at machine precision and rounded only at output. | one decimal place in °F |
NCEI even give a worked example of the homogenisation adjustment, at exactly the magnitude this study measures:
| NOAA NCEI | This workflow | |
|---|---|---|
| Input | GHCN-Daily plus the homogenised monthly series | GHCN-Daily alone |
| TOB adjustment | applied, per station | none |
| Homogenisation | applied, per station | none |
| Gap filling | where neighbours allow | none |
| Averaging | over the corrected series | arithmetic mean of raw dailies |
So the difference we measure is not an error on either side — it is the size of NOAA's corrections. That reframes the whole study: it is not "can the normals be reproduced?" but "how much of the published normal comes from the raw record, and how much from the adjustments applied to it?" Those adjustments are station-specific by construction, which is precisely what the twelve-station spread shows.
Two things we assumed and the documentation corrects. The
comp_flag value S does not mean 30 complete
years — under the 2020 WMO guidelines it means at least 24 years
(80%). And TAVG is not defined as (TMAX+TMIN)/2: NCEI note the average
temperature normal "may not be identical, but within rounding error, of the sum
of the corresponding maximum and minimum temperature normals". Our TAVG column
uses the conventional midpoint, so a small part of its residual is definitional.
Full field layout: the by-station readme.
Each station is fetched, recomputed (mean of daily values per calendar month, tenths °C → °F), and compared against the published normals. 12 stations, 61 tasks, distributed across a four-host fleet. 432 values in total (12 months × TMAX/TMIN/TAVG × 12 stations).
| Station | median error °F | max error °F | TMAX bias | TMIN bias |
|---|---|---|---|---|
| Los Angeles CA | 0.040 | 0.300 | none | none |
| Charlotte NC | 0.090 | 0.200 | -0.146 | none |
| Cincinnati OH | 0.115 | 0.290 | -0.233 | +0.110 |
| Miami FL | 0.125 | 0.340 | +0.232 | none |
| Minneapolis MN | 0.205 | 0.540 | none | -0.410 |
| New York NY | 0.235 | 0.540 | +0.242 | -0.511 |
| Atlanta GA | 0.270 | 0.460 | -0.395 | none |
| Denver CO | 0.335 | 0.770 | none | -0.237 |
| Houston TX | 0.340 | 0.520 | -0.278 | -0.385 |
| Las Vegas NV | 0.340 | 1.020 | +0.161 | -0.859 |
| Seattle WA | 0.685 | 0.960 | -0.899 | -0.472 |
| Chicago IL | 0.875 | 1.540 | -0.291 | -1.452 |
"Bias" is the mean signed residual (ours − NOAA) where an exact sign test finds the residuals systematically one-sided (p < 0.01 over 12 months); none means consistent with noise. The sign test is threshold-independent, so unlike the error magnitudes it does not move when the tolerance changes.
Los Angeles reproduces to 0.040 °F median; Chicago is off by 0.875. That is a twentyfold spread across stations processed from the same archive by the same code. Overall:
| All 432 values | |
|---|---|
| median absolute error | 0.230 °F |
| 90th percentile | 0.820 °F |
| maximum | 1.540 °F (Chicago TMIN, January) |
| within 0.25 °F | 53.2% |
| within 0.50 °F | 81.7% |
| within 1.00 °F | 97.0% |
| NOAA | This workflow | |
|---|---|---|
| Question | What is the climate normal? | Can it be recomputed from the archive? |
| Input | Full station history + homogenisation | The published daily archive alone |
| Method | Documented estimation, QC, infilling | Arithmetic mean per calendar month |
| Output | A reference product | A measured gap, per station |
At New York every one of 36 values sits within a generous tolerance and every one falls on the same side per column — TMAX high 12/12, TMIN low 12/12.
verdict : agree at level `values`, differing: 0 TMAX residuals SYSTEMATIC: 12+/0- of 12, mean +0.2425 (p=0.00049) TMIN residuals SYSTEMATIC: 0+/12- of 12, mean -0.5108 (p=0.00049)
Agrees and differs at once. "36 small random errors" and "36 errors in the same direction" are completely different claims about a reproduction, and only the second says the method differs. A tolerance is a per-value predicate; it cannot express that. So the comparison runs an exact sign test over the signed residuals — Binomial(n, ½) under the null, no distributional assumption — and reports the structure beside the verdict. Being threshold-free, it is the part of this study that does not move when the tolerance does.
The comparison returns the highest rung reached on a ladder, so the level names what did match and thereby locates the first thing that did not:
| level | means | the failure is |
|---|---|---|
exists | readable, fields disagree | wrong file, schema change |
schema | fields agree, record sets do not | records added, dropped, or out of scope |
cardinality | counts agree, which records do not | same number, different members |
keys | the same records exist; their values differ | the numbers — a method difference |
values | values agree under declared normalisations | only serialisation differs |
bytes | identical files | nothing |
keys is the informative rung and the easiest to misread. It says
both sides looked at exactly the same population and computed different
answers — nothing lost in transfer, no filter diverged, no schema drifted.
That is a finding about method. A verdict of cardinality or
lower usually means something broke before the science started.
The key columns define what "the same record" means, which is a
substantive claim rather than configuration. Here the key is
(month): twelve records, all twelve present on both sides, so every
disagreement is necessarily at keys — the months are the same,
the temperatures are not. In the
companion GIAB study the key is
(CHROM, POS, REF, ALT); without it records would match by position in
the file, and one inserted row would misalign everything after it.
fw ffl run --primary examples/repro-normals/noaa_normals_repro.ffl \
--library facetwork/ffl/file.ffl \
--workflow repro.normals.ReproduceManyStations
The recomputation is an FFL script block bound to
fw.file.FileOps — no handler and no domain package. The ~18 MB
daily file is read inside the script rather than passed through a step payload,
which would exceed the 16 MB document limit. foreach … limit 4
bounds how many stations are in flight so the run does not pull twelve large
files from NOAA at once.
January NOAA 25.2 2% = 0.50 → FAILJuly NOAA 77.5 2% = 1.55 → passabs_tolerance for interval scales and the study was re-run. The
station-level bias findings are unchanged — they come from the sign test,
which has no threshold — but the pass/fail split is gone, replaced by the
error distribution above, which was always the more honest presentation.