Reading the residual
The residual is observed minus fitted, and it is where everything the tide does not explain has to live. Four different causes leave four different shapes in it, and telling them apart is most of what the series is good for.
Four shapes
A residual root-mean-square of 0.186 m, as on station-7, is a summary and nothing more. Two records can share that number and mean completely different things, so the first move is always to plot the residual against time and read its shape before its magnitude.
| Cause | Shape in the residual | Time scale |
|---|---|---|
| Storm surge | smooth positive hump | hours to a few days |
| Datum shift | flat step, holds afterwards | instant, then permanent |
| Gauge fault | spikes or a flat line | seconds to hours |
| Seasonal set-up | slow sinusoid under everything | months |
Surge
A meteorological surge is the signal most people are after, and it is the easy one: a rounded elevation of a few tens of centimetres that builds over a day and relaxes over the next. On station-7 the largest residual excursion, 0.53 m on 11 March 2026, is exactly this, and because the harmonic fit has already removed the tide underneath it the surge can be read off directly instead of being eyeballed out of the raw curve.
Datum shift
A level shift shows as a step that starts at one instant and never returns to zero afterwards, and it almost always means the gauge was serviced, re-levelled or renumbered rather than that the sea changed. The library will not correct it, because it cannot know which side of the step is right; what it gives you is a residual in which the step is unmistakable and dated, so the metadata can be checked against that date.
rest = tc.residual(rec, sol)
before = tc.rms(rest[:k]); after = tc.rms(rest[k:])
# a clean step moves the mean, not the rms, on each side
Gauge fault
Instrument trouble is the noisy one and comes in two flavours: bursts of isolated spikes, which the spike flagger should already have caught, and a dead flat stretch where a stuck sensor reports one value while the real level keeps moving. The flat case is worth a direct test, because a frozen reading can sit perfectly inside the plausible range and pass every threshold aimed at outliers.
rec = tc.qc.flag_flat(rec, window="2h")
# a value unchanged for two hours in a six-minute record is not tide
Next: fitting a short record when a year of data is not on offer.