Skip to content

Signal-ladder API renames, and two real defects the naming proposal missed (2.0.0) - #534

Merged
kgdunn merged 9 commits into
mainfrom
claude/critical-review-planning-29pkvp
Aug 30, 2026
Merged

Signal-ladder API renames, and two real defects the naming proposal missed (2.0.0)#534
kgdunn merged 9 commits into
mainfrom
claude/critical-review-planning-29pkvp

Conversation

@kgdunn

@kgdunn kgdunn commented Aug 30, 2026

Copy link
Copy Markdown
Owner

Summary

  • Rename the permutation-null functions so their names state the question they answer: permutation_q2 -> check_predictive_signal, pipeline_null -> count_discoveries_under_null, discriminate_observational -> find_predictive_descriptors. The "discriminator" vocabulary goes with them (result.relate["discriminator"] -> ["predictive_descriptors"], discriminator_significant -> is_predictive, kwarg discriminator= -> find_predictive=, including on the sensory_analyze_descriptive tool schema). Removed names raise AttributeError naming their replacement, following the precedent in univariate/metrics.py.
  • Fix two real naming defects that a review turned up and the original proposal walked past. find_predictive_descriptors claimed in its docstring to apply Benjamini-Hochberg correction; it does not, and never did, the code builds a Westfall-Young max-statistic null as its own inline comment says. And the field carrying that value was named q_value, FDR vocabulary for a family-wise-error-adjusted p-value, while q_value in the sibling associations list of the same result dict genuinely is a BH q-value. One key name, two quantities. It is now p_value_fwer.
  • check_predictive_signal takes x and y first and makes fit_predict optional, defaulting to leave-one-out cross-validated PLS on the raw blocks, so each fold derives its own constants rather than inheriting constants computed from the row it is predicting.
  • empirical_fdr -> null_to_observed_ratio, with the [0, 1] clip dropped. On pure noise the old field reported a tidy 1.0 for draws where shuffling actually out-found the real response, which is the single most informative reading the function can give. A test now pins that at least one such draw exceeds 1.

Two things the proposal asked for that this does not do

  • Its section-5 "defect fix" is dropped. predictable = q2_cv > 0.0 is one of three conjunctive conditions at analysis.py:456, ANDed with a FWER-controlled max-statistic p-value and a jackknife flag, so a loose gate cannot manufacture findings. Its real role is to skip the 199-refit permutation loop. Routing it through a permutation null would have cost roughly 20-46x on the default analyze_descriptive path to fix a problem that is not there. The gate is now documented as the deliberate pre-screen it is.
  • permutation_column_null is kept, not deleted. All three stated reasons fail: it answers a block-level question (one multi-response fit, one record per descriptor) that the per-attribute function does not; the vip note it cited condemns counting VIP > 1 exceedances under response permutation, which is not what it does; and a calibration experiment measured it conservative (VIP 1.9%, cv_beta 4.4% against a nominal 5%). It and find_predictive_descriptors now cross-reference each other so the choice between them is explicit.

The cross-attribute multiplicity gap is real (the max-statistic family is rebuilt per attribute, so nothing corrects across them) and is documented, not closed in this PR, since closing it changes results.

This is a breaking release with no deprecation cycle, departing from CONTRIBUTING.md. Deliberate and recorded in the CHANGELOG: the two multivariate names shipped in 1.76.0, one release and one day earlier, so shims would have carried dead weight to 2.0.0 for names nobody could have adopted.

Test plan

  • uv run pytest green: 2978 passed, 3 skipped, coverage 94.45% against the 92% gate
  • New coverage: the LOO default separates signal from noise, matches an explicit equivalent callable, and clamps an oversized n_components; pre-scaling the blocks does not change the answer; every removed name raises with its replacement on all import surfaces; null_to_observed_ratio equals the raw quotient and exceeds 1 on noise; permutation_column_null is still exported
  • uv run ruff check . and uv run ruff format --check . both pass
  • uv run mypy src/process_improve passes, 153 files
  • uv run sphinx-build docs docs/_build/html -b html -W succeeds, which is where a stale :func: role would have failed CI

Checklist

  • Version bumped in pyproject.toml to 2.0.0 (MAJOR: public names removed), with CITATION.cff set to the identical version and date in the same commit
  • Tests added or updated where relevant
  • ruff check . passes
  • CHANGELOG.md updated

claude added 2 commits August 30, 2026 09:44
Rename the permutation-null functions so their names state the question they
answer, and retire the "discriminator" vocabulary from the sensory relate step.

Scope is set by an adjudicated review: the proposal's section-5 "defect" is not
one (the q2 gate is conjunctive with a FWER-controlled test), the deletion of
permutation_column_null is dropped (all three reasons fail, and a calibration
experiment measured it conservative), and two real naming defects it missed are
fixed instead.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016vCXGaB7zcXSd7GXQoHJig
permutation_q2 -> check_predictive_signal, pipeline_null ->
count_discoveries_under_null. Both old names now raise AttributeError naming
their replacement, following the migration-helper pattern already used in
univariate/metrics.py, on all three surfaces users import from.

check_predictive_signal takes x and y first and makes fit_predict optional,
defaulting to leave-one-out cross-validated PLS. The default deliberately
receives the raw blocks so each fold derives its own centring and scaling
constants, which is the trap the parameter documentation warns callers about.
An out-of-sample null has to refit per fold per permutation, so the default
costs n_perm * n_products fits; the docstring now says so with a figure rather
than leaving the reader to discover it.

count_discoveries_under_null renames empirical_fdr to null_to_observed_ratio
and drops the [0, 1] clip. A ratio above 1 means shuffling found more than the
real response did, which is the most informative reading the function can
give, and clipping hid exactly that case behind a number that looked like a
well-behaved rate.

class_enrichment keeps its name; the module docstring now says why, so the
boundary of the scheme is stated rather than left implicit.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016vCXGaB7zcXSd7GXQoHJig
@codecov

codecov Bot commented Aug 30, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

claude added 3 commits August 30, 2026 09:55
…rted

discriminate_observational becomes find_predictive_descriptors, and the
"discriminator" vocabulary goes with it: result["discriminator"] becomes
result["predictive_descriptors"], the discriminator_significant flag becomes
is_predictive, and the discriminator= kwarg on relate_observational and
analyze_descriptive becomes find_predictive=. The old function name raises
AttributeError naming its replacement.

Two defects fixed along the way, both of them naming errors that the rename
made visible:

- The docstring claimed the permutation p-value was Benjamini-Hochberg
  corrected across the whole family. It is not, and never was: the code builds
  a max-statistic (Westfall-Young) null, as its own inline comment says.
  _attach_fdr, the real BH helper, is only ever called from
  relate_observational.
- The field carrying that value was named q_value, which is FDR vocabulary for
  a family-wise-error-adjusted p-value. Worse, q_value in the sibling
  associations list of the same result dict really is a BH q-value, so one key
  name meant two different quantities. It is now p_value_fwer.

The docstring also now states what the correction does and does not cover: it
is within an attribute, not across them, so on a panel of A attributes roughly
alpha * A are expected to yield a spurious family. That gap is documented
rather than closed here, because closing it changes results.

The q2_cv > 0.0 gate is annotated as what it is: a cheap, deliberately
uncalibrated pre-screen that decides whether the permutation loop is worth
running, ANDed with the calibrated p-value and the jackknife flag. Passing it
cannot make a descriptor a finding.

permutation_column_null is kept and now cross-references its per-attribute
counterpart in both directions, so the choice between block-level and
per-attribute screening is stated rather than left to be guessed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016vCXGaB7zcXSd7GXQoHJig
Mechanical renames across test_multivariate_null.py, test_sensory.py and
test_sensory_end_to_end.py, including the argument reorder now that
check_predictive_signal takes x and y first.

The q_value assertions needed splitting rather than replacing: the one on the
descriptors table is now p_value_fwer, but the two on relate_observational's
associations stay as q_value, because those really are Benjamini-Hochberg
q-values. That the same key name meant two different quantities is the defect
being fixed.

New coverage:

- The leave-one-out default separates signal from noise, matches an explicit
  equivalent callable, and clamps a silly n_components rather than raising.
- Pre-scaling the blocks does not change the answer. This pins down a claim
  worth having in the suite: Q-squared is invariant to an affine transform of
  y, and the default re-derives constants per fold, so a caller who scales
  first is not quietly rewarded with an inflated score.
- Every removed name raises AttributeError naming its replacement, on all the
  surfaces users import from, while an unrelated missing attribute still
  raises plainly.
- null_to_observed_ratio equals the raw quotient, and on pure noise at least
  one draw exceeds 1. The old clipped empirical_fdr reported 1.0 for those
  draws, which is the information the clip was hiding.
- permutation_column_null is still exported, with the reasons its proposed
  deletion failed recorded next to the assertion.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016vCXGaB7zcXSd7GXQoHJig
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016vCXGaB7zcXSd7GXQoHJig
Comment thread tests/test_multivariate_null.py Fixed
claude added 3 commits August 30, 2026 10:06
…cope

permutation_nulls.rst now leads with the zero-argument form, since
check_predictive_signal supplies a leave-one-out default, and says plainly that
the blocks go in unscaled and why. It also gives the cost in fits rather than
letting a reader discover it: n_perm refits per fold is twenty thousand fits at
n_perm=999 on twenty products.

sensory_panel.rst renames the "discriminator" section and, more usefully, adds
what was missing: the max-statistic correction is within an attribute, not
across attributes, so on a panel of A attributes roughly alpha*A are expected
to throw up a spurious family. The page previously implied wider protection
than the code provides. It now also points at permutation_column_null as the
block-level screen, so the choice between the two is stated.

Verified against the -W docs gate, which is where a stale :func: role would
have failed CI.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016vCXGaB7zcXSd7GXQoHJig
MAJOR: public names are removed without a deprecation cycle. CITATION.cff
carries the identical version and today's date in this same commit.

The changelog states the departure from CONTRIBUTING.md rather than leaving a
reader to infer it. The two multivariate names were added in 1.76.0, one
release and one day earlier, so warning shims would have carried dead weight to
2.0.0 for names nobody could have adopted. That is a judgement about this
release, not a precedent.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016vCXGaB7zcXSd7GXQoHJig
Codecov flagged two uncovered lines in multivariate/__init__.py: the
__getattr__ fallback that handles a name which is not a rename. The test only
exercised that path on _null.

Worth covering rather than waving through. The helper intercepts every failed
attribute lookup on the module, so its fallback is what stands between an
ordinary typo and a confusing rename message pointing at an unrelated
function.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016vCXGaB7zcXSd7GXQoHJig
Comment thread tests/test_multivariate_null.py Fixed
CodeQL flagged process_improve.multivariate being imported with both 'import'
and 'import from' in the same scope, and the coverage fix had just duplicated
the pattern into a second test.

Both now use the from-import form, which is the shape ruff had already settled
on in the sibling sensory test.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016vCXGaB7zcXSd7GXQoHJig
@kgdunn
kgdunn merged commit c7ae8aa into main Aug 30, 2026
14 checks passed
@kgdunn
kgdunn deleted the claude/critical-review-planning-29pkvp branch August 30, 2026 11:14
kgdunn added a commit that referenced this pull request Aug 30, 2026
…nstead of 2.0.0 (#535)

* Make the signal-ladder renames backward compatible, and ship 1.77.0

The renames landed in #534 as a breaking 2.0.0. Nothing had been published, so
this reworks them into the Announce phase the deprecation policy calls for:
every old spelling still works and warns, and the release is a MINOR.

Restored, each forwarding to its replacement with a DeprecationWarning naming
it, per docs/development/deprecation_policy.rst:

- permutation_q2, pipeline_null, discriminate_observational as function shims.
  The permutation_q2 shim keeps the old argument order and adapts to the new
  signature, since check_predictive_signal now takes the blocks first.
- q_value and discriminator_significant on the descriptor records, emitted
  alongside p_value_fwer and is_predictive with identical values.
- result.relate["discriminator"], referencing the same object as
  result.relate["predictive_descriptors"].
- The discriminator= keyword on relate_observational, analyze_descriptive and
  the sensory_analyze_descriptive tool input. Passing both spellings raises
  ValueError rather than silently picking one, following the
  desirability_weights precedent in experiments/optimization.py.
- empirical_fdr, still clipped to [0, 1] exactly as before, so an existing
  caller reads the same number it always did. null_to_observed_ratio is the
  same quantity unclipped; the two only diverge once shuffling out-finds the
  real response, which is the case the old key was hiding.

The shims take explicit signatures rather than **kwargs so help() and
inspect.signature stay accurate, matching the reasoning behind _model_method
elsewhere in the package.

Verified that the package does not warn on itself: the default
relate_observational path runs clean under -W error::DeprecationWarning.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016vCXGaB7zcXSd7GXQoHJig

* Include the restored empirical_fdr in the documented key set

test_keys_are_as_documented asserts the exact returned key set, and bringing
empirical_fdr back as a deprecated alias made it fail. Caught by CI on
3.13/ubuntu, not locally: after adding the key I re-ran only the -k Deprecated
subset of the file rather than the whole file, so the one test that pins the
full set never ran.

The key is listed with a note saying it is deprecated and due out in 2.0.0, so
removing it then fails this test rather than passing silently.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016vCXGaB7zcXSd7GXQoHJig

---------

Co-authored-by: Claude <noreply@anthropic.com>
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.

3 participants