Skip to content

autoimprove: features as data (FeatureSpec, FeatureSpace, future-blind test) - #1319

Draft
solarsys wants to merge 3 commits into
sunlabuiuc:masterfrom
solarsys:feat/autoimprove-features
Draft

solarsys wants to merge 3 commits into
sunlabuiuc:masterfrom
solarsys:feat/autoimprove-features

Conversation

@solarsys

Copy link
Copy Markdown
Collaborator

Stacked on #1318, then #1317 and #1316. Review the last commit, autoimprove: features as data.

Why

Model and setting search reaches a ceiling quickly; better inputs are where the remaining gains are. Changing inputs must not mean running code that an LLM (or anyone) wrote, on a machine that holds patient data. A feature also has to be reviewable: a clinician must be able to read exactly what was computed, and know that it cannot see the future.

What changes

  • FeatureSpec / FeatureSpace. A feature is JSON built from a fixed set of operators over PyHealth's event log (Patient.get_events), evaluated at each sample's prediction time t:

    • over a window that ends strictly before t: count, exists, days_since, stat (min, max, mean, last or slope of a value), n_distinct, trend and blocks_with;
    • combinations: ratio, diff, band, ge, any and cross;
    • column, for an existing task feature.

    Selectors name an event type, an optional vocabulary group (e.g. SABA) and optional where filters. Filter values must occur for at least 11 patients.

  • Definitions are generated from the spec, deterministically. For example, ge(3, count(365 d, SABA orders)) becomes "1 if there were 3 or more SABA orders in the 365 days before the landmark, else 0". So the sentence a clinician reviews is exactly what was computed. Every spec must also carry its clinical rationale and expected direction.

  • Refusals carry a reason, the same way invalid configs already do:

    • an unknown operator, group or attribute;
    • a window outside the menu;
    • nesting deeper than 3;
    • a missing rationale;
    • more than 8 features per proposal.
  • The future-blind test. Each spec is computed twice: on the full record, and on the record as known at t. Rows dated at or after t are removed, and values known only later (known_at, e.g. a stay's length known at discharge) are blanked. Derived events are rebuilt from the truncated record. Any difference rejects the spec.

  • check() reports each spec's definition, coverage and future-blind result. augment() adds an engineered tensor field (EngineeredDataset). run_config_cv now imputes and scales that field on each fold's training part (fold_parts), so a held-out fold never informs its own medians and scales. The other fields keep the processors set_task fitted, as before.

  • Optional BaseTask hooks: time_key, feature_vocabulary(), and derived_events(patient, t) with derived_event_types. They are class-level defaults, so set_task cache keys (built from vars(task)) are unchanged, and tasks without them work as before.

  • Speed: each compute call fetches the needed event types for all its patients in one query.

Example and checks

examples/autoimprove_features_mimic3.py checks one hand-written feature on the full synthetic MIMIC-III, with no search and no LLM. The feature is hospital admissions in the 365 days before discharge, used to predict next-visit mortality:

count_365d__admissions: the number of hospital admissions in the 365 days before discharge
  rationale: frequent recent hospital stays mark frailer patients; expected direction +
  coverage 76%; future-blind: passed
CV ROC-AUC without the feature 0.532, with it 0.557: gain +0.025 ± 0.020

(It runs in about 20 seconds. Coverage is below 100% because a third of the synthetic stays end before they begin.)

Tests

tests/core/test_autoimprove_features.py has 10 tests:

  • operators: every one checked against hand-computed values on a small event log, including window boundaries, missing values and the slope;
  • definitions: matched exactly;
  • refusals: each with its reason;
  • round trip: JSON → validate → compile → name and definition;
  • planted leaks: both rejected by the future-blind test (a length of stay known only after t, and a derived event built from all events), while their correct versions pass;
  • per-fold statistics: fitted on the training part only.

The earlier autoimprove and metrics tests still pass (48 in total), 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.48% (19280 / 27357 lines)

Modified file Coverage Lines
pyhealth/autoimprove/__init__.py 100.00% 8 / 8
pyhealth/autoimprove/evaluate.py 92.49% 160 / 173
pyhealth/autoimprove/explain.py 86.63% 175 / 202
pyhealth/autoimprove/features.py 90.91% 510 / 561
pyhealth/autoimprove/improver.py 93.28% 541 / 580
pyhealth/autoimprove/proposers.py 93.91% 108 / 115
pyhealth/autoimprove/report.py 93.23% 468 / 502
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 · e025a02

solarsys and others added 3 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
@solarsys
solarsys force-pushed the feat/autoimprove-features branch from abcf367 to e025a02 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