Skip to content
// morris-labs
livebayesianmarimoagriculture

Clay County auction analysis

Seven years of USDA sale-barn reports for one Alabama cattle auction — Bayesian market regimes, a time-varying value of gain, and a validation rule that caught a typo in the source data.

Every Tuesday the USDA publishes a market report for the Clay County Livestock Auction in Lineville, Alabama — head count, weight range, price range, one line per lot, as a PDF. This pulls 300 of them into 15,583 rows and asks what the market actually did.

The dashboard is a marimo notebook exported to WASM, so it runs entirely in the reader’s browser with no server behind it.

The constraint that shaped everything

A WASM export runs on Pyodide, and Pyodide has no jax, numpyro, torch, pymc or stan — verified against its 334-package recipe list. So none of the Bayesian work can happen where the dashboard runs.

Inference moved offline. fit_changepoints.py and fit_value_of_gain.py run NumPyro on a real machine as part of the weekly sync, write their posteriors to JSON, and commit them; the notebook fetches those artifacts and does nothing but render. Any interactive control has to be expressible as a precomputed grid — the regime-sensitivity slider is a lookup across six prior settings, not a re-fit.

The sync fails the run if any fit diverges, so a non-converged posterior is never silently committed.

Market regimes

A product-partition changepoint model splits each price and volume series into stretches with their own level and their own monthly drift. Every possible set of breakpoints is marginalised exactly by a forward algorithm in O(n²) — nothing is approximated at the discrete level. NUTS samples only the continuous hyperparameters, so changepoint probabilities carry hyperparameter uncertainty rather than conditioning it away at a point estimate.

Three decisions did most of the work:

  • Regimes have slopes. A level-only model renders a sustained climb as a staircase — ten regimes for steer prices, several of them three months long. Giving each regime its own drift won on marginal likelihood for all five price series, by up to Δ log-evidence 13.3, and roughly halved the regime count.
  • A regime lasts at least three months. Negative-binomial overdispersion alone was not enough; a single freak month still claimed its own regime at P = 0.985 until the floor went in. A one-month excursion is an event.
  • Volume is head per sale-day. The barn holds one to six sales a month, so a raw monthly count measures how many auctions happened as much as how much cattle moved.

54 fits, zero divergences, worst r-hat 1.0054, 108 KB of posterior shipped.

Value of gain, as a coefficient that moves

The number that decides a trade is the value of gain — what the market pays for one more pound of liveweight, read against what a pound costs you to add.

Fitting it once across seven years turns out to be the wrong shape: the coefficient roughly doubled in every market type over the period. So it is fit as a dynamic linear model instead, the regression coefficient following a random walk from month to month, which makes beta_t the value of gain in that month with a credible interval attached.

The walk scale is partially pooled across the five market types. Slaughter bulls have 814 lots against feeder bulls’ 4,959, and pooling lets the thin series borrow strength on how fast a coefficient plausibly moves rather than inventing that from almost nothing. The state runs on a complete monthly grid with Δt scaling, so a market that did not trade in a given month widens its band instead of having the gap quietly closed up.

Checking the source against itself

USDA reports are published documents, not clean data, and they can contradict themselves. One rule is enforced before rows enter the dataset: no lot’s head count may exceed the total receipts its own report declares for that day.

It exists because of one line in the report for 21 August 2019, which listed 1,125 boner cows at an average weight of 1,125 lb — head count and weight being the same number is the tell — on a page whose own header declared ten head of slaughter cattle. Left in, that single cell manufactured a market event that never happened: an entire four-month “regime” in the slaughter series at +112.3 head/sale-day of drift, which dissolved the moment the row came out.

Across all 300 reports the rule rejects exactly one row out of 15,584, with no false positives, and what it rejects is quarantined to a file rather than silently dropped.

Stack

  • marimo — reactive notebook, exported to WASM for a serverless deploy
  • NumPyro / JAX — changepoint and dynamic-linear models, offline only
  • pdfplumber — extraction from the source AMS reports
  • Plotly — charts inside the notebook
  • GitHub Actions — weekly sync, re-fit, and commit of new posteriors