Skip to content

feat(compare): per-fit --exclude-chains (gh#417, gh#418)#420

Merged
vsbuffalo merged 1 commit into
mainfrom
feat/convergence-tooling-gaps
Jul 10, 2026
Merged

feat(compare): per-fit --exclude-chains (gh#417, gh#418)#420
vsbuffalo merged 1 commit into
mainfrom
feat/convergence-tooling-gaps

Conversation

@vsbuffalo

Copy link
Copy Markdown
Owner

What

Adds camdl compare --exclude-chains, resolved per fit and bound to each model's name, so a comparison can score a fit over the same chain subset a predict/summary would band (gh#409).

camdl compare @a @b --exclude-chains @a:4       # drop chain 4 from @a only; @b untouched
camdl compare a.toml b.toml --exclude-chains 3  # bare ids = cohort-wide shorthand

Why

fit predict/fit summary --exclude-chains (gh#409) can drop a known-stuck chain before banding, but compare's plug-in θ̂ was always the posterior mean over all chains. A fit with a stuck chain (R̂≈140, one chain at 0% acceptance) couldn't be scored on its good chains — predict could drop them, compare couldn't — letting a report's bands and scores disagree about which draws were used.

Design

  • Per-fit is first-class. @fit:ids targets one fit and leaves every other whole; the fit name is the string the table shows and --baseline matches. Bare ids stay as a cohort-wide shorthand (fits sharing a stuck-chain index).
  • Explicit-only, by intent. The user names the chains to drop, per fit. There is deliberately no auto-detect/auto-drop mode — post-hoc exclusion biases the posterior toward the retained mode, so that decision must stay visible and auditable, never buried behind a flag. (Every active selection prints a loud, non-quietable bias warning.)
  • No loose semantics. Mixing bare and @fit:ids, an unknown/ambiguous fit name, or a fit listed twice is a hard error — a token can never silently bind nowhere or to the wrong fit.
  • Reuses the shared seam. θ̂ routes through PosteriorDrawsRef::with_selection(...).load_params_with_info() (the same apply_keyed filter predict/summary use), which also folds out compare's second, independent raw draws.tsv reader.
  • Satisfies the 2026-07-09 chain-selection proposal, which deferred compare pending exactly this per-fit syntax.

Tests (red → green)

  • Unit posterior_mean_excludes_selected_chains — all-chain θ̂ pools both chains (β=5.0), subset θ̂ drops the stuck one (β=1.0).
  • Unit parse_cohort_exclude_forms / _rejects_bad_forms — per-fit vs cohort binding; unknown/ambiguous/duplicate/mixed → hard error.
  • E2e compare_per_fit_exclude_chains_rescores_only_the_named_fit — injects a stuck chain into @A; --exclude-chains @a:2 moves @A's prequential elpd while @b is byte-identical (per-fit, not cohort-wide), warning names @A.

Local: compare+chain_selection unit 14, compare_auto_prequential 4, exclude_chains_e2e 11, fit_table_quantities 3 — all green.

Closes #417, closes #418.

…et (gh#417, gh#418)

`fit predict`/`fit summary --exclude-chains` (gh#409) can drop a
known-stuck chain before banding, but `camdl compare` had no such flag:
its plug-in θ̂ was always the posterior MEAN over ALL chains. So a fit
with a stuck chain (R̂≈140, one chain at 0% acceptance) could not be
scored on its good chains — `predict` could drop them, `compare` could
not — letting the two disagree about which draws a report used.

`compare` now takes `--exclude-chains`, resolved PER FIT and bound to
each model's name:

  camdl compare @A @b --exclude-chains @A:4      # drop chain 4 from @A only
  camdl compare a.toml b.toml --exclude-chains 3 # bare ids: cohort-wide

`@fit:ids` targets one fit and leaves every other whole; a bare `ids`
token applies cohort-wide (retained as a shorthand for the case where the
compared fits share a stuck-chain index). Mixing the two forms, an
unknown/ambiguous fit name, or a fit listed twice is a hard error, so a
token can never silently bind nowhere or to the wrong fit. The read-side
chain-selection proposal (2026-07-09) deferred `compare` pending exactly
this per-fit syntax — the cohort-wide form over-excludes when the
compared fits have DIFFERENT stuck chains — so this lands the proposal's
stated prerequisite rather than deviating from it.

Chain exclusion is EXPLICIT-only by design: the user names the chains to
drop, per fit. There is deliberately no auto-detect/auto-drop mode —
post-hoc exclusion biases the posterior toward the retained mode, so that
decision must stay visible and auditable, never buried behind a flag.

θ̂ derivation routes through the shared draws authority
(`PosteriorDrawsRef::with_selection(...).load_params_with_info()`), so
the mean is taken over the RETAINED cloud via the same `apply_keyed`
filter predict/summary use. This also folds out the second, independent
raw `draws.tsv` reader that `posterior_mean_params_toml` carried.
`warn_requested`'s wording is generalized ("from each fit's posterior
cloud"); a new `warn_requested_for_fit` names the targeted fit for the
per-fit form, sharing the one bias caveat.

Optimizer fits and explicit `prequential.json` inputs have no cloud, so
the selection is inert there.

Tests (red → green):
  - Unit `posterior_mean_excludes_selected_chains`: red was a compile
    failure (`posterior_mean_params_toml` took `&Path`, no selection);
    green after routing through the draws ref. All-chain θ̂ pools both
    chains (beta=5.0), subset θ̂ drops the stuck one (beta=1.0).
  - Unit `parse_cohort_exclude_forms` / `_rejects_bad_forms`: per-fit vs
    cohort binding; unknown/ambiguous/duplicate/mixed → hard error.
  - E2e `compare_per_fit_exclude_chains_rescores_only_the_named_fit`:
    injects a stuck chain into @A's cloud; `--exclude-chains @A:2` moves
    @A's derived prequential elpd while @b's is byte-identical (per-fit
    binding, not cohort-wide), and the warning names fit @A.

  compare + chain_selection unit: 14 passed
  compare_auto_prequential (incl. new e2e): 4 passed
  exclude_chains_e2e: 11 passed; fit_table_quantities: 3 passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant