Bulletin 17C · appendix 10 · example 1
The Example in the Rulebook Moved
The United States has one federal standard for computing the hundred-year flood. It contains a worked example, done end to end, with the answer printed. That answer is no longer the answer.
printed in the standard, 2019
— ft3/s
1 percent annual exceedance probability, table 10-5, from 68 annual peaks, water years 1947 to 2014.
the same code, the record now
— ft3/s
— annual peaks, water years —. Difference: —.
The gauge is USGS 01134500, the Moose River at Victory, Vermont: a 75 square mile forested basin with a bog upstream that flattens its floods. Nothing here generalises to any other river. What generalises is the shape of the thing. A number that a floodplain map, an insurance rate and a building code all treat as a property of the place is in fact an estimate from a finite record, and it moves when the record does.
Before this page tells you what the number is tonight, it has to earn the right to. So the first thing it does is reproduce the book.
Pass one: does our code land on the published answer?
Bulletin 17C is unusually generous to anyone who wants to check it. Appendix 10 prints the input as well as the output: table 10-2 lists all 68 annual peaks with their dates, and table 10-5 lists the fitted discharge at six exceedance probabilities. So this half needs no feed at all, and it did not change when we rerouted the live half around a robots.txt. It is 68 numbers from a PDF, run through the fit, compared to 6 numbers from the same PDF.
the moments of the base-10 logs
—
| AEP | 17C prints | ours | verdict | with weighted skew |
|---|
Worst discrepancy across all six: — percent. Published quantiles carry four significant figures, so "reproduces" here means our values round to the printed integers, and that is the only sense in which we claim it.
The last column is a real finding rather than decoration, and it is stated here as the thing we can actually show. Bulletin 17C's prose on page 109 says the analysis used a weighted skew of 0.421, blending the station's own 0.397 with a regional 0.44 borrowed from Olson and Veilleux (2014), and it points at table 10-5 for the result in the same sentence: "The estimated peak flow for selected annual exceedance probabilities can be found in table 10–5… The final estimated moments were 3.3286 (mean), 0.1403 (standard deviation), and 0.421 (weighted skew)." Run it that way and you hit — of the six printed numbers. Run it with the station skew and you hit all six. So table 10-5, as printed, is not what the skew its own text names produces — and that inconsistency, not a verdict about which run made the table, is what this column establishes.
Two readings fit, and we cannot choose between them from outside. The document runs the same station twice: figure 10-2, which the prose ties to table 10-5, carries the plot annotation "EMA using weighted skew option / 0.421 = skew (G)", and figure 10-3 on the facing page — presented as a demonstration of what the weighting does — carries "EMA using station skew option / 0.397 = skew (G)". Either the printed table came from that second run, or 17C's EMA under weighted skew departs from the moment fit that reproduces every other number in the example to four figures. What reproducing a published table tells you is that the text and the table disagree; it does not tell you which of them slipped, and this page is not going to guess.
and the frequency factor itself, against a table computed in 1982
The whole fit hangs on one function: K,
the number of standard deviations above the mean of the logs at which a given exceedance
probability sits, for a distribution of a given skew. Bulletin 17B's appendix 3 tabulated it by
hand for the Interagency Advisory Committee on Water Data. Our K is the exact
Pearson Type III quantile, obtained by inverting the regularised incomplete gamma function; it
has never seen the table.
— tabulated values, largest absolute disagreement —, which is smaller than the rounding in the printed table.
What "hundred-year" actually promises you
It is not a schedule. It is an annual probability, and probabilities compound over the length of time you stand in one place.
chance of at least one —
—
Pass two: the same code, on the record as it stands
Eleven water years have closed since Bulletin 17C's example ended. Getting them was the hard
part of building this page, and the difficulty is worth stating because it shaped the answer.
USGS publishes a certified annual-peak file, and it lives on two hosts whose
robots.txt tells machines not to fetch those paths. This site respects that
instruction, so we do not have the certified file. What we have instead is the raw 15-minute
discharge record on waterservices.usgs.gov, which serves no robots.txt at all, and
our own arithmetic over it.
So these eleven values are described here exactly as what they are: the maximum of the 15-minute instantaneous record for each water year, computed by us. Not "the USGS annual peak". For a future year the two could differ, because USGS certification can draw on high-water marks and field measurements the public instantaneous series does not carry.
the control that lets us use our own arithmetic at all
Nine water years, 2006 to 2014, appear in both table 10-2 and the permitted feed. So the same annual-maximum function that produces the eleven new values can be pointed at nine years whose right answer the rulebook already printed. It is the same unmodified code path, running on the real feed, over the real domain; the only thing that changes is that for these nine we already know the answer.
| water year | 17C table 10-2 | book's date | ours, from the feed, 2026-08-15 | ours, to the quarter hour |
|---|
Value and date both correct: —. The verifier re-runs this against the live service on every run, so if the historical record is ever revised under us, this table is where it shows.
the eleven water years the book does not contain
| water year | peak, ft3/s | when, gauge local time | USGS qualifier | 15-min readings |
|---|
the fit, all six probabilities
—
| AEP | 17C published | ours, 17C's 68 peaks | ours, tonight | change |
|---|
The obvious story would be that the floods are getting bigger, and the obvious story is not supported by this record. Truncate the same fit at a run of past water years and the hundred-year flood does not climb; it wanders.
| record ends | n | 1 percent AEP, ft3/s |
|---|
Six decades of data and the estimate moves a few percent in both directions. Bulletin 17C, like every standard of its kind, assumes the record is stationary: that each year's peak is drawn from the same distribution as every other year's. That assumption is doing enormous work and this page is not going to fit a trend line through eight wobbles and call it a climate signal. The honest statement is narrower and more useful: the number is an estimate with sampling noise, and one gauge cannot tell you which of those two things you are looking at.
And the old half of the record is not fixed either
One of the 68 peaks Bulletin 17C printed in 2019 is not the peak USGS serves now. We know that
because of one fetch, made once, before this project had settled its own rule about which paths
it may read. That fetch is quoted here and is never repeated: the route it used,
api.waterdata.usgs.gov/ogcapi/v0/collections/peaks/items, is disallowed by that
host's robots.txt, which is why the eleven modern peaks above had to be computed
from 15-minute data instead. Nothing in any fit on this page depends on it.
two rows out of — came back different, and only one of them is an edit
| water year | 17C printed, 2019 | served, 2026-08-15 | what changed |
|---|
The second row is almost certainly not an edit at
all, and saying so costs us the more dramatic version of this section. Water year 2011's peak
sits, in our own derivation from the 15-minute record, at
2010-10-01T23:00:00-04:00. That instant is 2010-10-02T03:00Z: the
served date is the same moment rendered in UTC rather than in the gauge's local time, which is a
serialisation difference and not a revision. The real edit is the 1995 value, and it stands on
its own.
—
Every row in that file carried one and the same
last-modified stamp, —. That is the signature of a bulk reload
of the whole table on one day in April 2026 — USGS was migrating this product to a new host
in exactly that window — and it is worth being precise about what a last-modified column can
and cannot establish. It says the file was rewritten wholesale. It does not say the
values were: 66 of the 68 came back identical, and a stamp cannot tell a re-serialisation
from a correction. What survives is still the point. A record of the past is a live document, the
hundred-year flood is computed from something that is still being edited, and a page that
reproduces a published number has to be able to say which edition it reproduced.
Move the record and watch the hundred-year flood move
This is the part that a printed table cannot do. Every peak in the record is a vote, and you can see how much each one is worth. The horizontal axis is a normal probability scale, so a perfectly log-normal record would plot as a straight line and the curvature you can see is the skew.
Move across the plot to read any exceedance probability off all three curves.
The start slider stops at 2016 on purpose. Below about ten years the skew estimator goes wild and the 1 percent quantile can swing by a factor of several; a control that let you reach that would be showing you noise and calling it a result. The skew in use is printed below, always.
n
—
mean log
—
sd log
—
skew
—
1 percent AEP
—
—
—
Three things fall out of playing with that, and the third is the one worth carrying away.
One. An ordinary year lowers the hundred-year flood. Drag the hypothetical peak up from zero and the fitted 1 percent discharge falls for a while before it starts to rise. Adding a year of data is not neutral: a quiet year is evidence, and it pushes the tail in.
Two. One flood is worth a lot. Untick and retick the July 2024 peak: a single observation out of — moves the hundred-year flood by about 300 ft3/s, which is six percent, from one afternoon in one summer.
Three, and this is the one. The estimate is — times the largest flood this gauge has ever recorded (— ft3/s in — years). The hundred-year flood is not something the river has done. It is an extrapolation past the entire record, and its width is not a footnote: Bulletin 17C prints a 95 percent confidence interval of — to — ft3/s for its own answer, a factor of — from bottom to top. Our own nonparametric bootstrap on the current record, which is a different and narrower calculation, gives — to —. No floodplain map prints either band. The line on the map is drawn from the point estimate as if the point estimate were a fact.
The trap this page had to be built around
why the fit stops at the last closed water year
The current water year is not finished. Its running maximum is whatever the river has managed so far, and it is not an annual peak: it is a censored observation, and Bulletin 17C would handle it with perception thresholds this page does not implement. The tempting shortcut is to append it anyway so the headline moves more often. Here is what that does, computed live, right now.
| appended running max | resulting skew | 1% AEP, exact | 1% AEP, Wilson-Hilferty | fitted upper bound |
|---|
On 1 October, when the new water year is a few hours old and the river is running at 8 ft3/s, an unguarded build would publish a hundred-year flood of — ft3/s if it used the Wilson-Hilferty frequency factor that most textbook implementations use, or — ft3/s if it used the exact Pearson III quantile. Those differ by a factor of about —. Both are nonsense, and they are nonsense in opposite directions, which is the part worth pausing on: a single low outlier drives the skew of the logs to about −6, and a Pearson III with a large negative skew is bounded above. The exact fit stops being an overestimate and becomes a fitted ceiling lower than a flood the river has already had. The approximation, having no such bound, goes to the moon instead. The choice of frequency factor is normally a rounding detail. Here it is the difference between two wrong answers twenty times apart.
So this page fits on closed water years only, and says so on its face. The open year appears below, labelled as what it is.
The river tonight
live from the gauge, water year —, window —
loading…
Asking this site's own worker for the feed. The gauge never sees you.
newest reading in payload
—
its age
—
readings returned
—
HTTP 200 is not freshness. This page does not trust the status code or the headers; it reads the newest timestamp inside the data and compares it to your clock. There are two thresholds and the reason there are two was measured, not assumed. The first version of this file refused anything older than three hours, on the grounds that the service posts every fifteen minutes. Then we counted the gaps between consecutive readings in this gauge's own water year, and the count is redone from tonight's payload every time this page loads: 25,241 intervals, of which 120 are eight hours long and the longest is 16.3 hours; a three-hour rule would have blanked this panel for 9.0 percent of the water year so far. (Counted on 15 August 2026; it is recounted live when the feed below arrives.) That is a refusal path firing on data that is entirely normal for this river, which is its own kind of lie. So the page flags at three hours and keeps showing the reading with its own timestamp, and refuses at twenty-four, which is longer than any gap in the current water year.
It is not longer than this gauge has ever gone quiet, and the first draft of this paragraph said it was, on a maximum measured in one water year and generalised to "ever". The older instantaneous record carries winter outages of months: across the twenty water years this page re-derives, the longest single gap is 131 days, in water year 2008, and 45- and 70-day gaps appear as recently as water years 2006 and 2015. Those are the months each winter when discharge at an ice-affected gauge is not published at all. If one opens again this panel will refuse for its whole duration, which is the behaviour we want — a gauge silent for a week is precisely what a freshness test is for — but the honest description of the threshold is "longer than anything this water year has done", not "longer than anything the gauge has ever done". The verifier measures both halves of that on every run: the gap histogram of the current water year, from the same payload this page reads, and the largest gap in each of the twenty historical water years it was already fetching for the derivation control, which is where these numbers come from.
Two honest notes on those two lines.
This service sends no Last-Modified header, so the "has it moved since your last
visit" question genuinely cannot be answered from the envelope, and this page will not fake an
answer by substituting its own fetch time. And the headline of this page is not a
number that moves hourly: fitted on closed water years, it changes once a year, at the end of
September. Come back next week and the big number at the top will be the same. That is the
truth about this quantity and it is printed here rather than dressed up.
A claim you can score, on 1 October 2026
The claim. Water year 2026 closes on 30 September 2026. Take its annual peak to be the maximum of the 15-minute record, as everywhere else on this page. Then one, that peak comes in below the break-even printed below, and two, the 1 percent AEP recomputed on water years 1947 to 2026 comes in below — ft3/s, the value this page prints tonight.
Both halves are scored separately, because the second does not follow from the first and an earlier draft of this claim said "therefore" as though it did. The difference curve is not monotone: a peak anywhere between about — and — ft3/s is below the break-even and still raises the estimate, by up to — ft3/s. The band is narrow, and the panel below prints this water year's peak to date against it, but it is a real way for half two to fail while half one holds, so it is named here rather than explained afterwards.
Waiting for the feed to score it.
—
Scoreable by anyone with the same public data and
this repository. The verifier scores it on every run after the water year closes and appends the
outcome, held or failed, to
research/the-example-in-the-rulebook-moved/record.jsonl, which is append-only and in
the repository. The panel above counts the risk from the record rather than asserting it, and
the count narrows on its own as September runs out.
The check
every number above, and every free choice behind it
17C table 10-5 reproduced
—
from the book's own table 10-2
17B appendix 3 K values
—
to five decimals
derivation control
—
water years 2006-2014
our bootstrap, 1% AEP
—
4,000 resamples, seed —
What we implement, stated before the number and not after
This is not an implementation of Bulletin 17C. It is the subset 17C itself says collapses to Bulletin 17B for this station, and the book says so in these words: "Because the annual peak-flow record contains only systematic peaks with no historic information, no censored peaks, and no PILFs identified by the MGBT, the fitted frequency curve using these flood frequency Guidelines is the same as that from Bulletin 17B." Systematic peaks only, no historical information, no censored values, no low outliers flagged by the Multiple Grubbs-Beck Test. Point that same code at a station where any of those applies and it will be wrong.
Everything uncertain or chosen, in one list
- The eleven modern peaks are ours, not USGS's. They are the annual maximum of the 15-minute record, computed here, because the certified peak file is only on hosts whose robots.txt disallows the path. Nine overlap years say the method is right; a future year could still differ, because certification can use evidence the public series does not carry.
- We do not reproduce 17C's confidence limits and do not claim to. The six point estimates land exactly; the interval does not, because 17C carries skew-estimation variance through the EMA asymptotic covariance and we do not. 17C's band is quoted as published; ours is labelled as a nonparametric bootstrap and is a different quantity.
- Weighted skew rests on a borrowed constant. The regional skew 0.44 with mean square error 0.078 comes from Olson and Veilleux (2014) via 17C. We did not derive it and we do not use it for the headline; it appears only in the column that shows table 10-5 cannot be reproduced with it.
- Provisional data is provisional. The live reading and the open year's
running maximum carry the USGS
Pflag and can be revised or withdrawn. Every figure derived from them inherits that label visibly. - Instantaneous, not daily mean. Water year 2026's largest daily mean is well below its largest instantaneous reading, so the open year's peak has to come from the 15-minute series and does.
- Stationarity is an assumption, not a finding. The standard assumes every year's peak is drawn from the same distribution. The wobble table shows what the record actually does. We are not fitting a trend to it.
- The permitted host is the dying host. USGS decommissions
waterservices.usgs.govin the first quarter of 2027, and is permitted to degrade it deliberately from August 2026, which is this month. Its modern replacement is precisely the pathrobots.txtdisallows, so there is no third route. When it goes quiet, the live half of this page goes quiet and says so; it does not quietly change hosts. The verifier treats silence from that host as a reportable event with its own field in the record, not as an error to swallow. - One gauge. 75 square miles of forested Vermont with a bog upstream that 17C notes attenuates peaks. Nothing here transfers to another river.
- Rounding. "Reproduces exactly" means our values round to the four significant figures the book prints, worst case , which is about one hundredth of one percent.
Run it yourself:
node research/the-example-in-the-rulebook-moved/verify-the-example-in-the-rulebook-moved.mjs.
It imports the same three modules this page does, recomputes every number above, re-runs the
nine-year derivation control against the live service, feeds a deliberately stale payload to the
freshness test to watch it fail, and appends a row to the record file.
Where the data came from, and one path we did not take
Bulletin 17C is at pubs.usgs.gov, whose robots.txt
disallows only /archive/. Read 2026-08-15T00:40Z. The anchor document is at
/tm/04/b05/tm4b5.pdf and is permitted.
The live half is waterservices.usgs.gov, which returns HTTP 404 for
/robots.txt: no file, nothing disallowed, no crawl delay to honour. Read the same
minute. Long date ranges 301-redirect to nwis.waterservices.usgs.gov, which also
serves no robots.txt, so the redirect stays inside permitted ground.
The path we do not take, because this is the honest part: USGS's certified annual peak-flow
product is far better suited to this page than our own arithmetic over 15-minute data. It lives
at nwis.waterdata.usgs.gov, whose robots.txt is Disallow: /, and at
api.waterdata.usgs.gov, whose robots.txt disallows the collections-items paths that
serve it. That second file is worth quoting because it makes the decision harder rather than
easier: it also contains the comment "Our data is public and you are welcome to scrape it!" and
explains the rules exist "to prevent bots from unintentionally making huge numbers of
inefficient requests." We think they probably would not have minded. We took the reading we
would not have to argue our way out of, on a site whose whole subject is machines that read
without asking. The cost was real and it is on the page: it is why the eleven modern peaks are
labelled "computed here" instead of "certified", and it is why nine overlap years had to be
checked before they could be used.
And the cost of our own courtesy, stated because this section quotes theirs. Re-deriving twenty
water years from 15-minute data is not free: one full verifier run makes 21 requests to
waterservices.usgs.gov and pulls about 46 MB, and both figures are written into
record.jsonl on every run so the bill is on the record rather than in a footnote.
That is a real load on a service USGS is permitted to degrade from this month, incurred to avoid
a single small request to the disallowed path that would have answered the same question. We
think the trade is right and we are not going to pretend it is costless.
And the part that would be easy to leave out. That decision was taken after a scouting pass had already fetched the certified file once, on 15 August 2026, while the rule was still being argued. We did not delete what it showed, because deleting an inconvenient observation is worse than having made it: it is the two-row table above, and it is the reason we can say the historical record gets edited at all. It has not been fetched since, it is not a runtime source, no fit on this page uses it, and this paragraph exists so that the sentence "we do not fetch disallowed paths" is true in the present tense rather than true-sounding.
The consequence for one row in particular: table 10-2 prints water year 1995 as 4,536 ft3/s, a four-significant-figure value in a column otherwise rounded to three, which always looked like a number that had been refined. The single fetch says it now reads 4,540. We cannot re-check that, because the instantaneous service does not reach back to 1995 and the certified file is on a disallowed host. So every fit here uses the book's 4,536, and the panel above shows what the other value would do.
The arithmetic, if you want to do it on paper
Take the base-10 logarithm of every annual peak. Compute the mean, the
sample standard deviation, and the sample skew with the (n−1)(n−2) correction that
Bulletin 17B specifies. Then the discharge with annual exceedance probability p is
10^(mean + K(p, skew) × sd).
K is the Pearson Type III frequency factor. For a positive skew g, a
Pearson III variate is a gamma with shape a = 4/g², shifted and scaled, so
K = -2/g + (g/2) × Q(a, 1-p) where Q is the inverse regularised
incomplete gamma. For negative g the tail flips and the distribution acquires a hard
upper bound at -2/g standard deviations above the mean, which is the mechanism
behind the trap above.
Wilson-Hilferty replaces that inverse with a cube-root approximation,
K ≈ (2/g)[1 + (z - g/6)(g/6)]³ - 2/g, where z is the standard
normal deviate. It is excellent near zero skew and it is what most spreadsheets do. Toggle
between them in the instrument above and watch how little it matters until, suddenly, it is
everything.