# Reproducible NY/NJ airport ambient-weather screen

## Provenance and scope

Analysis prepared November 1, 2026 from public NOAA observations, not Score field records.
NOAA's June 23, 2026 notice says ISD/Global Hourly was superseded and has no updates
beyond August 24, 2025; its old HTTPS distribution retired July 31, 2026:
https://www.nesdis.noaa.gov/news/service-location-change-integrated-surface-data-global-hourly
The notice links the official NOAA Open Data Dissemination archive:
https://noaa-global-hourly-pds.s3.amazonaws.com/index.html
Use GHCNh for a future current-data study; do not extrapolate this archived series.

We chose 2024, the last full calendar year before that cutoff. The selection was
made before evaluating screen counts. One year at two airports is not a climate
normal, trend, forecast, statewide sample, or a prediction for a particular roof.

Inputs, retrieved November 1, 2026:
- https://noaa-global-hourly-pds.s3.amazonaws.com/2024/72502014734.csv
  Newark Liberty International Airport, NJ US; USAF 725020 / WBAN 14734;
  latitude 40.68275, longitude -74.16927; file elevation 1.9 m.
- https://noaa-global-hourly-pds.s3.amazonaws.com/2024/72503014732.csv
  LaGuardia Airport, NY US; USAF 725030 / WBAN 14732;
  latitude 40.77945, longitude -73.88027; file elevation 3.0 m.

Identity is checked against every row's STATION, NAME, LATITUDE and LONGITUDE,
allowing 0.02 degrees coordinate variation, not inferred from the filename alone.
These are airport observations, not roof-height site observations.
The JSON audit saves exact input SHA-256 hashes, byte counts, row counts, flag
frequencies and coverage; compare hashes on rerun to detect upstream revisions.
Raw files (~8 MB each) are cached outside version control, not shipped with the app.
The small monthly CSV and audit JSON are retained and served with the article.

## Decode and quality policy

Reference: https://www.ncei.noaa.gov/pub/data/noaa/isd-format-document.pdf
See the control section's UTC/report-type definitions and mandatory fields for
wind speed, air temperature and dew point (printed pages 4–12).

Only FM-15 routine METAR reports are used. FM-12 synoptic, special reports and
other report types are excluded to avoid increasing sampling density in changing
weather. TMP and DEW encode a signed value in tenths of Celsius plus a quality
flag. WND's fourth component is tenths of m/s and fifth is the speed quality
flag; direction flags are not used to judge speed. +9999/9999 means missing.
Only flags 1 and 5 are accepted: passed all QC, with 5 denoting an NCEI source.
Gross-limit-only flags 0/4/9, suspect 2/6, erroneous 3/7 and all letter flags
are excluded, even when NOAA permits some special flags as valid. This is an
explicit stricter selection, not a claim that every excluded observation is wrong.
Malformed fields are unusable. Broad documented field-domain limits are checked;
dew point above air temperature is unusable rather than silently corrected.
All three measurements must be usable; otherwise the whole selected bin is unknown.

## Time grid, duplicates and missingness

The source year is UTC, January 1 00:00 through December 31 23:59, 2024:
8,784 expected hourly bins (leap year). Both files start January 1 00:00 and
end December 31 23:51 UTC, but endpoint coverage does not prove completeness.
The JSON separately reports actual routine-report bins, absent bins, unusable
bins and bins with multiple routine reports across that whole UTC grid.

Floor each timestamp to its containing UTC hour. If more than one routine
report exists in a bin, select the latest timestamp BEFORE applying QC. Do not
select a favorable earlier value. Equal-time duplicate rows with identical
TMP/DEW/WND count once. Conflicting selected-time values make the bin unusable.
We do not merge measurements from different reports.

Convert bins to America/New_York with Python zoneinfo (UTC-5 standard time,
UTC-4 daylight time). Select local Monday–Friday 08:00–16:59, holidays included,
for 2,358 expected working-hour bins per station. These daytime shifts avoid
the repeated/missing local hours at DST changes. The UTC-year boundaries omit
only nonworking local hours, not any eligible 2024 work-shift bin.

Absent reports and present-but-unusable reports are separate columns. No
interpolation, forward fill, or inference from a special report is used. Both
unknown categories fail the screen and break consecutive sequences; they do
not imply that adverse weather was observed. Denominators remain the complete
expected grid. Working-hour coverage:
- Newark: 6 absent + 7 unusable + 2,345 usable = 2,358.
- LaGuardia: 10 absent + 3 unusable + 2,345 usable = 2,358.

## Thresholds and sequences

These are arbitrary, transparent editorial screening assumptions for comparison,
NOT product specifications, safety limits or a validated installation model.
All comparisons are inclusive and made in the original SI units:
- Baseline air temperature: 10–35 C (50–95 F).
- Sensitivity air temperature: 15–35 C (59–95 F), changing only the lower limit.
- Air temperature minus station dew point: at least 3 C (5.4 F).
- Reported wind speed: at most 4.5 m/s (~10.1 mph).

The temperature range compares warmer/cooler ambient scheduling assumptions;
the spread introduces a near-saturation screen; the wind threshold tests a
low-wind assumption. None is presented as a product-approved threshold.
The air-minus-dew-point spread is explicitly NOT the substrate-minus-dew-point
margin required by a roofing system. Station air is never a substrate proxy.
The original ambient columns have NO precipitation screen. The separately
labeled comparison below adds limited precipitation evidence, not a snow-cover,
gust, roof dryness, roof moisture, frost, solar radiation, cure or forecast model.

A run is a maximal sequence of passing adjacent UTC-hour bins on one local
working day. Failed/unknown bins, missing hours, nights, weekends and date
boundaries break it. A run of four or more bins contributes its full length
once to hours_in_runs_4plus; runs_4plus counts these maximal sequences, not
overlapping four-bin windows and not days. Four sampled bins do not prove
four hours of continuously favorable conditions. Reports occur at varying
minutes; weather between observations is unknown.

The CSV has 2 stations x 12 months x 2 scenarios = 48 rows. expected_hours =
no_report_hours + unusable_report_hours + usable_hours. passing_hours includes
isolated passes; hours_in_runs_4plus is a subset. Scenarios overlap; never add
their totals. The article imports the generated JSON for its comparison table.

## Reproduce

Python 3.9+ with IANA timezone data; no pip packages, credentials or APIs required.
From the repository root:

```sh
python3 artifacts/score-spray-foam/scripts/roof-weather.py
python3 artifacts/score-spray-foam/scripts/test_roof_weather.py
```

Defaults: raw cache .cache/roof-weather; outputs
artifacts/score-spray-foam/public/resources/downloads/roof-weather-2024.{csv,json}.
Use --refresh to fetch again. Compare the JSON's source hashes with the committed
audit before accepting changed results. Identical raw inputs produce byte-identical
CSV/JSON outputs; no changing run timestamp is embedded.

The downloadable script is the same source. Outside the repository, explicitly
choose local directories (the downloaded method remains a separate companion):

```sh
python3 roof-weather-analysis.py --cache ./raw-noaa --output ./derived
```

The script validates CSV headers before caching, so an HTML migration page or
wrong station fails loudly instead of becoming an empty or misleading analysis.
Tests cover QC, sentinels, threshold boundaries, missing/gapped sequences,
overnight boundaries, winter/summer conversion and conflicting duplicates.
Precipitation fixtures in scripts/fixtures/precipitation.json cover trace,
overlapping accumulations, absent groups/reports, missing depths, present weather
and conflicts. Additional tests check condition flags, endpoint overlap,
duplicate invariance and a nine-bin sequence split into two four-bin runs.

## Separate precipitation-aware comparison

### Source investigation

The same NOAA ISD format document's **Precipitation Data / AA1–AA4** section
defines period in hours (00–98, 99 missing), depth in tenths of millimetres
(9999 missing), condition, and quality. AA1 is not inherently a one-hour field:
the period component must be read. Our routine records include 1-, 3-, 6- and
24-hour accumulations. They overlap and must not be added or divided into
invented hourly rainfall totals.

Condition 2 means trace: even depth 0000 is not a precipitation-free report.
Condition 9 describes a missing condition code, **not a missing numeric depth**.
We accept an explicit numeric zero with condition 9 and QC 1/5 as a reported
zero, not proof that sub-resolution precipitation was absent. Conditions 1
(impossible/inaccurate), 3/4 (begin/end accumulation), 5/6 (deleted period),
7/8 (missing period), E (estimated), I/J (incomplete) remain unknown here.
We do not reconstruct flagged accumulated/missing periods. Period 00 cannot
establish a one-hour interval; period 99 cannot locate one.

The **Present Weather / AU1–AU9 and AW** sections distinguish precipitation
types from obscuration and other phenomena. AU precipitation 01–09 includes
drizzle, rain, frozen forms and unknown precipitation; 00 means no precipitation
in that report, not throughout a preceding interval. AW 21–25 describes
precipitation in the preceding hour, not at observation time. AW 40–48, 50–58,
60–68, 70–78, 80–87, 89, 92/93/95/96 and diamond dust 11 are precipitation
evidence. AW 26/90 is ambiguous thunderstorm evidence (with or without rain);
it remains unknown. AW 91/94 explicitly lacks precipitation; AW 99 is a tornado,
**not** a missing sentinel. Neither implies safe roofing conditions.
The document lists AW1–AW4, but these files also contain AW5; we decode the
same field format through AW9 and retain frequencies in the audit.
AT manual weather groups are empty in the routine rows in both inputs.
Remarks, special reports, daily summaries and snow-depth groups are not decoded.

### Rule, timing and overlap

This is a **retrospective reported-zero lookback screen**, not a dry-roof rule:

1. Keep the original ambient report selection, thresholds and columns unchanged.
   Independently select the latest routine report per bin for precipitation.
   Conflicting precipitation fields at the selected timestamp make that bin
   unknown even if its ambient readings agree. Never substitute earlier favorable
   readings. A missing routine report is unknown.
2. Require an explicit valid **one-hour** zero in AA1–AA4 at that selected report.
   Its interval is `(report timestamp minus one hour, report timestamp]`, not
   the floored clock-hour bin. Longer zero totals cannot fill absent one-hour
   reports. Empty optional groups do not mean zero.
3. Index every nonempty AA accumulation in **all routine reports**, including
   reports outside working hours and later in the year. Treat its duration as
   ending at its report timestamp. If any valid positive/trace accumulation
   overlaps the selected one-hour interval by positive duration, exclude that
   bin as `precipitation_evidence`. A six- or 24-hour total does not identify
   which sub-hour was wet: this deliberately conservative exclusion means
   **overlapping evidence**, not rain observed in every excluded bin.
   Touching endpoints alone do not overlap. Do not sum, distribute or subtract
   overlapping amounts. Identical duplicates do not change the result.
4. An overlapping known-duration but unusable accumulation, or any supplied
   unusable selected AA field, leaves the bin unknown unless positive evidence
   already excludes it. Unknown-duration records cannot be projected onto other
   bins; at their own selected report they block acceptance.
5. Selected-report AU/AW precipitation also vetoes acceptance, including AU
   vicinity reports as a conservative choice, without calling it on-roof rain.
   Supplied malformed, unsupported or non-1/5-QC weather fields are unknown.
   Empty optional AU/AW fields neither veto an explicit AA zero nor establish
   one; absence of weather codes alone can never pass the screen.

Precedence is selected-report absence/conflict → unknown; otherwise positive
evidence → exclusion; otherwise uncertainty → unknown; otherwise explicit
one-hour zero → reported_zero; otherwise unknown. No prior/following-year
records are fetched, so accumulations reported outside the source year cannot
be used. This is not a real-time scheduling algorithm: later reports may
exclude earlier bins. Variable report minutes can leave gaps between lookbacks;
even consecutive accepted bins do not prove continuous precipitation-free time.
There is no assumed 24/48-hour drying delay and no inference about evaporation,
ponding, absorbed moisture, frost, substrate condition or coating cure.

### Results and accounting

The existing 48 monthly rows gain separate precipitation columns, while
`passing_hours`, `runs_4plus` and `hours_in_runs_4plus` remain ambient-only.
For every row:
- expected_hours = precip_reported_zero_hours + precip_evidence_hours +
  precip_unknown_hours (independent of ambient eligibility).
- passing_hours = precip_screen_passing_hours +
  ambient_pass_precip_evidence_hours + ambient_pass_precip_unknown_hours.
- Combined accepted bins require both screens; unknowns and evidence exclusions
  break runs under the original same-day rule.

Verified against the original input hashes and ambient results:

| Airport / minimum | Ambient passes | Evidence exclusions among passes | Unknown among passes | Combined passes | Ambient → combined bins in runs 4+ | Ambient → combined run count |
| --- | ---: | ---: | ---: | ---: | ---: | ---: |
| Newark / 10 C | 841 | 245 | 21 | 575 | 587 → 418 | 92 → 66 |
| Newark / 15 C | 685 | 191 | 15 | 479 | 481 → 336 | 74 → 51 |
| LaGuardia / 10 C | 633 | 163 | 6 | 464 | 358 → 279 | 61 → 47 |
| LaGuardia / 15 C | 526 | 126 | 2 | 398 | 298 → 235 | 51 → 40 |

Across all 2,358 working bins, Newark has 1,425 reported-zero, 865 evidence
exclusions and 68 unknowns; LaGuardia has 1,480, 845 and 33. These categories
repeat across the two temperature scenarios and must not be added together.
Run counts need not decrease in general: an exclusion can split one long run
into two shorter qualifying runs. Total accepted bins and bins within runs
cannot increase. These results quantify this specific conservative rule,
not actual rainfall duration or lost installation days.

## Interpretation

Baseline: Newark 841 passing bins, 587 in qualifying runs; LaGuardia 633 and 358.
With the 15 C minimum: Newark 685 and 481; LaGuardia 526 and 298.
This shows why isolated ambient readings and sustained sampled sequences should
not be conflated, and why assumptions and station choice affect a planning screen.
It does not identify the best month, establish a causal airport difference, or
estimate sprayable days. Rain may occur in ambient passing bins; even the separate
reported-zero screen is limited to the evidence and intervals above. Airport exposure and
building microclimates differ. Actual work requires site measurements and all
manufacturer, project, safety and forecast requirements, including a sufficient
coating/cure window.
