Skip to content

Navigation Menu

Sign in
Sign up

Repository files navigation

howsure

CI License: MIT

Your weather app shows you one number. That number is the middle of forty forecasts that disagree with each other, and the disagreement is the interesting part.

The fan drawing in from left to right so the band visibly widens with lead time, then repeating for Reykjavik and Singapore to show how differently spread behaves by climate

18.6°C now — every model within 0.6°C of that
40 runs of the same model, started from slightly different guesses at today.
The band widens by about 0.5°C per day. By day 5 they span 5.4°C — 20 to 25.
That's the point where the forecast stops being a number and starts being a range.

Why

Forecast models are run many times over, each from a slightly different guess at what today looks like, because we can't measure the current atmosphere precisely enough to be certain about next Tuesday. Each run is a member. Forty of them for ICON, fifty-one for ECMWF.

Every consumer weather app takes those forty numbers, shows you the middle one, and throws the rest away. So day seven arrives in the same typeface and the same confident tone as tomorrow afternoon, and they are not remotely the same kind of statement.

This keeps the rest.

What it shows

The fan. Nested bands — the full range faintest, then 8-in-10, then the middle half, with the median on top. The widening is the message, so the shape has to be the first thing you see rather than the line through the middle.

Where it stops being useful. A dashed line at the lead time where the 80% band first exceeds 5°C — the point where "18" and "23" are both plausible and you'd dress differently for each. That threshold is a choice, not a law, so it's stated in the UI and configurable in the code rather than buried.

How fast it falls apart. A least-squares fit of spread against lead time, in degrees per day. A settled week and an unsettled one look completely different, and the number makes that comparable.

Switch between London, Reykjavík and Singapore to see it. Tropical spread is almost flat for a week; the North Atlantic falls apart by Thursday.

The bit I want to be honest about

Ensemble spread is not forecast error, and it's known to be too narrow.

Ensembles are underdispersive — the truth falls outside the ensemble range more often than the ensemble implies it should. This is a well-documented property, not a criticism of any particular model.

So the fan is a floor on the uncertainty, not the whole of it. If the band says 3°C, the real uncertainty is somewhat more than 3°C. It would have been easy to leave that out and let the chart imply more rigour than it has, and that caveat is in the UI as well as here.

"60% chance of rain" means something specific too. Not that it rains over 60% of the area, or for 60% of the day. In an ensemble it's very nearly the fraction of members that produced rain — 24 runs out of 40. The agreement module reports the count alongside the percentage for that reason, and rounds the wording into coarse bands, because 58% and 62% are not meaningfully different in a 40-member ensemble and printing two digits implies they are.

Run it

npm install
npm run dev

No API key and no account — Open-Meteo's ensemble endpoint is free for non-commercial use.

How it's built

Every decision is a pure function over plain arrays, so the awkward parts are testable without a browser or the network:

src/
 stats/ quantiles, per-timestep bands, useful horizon, member agreement
 api/ the fetch, and parsing the response shape into something usable
 chart/ axis maths, and the canvas fan
 ui/

Quantiles state their method. There isn't one definition of "the 25th percentile" — R implements nine and they disagree on small samples, and forty members is a small sample. This is type 7, the NumPy and R default, and the tests assert against values computed in Python so anyone checking gets the same digits.

Missing members are dropped, not zeroed. Members run short or carry nulls where their model stops. Substituting zero would drag the median toward freezing — wrong, and plausible-looking, which is worse.

The unsuffixed API key is the control run, not an average of the others. Open-Meteo returns members as sibling keys (temperature_2m, temperature_2m_member01, ...), and the one without a suffix is a full member. Dropping it would throw away real data.

70 unit tests, 99% statements.

What it doesn't do

  • One variable at a time. Precipitation and wind are in the API and the agreement module already handles them; the UI only wires up temperature.
  • No location search — eight preset places rather than a geocoder and a debounce and an empty state.
  • No verification. It never checks past forecasts against what actually happened, which is the honest way to measure whether spread means anything. That's the interesting next thing and it needs an archive.
  • Ensembles only. Deterministic high-resolution models have no spread to show.

The GIF above

Generated, not screen-recorded — npm run demo:gif. Playwright drives the page, stepping redraw(progress) directly rather than filming a real animation, since a screenshot costs tens of milliseconds and filming one gives four frames and a slideshow.

gifenc writes the GIF; Playwright's own bundled ffmpeg is a stripped webm-only build with no GIF muxer. The same frames also go to docs/demo.mp4 via a full ffmpeg if one is on PATH — encoded from the frames rather than converted from the GIF, since the GIF has already been flattened to 128 colours and re-encoding that just carries the banding into the video. 0.14 MB against 2.15 MB for the same seven seconds.

Licence

MIT

About

Your weather app shows one number. That number is the median of 40 model runs that disagree with each other - this shows the disagreement.

Topics

Resources

Stars

2 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

AltStyle によって変換されたページ (->オリジナル) /