Skip to content

autoimprove: literature knowledge (KnowledgeBase, citation check, gap report) - #1321

Draft
solarsys wants to merge 14 commits into
sunlabuiuc:masterfrom
solarsys:feat/autoimprove-knowledge
Draft

solarsys wants to merge 14 commits into
sunlabuiuc:masterfrom
solarsys:feat/autoimprove-knowledge

Conversation

@solarsys

@solarsys solarsys commented Oct 20, 2026 •

Copy link
Copy Markdown
Collaborator

Stacked on #1320, then #1319, #1318, #1317 and #1316. Review the commits from autoimprove: literature knowledge on.

Why

The feature space is large: operators × code groups × windows × thresholds. Published predictors are the best guide to what to try first. A clinician also wants each feature's rationale backed by evidence, not by the LLM's memory. LLMs invent references, though, so every piece of evidence has to be checked.

What changes

New pyhealth/autoimprove/knowledge.py:

  • Sources:

    • Curated tables (add_table_evidence): one row per predictor and effect. Effects are parsed from text such as RR 1.94 (1.61–2.32) or OR 2.0–4.1.
    • Documents: tables of abstracts, text files, and PDFs when pypdf is installed.
    • PubMed through E-utilities: off unless called, cached, with queries built from the task card's population and outcome only. Every query is kept in kb.queries for audit.
  • extract(). The LLM returns records with the exact sentence that states each effect. Each record's source is set from the document, never by the LLM.

  • verify() keeps a record only if:

    • its source was gathered;
    • its quote is in that source;
    • its effect size and CI appear in the quote.

    An invented citation, a misquote or a made-up number is rejected by construction. The direction of a ratio effect is set from its value.

  • map(fs, card). The LLM sees only the task card, the feature menu and column names. Each predictor becomes one of:

    • have: an existing column;
    • build: a spec, validated by the FeatureSpace, carrying the literature's citations and direction;
    • not in data: a data request.

    Each gets a prior of strength × population match × data support. The prior only orders candidates; the feature search decides what is kept.

  • Outputs:

    • directions(), which is ambiguous when sources disagree (e.g. controller use);
    • ranked_specs(), for QueueProposer or the LLM;
    • gap_report();
    • benchmark_range();
    • save() / load().
  • AutoImprover(knowledge=kb). Feature cycles gain an "evidence" strategy, which shows the LLM the untried published predictors with their citations and priors. The report gains a Literature section (the citation-check counts, the gap report and the published performance range) and a citations column in Features.

Fixes from the first real run (ARCHES pediatric asthma)

The ARCHES experiment (v2 proposal PR 5) built a knowledge base from 16 curated rows, 22 review abstracts and 64 reference abstracts, then ran four feature-search arms. Running it turned up these issues, each fixed in its own commit with a test:

  • fetch_pubmed(pmids) adds a review's references by id; only the ids are sent.
  • The citation check rejected real records. A quoted MD -0.37 or AUC 84% failed, because numbers were matched without sign and percentages were not read as proportions. This affected 7 of 101 records.
  • Spreadsheet ids. PMIDs exported from Excel as =HYPERLINK(url, "id") were used whole as document ids.
  • Derived events are now computed only when a spec selects a derived type, not for every table.
  • Report: a kept baseline was misdescribed. When candidates passed the search bound but none was confirmed, the report said the kept baseline "beat the starting pipeline". It now says how many trials passed the search and that none kept its gain on the confirmation folds.
  • Repeated feature proposals. LLM feature turns were wasted repeating features tried more than 12 trials earlier, which the prompt no longer showed. The prompt now lists every feature tried, the retry names the repeats, and trial notes are no longer labelled as space clipping.
  • Plain any() definitions. '1 if either 1 if there was any X …, else 0 or … is above 0, else 0' now reads '1 if there was any X … or there was any Y …, else 0'. Selected features' definitions go to clinician review.

Tests

tests/core/test_autoimprove_knowledge.py (10 tests):

  • Citation check: a real record is kept, and its wrong direction is corrected from the value. An invented source, a misquote, a wrong effect size and a wrong CI are each rejected with the reason.
  • Extraction: it cannot choose its own source. A citation the LLM supplies is ignored, and a fabricated record is rejected.
  • Curated tables: a risk-factor table is parsed and verified.
  • PubMed: every outgoing query contains only task-card words and fixed operators.
  • Mapping: have, build, a refused spec (moved to not in data with the reason) and not in data. Conflicting sources give an ambiguous direction. Save and load round-trip.
  • Number matching and ids: negative effects and percentages are found; a near miss is still rejected; HYPERLINK cells and 33831622.0 give clean PMIDs; fetching by id sends only the ids.
  • End to end: a search driven by QueueProposer(kb.ranked_specs()) keeps the planted feature, and the report shows the literature and citations.

All 65 autoimprove and metrics 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: 71.06% (20085 / 28266 lines)

Modified file Coverage Lines
pyhealth/autoimprove/__init__.py 100.00% 9 / 9
pyhealth/autoimprove/evaluate.py 92.57% 162 / 175
pyhealth/autoimprove/explain.py 89.16% 181 / 203
pyhealth/autoimprove/features.py 88.67% 532 / 600
pyhealth/autoimprove/improver.py 91.79% 816 / 889
pyhealth/autoimprove/knowledge.py 86.68% 319 / 368
pyhealth/autoimprove/proposers.py 90.96% 171 / 188
pyhealth/autoimprove/report.py 94.66% 585 / 618
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 · ff6447d

solarsys and others added 14 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
… report)

A KnowledgeBase turns published evidence into ranked, justified feature
candidates without ever seeing patient data.

- Sources: curated tables (add_table_evidence; effects parsed from text
  such as "RR 1.94 (1.61-2.32)"), documents (tables of abstracts, text
  files, PDFs when pypdf is installed) and PubMed via E-utilities, with
  queries built from the task card's text only and kept for audit.
- extract(): LLM records with the sentence that states each effect; the
  source is set from the document, never by the LLM.
- verify(): a record is kept only if its source was gathered, its quote is
  in that source, and its effect size and CI are in the quote; directions
  of ratio effects are set from the value.
- map(): each predictor becomes have (an existing column), build (a spec
  validated by the FeatureSpace, carrying its citations and the
  literature's direction) or not in data (a data request), with a prior
  of strength x population match x data support that only orders
  candidates.
- directions() (ambiguous when sources disagree), ranked_specs() for
  QueueProposer or the LLM's new "evidence" strategy, gap_report() and
  benchmark_range().
- AutoImprover(knowledge=kb): feature cycles add the "evidence" strategy;
  the report gains a Literature section and feature citations.

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

KnowledgeBase.fetch_pubmed(pmids) adds a review's references by id (only the
ids are sent); search_pubmed uses it. A feature view now calls the task's
derived_events hook only for a derived event type, not for every table.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015ntMLEmFsQEQJKTmPGGw4T
A quoted 'MD -0.37' or 'AUC 84%' was rejected because numbers were matched
without sign and percentages were not read as proportions. Found on the
ARCHES literature: 7 of 101 real records were rejected this way.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015ntMLEmFsQEQJKTmPGGw4T
A review exported from Excel stores PMIDs as =HYPERLINK(url, "id"); the
documents were cited by the whole formula.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015ntMLEmFsQEQJKTmPGGw4T
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015ntMLEmFsQEQJKTmPGGw4T
When candidates passed the search bound but none was confirmed, the report
said the kept baseline 'beat the starting pipeline'. It now says how many
trials passed the search and that none kept its gain on the confirmation
folds. Found on the ARCHES literature arm.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015ntMLEmFsQEQJKTmPGGw4T
On ARCHES, LLM feature turns were wasted repeating features tried more than
12 trials earlier, which the prompt no longer showed. The prompt now lists
every feature tried so far, the retry names the repeated features, and trial
notes are labelled as notes rather than as space clipping.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015ntMLEmFsQEQJKTmPGGw4T
'1 if either 1 if there was any X ..., else 0 or ... is above 0, else 0'
becomes '1 if there was any X ... or there was any Y ..., else 0'. Found in a
feature selected on ARCHES, whose definitions go to clinician review.

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-knowledge branch from 6fbdb2d to ff6447d Compare October 25, 2026 15:20
@jhnwu3 jhnwu3 added the status: automatic The PR will go through automatic review process. label Oct 25, 2026
@jhnwu3

jhnwu3 commented Oct 25, 2026

Copy link
Copy Markdown
Collaborator

/review

@github-actions github-actions Bot added status: manual Pending maintainer's review and removed status: automatic The PR will go through automatic review process. labels Oct 25, 2026
@github-actions

Copy link
Copy Markdown

AI Review

suggestion: human review
decision: not available

This PR is too large for AI review (over 300 files or 200000 diff characters), so it is labeled status: manual and needs a human review.

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

status: manual Pending maintainer's review

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants