Skip to content

Add matrix-free multinomial solver, prediction guardrails, and monotone spline interactions - #35

Merged
PricingFrontier merged 9 commits into
mainfrom
agent/multinomial-matrix-free-guardrails
Jul 16, 2026
Merged

PricingFrontier merged 9 commits into
mainfrom
agent/multinomial-matrix-free-guardrails

Conversation

@PricingFrontier

Copy link
Copy Markdown
Owner

Summary

Rolls up the insur-arena work stream since v0.8.15, in three layers:

  • Monotone spline interactions (488fd1f): monotone/smooth spline terms inside interaction terms — interaction-local spline specs, category-major basis ordering, and per-category smooth constraint blocks.
  • GLM guardrails + groundwork (33dcea5): predict() fails closed on extreme linear predictors (on_extreme_eta="raise", override with "clip"/"warn"); alpha > 0 with smooth terms routes to the fixed λ=α D'D penalty solver and enforces monotonicity (previously scalar ridge + a warning); L1/elastic-net with smooth terms now raises.
  • Matrix-free multinomial + response ceiling (66cfe38, 967733d): opt-in solver="matrix_free_cg" (diagonally preconditioned Newton-CG via Hessian-vector products, no dense Hessian) with solver="auto" fallback and solver_name/matrix_free_cg_iterations reporting; opt-in predict(response_ceiling=...) extrapolation cap ("auto" = 10x max observed training response, warns on engagement, persisted across serialization).

The final layer includes fixes for all 10 confirmed findings from a 33-agent adversarial review: uncapped internal consumers (complement offsets, loss, calibration, diagnostics), serialized response_scale, null-deviance dense-guard floor, CV fail-fast preflight for matrix-free incompatibilities, CG warning dedup, sparse-cache gating, shared Newton-loop convergence contract, and .pyi stub sync.

Behavior changes vs 0.8.15 (release-notes material)

  • predict() raises PredictionError on |eta| > 50 by default instead of returning overflow-scale means.
  • alpha > 0 + smooth terms: monotonicity is now enforced via the fixed-penalty smooth path (fitted coefficients change for smooth+ridge fits); l1_ratio > 0 + smooth terms now errors.
  • Everything else is opt-in and backward-compatible: response_ceiling defaults to None; multinomial solver defaults to "dense".

Suggested release: 0.9.0 (minor, for the default-behavior changes above).

Testing

  • cargo fmt / clippy -D warnings clean; 833 Rust tests pass (2 new regression tests)
  • Full Python suite: 1,584 passed, 0 failed; ruff + pre-commit clean
  • Termination-contract, oracle-fixture, and guardrail suites all green

🤖 Generated with Claude Code

PricingFrontier and others added 9 commits July 8, 2026 09:28
Multinomial: opt-in solver="matrix_free_cg" (diagonally preconditioned
Newton-CG via Hessian-vector products, no dense Hessian assembly), with
solver="auto" falling back to it when the dense guards reject the fit.
Results expose solver_name and matrix_free_cg_iterations.

GLM predict(): response_ceiling="auto" guardrail caps unbounded-mean
families (Poisson, Gamma, Tweedie, negbinomial) at 10x the largest
observed training response, so runaway extrapolation cannot silently
dominate deviance-based scores.

Review hardening on top of the original work:
- warn when the auto ceiling engages; never truncate silently
- internal consumers (complement offsets, compute_loss, calibration,
  diagnostics) score the true mean via response_ceiling=None
- persist response_scale in to_bytes/from_bytes so save/load does not
  change default predict() behavior
- floor the null-deviance sub-fit's dense guards at q=K-1 so a
  converged matrix-free fit cannot fail at the null-deviance stage
- fail fast on matrix_free_cg + lasso/sign-constraints/covariance in
  the CV path instead of swallowing per-fold errors into alpha=0, and
  keep the dense CV preflight for solver="auto" when the final refit
  must compute covariance
- dedup the CG non-convergence warning, gate the sparse row cache on
  the dense solver, share one Newton-loop convergence contract
- update the stale monotonicity test: the alpha>0 smooth path now
  enforces monotone constraints via fixed lambda=alpha D'D penalties

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
predict() stays the honest inverse-link by default: a pricing library
must not alter numeric outputs across an upgrade, capped values look
plausible while runaway ones look broken, and the pathological tail is
already fail-closed by on_extreme_eta="raise". Scoring pipelines that
want the extrapolation cap opt in with response_ceiling="auto" (10x max
observed training response, warns on engagement) or an explicit float.
Internal consumers keep pinning response_ceiling=None explicitly.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Raises the Python coverage back over the 97% CI gate by testing the
paths 33dcea5 introduced without tests: fit_diagnostics, the per-family
prediction deviance rows in predict_diagnostics (poisson/gamma/binomial/
tweedie/gaussian + validation), the on_extreme_eta="warn" policy,
complement overrides (column and model+exposure) at predict time,
spline df=/k= spec parsing, CV path-info properties, and the multinomial
CV lasso classification.

Writing the explore() test caught a real bug: the array-exposure branch
raised NameError because formula.py uses function-local polars imports
and explore() was missing its own. Fixed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The explore() coverage test ran explore_data in the repo root, whose
documented auto-save overwrote analysis/exploration.json; the test now
chdirs into tmp_path, and the artifact is restored to its prior state.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…eview

Interaction splines (shipped broken in v0.8.16):
- interaction-local spline specs no longer clobber a same-named
  main-effect spline (registry and basis caches are context-scoped),
  fixing predict crashes under divergent widths and silent mis-scoring
  under same-width divergent specs
- monotone smooth x categorical blocks now encode one OWN curve per
  category (full-k, category-major) instead of k-1 sign-constrained
  deviations, which forced every category at/above the reference and
  fitted true negative category gaps as zero; the categorical main
  effect is required for free per-category levels (fails closed
  otherwise)
- monotone smooths and D'D penalties are now honored with continuous
  co-factors (the constraint governs the spline function f itself) and
  in pure continuous x spline interactions; previously both were
  silently dropped
- fail closed on the remaining unsupported shapes: monotone fixed-df
  splines in interactions, multi-categorical monotone interactions,
  and monotone tensor-product smooths
- serialization schema bumped to 5; v4 blobs are rejected with a
  targeted error (v0.8.15 and v0.8.16 wrote incompatible cat x spline
  layouts under the same version number)

Guardrail/diagnostics defects (unreleased, from 33dcea5):
- diagnostics() clips extreme eta at its internal predict calls instead
  of dying on the pathological models it exists to inspect
- predict_diagnostics deviance rows use the Rust unit-deviance helper
  with the model's var_power/theta embedded (negbinomial rows were
  scored with the Poisson formula)
- the 2000-iteration monotone budget applies on the alpha>0
  fixed-penalty route too (was 25)
- on_extreme_eta/eta_clip are validated on every link, not only log
- fit() documents the alpha>0 smooth-route semantics (parametric
  columns unpenalized, standardize not applied)

Every fix carries a regression test mirroring the review verifiers'
reproductions.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
CI exposed platform-dependent monotonicity violations (0.09-0.12 on
ubuntu/windows BLAS, clean on macOS): with include_main=True the main
x-smooth overlapped the full-k per-category curves, and the free split
between the two sequence-constrained components does not compose into a
function-level monotonicity guarantee. The per-category curves already
nest any common component, so the redundant same-variable main spline
is now rejected with an instructive error, making per-category
monotonicity rest on the solver's per-block guarantee alone — a 12-seed
x 3-category sweep shows exactly zero violation.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@PricingFrontier
PricingFrontier merged commit 97e2bc5 into main Jul 16, 2026
17 checks 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

Development

Successfully merging this pull request may close these issues.

1 participant