Skip to content

Grouped and pipeline-level resampling for selection stability - #22

Merged
jkitchin merged 1 commit into
mainfrom
claude/jaxsr-issue-17-2h5e1r
Aug 12, 2026
Merged

Grouped and pipeline-level resampling for selection stability#22
jkitchin merged 1 commit into
mainfrom
claude/jaxsr-issue-17-2h5e1r

Conversation

@jkitchin

Copy link
Copy Markdown
Owner

Closes #17.

Row resampling measures the wrong uncertainty whenever rows are not independent observations. This adds the two missing resampling levels and makes the reporting half usable on replicates the caller produced. All additions are backwards compatible — existing calls behave as before.

What's new

bootstrap_model_selection(model, X, y, groups=...) — resamples whole groups with replacement instead of rows, so a replicate never trains on part of a group it also holds out. This is the isotherm/condition/subject case: a row bootstrap that drops half a group still sees the rest, and reports a spread far narrower than the real between-group variability.

bootstrap_model_selection(model, None, None, resample_fn=...) — the pipeline-level hook. resample_fn(rng) -> (X_b, y_b) is called once per replicate with the bootstrap's RandomState, so whatever stage produced the rows (smoother, derivative estimate, simulation) is re-run per replicate instead of being frozen. X/y may be None; groups and resample_fn are mutually exclusive.

summarize_selection_replicates(replicates, reference=..., resampling=...) — the reporting half, made public. Accepts fitted models, {"features": [...], "expression": ...} mappings, or plain lists of term names, so replicates orchestrated entirely outside SymbolicRegressor get the same summary. It adds structures (each distinct selected feature set with its count and frequency) and n_distinct_structures, which is what makes "nine distinct structural forms" reportable at all. Without a reference, stability_score falls back to the modal structure's frequency.

Bootstrap results now also record resampling ("rows" / "groups" / "pipeline") and n_failed.

cross_validate(..., groups=..., strategy=...) — adds "group-kfold" (whole groups balanced across cv folds) and "leave-one-group-out". Passing groups promotes the default strategy to "group-kfold". Results carry per_group_scores, and edge_groups flags the lowest and highest group labels when labels are numeric — those folds are extrapolation, not interpolation, and averaging them into mean_test_score hides the failure most likely to be asked about. On the demo data the edge groups score roughly 4x worse than the interior ones.

Incidental fix

Replicate cloning now goes through _clone_estimator rather than a hand-written constructor call that listed eight parameters. Options the old clone silently dropped — param_optimizer, param_optimization_budget, constraint_enforcement, constraint_selection_weight, prune_tol — now reach every replicate.

Docs

guides/uncertainty.md gains a "Resample at the level your data actually varies" section with a routing table and worked examples for all three levels; guides/model-fitting.md gains a grouped cross-validation section covering edge groups. The skill decision table now routes on resampling level before method, and both review checklists (CLAUDE.md, jaxsr-review) gained rows for the new signatures. src/jaxsr/skill/ re-synced from .claude/skills/jaxsr/.

Testing

  • 657 passed on JAX, 657 passed on the NumPy backend (scripts/test_under_numpy.py) — no new JAX API calls, so the shim is untouched
  • black --check src/ tests/ and ruff check src/ tests/ clean
  • 47 new tests. The grouped bootstrap is verified by recording the design matrix of every replicate fit and asserting each group appears a whole number of times over, never split; the group splitters are tested for train/test disjointness and fold balance directly
  • Every code block added to the guides was executed end-to-end rather than read for plausibility

Not addressed

  • bootstrap_model_selection cannot aggregate parametric bases (stability score is meaningless) #16 (parametric basis names) is untouched — feature_frequencies still keys on rendered names, so a grouped bootstrap over a parametric basis will still fail to aggregate. The two issues compose but are independent.
  • SymbolicRegressor.cv_folds turns out to be stored but never used (selection is information-criterion driven), so there was no internal CV path to make group-aware. cross_validate(groups=...) is the entire selection-scoring surface.

🤖 Generated with Claude Code

https://claude.ai/code/session_01Vp45J9ECvQ9he31ALBdBFz


Generated by Claude Code

Row resampling measures the wrong uncertainty whenever rows are not
independent observations. Two cases from #17:

- Rows are grouped (several rows per condition, curve, subject). A row
  bootstrap leaks: a replicate that drops half a group still trains on
  the rest, so the reported spread is far narrower than the real
  between-group variability.
- Rows are outputs of an upstream fit (spline evaluations, estimated
  derivatives, simulation output). Resampling those rows perturbs
  nothing about the stage that produced them, so the dominant error
  source is invisible to the bootstrap entirely.

Adds, all backwards compatible:

- bootstrap_model_selection(..., groups=...) resamples whole groups
  with replacement instead of rows.
- bootstrap_model_selection(..., resample_fn=...) hands each replicate
  back to the caller as resample_fn(rng) -> (X_b, y_b), so the upstream
  stage can be re-run per replicate. X/y may then be None.
- summarize_selection_replicates() is the reporting half, made public:
  point it at replicates you produced yourself (fitted models, mappings
  with a "features" key, or plain name lists) and get the same summary.
  It now also reports "structures" (each distinct selected feature set
  with its frequency) and "n_distinct_structures", which is what makes
  "nine distinct structural forms" reportable at all.
- The result records which level was resampled ("rows", "groups",
  "pipeline") and how many replicate fits failed.
- cross_validate(..., groups=..., strategy=...) supports "group-kfold"
  and "leave-one-group-out"; passing groups promotes the default
  strategy to "group-kfold". Results carry per-group scores and flag
  the lowest/highest numeric group labels as "edge_groups", since those
  folds are extrapolation and carry different risk than the mean.

Replicate cloning now goes through _clone_estimator, so options the old
hand-written clone dropped (param_optimizer, constraint_enforcement,
prune_tol, ...) are carried into every replicate.

Guides, skill decision tables and the review checklists are updated to
route on the resampling level first.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vp45J9ECvQ9he31ALBdBFz
@jkitchin
jkitchin merged commit fd12762 into main Aug 12, 2026
7 checks passed
jkitchin pushed a commit that referenced this pull request Aug 12, 2026
Main's #22 rewrote bootstrap_model_selection into a generator plus a public
summarize_selection_replicates, and added row/group/pipeline resampling.
This branch's fix is re-expressed inside that architecture rather than
alongside it:

- Canonical keying moves into _replicate_terms, so both the summariser and
  the bootstrap key model replicates by basis identity. Callers passing
  their own replicates (mappings, name sequences) are unaffected.
- Parameter distributions are accumulated in summarize_selection_replicates,
  so pipeline- and group-level replicates report them too, not just rows.
- The per-replicate library copy moves into _clone_estimator, which #22 made
  the single clone path. That also fixes MultiOutputSymbolicRegressor, whose
  per-output clones shared one parametric library.
- Dropped this branch's "n_successful" (main's n_replicates/n_failed say the
  same thing) and its unfitted-template check (main deliberately accepts an
  unfitted template and reports the modal structure instead).

test_clone_symbolic_regressor asserted the clone shares the template's
library; that identity is exactly what had to change, so it now asserts an
equivalent but independent library.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ARRqz2SjSoYH5N3t66HDjm
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Grouped and pipeline-level resampling for selection stability

2 participants