Measuring what a cyclotron learned
This page defines Cyclotron's headline evidence: a before-and-after number on a fixed held-out set of human labels. It covers the metric, how the held-out set is chosen and frozen, the report format, and how the shared UI shows it in an embedded application (Papyrus) and in the central lab.
The question it answers
On reviews it never trained on, how much better does the cyclotron agree with editors now than when it was created?
Two classifiers answer the same items:
- Before is version 1 as the store was created: the seed rubric and any priming examples, with no reviews and no ML model.
- After is the classifier active now, with its learned rubric, example list, supporting questions and fitted ML model.
Both are scored on every item of one frozen held-out set, with the same request clock, so the only difference between the two columns is what the cyclotron learned from reviews.
The metric
The headline is balanced accuracy: for each label, the share of items
with that label the classifier got right, averaged over labels. A rare label
(include in Papyrus) counts as much as a common one, so a classifier that
says exclude to everything scores 50%, not the 85% plain accuracy would
give it.
The report also carries accuracy, precision and recall for the positive label (macro averages when a classifier has no positive label) and agreement on each label, for both endpoints.
Weighting. Once the review rate tapers, confident decisions are reviewed only at a sampled rate. Each held-out item therefore counts with the inverse of the probability it was sent to review (its recorded selection propensity), so the numbers stand for every decision in the audit share and not only the uncertain ones that were reviewed. While every decision is reviewed, every weight is 1.
Is the change real? The report puts a 95% interval on the headline before
and after and on the paired change (headline.beforeInterval,
afterInterval, changeInterval; the change in percentage points). The
intervals come from a bootstrap over the held-out items: 1,000 resamples with
replacement, each recomputing the weighted balanced accuracy of both endpoints,
from a generator seeded by the set id so the same set and predictions always
give the same intervals. The verdict follows the weighted change interval, so
it cannot contradict the headline number:
| Verdict | When |
|---|---|
nothing-learned-yet |
The active classifier is still version 1 with no ML model. |
too-few-labels |
The rarest label has fewer than minimumPerLabel held-out items (default 10). Numbers are shown, no claim is made. |
improved |
The change interval is entirely above zero. |
worse |
The change interval is entirely below zero. |
no-clear-change |
The change interval includes zero. |
The report still counts the items the active classifier newly gets right
(gained) and newly gets wrong (lost) and runs an exact two-sided sign test
on those pairs (pValue). Those are unweighted item counts shown for
transparency; once the review rate tapers they can point the other way from the
weighted headline, and change.summary says the verdict follows the weighted
interval. Reports written before the intervals existed carry no interval fields.
Only improved supports a public claim such as the marketing site's.
The held-out set
The engine already splits items by identity before any label is seen: about 20% of items fall in the audit share, and their reviews never train, tune, prime or select examples for any classifier. A held-out set is frozen from that share:
- Only reviews the cyclotron selected (by the review program, low confidence or the random audit draw). Reviews a reviewer chose to make are excluded: they are not a sample of decisions, and they are used for training.
- The latest active label for each item; undone reviews are dropped.
- Frozen means copied: item values, labels, selection propensities and the freeze time are stored in the set. Later labels go into a new set, which freezing again creates; freezing with no new labels returns the same set.
- If a frozen item is later reviewed by a reviewer's own choice it moves into
training; it is then left out of evaluations and counted as
excludedSinceFrozen. A label corrected after freezing is counted aslabelsChangedSinceFrozen; scoring always uses the frozen label, so a set never changes underneath a comparison, and that count is informational only (the report says so inheldOutSet.scoringNote).
Sets and reports are stored in the cyclotron's own store, so they work the
same on the local SQLite store and the S3 cloud store, and travel with
snapshot and restore.
Cost
Freezing makes no model call. Evaluating makes at most two decision-model
requests per scored item (held_out_request_bound()), through the store's
shared request cache: re-evaluating after no change costs nothing, and after a
new version only the "after" column is new. An evaluation that could exceed
the open's request ceiling is refused before any call. Nothing is learned,
fitted or promoted during an evaluation.
Engine API
summary = cyclotron.freeze_held_out_set() # no model call
bound = cyclotron.held_out_request_bound() # most new requests an evaluation can make
report = await cyclotron.evaluate_held_out() # cyclotron-held-out-report/v1
history = cyclotron.held_out_reports() # the last 10, oldest first
Each takes classifier= for a cyclotron with several outputs, and
evaluate_held_out takes set_id=, minimum_per_label= and the same
execution_guard= as decide.
Report format
cyclotron-held-out-report/v1 is described by
src/decision_flywheel/schemas/cyclotron-held-out-report.v1.schema.json,
with an example beside it and TypeScript types in the package
(cyclotron/held-out-report). Its parts:
headline: the metric, its definition, before, after,changePointsand the three 95% bootstrap intervals.beforeandafter: version, refits, fingerprint and all metrics.change: gained, lost, both right, both wrong, the p-value, the verdict and a one-paragraphsummarya person can read.heldOutSet: the set's id, freeze time, size and label counts, and what was left out since freezing.cost: new and cached requests and tokens.items: per item, the label, both predictions, its weight and the outcome. Only identities and labels, never item text.
Showing it in either deployment mode
The package exports HeldOutReportView (render a report) and HeldOutPanel
(load, freeze and evaluate through the host's transport) from
cyclotron/components/held-out-report, and createHeldOutClient from
cyclotron/held-out-report. Styles are in styles/components.css.
Both modes serve the same two GraphQL operations, so the card is the same component on the same data:
query { heldOutOverview(cyclotronId: ID!, classifier: String): JSON }
mutation { heldOutCommand(action: "freeze" | "evaluate", payload: JSON!): JSON }
decision_flywheel.held_out_service.HeldOutService implements them. The host
supplies open_cyclotron(cyclotron_id, max_requests=None), which opens the
authorized store for one call and raises for any other (evaluating reopens with
max_requests equal to the confirmed request bound); a scale-to-zero host such as Papyrus holds
the store's lease only while the call runs.
- Embedded (Papyrus). Papyrus's own API mounts the two operations with a
HeldOutServicewhose opener checks the account and publication grant, and placesHeldOutPanelbeside the status card. Papyrus owns that UI path. - Central lab.
create_app(held_out=HeldOutService(...))serves the same operations. Hosted account mode accepts only an account-scoped service on the workspace (workspace.held_out), never a global one.HostedRuntime(open_cyclotron=...)wires that service fromopen_cyclotron(account_id, cyclotron_id, max_requests)and the oneHostedConfig.allow_live;create_apprefuses services whoseallow_liveflags disagree, andcapabilities.liveEnabledreports the shared flag.
evaluate needs live authority (allow_live=True) and the caller must send
confirmedRequestBound equal to the bound the overview showed; the panel's
button says "Evaluate (up to N model requests)".