This document derives the statistical machinery of the ILAE skill certification system from first principles and ties every equation to the line of code that implements it. It is written to be read on its own, but each section ends with a "where it lives" pointer so a reviewer can check the claim against the source.
Two ideas carry the whole project. The first is that a clinician's skill at reading an EEG pattern is a latent quantity we can only observe through noisy yes or no answers, so we should treat scoring as Bayesian inference, not arithmetic. The second is that questions are expensive (each one costs a busy clinician real time), so the test should choose each question to be maximally informative and stop the moment it has learned enough. Everything below is in service of those two ideas.
A note on honesty up front. Where the code deliberately departs from a
textbook method, this document says so rather than borrowing the textbook's
authority. The clearest example is the stopping rule: it is a posterior
precision criterion, not a Wald sequential probability ratio test, and the
engine source says as much at engine/core_mcmc.py:40. See the closing
section "What this is not."
Fix a single task (say, "is there an epileptiform spike?"). A clinician
facing segment
Write the clinician's latent state as a bias
Here
A perfect probit model says a clinician with huge
where docs/INVARIANT_AUDIT.md.
The lapse rate lives at engine/core.py:21 (LAPSE_RATE = 0.025) and is
mirrored as LAMBDA = 0.025 in
pipeline/reference_calibration/fit_sdt_per_domain.py:37.
The segment signal
When engine/core.py:59; the runtime form is at
engine/core_mcmc.py:356.
Confident readers produce scipy.special.log_ndtr and mixes in
the lapse floor with logsumexp:
This is the fix recorded as FIX-T0.7; the rationale (a clipped
norm.cdf "collapses to 1.0 at |z| >= 6 and biases the posteriors of
confident raters") is in the comment at engine/core.py:85, and the
implementation is _log_p_response at engine/core_mcmc.py:87.
The production research engine is
engine/core_mcmc.run_session_mcmc_auroc(method="hier", ...). It carries a
joint posterior over all
The latent state is a
The posterior is represented by make_state_hier, engine/core_mcmc.py:290).
The prior is a zero mean multivariate normal in two blocks,
Crucially Sigma_l_fitted_k7.npy), so the off-diagonal correlations let
a strong showing on, say, GPD raise the prior expectation for GRDA before a
single GRDA question is asked. A compound symmetry fallback
log_prior_hier at engine/core_mcmc.py:167 and the fitted correlations
quoted in the comment block at engine/core_mcmc.py:117.
After the clinician answers item
The cumulative log likelihood is incremented in place so a later
rejuvenation can replay it exactly (update, engine/core_mcmc.py:346).
Repeated reweighting concentrates the mass on a few particles. We measure that with the effective sample size
which ranges from ess at engine/core_mcmc.py:375.
When ess_threshold_frac = 0.5 at engine/core_mcmc.py:701), the engine
resamples. It draws
duplicating high weight particles and discarding low weight ones
(resample_and_rejuvenate, engine/core_mcmc.py:432).
Resampling fixes the weights but leaves duplicates stacked on identical
coordinates, which throws away diversity. The engine restores it with a
genuine MCMC move that leaves the current posterior invariant. For each
particle it runs
The proposal is Gaussian with covariance shaped by the cloud itself: each
sweep recomputes the empirical
so the proposal covariance is
$$ \alpha ;=; \min!\Big( 1,; \exp\big[ \big(\log p(\theta') + \log L(\theta')\big)
- \big(\log p(\theta) + \log L(\theta)\big) \big] \Big), $$
where mh_rejuvenate at engine/core_mcmc.py:381, with the history replay in
_log_lik_history at engine/core_mcmc.py:220.
This is the step that the docstring at engine/core_mcmc.py:22 was built to
provide: it replaces an older West style kernel smoothing jitter that
quietly shrank the support toward its mean. MCMC rejuvenation moves
particles without distorting the distribution they came from.
A good question is one whose answer we cannot predict and whose resolution
shrinks our uncertainty the most. The engine makes that precise. For a
candidate item
$$ (k^\star, s^\star) ;=; \arg\min_{k, s} ;\Big[ p_{\text{yes}} \sum_{k'} \big( \mathrm{Var}{y=1}[t{k'}] + \mathrm{Var}{y=1}[\ell{k'}] \big)
- (1 - p_{\text{yes}}) \sum_{k'} \big( \mathrm{Var}{y=0}[t{k'}] + \mathrm{Var}{y=0}[\ell{k'}] \big) \Big], $$
with _expected_loss_vec at
engine/core_mcmc.py:472, wrapped by choose_item at
engine/core_mcmc.py:566. An earlier KL information gain criterion was
removed (FIX-T0.5, docstring at engine/core_mcmc.py:9); A-optimality is
what ships.
Skill is reported on a scale clinicians already trust: the area under the
ROC curve. Under the signal detection model, a reader with log
discrimination
Bias auroc_from_l and
auroc_quantiles_from_particles_hier, engine/auroc.py:32 and
engine/auroc.py:39).
The test stops when every task is known precisely enough. Writing the 95%
credible interval of task
In words: keep asking until the widest task specific AUROC interval is
narrower than engine/core_mcmc.py:791. It is not a sequential hypothesis
test; see the closing section.
The clinical runtime in deployment/simulate_test.py answers a different
need. It must be fast, online, and explainable to a regulator, so it trades
the particle cloud for a Gaussian (Laplace) posterior updated by an
extended Kalman filter. It never imports the Paper-1 engine; the module
boundary is the architecture (decision D1).
The belief over
and the information form update is a rank one correction,
The equations are quoted in the module docstring at
deployment/simulate_test.py:30 and implemented in irls_w_z and
update_state at deployment/simulate_test.py:160 and
deployment/simulate_test.py:230. Item selection maximizes the log
determinant gain of that rank one update, select_next_case, deployment/simulate_test.py:267).
A task is decided by how much posterior mass sits above its cut score
The contract (in deployment/deployment_config.yaml) certifies the task
PASS when this probability reaches simulate_candidate, deployment/simulate_test.py:467).
The engines above are only as honest as the numbers they are fed: each
segment's signal
Stage one is a Rasch style main effects fit that gives every segment a
difficulty on the logit scale. Stage two fits, for each rater
optimized over the criterion fit_sdt_per_domain.py:51 for the negative log likelihood and :71 for the
optimizer.
Stage one speaks logits; stage two speaks probits. The logistic and normal
CDFs differ by a scale factor close to
The constant is LOGIT_TO_PROBIT = 1.0 / 1.7 at
fit_sdt_per_domain.py:38, and the orchestrator asserts it at runtime
(pipeline/run_unified_calibration.py:201) so the bridge can never silently
drift.
The cut score
with pipeline/reference_calibration/run_youden_calibration.py:1;
the splits are enforced at pipeline/run_unified_calibration.py:458.
Behind the segment signals is a hierarchical item response model fit in NumPyro. Each observation has linear predictor
with per rater skill and bias drawn from expertise tier means, a per source
discrimination pipeline/joint_calibration/model_k7.py:178 (the shipped variant B), the
inference driver at pipeline/joint_calibration/fit_joint_k7.py:139.
Finally the prior covariance is estimated by pairing raters who scored in
two tasks and taking the empirical covariance of their skills and biases,
yielding the scripts/build_sigma_l_k7.py:108 for the pairwise estimator and
deployment/freeze_deployment_prior.py:124 for the average.
Calibration and inference are validated, not asserted.
- Simulation based calibration (Talts et al. 2018): simulate from the
prior, fit, and check that the rank of the truth in the posterior is
uniform. Skeleton and runners at
tests/test_phase35_sbc_engine.pyandscripts/run_sbc_validation.py. - Posterior coverage: nominal 90% intervals should cover the truth 90% of
the time; tracked in
tests/test_posterior_coverage.py. - Bit exact determinism: the engine is reproducible across machines under
single thread BLAS, and serial equals parallel to the bit
(
tests/test_parallel_determinism.py). - Real rater replay: rather than only simulate, the system replays the
recorded answers of 21 real clinicians through the engine
(
docs/PHASE7_REPLAY_HEADLINE.md).
To keep the documentation trustworthy, three explicit non claims.
- The stopping rule is not a Wald sequential probability ratio test. It is
a posterior precision criterion (AUROC half width below
$\delta$ ). The numerical boundaries can be made to look SPRT like, but the operating characteristics differ under adaptive question selection, and the source says so atengine/core_mcmc.py:40. - The joint calibration model is a two parameter probit item response model, not a pure Rasch model; Rasch fixes discrimination, this estimates it per rater and per source.
- The production calibration posterior is variational (SVI), with NUTS used only to validate it. We do not claim full MCMC posteriors in production.
The methods above draw on, and where noted depart from, the following.
- Metropolis, Rosenbluth, Rosenbluth, Teller and Teller (1953); Hastings (1970). The Metropolis-Hastings sampler used in rejuvenation.
- Gordon, Salmond and Smith (1993); Doucet, de Freitas and Gordon (2001). The bootstrap / SIR particle filter.
- Roberts, Gelman and Gilks (1997). Optimal scaling of random walk
Metropolis, the source of the
$2.38/\sqrt{d}$ proposal guidance. - Kong, Liu and Wong (1994). Effective sample size for importance sampling.
- Gelman et al. (2013). Split
$\hat{R}$ convergence diagnostic. - Talts, Betancourt, Simpson, Vehtari and Gelman (2018). Simulation based calibration.
- Plummer (2015); Jacob, Murray, Holmes and Robert (2017). Cuts in Bayesian graphical models, the modular inference framework.
- Patz and Junker (1999); Bafumi and Gelman (2007). MCMC and hierarchical treatment of rater item response models.
- Youden (1950). The J statistic for threshold selection.
- Chaloner and Verdinelli (1995). Bayesian optimal (A-optimal) experimental design.
Each claim in sections 1 through 5 carries a file:line pointer; the code
is the final authority where it and any prose disagree.