Repository navigation
Compute risk and subgroup flags in Python; switch the reporter to Opus 5.5 - #2
Merged
Merged
Conversation
.agents/ is editor-local configuration, the same class of file as .claude/ and .codex/, so it should not appear as untracked project work. The scenarios comment also misspelled the fixtures directory.
An uploaded CSV is the one input the tool does not control. A missing
column, a split with no train or test rows, a label outside {0, 1}, a
one-class test set, or a probability that is not a number in [0, 1]
used to surface as a KeyError or a scikit-learn traceback.
validate_predictions reports those problems in plain language. The CLI
prints them on stderr and exits 2, before any check runs. The browser
UI shows the same list and stops.
The patient sample behind the second AUC is drawn from every row, and about two thirds of those rows are training rows. Calling it a grouped split implied a held-out fold and a measurement of how much leakage inflated the metric. The scores are fixed before the split is assigned, so that comparison cannot show inflation. Rename the two AUCs, keep gap as their difference, and report the 2.5th–97.5th percentile of the sample AUC over seeds 0–199. The same seed-0 sample remains inside that range, and repeating the audit returns the same numbers.
Comparing a subgroup with the pooled AUC hides the drop, because the degraded rows are inside the pool. On subgroup_density_degradation, density C is 0.670 against 0.792 on the other rows, and the old check flagged nothing. Each subgroup is now compared with the rest of the table. machine_id and site_id are always evaluated; density and decade age bands are evaluated when those columns are present. A subgroup is flagged when its AUC is at least 0.10 below the rest and the upper end of a patient-level 95% interval (200 resamples, seed 0) is below zero. Mean predicted probability and the observed rate are reported and do not flag. The pooled AUC is unchanged, and it assumes every row is a held-out or out-of-fold prediction.
The reporter prompt told Claude to choose RED when leakage was present or the random-versus-grouped gap was "large", and nothing defined large. The level is a pass/fail decision, so it belongs with the other flags. compute_risk returns RED when any patient is in both train and test, YELLOW when a subgroup is flagged, and GREEN otherwise. The split audit stays in the payload as a diagnostic and does not enter the rule. run_checks is the single payload the CLI, the UI, and the tests share. The prompts tell Claude to state that level verbatim, to treat the split audit as a rescore that cannot show inflation, and, when patients overlap, to ask for a retrained patient-grouped evaluation rather than another score of the same predictions.
app.py wrote the key from the password box into os.environ. That map is process-wide, so one Streamlit session could overwrite the key seen by another session in the same process. render_report and render_reviewer_critique now take an optional api_key and use it in place of ANTHROPIC_API_KEY. The UI passes the typed key and leaves the environment alone. The sample payload in the reporter test also used a leakage rate of 0.4702; on weak_random_split_leakage the rate is 0.9802.
The README still told Claude to judge whether a split gap was large, and it still described the second AUC as a grouped retest. Python now chooses RED, YELLOW, or GREEN from patient overlap and subgroup flags, and the split audit only rescores the predictions already in the file. The sample payload is the output of run_checks on weak_random_split_leakage, with the subgroup list marked as truncated. The report excerpt is hand-written and asks for a retrained patient-grouped evaluation. The demo script runs the clean control and then optimistic_site_shortcut, matching the recorded narration. The July 2026 planning notes move to docs/archive.
CLAUDE.md still described the validators, the CLI, and the UI as work left to build, and it still said a subgroup was compared with pooled AUC. Both entry points exist. Python sets the risk level from patient overlap and from subgroup flags against the rest of the table, and the split audit does not enter that rule. The command list is the install, pytest, clinval-validate, Streamlit, and the local RSNA adapter. Three gotchas are easy to miss from the code alone: scores are drawn before the split, the flag thresholds were calibrated on the committed fixtures, and only the prompt text after the first --- is sent.
A fresh clone had no check that the validators still pass on the two Python versions the project claims. The workflow installs the dev extra and runs pytest. It does not use secrets, and it does not call the Anthropic API.
The fresh-seed test asserted that every degraded table flagged M030 and that every clean table was quiet. Its seed lists skipped the misses and the false alarms: M030 is missed on 4 of 30 scanner tables, and 6 of 30 clean tables receive a flag. Seeds 7–11 are contiguous and were fixed before the assertions. The test now records those outcomes: M030 is flagged on 7, 8, and 9 and not on 10 and 11; clean seeds 8 and 10 each flag age 80-89, and 7, 9, and 11 flag nothing. The README states the 30-seed rates from the bundled generator.
The README gave the false-alarm and M030 miss rates but not how often the density drop is caught, and it did not say what a flag means at 2,000 patients. A flag is a candidate drop to confirm on more data, and a missing flag does not clear a subgroup. A one-sided Bonferroni correction across subgroups was measured on seeds 7-36 and rejected: clean-table flags fell from 6 to 3 of 30, but M030 detection fell from 26 to 21 of 30, density detection from 21 to 12 of 30, and the committed density scenario went unflagged. CLAUDE.md records this so the correction is not added again without re-measuring.
A live run of the reporter (24 reports per model) showed Claude counting
the subgroup list wrong ("22 subgroups" for 21), claiming every interval
spans zero when some did not, and calling the pooled AUC a test-set AUC.
Each of these is a number or label Python can supply.
The payload gains subgroup_summary: subgroups evaluated, subgroups
flagged, and how many delta_ci95 intervals lie entirely below or entirely
above zero. pooled_metric gains rows: "all", and both prompts say the
pooled AUC covers train, val and test rows and is not the test-split AUC.
report.py returned message.content[0].text. A model that thinks puts a thinking block first, so a live run with Claude Haiku 5.5 would have raised AttributeError on every report. The reporter now joins the text blocks. It also raises ReportNotRenderedError when the reply stops at max_tokens, is refused, or holds no text. The CLI prints the payload and the stop reason and exits 1; the UI shows the message and keeps the payload. max_tokens rises from 1024 to 4096: the longest live Opus 4.8 report used 998 output tokens, and only generated tokens are billed. The UI spinner names the model from MODEL instead of a hard-coded string.
A blind review of 48 live reports found the same slips in both models tested: the split audit cited as "no evidence of inflation" or as "consistent with" the leakage story, calibration gaps of 0.50 against 0.02 called small or consistent, risk reports over the 300-word limit (Opus 4.8 exceeded it in 12 of 12), and every RED critique from Haiku 5.5 writing "Reject or Major revision" without choosing. Both prompts now say the audit cannot show inflation or its absence and must not be used as evidence either way, ask for both numbers whenever a calibration gap is described, and name what to cut to stay within the word limit. The critique asks for exactly one recommendation on the first line.
A blind review of 24 live reports per configuration compared seven setups across four scenarios. Opus 5.5 at low effort made no major error and had the most error-free reports (19 of 24), at $40 per 1,000 reports against $46 for Opus 4.8, which made one major error and had 11 of 24 error-free. Medium effort made no major error either and cost more. Opus 5.5 always thinks, so the request sets output_config.effort and sends no thinking parameter; replies start with thinking blocks, which the reporter already skips. Requests opt into server-side refusal fallback (fallbacks "default" under the server-side-fallback-2026-07-01 beta), so a safety-classifier decline is rerun on the model Anthropic recommends for that category instead of failing the report. The SDK types fallbacks as a list, so the "default" string goes in extra_body. The prompts move from prompts/opus-4-8/ to prompts/, since they no longer belong to one model. CLAUDE.md records the comparison so the choice is not changed without re-running it.
Owner
Author
Code reviewFound 1 issue:
clinval-validator/clinval_validators/checks.py Lines 186 to 191 in bf99252 🤖 Generated with Claude Code - If this code review was useful, please react with 👍. Otherwise, react with 👎. |
On a small table a 15% patient sample can contain no positive row. Its AUC is undefined, roc_auc_score returned NaN, and np.percentile then turned sample_range95 into [NaN, NaN]. When the seed-0 sample was the one-class draw, patient_disjoint_sample and gap were NaN as well. json.dumps writes NaN as a bare token, which is not valid JSON, so that payload reached Claude and the JSON download in that form. validate_predictions accepts such tables, because it requires both classes only on the test split. On 200-patient tables from the bundled generator, 7 of seeds 0-19 hit this. Seeds whose sample holds one class are now left out of sample_range95, the same way the subgroup bootstrap skips one-class replicates. patient_disjoint_sample and gap are null when the seed-0 sample holds one class, and sample_range95 is null when every seed's sample does. The UI shows "n/a" for a null sample AUC, and both prompts say a null value is undefined and must not be filled in. The prompts' JSON shape also marks delta_ci95 as nullable, which the code already allowed. The payloads of the six committed scenarios are byte-identical before and after this change.
The reporter let anthropic.APIError escape. A request that failed with 403 (no credits), 401 (bad key), 429, 529, or a dropped connection printed a traceback from the CLI and no payload, and the browser UI showed the same traceback. _render now raises ReportNotRenderedError from the API error, so both entry points print or show the payload and the reason, as they already did for a cut-off or refused reply, and the CLI exits 1. The UI prefilled the key field with the server's ANTHROPIC_API_KEY. A widget's value is sent to the browser, so anyone who opened a deployed app could reveal that key with the password field's show button. The field now starts empty, and an empty field makes the reporter read ANTHROPIC_API_KEY on the server.
actions/checkout@v4 and actions/setup-python@v5 run on Node 20, which GitHub has deprecated for actions. Version 7 of both runs on Node 24. Their breaking changes concern pull_request_target, workflow_run, and the removed pip-install input, none of which this workflow uses.
CI installs the newest anthropic release, 1.15.0, which moved its HTTP layer from httpx to httpx2, so the test module failed to import there. The test now hands PermissionDeniedError a stand-in response with the three attributes the error reads. The reporter itself needs no change for 1.x. An offline run against a mock transport sent the same request on anthropic 0.116.0 and 1.15.0 (model, effort, the fallback beta header and fallbacks "default"), and the full suite passes on both.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
This branch fixes problems a code and methods review found in the validators, the reporter, and the docs. It also switches the reporter to Claude Opus 5.5 at low effort. Python computes every metric, flag, and the risk level; Claude only writes prose from the payload.
Validators (
clinval_validators/checks.py)validate_predictionsrejects a CSV with a missing column, no train or test rows, a label outside {0, 1}, a one-class test set, or a probability outside [0, 1]. The CLI prints the problems and exits 2; the UI shows them and stops.compute_risksets the level: RED when any patient is in both train and test, YELLOW when a subgroup is flagged, GREEN otherwise. The prompt used to ask Claude to choose the level from an undefined "large" gap.subgroup_density_degradation, density C (0.670 against 0.792 on the rest) was not flagged before and is flagged now.test_split_as_given,patient_disjoint_sample, andsample_range95. Its patient sample includes training rows and it rescores fixed predictions, so it cannot measure inflation. It no longer enters the risk rule. On a small table a patient sample can contain one class, where AUC is undefined; such seeds are left out ofsample_range95, andpatient_disjoint_sampleandgaparenullwhen the seed-0 sample is one of them. The payload no longer contains NaN, which is not valid JSON.subgroup_summary(evaluated, flagged, and intervals entirely below or above zero) andpooled_metric.rows: "all". Live reports had miscounted subgroups and called the pooled AUC a test-set AUC.Reporter (
reporter/report.py,prompts/)claude-opus-5-5, effortlow,max_tokens4096, with server-side refusal fallback (betaserver-side-fallback-2026-07-01). The choice comes from a blind review of 24 live reports for each of seven configurations; CLAUDE.md records the results under Gotchas.ReportNotRenderedErrorwhen the API request fails (no credits, bad key, rate limit, no connection), or when the reply stops atmax_tokens, is refused, or has no text. The CLI then prints the payload and exits 1, and the UI shows the reason instead of a traceback.os.environ, which every Streamlit session in the process shares. The key field also used to be prefilled with the server'sANTHROPIC_API_KEY, which sent that key to every browser; it now starts empty, and an empty field uses the server key on the server.prompts/opus-4-8/toprompts/. They add rules for mistakes seen in live reports: the split audit is not evidence for or against inflation, a calibration gap is stated with both numbers, and the critique gives exactly one recommendation.Tests and CI
actions/checkout@v7andactions/setup-python@v7(Node 24). It uses no secrets and makes no API call.Docs
README, CLAUDE.md, and the data and scenario READMEs describe the current code. CLAUDE.md records why a one-sided Bonferroni correction was measured and not adopted. The July 2026 planning notes move to
docs/archive/.Verification
python -m pytest -q: 59 passed on anthropic 0.116.0 and on 1.15.0, the version a fresh install gets.ruff check: clean.fallbacks: "default".AppTestrun with a stand-in server key and a client that returns 403 showed an empty key field, no server key in the widget tree, and the API error as an on-page message.clinval-validate --predictions claude-science-artifacts/scenarios/optimistic_site_shortcut.csvreturned a complete RED report whose numbers match the payload.Known limit: risk reports from every model tested sometimes run past the prompt's 300-word limit.