Skip to content

Repository files navigation

Weather nowcasting from a single station

A cheap weather station costs about thirty euro. Mine sits on a shelf and shows the temperature, the pressure, the humidity, and a guess at the next few hours of weather. That guess is not machine learning. It is a fixed rule from the 1920s called the Zambretti forecaster. It reads the barometer, checks whether the pressure is rising or falling, glances at the wind, and picks one of twenty six canned forecasts.

This project asks a simple question. Given the same kind of readings a home station has, can a model do better than that rule? The answer is yes, and by a clear margin.

What it does

It predicts the next few hours from the last few hours of readings. This is called nowcasting. The main target is rain: will it rain in the next N hours, three by default. The suite adds three more targets: temperature, fog, and frost.

Everything is trained on about ten years of hourly data and tested on the most recent two years, which the model never sees while training.

What it does not do

It does not forecast days ahead. A single station only sees one point on the map. To forecast well beyond a few hours you need to see weather systems coming from far off, and that means data from many places. So the aim is not to beat the national forecast. The aim is to beat the gadget on the shelf, and to find out how far one station, or a few, can actually go.

How it works

The model is a gradient boosted decision tree. I use scikit-learn's HistGradientBoosting, which is the same idea as LightGBM. It builds a lot of small decision trees one after another, and each new tree tries to fix the mistakes of the trees before it. For rain, fog and frost it predicts a probability. For temperature it predicts a number of degrees.

Here is the flow, from the readings to the prediction:

flowchart TB
    A["Hourly readings<br/>temperature, humidity, dew point, pressure, wind"] --> B["About 40 features<br/>current values, values 1 to 6 hours ago,<br/>pressure and humidity trends,<br/>rolling summaries, time and season"]

    subgraph GBT["Gradient boosted trees"]
        direction LR
        T1["tree 1"] -->|correct the error| T2["tree 2"] -->|correct the error| T3["tree 3"] --> TN["tree N"]
    end

    B --> GBT
    GBT --> D["Prediction<br/>rain probability for the next few hours"]
Loading

Each tree in the chain works on what the trees before it got wrong. Add them all up and you get the prediction. That is what gradient boosting means.

I do not feed the raw readings straight in. For each hour I build about forty features from the readings up to that point:

  • the current values: temperature, humidity, dew point, pressure, and wind.
  • the recent past: those same values one, two, three, and six hours ago.
  • the trends: how far the pressure has moved over the last one, three, and six hours, and the same for humidity and temperature. The trend is what the barometer is really telling you.
  • rolling summaries: the lowest, highest, and spread of pressure over the last six hours.
  • the time and the season, encoded so hour 23 sits next to hour 0.

Every feature only looks backwards. Nothing at time t uses a reading from after t. The label is the only thing that looks forward. A test checks this holds (see How I kept it honest).

To train, I sort by time and split into three parts. The oldest seventy percent is for training. The next ten percent is for picking the decision threshold. The most recent twenty percent is the test set, about two years the model never sees while training.

The model competes against three simple baselines: persistence (assume the next few hours look like right now), climatology (always guess the long run average), and Zambretti (the barometer rule the cheap station uses).

What the numbers mean

Rain, fog, and frost are yes or no questions, and the model answers each with a probability. Three scores grade those probabilities.

ROC-AUC is about ranking. Take one hour where it rained and one where it did not, at random. ROC-AUC is the chance the model scored the rainy hour higher. Half is a coin flip. One is perfect. It does not depend on where you set the cut off. Its weakness is that when an event is rare it can look flattering, because there are so many easy dry hours to get right.

PR-AUC is the honest score when the thing you predict is rare. It blends precision (of the hours you flagged, how many were right) and recall (of the hours that mattered, how many you caught). Its floor is not a half. Its floor is the base rate, the share of hours that actually rain. So a rain PR-AUC of 0.68 when one hour in five rains sits well above the 0.21 floor.

Brier skill is about whether the probabilities are honest, not just well ranked. The Brier score is the average squared gap between the probability and what happened. Say ninety percent and it rains: small gap, good. Say ninety percent and it stays dry: big gap, bad. Lower is better. Brier skill rescales that against always guessing the average. Zero means no better than that guess, higher is better, and a negative value means worse. Zambretti lands negative because it only gives a hard yes or no, so its probabilities are poorly calibrated.

Temperature is a number, not a yes or no, so it uses different scores. MAE is the average miss in degrees. R2 is the share of the ups and downs the model explains, where one is perfect. Skill versus persistence is how much smaller the error is than just assuming the temperature holds steady.

Results

The numbers below are for Dublin Airport, using real Met Eireann station data, tested on the last two years.

Rain, against the baselines

Rain model against the baselines

Method PR-AUC ROC-AUC Brier
Model (gradient boosted trees) 0.67 0.87 0.11
Persistence (is it raining now) 0.46 0.71 0.14
Zambretti (the barometer rule) 0.33 0.71 0.32
Climatology (always guess the average) 0.21 0.50 0.16

PR-AUC and ROC-AUC measure how well each method ranks rain risk. Neither depends on a chosen cut off, so they are the fair comparison. The model roughly doubles the ranking skill of the Zambretti rule. Its probabilities are honest too. When it says sixty percent, it rains about sixty percent of the time.

The other targets

The four target scorecard

Target Task Model score Naive baseline Grade
rain (next 3h) classification ROC-AUC 0.88, PR-AUC 0.68 base rate 21% Excellent
temperature (+3h) regression MAE 0.91 C, R2 0.94 persistence 1.48 C Excellent
fog (next 3h) classification ROC-AUC 0.95, PR-AUC 0.21 base rate 1.2% Good, but rare
frost (next 3h) classification ROC-AUC 1.00, PR-AUC 0.90 base rate 2.9% Easy target

Temperature is the strongest result. The model predicts the temperature three hours out to within about one degree. The naive "same as now" guess is fine at midnight, but it falls apart at dawn and dusk when the temperature turns. The model learned that daily rhythm, so its error stays flat while the naive error more than doubles.

Fog is ranked well, but it is rare, so the precision you can reach is limited. I left that number honest rather than dress it up. Frost looks almost perfect, but that is mostly because freezing in three hours is already decided by how cold it is right now. It is the easy one.

Does adding upwind stations help?

Upwind stations across horizons

Irish weather comes off the Atlantic from the west and south west. So I added recent readings from three stations upwind of Dublin: Valentia, Shannon, and Casement. A station upwind sees the weather before Dublin does.

Horizon Dublin only (PR-AUC / ROC) With upwind (PR-AUC / ROC)
1h 0.62 / 0.90 0.71 / 0.93
3h 0.67 / 0.87 0.75 / 0.91
6h 0.73 / 0.86 0.80 / 0.90
12h 0.80 / 0.85 0.84 / 0.88

It helps at every horizon. The gain is largest from one to six hours, then it fades by twelve. That matches the geography. Those stations are only about one and a half to six hours upwind, so that is the window where they carry the most information. By twelve hours the weather they saw has already arrived. I like this result because the model's skill lines up with real physics, not with noise.

LSTM against gradient boosted trees

LSTM against the trees

I built an LSTM on the raw hourly sequence and put it head to head with the trees, on the same test rows.

Model PR-AUC ROC-AUC Brier skill
LSTM (24 hour sequence) 0.65 0.87 0.14
Gradient boosted trees 0.68 0.88 0.36

The trees win. That fits what most people find on tabular data. The LSTM is close on ranking, but its probabilities are worse and it costs far more compute. Knowing when not to reach for deep learning is part of the job.

How to run

Setup:

python3 -m venv .venv
.venv/bin/pip install -r requirements.txt

Set your location in config.py (LATITUDE, LONGITUDE, LOCATION_NAME). Then:

.venv/bin/python train.py          # rain model, benchmarked against the baselines
.venv/bin/python train_suite.py    # all four targets, with a scorecard
.venv/bin/python train_spatial.py  # the upwind experiment (needs the extra station files)
.venv/bin/python train_dl.py       # LSTM against trees (needs torch)
.venv/bin/python predict.py        # rain chance for the next few hours, right now

Charts and metrics land in reports/.

Data

Two sources, set by DATA_SOURCE in config.py.

Open-Meteo downloads about ten years of hourly history for any coordinates, with no key. It is the easy way to get started anywhere.

Met Eireann gives real station readings, including a real rain gauge. Their bulk download host is gone, so you export the files from met.ie by hand (Climate, Available Data, Historical Data) and drop them in data/. See data/README.md for the station numbers and the steps.

How I kept it honest

  • Split by time, not at random. Train on the oldest seventy percent, test on the most recent twenty. Shuffling a time series lets the future leak into training.
  • No cheating inputs. By default the model only sees what a home station measures: temperature, humidity, dew point, pressure, and wind, plus their trends. Cloud cover and a rain gauge reading are off by default, behind flags in config.py.
  • Real baselines. Persistence, climatology, and Zambretti itself. The model has to beat all three.
  • Metrics that suit rare events. Rain is uncommon, so plain accuracy misleads. I lead with PR-AUC, Brier score, and recall.
  • A test that checks for leakage. It perturbs the future and confirms that past features do not move.

Run the tests with:

.venv/bin/python -m pytest -q

Files

File Role
config.py location, targets, data source, and all the knobs
fetch_data.py download and cache hourly history from Open-Meteo
fetch_meteireann.py read real Met Eireann station files
features.py build features with no leakage, and the labels
baselines.py persistence, climatology, Zambretti
targets.py the four target definitions (rain, temp, fog, frost)
train.py train and benchmark the rain model
train_suite.py train and score all four targets
spatial_features.py build upwind station features and merge them
train_spatial.py the upwind experiment across horizons
train_dl.py the LSTM against trees benchmark
predict.py score the current conditions with the trained model
tests/ unit tests, including the leakage guard

Where this could go next

  • Longer horizons, and predicting how much rain rather than just whether.
  • More upwind stations, or data from further out over the Atlantic.
  • A small ablation to see which upwind station carries the most signal.

About

Short-range weather nowcasting from a single station. Beats the Zambretti barometer rule a cheap station uses.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Contributors

Languages