Bundle review table

Cities store heat — a strong night-time signal, but the cloud control is missing

Supplied working report; original verdicts not validated.

Review status: Needs review. This is an archival source note, not a passed proof.

README.md

# Cities store heat — a strong night-time signal, but the cloud control is missing

**Preprint draft — not peer reviewed.**

## Result

The night-time urban–rural temperature difference grows with the previous day's
temperature. Across 25 US airport pairs, a 5 °C warmer previous day is
associated with a 1.11 °C larger night-time urban heat island once the current day's
rural temperature is held fixed (95% CI 0.84–1.39 °C per 5 °C; permutation
p = 0.0001, the permutation floor; 133,836
analysable station-days: `primary.beta_per_5c`, `primary.ci95_cluster`,
`primary.p_perm`, `meta.n_station_days`, `meta.n_pairs`). The matched-temperature
contrast says the same thing without a linearity assumption: within 1 °C bins of current
rural temperature, the night after a hot previous day is 1.21 °C warmer, urban minus
rural, than the night after a cool one (p = 0.0002; `phase_test.delta`,
`phase_test.p_perm`). Both survive Benjamini–Hochberg correction (`primary.q_bh`).

The verdict is **PARTIAL** (`verdict`). Two of the brief's three support criteria are met
in full. The storage coefficient is positive, far above the pre-specified 0.5 °C per 5 °C
floor, with p < 0.001 (`criteria.c1_storage`), and the matched-temperature phase contrast
is positive and significant (`criteria.c2_phase`). The third criterion requires the
association to survive **cloud cover and wind**. It cannot be tested: the GSOD daily
files served by NCEI contain no cloud variable, so `criteria.c3_full` is
`"not testable"`. Wind alone leaves the coefficient untouched — 1.11 °C per 5 °C,
p = 0.0001 (`criteria.c3_controls_wind_only`, `controls.wind`) — but a wind-only pass is
not the criterion the brief wrote, and the label should not claim more than the data can
support.

Cloud cover sets downwelling longwave radiation at night; if urban and reference skies differ systematically after warm weather,
the night-time difference would grow with the previous day's temperature for reasons
unrelated to stored heat. This test cannot rule that out. What it can
say is that the pattern is large, appears in 23 of 25 pairs
(`pairs_summary.n_positive`), and is specific to the past rather than the future.

![Night-time UHI vs previous-day temperature](figures/fig1_uhi_night_vs_prev_temp.png)

## What was asked, and what the data are

The proposal states that urban heat follows a hysteresis loop between day and night, so
that the same air temperature produces different surface temperatures depending on
whether the city is heating or cooling. The brief turns that into a daily test: at a
matched rural air temperature, does the urban–rural night-time difference depend on the
previous day's temperature?

The data are **air temperature at 2 m**, not surface temperature. The proposal is about
the surface — the skin of roofs, roads and walls that absorbs heat by day — and this test
never observes that quantity. Air temperature responds to the release of stored heat, but
it is an attenuated, advected and mixed proxy. A null would not have proved that surface
temperatures do not loop; the positive result is evidence about the boundary layer, not
the surface. That is a real limitation relative to the proposal's wording.

## How the test was built

The 25 largest US metropolitan areas were fixed in advance. Each urban anchor
is the ISD station carrying the metro's principal airport code; each reference station was
chosen mechanically from `isd-history.csv` — a land station 40–120 km away, latitude
within 2°, elevation within 200 m, not a major-city airport or an offshore platform,
ranked by closeness to 100 km then elevation match, and screened for 90% complete
records. One metro (Charlotte) fell to its third candidate; the substitution is recorded
in `SOURCE.md`. No pair was chosen or changed after any temperature statistic was
computed.

Daily GSOD temperatures are served in Fahrenheit, not the tenths of a degree Celsius the
brief anticipated; both temperatures and wind (knots) were converted. A day enters the
panel only if both stations have valid mean, minimum and maximum temperature and a valid
previous day. 2.3% of pair-days were dropped (`meta.drop_rate`): 2,806 for a missing
temperature and 333 for a missing previous day (`meta.drop_counts.missing_temperature`,
`meta.drop_counts.no_prev_day`). Two station-years return HTTP 404
(`sources.missing_station_years_used`); 748 of the 750 files for the 50 stations were served
(`meta.files_used_200`, `meta.files_used_total`).

Every variable is an anomaly from its own pair- and day-of-year climatology, so the
seasonal cycle and permanent site differences are removed before estimation. The model
regresses the night-time UHI anomaly on the current rural temperature anomaly and the
previous day's, with pair fixed effects. Inference is a within-pair permutation of the
previous-day term, 10,000 draws, seed 20261030.

## What the model explains

The model explains 11.1% of the variance in the night-time UHI anomaly (`primary.r2`);
the previous-day term alone adds 6.1 percentage points over current temperature alone
(`primary.tcur_partial_r2`). Holding the previous day fixed, a warmer current
night has a *smaller* urban–rural difference (−0.30 °C per °C;
`primary.beta_tcur_per_c`), the regression-to-the-mean side of storage.

The association is not driven by a handful of pairs. 23 of 25 pair-level slopes are
positive (`pairs_summary.n_positive`), from −0.31 °C per 5 °C in Denver to +2.79 in San
Francisco (`pairs_summary.min_beta_per_5c`, `pairs_summary.max_beta_per_5c`); the negative
pairs are Denver and Riverside (`pairs_summary.negative_metros`). Dropping each pair in
turn leaves the pooled coefficient between 1.07 and 1.19
(`sensitivity.leave_one_pair_out`). Summer alone gives 0.96, calm days 1.29, and removal
of the most extreme 1% of nights 0.95 (`sensitivity.summer_jja.beta_per_5c`,
`sensitivity.calm_days.beta_per_5c`, `sensitivity.drop_outliers_99pct.beta_per_5c`); a
seven-day block permutation gives p = 0.001 (`sensitivity.block_permutation_7d.p_perm`).
The three pairs with a cooperative reference give 1.36
(`sensitivity.coop_reference_subset.beta_per_5c`).

The obvious alternative explanation is weather persistence: warm yesterday and warm today
are correlated, and the urban site retains heat from any warm day. Using the **urban**
previous-day temperature instead of the reference station's raises the coefficient to 1.62
(`sensitivity.urban_tprev.beta_per_5c`), which is what a persistence artefact would do. A
post-hoc check, run after the primary result was seen, separates past from future: 0.68 at
a two-day lag (`diagnostics_post_hoc.lag_minus_2`), 1.11 at one day, 0.32 one day ahead
(`diagnostics_post_hoc.lead_plus_1`) and 0.27 two days ahead
(`diagnostics_post_hoc.lead_plus_2`). The effect decays in both directions but is 3.5 times larger for yesterday than for
tomorrow (`diagnostics_post_hoc.lag_lead_ratio`) — the asymmetry storage predicts,
though not proof of one.

![Storage slope by pair](figures/fig3_pair_slopes.png)

## Limits

Both members of every pair are airports, so this is an **airport-pair contrast**, not a
city-centre-versus-countryside contrast. The urban station is an international airport on
open, mown ground at the edge of the built-up area, not a residential neighbourhood; it is
probably cooler than the city core, which makes the urban signal conservative. The
reference stations are small-town airports and cooperative sites 40–120 km away, not open
countryside: they have their own weak heat islands, and in the largest metropolitan
regions the annulus still contains suburbs. Coastal references — Cape May for
Philadelphia, Oxnard for Los Angeles — mix urban heat with a marine climate. Those
influences should attenuate the association rather than manufacture a past-specific
gradient, but they are not controlled.

Cloud and wind are coarse. Wind is a single daily mean at the reference station, and cloud
cover is not available at all, so the boundary-layer mixing that would erase a storage
loop is measured badly or not at all. The GSOD daily mean, maximum and minimum cannot show
the hourly loop: a daily minimum is one number per night and cannot distinguish the
evening release of stored heat from the pre-dawn minimum. The hourly LCD arm was **not
completed** (`meta.hourly_arm`): the reference stations are small airports that largely
lack hourly LCD records, so an hourly contrast would have needed a different, unpaired
reference, and the daily arm already used most of the time budget. Figure 2 is the daily-resolution analogue — two branches at matched current temperature
— not the hourly loop the proposal describes.

![Daily loop](figures/fig2_daily_hysteresis_loop.png)

Station moves, instrument changes and network automation during 2010–2024 are not
modelled, and two station-years are missing.
These are observational data: the language of association is deliberate, and nothing
identifies a causal mechanism.

## Reproducibility and disclosures

`run.py` regenerates `data.csv` (133,836 rows, one per analysable pair-day), `results.json`
and the three figures from `data/raw/`; the measured wall time is in
`meta.runtime_seconds`. A cold cache adds `meta.files_used_total` station-year downloads; the
cache holds `meta.raw_bytes_total` bytes. `SOURCE.md` records every URL, fetch timestamp, row count and
SHA-256; all random draws are seeded with 20261030. The pre-specified criteria and verdict
mapping are in `hypothesis.md`, including a recorded addendum on criterion 3.

**Preprint draft — not peer reviewed.** This test was designed and executed with AI
assistance (DeepSeek, via its coding agent). The analysis plan was written before the
temperature data were analysed. No medical or safety claim is made.

hypothesis.md

# Hypothesis and pre-specified analysis plan

**Test 08 — Cities Store Heat**  
Suite: Incoprea hypothesis tests, 30 October 2026  
Source proposal: *Original Theory Notebook*, Volume XVI, entry 17, "Cities Store Heat" —
<https://incoprea.com/research-library/original-theories-vol16-20261030/nave.html>

This document was written **before** any urban–rural temperature difference or
regression coefficient was computed. At the time of writing, only the station list had
been inspected: which stations exist, which pass the geometric pairing rule, and whether
their daily files are present. No temperature value from a paired day had been looked at.

## Proposal, quoted verbatim

> Urban heat follows a hysteresis loop between day and night, so the same air
> temperature produces different surface temperatures depending on whether the city is
> heating or cooling.

The brief's restatement is the one this test operationalises: urban fabric absorbs heat
by day and releases it at night, so the urban–rural temperature difference should depend
on the city's recent thermal history, not only on the current air temperature. At the same
rural air temperature, the urban–rural difference should be larger when the city is
cooling than when it is warming.

## What is measured, and what is not

The data are **air temperature at 2 m**, screen height, from weather stations. They are
not surface temperature (skin temperature), not satellite land-surface temperature, and
not the temperature of the urban fabric itself. The proposal's wording says "surface
temperature"; this test cannot observe that quantity. Air temperature responds to the
release of stored heat — at night the urban boundary layer is warmed by it — so a storage
signal should be visible in air temperature. But the signal is attenuated, lagged and
mixed by advection, and a null result here would not prove that surface temperatures do
not loop. This limitation is repeated in `README.md`.

## Data

NOAA NCEI Global Summary of the Day (GSOD), daily summaries, 1 January 2010 –
31 December 2024 (15 complete years), for 25 station pairs (50 stations).

- Station list: <https://www.ncei.noaa.gov/pub/data/noaa/isd-history.csv>
- Daily files: `https://www.ncei.noaa.gov/data/global-summary-of-the-day/access/{YYYY}/{STATION}.csv`

**Units.** The brief anticipated GSOD temperature in tenths of a degree Celsius with
9999.9 as the missing code. The files actually served carry Fahrenheit to one decimal
(9999.9 missing) and wind speed in knots. All temperatures are converted to °C as
`(F − 32) / 5 × 9` and wind to m s⁻¹ as `knots × 0.514444`. This is a correction of a
factual assumption in the brief, not a change to the question.

**Cloud cover.** The brief lists `CLDC` as the cloud variable. The served GSOD daily
files do not contain a `CLDC` column (the field list is STATION, DATE, LATITUDE,
LONGITUDE, ELEVATION, NAME, TEMP, DEWP, SLP, STP, VISIB, WDSP, MXSPD, GUST, MAX, MIN,
PRCP, SNDP, FRSHTT). Cloud cover therefore cannot be controlled. Wind can. This is
recorded as a data limitation before any analysis: criterion 3 of the brief's support
conditions is **partly untestable**, and the verdict must say so.

## Station pairing (fixed before any temperature data were read)

The 25 largest US metropolitan statistical areas (2020 Census) were taken in population
order. For each, the urban anchor is the ISD station carrying the metro's principal
airport ICAO code, restricted to stations whose ISD record begins on or before
1 January 2010 and ends on or after 31 December 2024. Where the file has more than one row
for an ICAO code (Denver and Ontario each have an old and a current row), the row with the
latest END and then the earliest BEGIN is used.

ICAO codes, in metro order: KJFK, KLAX, KORD, KDFW, KIAH, KDCA, KPHL, KMIA, KATL, KBOS,
KPHX, KSFO, KONT, KDTW, KSEA, KMSP, KSAN, KTPA, KDEN, KSTL, KBWI, KCLT, KMCO, KSAT, KPDX.

The reference station for each metro is chosen by a fixed algorithm from
`isd-history.csv`, not by hand:

1. **Land station**: USAF code beginning with `7` (WMO block 7, fixed land stations) or
   `999999` (US cooperative network); WBAN not `99999` (which marks mobile, ship and buoy
   platforms). This excludes buoys, lightships and CMAN platforms, which dominate the
   40–120 km annulus around coastal cities.
2. **Not an airport of a major city**: station name must not contain INTERNATIONAL, INTL,
   ISLAND, NALF, MCOLF, MCAS, NAS, AFB, AAF, ARMY, NAVAL, COAST GUARD, LIGHT, PIER, BUOY,
   RESERVE, CATALINA, MARINE CORPS or AIR STATION, and the station must not be one of the
   25 anchors. Airports named "municipal", "regional" or "field" are allowed; those are
   small-town sites.
3. **Geometry**: great-circle distance from the anchor 40–120 km; |latitude difference|
   ≤ 2°; |elevation difference| ≤ 200 m. The brief permits relaxing elevation to 300 m if
   no station qualifies; the relaxation was to be applied per metro only if needed, and
   is recorded per pair.
4. **Ranking**: closest to 100 km first (the far half of the permitted annulus, to
   minimise urban influence), then smallest elevation difference, then station ID.
5. **Data screen**: at least 90% of days in 2010–2024 with non-missing `TEMP`, `MIN` and
   `MAX` in the GSOD file. If the top-ranked candidate fails, the next candidate in rank
   order is taken. This is a completeness screen, not an outcome screen: no UHI value is
   used.

No pair was changed after any temperature statistic was computed. The full list with
station IDs, coordinates, elevations and distances is in `SOURCE.md` and in
`results.json → pairs`.

## Variables

For pair *p* and calendar day *t*:

- `Tmean`, `Tmin`, `Tmax` from GSOD `TEMP`, `MIN`, `MAX`, converted to °C. A day is used
  only if both stations have non-missing `Tmean`, `Tmin` and `Tmax` on that day and both
  have a valid previous-day `Tmean`.
- `UHI_day = Tmax_urban − Tmax_reference`.
- `UHI_night = Tmin_urban − Tmin_reference`.
- `Tprev` = previous calendar day's **reference-station** `Tmean`. The reference value is
  used, not the urban value, so that the urban temperature does not appear on both sides
  of the equation: urban `Tmin` is strongly autocorrelated with urban `Tmean` on the
  previous day through ordinary weather persistence, and using the urban value would
  build that persistence into the coefficient. Using the reference value makes `Tprev` a
  measure of how warm the regional air mass was the day before, which is the physically
  relevant charge on the urban fabric.
- `wind` = reference-station `WDSP` in m s⁻¹.

**Anomalies.** For every pair, each variable is expressed as an anomaly from a pair- and
day-of-year climatology: the mean of that variable over all days within ±7 calendar days
of the day of year, across 2010–2024 (a circular 15-day window, 15 years). This removes
site differences, the seasonal cycle and the pair's mean UHI, as the brief's analysis
notes require. An anomaly is denoted with a tilde (`ũ`).

## Primary test (test 1, daily resolution, storage coefficient)

For all valid pair-days, by ordinary least squares with pair fixed effects:

```
ũ_night(p,t) = α_p + β1 · T̃(p,t) + β2 · T̃prev(p,t) + ε(p,t)
```

where `T̃` is the current-day reference `Tmean` anomaly and `T̃prev` the previous-day
reference `Tmean` anomaly. `β2` is the storage coefficient: the change in the urban–rural
night-time difference associated with a 1 °C warmer previous day, holding today's
regional temperature fixed. The brief's criterion is phrased in °C per 5 °C, so the
reported quantity is `5 · β2`.

**Inference.** Permutation within station pair: `T̃prev` is shuffled across days within
each pair, 10,000 permutations, seed 20261030, and the model is refitted. The p-value is
two-sided, `(1 + #{|β2*| ≥ |β2|}) / 10001`. Shuffling within pair preserves the pair
means, the pair fixed effects, the current-day temperature, and every other variable; it
breaks only the day-to-day alignment between the previous day's temperature and the
night-time UHI. Because shuffling also breaks the autocorrelation of `T̃prev`, a
**7-day block permutation within pair** is reported as a conservative check (sensitivity
5 below): observed and permuted statistics are computed on the same model.

**Interval.** A pair-cluster bootstrap: the model is fitted separately in each of the 25
pairs, giving 25 values of `β2,p`; the mean across pairs is bootstrapped with
`lib/stats.bootstrap_ci` (percentile, 10,000 resamples, seed 20261030). This is a
between-pair interval and is wider than the nominal OLS interval; it is the interval
quoted in `README.md`. A cluster-robust (CR1, 25 clusters) standard error is reported
alongside.

**Multiplicity.** The declared family is the two pre-specified daily tests: the storage
coefficient and the matched-temperature phase contrast below. Benjamini–Hochberg q-values
are reported for the two. The brief's SUPPORT threshold (p < 0.001) is applied to the raw
permutation p, as written; the q-value is reported for completeness.

## Confirmatory test (brief's test 1, binned form)

The exact question the brief poses: "at matched rural Tmean (binned in 1 °C bins), is
UHI_night larger on days following a hot day than on days following a cool day?"

- Bin days by the current reference `Tmean` anomaly in 1 °C bins.
- Within each pair × bin cell with at least 20 valid days, split days into the top third
  and bottom third of `T̃prev`.
- Compute the cell contrast `Δ = mean(ũ_night | hot previous day) − mean(ũ_night | cool
  previous day)`, and pool across cells weighted by cell count.
- Inference: shuffle `T̃prev` within pair × bin cells, 5,000 permutations, seed 20261030,
  two-sided.

This is the same mechanism as the regression, tested without a linearity assumption and
with temperature matched non-parametrically. It is confirmatory: the verdict mapping
requires it to agree in sign with the regression for SUPPORT.

## Controls (criterion 3)

`ũ_night = α_p + β1 T̃ + β2 T̃prev + β3 w̃ + ε`, with `w̃` the reference-station wind
anomaly. Criterion 3 asks that `β2` survive this inclusion. Cloud cannot be included
(`CLDC` absent); wind can. The wind-controlled `β2` and its permutation p are reported and
are part of the verdict mapping. Because the cloud half of criterion 3 cannot be tested,
no verdict in this test can claim that the pattern survives cloud control, and `README.md`
says so.

## Pre-specified support / refute criteria

These are the brief's criteria, unchanged, with the operational definitions above.

**SUPPORT** requires:

1. `β2 > 0` with permutation p < 0.001 (within-pair, 10,000 permutations), and
   `5 · β2 ≥ 0.5 °C`;
2. the matched-temperature phase contrast is positive (hot-previous days show a larger
   night-time UHI than cool-previous days) with p < 0.05; and
3. the wind-controlled `β2` remains positive with p < 0.05 and `5 · β2 ≥ 0.5 °C`.

The hourly arm (test 2 of the brief) is optional. If it is not run, condition 2 refers to
the daily matched-temperature contrast above; the brief's hourly-loop condition cannot be
evaluated and is recorded as NOT COMPLETED with a reason.

**REFUTE** if `β2 ≤ 0` or its permutation p ≥ 0.05 — the UHI depends on contemporaneous
temperature and not on the previous day's — or if the matched-temperature phase contrast
is zero or negative with p < 0.05 (the warming/cooling asymmetry is absent or reversed).

**PARTIAL** if the association is positive and significant but one of the quantitative
conditions fails: `5 · β2 < 0.5 °C` (real but immaterial), the wind-controlled coefficient
loses significance or falls below 0.5 °C, or the binned phase contrast does not reach
p < 0.05. PARTIAL is also the verdict if the daily test holds but the hourly arm, had it
been run, found no loop; since the hourly arm is not run, that clause does not apply.

**Verdict mapping, applied in this order (fixed before analysis):**

1. `β2 ≤ 0` or p ≥ 0.05 → **REFUTE**.
2. Otherwise `5 · β2 < 0.5` → **PARTIAL**.
3. Otherwise wind-controlled `β2` not positive with p < 0.05 and ≥ 0.5 °C per 5 °C →
   **PARTIAL**.
4. Otherwise phase contrast ≤ 0 or p ≥ 0.05 → **PARTIAL**.
5. Otherwise → **SUPPORT** (with the cloud caveat recorded).

## Sensitivity analyses (declared in advance)

1. Summer only (June–August), when storage should be strongest.
2. `Tprev` taken from the **urban** station instead of the reference station (the obvious
   alternative choice, and the one most likely to inflate the coefficient through
   persistence).
3. Raw pair-mean-centred variables instead of day-of-year anomalies.
4. Wind-stratified: calm days only (reference wind below the pair median), where the
   boundary-layer mixing that erases the loop is weakest.
5. Seven-day block permutation within pair (conservative p).
6. Outliers removed: drop days with |`UHI_night` anomaly| above the 99th percentile.
7. Leave-one-pair-out: the range of `β2` and its p-value across the 25 dropped-pair
   refits.
8. Reference-network check: restrict to pairs whose reference station is a cooperative
   station (USAF `999999`), which are the least airport-like sites. Descriptive, because
   that subset is small.

No sensitivity analysis changes the verdict; they are reported beside it.

## What this design cannot see

- Air temperature at 2 m, not surface temperature; the skin-temperature loop the proposal
  describes is not observed.
- Airport stations are not city centres, and airport grounds are grassy, open and often
  cooler than the surrounding built-up area; the urban signal is therefore conservative.
- Reference stations are small-town airports 40–120 km away, not open countryside. In the
  largest metropolitan regions the annulus still contains suburbs and satellite cities,
  and a small town has its own weak urban heat island. This attenuates the contrast.
- Cloud cover is unavailable in GSOD, so the cloud half of criterion 3 cannot be tested.
  Wind is a single daily mean at one station and is a coarse measure of boundary-layer
  mixing.
- Daily means, maxima and minima cannot show the hourly loop. A daily minimum is one
  number per night; it cannot distinguish the evening release from the pre-dawn minimum.
- GSOD daily values are rounded to 0.1 °F and derived from synoptic reports; station
  moves, instrument changes and the 2010–2024 automation of the cooperative network are
  not controlled.
- Observational data: no causal language is warranted. "Associated with" is the strongest
  claim this design supports.

## Addendum — criterion 3 and the verdict label (30 October 2026, after the daily results were seen)

The section above operationalised criterion 3 as the wind control alone, on the ground
that `CLDC` is absent from the served GSOD files. That was too lenient. The brief requires
the association to survive cloud cover **and** wind, and a variable that is not served
does not make the missing half of a criterion pass. At the project lead's instruction the
criteria are now applied as written:

- `results.json → criteria.c3_controls_wind_only` reports the wind test (`pass_wind`),
  with `cloud_testable: false`; `criteria.c3_full` is `"not testable"`.
- Because criterion 3 cannot be met in full, the verdict is **PARTIAL**: two of three
  criteria fully met; the third (cloud) could not be tested. This is a change to the
  verdict mapping made after the numbers were seen; it is recorded here rather than
  hidden. The coefficient, the phase contrast and every numerical threshold are unchanged.

Included code and results