Skip to content

autoimprove: feature search (alternating cycles, confirmation, pruning, review gate) - #1320

Draft
solarsys wants to merge 5 commits into
sunlabuiuc:masterfrom
solarsys:feat/autoimprove-feature-search
Draft

solarsys wants to merge 5 commits into
sunlabuiuc:masterfrom
solarsys:feat/autoimprove-feature-search

Conversation

@solarsys

Copy link
Copy Markdown
Collaborator

Stacked on #1319, then #1318, #1317 and #1316. Review the last commit, autoimprove: feature search.

Why

#1319 lets you write and check features by hand. This PR lets the improver search for them. Searching features as well as settings brings three risks:

  • noise features that win by chance;
  • redundant features that pile up;
  • features that work for reasons nobody can defend.

Each part below addresses one of these.

What changes

  • A trial is a config plus a feature set. trials.jsonl records the full spec list, the group the trial added, whether it was accepted, and any suspicion.

  • Alternating cycles (feature_cycles=(2, 1)):

    • Feature cycles hold the model at the current selection, and each agent proposes a group of specs with rationale and expected direction.
    • Model cycles hold the accepted features and propose configs.

    With one side fixed, every accepted change has a gain of its own.

  • Forward selection. A group joins the accepted set only if its paired gain over the accepted set clears the lower bound. Specs that fail the future-blind test are refused before training, and the reason goes back to the proposer. A rejected group never becomes the result.

  • Proposers:

    • LLMProposer.propose_features sees the operator menu, the task card, the accepted features and the earlier groups.
    • RandomProposer proposes random valid specs, labelled as having no rationale.
    • New QueueProposer tries a ranked list in order, with no LLM.
  • confirm() (confirm=True). The leading candidates are re-scored on new folds with 3 training seeds, and must clear a stricter bound, confirm_se=2. Candidates picked as the best of many on the same folds are partly lucky.

  • prune(). Accepted groups whose removal costs less than one SE are dropped, and the pruned set is the result. The ablation records what each group added.

  • Safeguards:

    • Suspicion check: a new feature with ROC-AUC above 0.95 on its own is held as possible leakage.
    • Review gate: features with no rationale, an opposite learned direction, or a suspicion are held back until approved. The test then also scores the model without them.
    • early_stop: a trial that is clearly worse is stopped after 2 folds.
  • The report gains Features (each feature's definition, rationale, expected direction, contribution and review status) and Confirmation (confirmed scores, the ablation and what was pruned).

What the tests showed, and what changed because of it

The planted-data tests found two statistical weaknesses. Both are fixed here:

  1. A noise feature, and an exact duplicate of it, passed the lenient one-SE acceptance bound by chance. In a small world the paired SE is noisy; the duplicate's "gain" was pure optimization noise. Confirmation and pruning now remove them. The test asserts the end state, not each provisional acceptance.
  2. With shuffled labels, a feature group survived. It had been rejected during the search, yet it came back as a candidate in confirmation and won by chance (+0.046 ± 0.032). Rejected groups are now never candidates, and the confirmed selection must clear 2 SE.

Tests

tests/core/test_autoimprove_search.py covers the proposal's done-when list with 6 tests on a synthetic event log, where the outcome depends on recent visits, with 15% label noise:

  • Planted feature: found, and the ablation attributes its gain to it (+0.28 PR-AUC). The "any visit" flag, which it makes redundant, is pruned. The test gain over baseline is above 0.05.
  • Shuffled labels: nothing survives confirmation; the baseline stays.
  • Reversed effect: a planted feature whose stated direction is wrong is flagged, held back, scored without, then released on approval.
  • Leakage: a feature that encodes the outcome is held as suspicious and not accepted.
  • LLM path: features proposed from the menu; the hypothesis is recorded.
  • Resume and early stop: a run resumes from trials.jsonl, and early stop works.

All 49 autoimprove tests pass, as do the doctests, ruff and tools/check_pr_rules.py.

🤖 Generated with Claude Code

https://claude.ai/code/session_015ntMLEmFsQEQJKTmPGGw4T

@github-actions

github-actions Bot commented Oct 20, 2026 •

Copy link
Copy Markdown

Test Coverage

Total: 70.76% (19698 / 27838 lines)

Modified file Coverage Lines
pyhealth/autoimprove/__init__.py 100.00% 8 / 8
pyhealth/autoimprove/evaluate.py 92.57% 162 / 175
pyhealth/autoimprove/explain.py 89.16% 181 / 203
pyhealth/autoimprove/features.py 88.36% 524 / 593
pyhealth/autoimprove/improver.py 91.67% 792 / 864
pyhealth/autoimprove/proposers.py 89.56% 163 / 182
pyhealth/autoimprove/report.py 93.47% 558 / 597
pyhealth/autoimprove/space.py 97.40% 187 / 192
pyhealth/metrics/__init__.py 100.00% 12 / 12
pyhealth/metrics/clinical.py 100.00% 94 / 94
pyhealth/tasks/__init__.py 88.57% 31 / 35
pyhealth/tasks/base_task.py 83.33% 30 / 36
pyhealth/tasks/task_card.py 98.04% 50 / 51

Workflow run · a3752fb

@solarsys
solarsys force-pushed the feat/autoimprove-feature-search branch 2 times, most recently from e0eedf5 to 1f4f810 Compare October 23, 2026 09:18
solarsys and others added 5 commits October 24, 2026 23:01
…ion, report)

A run now ends with a report that explains the selected model and how the
search got there, built from the decision log alone.

- Decision log: every trial records the proposer's rationale and
  expectation (written before it runs), the trial it was changed from and
  what changed. LLM proposers are asked for both.
- Selection: among the trials that beat the baseline consistently across
  folds, the most interpretable one within one paired SE of the best (the
  one-SE rule, select_simplest). Models sit on an interpretability ladder
  (SearchSpace.rung): logistic regression, depth-1 XGBoost (additive, now
  in the space), shallow trees and MLPs, then deeper trees and sequence
  models. one_se=False keeps the highest-lower-bound rule.
- Plain words: every model has a description and every setting a meaning
  (Param.meaning, SearchSpace.explain).
- AutoImprover.attribute(): reverts each changed setting of the selected
  trial one at a time and scores it on the same folds.
- evaluate_on_test() also scores the highest-scoring candidate when a
  simpler one was selected, and saves test.json.
- AutoImprover.report() / load_run(): HTML, Markdown and JSON with the
  summary, task, model and why (candidates, ladder, price of
  interpretability, attribution), test comparison and reasoning trace
  (timeline, every trial with hypothesis, change, result and verdict,
  single-setting comparisons, lessons). Statements are labelled measured,
  tested or hypothesis; numbers in lessons that match nothing in the log
  are flagged (unverified_numbers).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015ntMLEmFsQEQJKTmPGGw4T
A run's report now explains how the selected model decides and how it
would perform in clinical use, and lists what a clinician must decide.

- pyhealth.tasks.TaskCard and the optional BaseTask.task_card() hook: the
  task's design choices with their rationale, and a feature dictionary
  (meaning, clinical group, expected direction of effect).
- pyhealth.metrics.clinical: alert_rate_metrics (sensitivity, PPV and
  number needed to evaluate when the top 5/10/20% of risk is flagged),
  net_benefit (decision curve), calibration_bins, subgroup_metrics and
  clinical_summary; counts below 11 patients are suppressed.
- AutoImprover(task_card=...); explain(): exact TreeSHAP for trees,
  integrated gradients for torch models (exact for logistic regression);
  importance by clinical group, top features with effect shapes in bins of
  at least 11 patients, learned vs expected direction, and the top-feature
  overlap of the fold models.
- evaluate_on_test(subgroups=...) adds the clinical summary per config.
- flags() and approve(): a missing card, undescribed top features, effects
  opposite to the expected direction, poor calibration and subgroup gaps;
  decisions are kept in review.json.
- The report gains How it decides, Performance in clinical terms,
  Clinician review and a TRIPOD+AI reporting checklist; load_run reads the
  new files. The eICU example has a task card and age/sex subgroups.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015ntMLEmFsQEQJKTmPGGw4T
…d test)

New inputs are written as JSON specs over PyHealth's event log, never as
code. FeatureSpace validates a spec against a fixed set of operators
(count, exists, days_since, stat, n_distinct, trend, blocks_with, ratio,
diff, band, ge, any, cross, column), compiles it at each sample's
prediction time t with windows that end strictly before t, and writes its
column name and plain-language definition from the spec itself. Every spec
carries its clinical rationale and expected direction; invalid specs are
refused with a reason.

- The future-blind test computes each spec on the full record and on the
  record as known at t (later rows removed, values known only later
  blanked via known_at, derived events rebuilt); any difference rejects it.
- FeatureSpace.check reports definition, coverage and the test result;
  augment adds an "engineered" tensor field (EngineeredDataset) whose
  imputation and scaling run_config_cv refits on each fold's training part
  (fold_parts).
- Optional BaseTask hooks: time_key, feature_vocabulary() and
  derived_events(patient, t) with derived_event_types. Class-level
  defaults, so set_task cache keys are unchanged.
- examples/autoimprove_features_mimic3.py checks one hand-written feature
  on synthetic MIMIC-III and measures its paired CV gain.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015ntMLEmFsQEQJKTmPGGw4T
…g, review gate)

With a FeatureSpace, AutoImprover searches inputs as well as settings.

- A trial is a config plus a feature set; trials.jsonl records the specs,
  the group a trial added, whether it was accepted, and any suspicion.
- Cycles alternate (feature_cycles=(2, 1)): feature cycles hold the
  selected model and propose groups of specs, accepted only if their
  paired gain over the accepted set clears the lower bound (forward
  selection); model cycles hold the accepted features. Specs failing the
  future-blind test are refused before training; rejected groups never
  become the result.
- Proposers: LLMProposer.propose_features (operator menu, task card,
  accepted features, earlier groups), random specs without a rationale,
  and QueueProposer for a ranked list without an LLM.
- confirm(): the leading candidates are re-scored on new folds with
  several seeds and must clear confirm_se (2 SE) there.
- prune(): accepted groups whose removal costs under one SE are dropped;
  the pruned selection is the result, and the ablation records what each
  group added.
- Suspicion check (a feature that alone has ROC-AUC above suspicion_auc),
  review gate (features with no rationale, an opposite direction or a
  suspicion are held back until approved; the test also scores the model
  without them) and early_stop for clearly worse trials.
- The report gains Features and Confirmation sections.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015ntMLEmFsQEQJKTmPGGw4T
After confirmation, the "Why this model" sentence compared the selection
with the top candidate on the search folds, although the one-SE rule chose
on the confirmation folds. A run could then read "costs +0.022 ± 0.006
(better in 5/5 folds), which is within noise". It now quotes the
confirmation scores, says why the rule preferred the selection (a lower
rung or fewer engineered features), and names the trial it was pruned
from. The trace verdicts follow the same scores.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015ntMLEmFsQEQJKTmPGGw4T
@solarsys
solarsys force-pushed the feat/autoimprove-feature-search branch from 1f4f810 to a3752fb Compare October 25, 2026 15:20

This branch has not been deployed

No deployments
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