Skip to content

fix(derivations): evaluate/validate never raise; non-finite results are violations; construction validates - #126

Merged
cnicholas merged 1 commit into
mainfrom
fix/derivations-robustness
Sep 15, 2026
Merged

cnicholas merged 1 commit into
mainfrom
fix/derivations-robustness

Conversation

@cnicholas

Copy link
Copy Markdown
Owner

Summary

  • What: make derived variables (transforms and binning) honour the module's own contract, that evaluate and validate never raise on routine data, and stop the silent wrong answers found alongside.
  • Why: an adversarial pass with 36,000 fuzzed evaluations found pandas raising through evaluate() on ±inf inputs ("bins must increase monotonically"), on range labels that collide at six significant digits ("labels must be unique"), on duplicate or null explicit labels, and on string parameters reaching numpy. It also found non-finite transform outputs passing with n_invalid=0, a z-score of a constant column that never triggered on_invalid, more bins than distinct values with no message, and custom labels spelled "NaN" or "Infinity" turned into floats on round trip. Line coverage was 92 percent, but no test fed any of these inputs. The feature is young and lightly used, so fixing now is cheap.
  • Scope: derivations.py (construction validation, evaluation, labelling, serialization); a new test file with 56 tests including a permanent seeded fuzz; CHANGELOG.

Contract / invariants

  • Defaults unchanged — finite inputs with distinct labels produce byte-identical edges, labels and values. The existing 73 derivation and bin-extreme tests pass unchanged, and the new file pins the same edges again.
  • Residuals unaffected — derivations run before formulation; nothing downstream changes.
  • Row/index alignment preserved — values keep the source index, including non-unique indexes.
  • Output schema compatibleevaluate/validate signatures and EvalResult/ValidationResult shapes unchanged. fitted for a degenerate bin now always carries n_bins: 0, edges: [], labels: [].
  • Pinned error strings unchanged — existing messages kept; new ones added.

Behaviour changes

  • ±inf. Transforms: a domain violation. Bins: excluded from the fit, NaN in the output, counted in n_invalid with the message "k non-finite value(s) cannot be binned".
  • Every non-finite transform result is a violation (log of inf, square overflow, inverse of a denormal). Only power overflow was flagged before.
  • Z-score with undefined sigma makes every present value a violation, so on_invalid='error' names the derivation at formulate instead of the study failing later with "No valid response values".
  • Bins capped at the distinct-value count with a message; 'ordinal' above five bins says it fell back to numbered bins; arcsin on values in (1, 100] hints "divide by 100".
  • Construction validates: n a positive integer (numpy integers accepted and normalised, booleans rejected); shift, exponent, breaks finite numbers; on_invalid in {'error', 'na'}; explicit labels non-empty, null-free, unique. All ValidationError, all before any data is touched.
  • Serialization: to_dict converts numpy scalars and arrays; from_dict decodes the Infinity/NaN tags only under numeric keys, so a label spelled "NaN" stays a string; a missing id is a ValidationError.
  • Non-numeric sources (datetime, categorical) evaluate to an all-NA result with a message instead of transforming nanoseconds or category codes.

Methodology

  • No methodology change — input hygiene and reporting only.

Tests

tests/test_derivations_robustness.py (56 tests):

  • TestConstructionValidation — every rejected shape, parametrised; numpy n accepted and normalised; direct construction shares the gate.

  • TestSerializationjson.dumps round trip with numpy inputs; label tags stay strings; non-finite numbers round-trip under numeric keys; missing id.

  • TestTransformNonFinite — inf in or out flagged for every function; constant z-score is a violation and formulate names it; arcsin hint; finite data unchanged.

  • TestBinRobustness — infinities leave the fit and are counted for every method; only-infinities reported not raised; tiny-range labels distinct; near-constant sd fits; bin cap; ordinal message; non-numeric sources; finite edges pinned.

  • test_evaluate_never_raises_and_never_returns_infinity — 400 random series × 63 specs (marked slow), plus a 40-series quick slice in the default selection.

  • TestProcessBehaviorDerivationErrors — the remove/replace error paths and free functions that had no test.

  • pytest tests/ — 2421 passed, 10 skipped

  • ruff check . — clean

  • mypy processbehavior/derivations.py — clean

  • Golden masters untouched; validator exit 0

Notes

  • App follow-up: derivation_ui.preview surfaces n_invalid and message automatically, so the new bin messages appear in the live preview. The preview's warning text for n_invalid was written for transforms ("outside this function's domain" plus the on_invalid radio); with bins now reporting non-finite counts that wording should be split. Not in this PR; belongs with the next app pin bump.
  • A user-guide page for derived variables is still unwritten.

🤖 Generated with Claude Code

https://claude.ai/code/session_01BKphCi1LC1hjGYteWEK9JV

…re violations; construction validates

## Summary
- **What:** honour the module's never-raises contract and stop silent wrong answers in
  derived variables (transforms + binning).
- **Why:** an adversarial pass (36,000 fuzzed evaluations) found pandas raising through
  evaluate() on ±inf inputs, colliding range labels, duplicate/null custom labels and
  string parameters; non-finite outputs passing with n_invalid=0; z-score of a constant
  never triggering on_invalid; more bins than distinct values with no message; labels
  spelled "NaN"/"Infinity" corrupted on round trip. Coverage was 92% but no test fed any
  of these inputs.
- **Scope:** derivations.py (construction validation, evaluation, labelling,
  serialization); tests/test_derivations_robustness.py (56 tests incl. a seeded fuzz);
  CHANGELOG.

## Contract / Invariants (must remain true)
- Finite inputs with distinct labels: byte-identical edges, labels, values (existing 73
  derivation/bin-extreme tests pass unchanged; pinned again in the new file).
- evaluate()/validate() signatures and EvalResult/ValidationResult shapes unchanged.
- Bishop gate 280/280 (derivations are not on that path).

## Behavior Changes (explicit)
- ±inf: transforms -> violation; bins -> excluded from fit, NaN, counted, message.
- Every non-finite transform output is a violation (was: only power overflow).
- zscore with undefined sigma -> every present value is a violation.
- n capped at distinct-value count with a message; ordinal >5 bins says so; arcsin hint.
- Construction rejects: bool/non-integral/<=0 n; non-finite or non-numeric shift,
  exponent, breaks; unknown on_invalid; empty/null/duplicate explicit labels.
  numpy integers/floats are accepted and normalised to Python int/float.
- to_dict JSON-safe for numpy; from_dict decodes float tags only under numeric keys;
  missing id -> ValidationError.
- Non-numeric (datetime/categorical) source -> all-NA result with message, no raise.

## Tests
- tests/test_derivations_robustness.py: construction validation (parametrised),
  serialization round trips through json.dumps, non-finite transforms, bin infinities,
  tiny-range labels, near-constant sd, n cap, ordinal fallback, non-numeric sources,
  finite-data-unchanged pins, seeded fuzz (400 series x 63 specs, marked slow) plus a
  40-series quick slice, and the ProcessBehavior remove/replace/free-function paths.

## Manual Verification
- pytest tests/: 2421 passed, 10 skipped; ruff clean; mypy derivations.py clean
- validation/e2e_bishop_report.py: exit 0
- Ad-hoc harnesses: 34,800 evaluations, zero raises, zero inf outputs, zero finite
  inputs dropped from a fitted bin
@cnicholas
cnicholas merged commit 43ca2af into main Sep 15, 2026
17 checks passed
@cnicholas
cnicholas deleted the fix/derivations-robustness branch September 15, 2026 02:10
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