Drift detection
Distribution drift is one of the primary risks to adaptive medical AI systems in production. AEGIS monitors for drift at every iteration using statistical tests applied to the feature distributions of the golden and drifting datasets. This page covers the methodology, configuration, and calibration of drift detection.
Covariate drift vs concept drift
AEGIS distinguishes between two forms of distribution shift:
| Drift type | What changes | Detection method | AEGIS response |
|---|---|---|---|
| Covariate drift | Input feature distributions P(X) | Statistical tests (KS, MMD, PSI) on feature distributions | Drift score feeds into CDM |
| Concept drift | Relationship between inputs and outputs P(Y|X) | Performance degradation on D_G as a proxy | Performance thresholds (P_fail, R_G) |
Covariate drift is detected directly by comparing D_G and D_D,k feature distributions. Concept drift is detected indirectly through its effect on model performance -- when P(Y|X) changes, performance on the stable golden dataset D_G will degrade.
**Sepsis example:** The sepsis model used 202 features total, but only K=34 key clinical features were monitored for drift. Drift was not introduced uniformly across all iterations. Iterations 1--5 used real Hospital A data with no artificial drift (drift scores 0.000). Iteration 6 was a cross-site scenario (50% Hospital B data, drift score 0.412). Iteration 7 was clean Hospital A data. Iteration 8 used extreme feature scaling (drift score 1.000). Iteration 9 was a clean recovery. Iteration 10 was catastrophic corruption (drift score 1.000, all 34 monitored features significantly shifted). Concept drift was detected indirectly through performance degradation, with sensitivity degrading from 0.723 to 0.428.
Bonferroni-corrected Kolmogorov-Smirnov test
AEGIS recommends the Kolmogorov-Smirnov (KS) test with Bonferroni correction as the default drift detection method. This is Equation 2 from the manuscript.
Method
For each of K features, perform a two-sample KS test comparing the distribution in D_G against D_D,k:
For feature j (j = 1, ..., K):
H_0: F_G,j = F_D,j (distributions are identical)
H_1: F_G,j ≠ F_D,j (distributions differ)
Compute p_j = KS_test(D_G[:, j], D_D,k[:, j])
Feature j has significant drift if p_j < alpha'
Bonferroni correction
To control the family-wise error rate when testing K features simultaneously:
alpha' = alpha / K
This is a conservative correction that reduces the probability of false drift detections at the cost of reduced sensitivity to true drift.
Drift score computation
The drift score is the proportion of features with statistically significant drift:
drift_score = |{j : p_j < alpha'}| / K
The drift score ranges from 0.0 (no features drifted) to 1.0 (all features drifted).
Implementation
from scipy import stats
import numpy as np
def compute_drift_score(golden_data: np.ndarray,
drifting_data: np.ndarray,
alpha: float = 0.05) -> dict:
"""
Compute Bonferroni-corrected KS drift score.
Parameters
----------
golden_data : np.ndarray Shape (n_golden, K)
drifting_data : np.ndarray Shape (n_drifting, K)
alpha : float Family-wise significance level
Returns
-------
dict {drift_score, n_drifted, K, alpha_prime, p_values}
"""
K = golden_data.shape[1]
alpha_prime = alpha / K
p_values = []
n_drifted = 0
for j in range(K):
stat, p_val = stats.ks_2samp(golden_data[:, j], drifting_data[:, j])
p_values.append(p_val)
if p_val < alpha_prime:
n_drifted += 1
return {
'drift_score': n_drifted / K,
'n_drifted': n_drifted,
'K': K,
'alpha_prime': alpha_prime,
'p_values': p_values
}
Feature selection
Not all features need to be monitored for drift. The choice of features affects both the drift score and the Bonferroni correction severity (since K determines alpha').
Selection criteria
| Criterion | Guidance |
|---|---|
| Clinical relevance | Include features that directly impact clinical predictions |
| Model importance | Include features with high feature importance scores |
| Stability | Exclude features known to have high natural variability |
| Redundancy | Avoid highly correlated feature pairs (inflates K unnecessarily) |
**Sepsis example:** The sepsis model used 202 total features, but only 34 key clinical features were selected for drift monitoring: HR, O2Sat, Temp, SBP, MAP, DBP, Resp, EtCO2, BaseExcess, HCO3, FiO2, pH, PaCO2, SaO2, AST, BUN, Alkalinephos, Calcium, Chloride, Creatinine, Bilirubin_direct, Glucose, Lactate, Magnesium, Phosphate, Potassium, Bilirubin_total, TroponinI, Hct, Hgb, PTT, WBC, Fibrinogen, Platelets. With K = 34 and alpha = 0.05, the Bonferroni-corrected threshold was alpha' = 0.05 / 34 = 0.00147. This conservative correction ensured that drift detections were genuine distribution shifts rather than multiple testing artefacts.
Minor and major drift thresholds
The drift score is classified into three severity levels using the theta thresholds:
drift_score < theta_minor,low → No significant drift
theta_minor,low <= drift_score
< theta_minor,high → Minor drift (informational)
theta_minor,high <= drift_score
< theta_major → Moderate drift (heightened monitoring)
drift_score >= theta_major → Major drift (triggers A3 alarm)
CDM integration
Drift severity maps to CDM behaviour as follows:
| Drift level | CDM effect |
|---|---|
| No drift | No impact on decision |
| Minor drift | May trigger CONDITIONAL APPROVAL at P3 |
| Moderate drift | Heightened monitoring; logged but no automatic escalation |
| Major drift | Triggers A3 alarm; combined with safety floor violation triggers P1 REJECT |
Calibration from baseline variability
To set meaningful drift thresholds, establish a baseline by measuring drift between temporal or random subsets of your own data:
Calibration procedure
- Split D_T,0 into two random halves (A and B)
- Compute drift_score(A, B) -- this represents natural variability
- Repeat 100 times with different random splits
- Build the baseline distribution of drift scores
def calibrate_drift_thresholds(training_data: np.ndarray,
n_bootstrap: int = 100,
alpha: float = 0.05) -> dict:
"""
Calibrate drift thresholds from baseline variability.
Returns suggested theta_minor_low, theta_minor_high, theta_major.
"""
n = training_data.shape[0]
baseline_scores = []
for _ in range(n_bootstrap):
idx = np.random.permutation(n)
half = n // 2
A = training_data[idx[:half]]
B = training_data[idx[half:2*half]]
result = compute_drift_score(A, B, alpha)
baseline_scores.append(result['drift_score'])
baseline_scores = np.array(baseline_scores)
return {
'theta_minor_low': float(np.percentile(baseline_scores, 75)),
'theta_minor_high': float(np.percentile(baseline_scores, 95)),
'theta_major': max(float(np.percentile(baseline_scores, 99)), 0.90),
'baseline_mean': float(baseline_scores.mean()),
'baseline_std': float(baseline_scores.std()),
'baseline_max': float(baseline_scores.max())
}
- Set thresholds based on percentiles:
| Threshold | Suggested percentile | Rationale |
|---|---|---|
| theta_minor,low | 75th percentile | Drift above 75% of baseline is noteworthy |
| theta_minor,high | 95th percentile | Drift above 95% of baseline is unusual |
| theta_major | 99th percentile or 0.90, whichever is higher | Extreme drift only |
Alternative drift detection methods
While KS + Bonferroni is the default, AEGIS supports other methods:
| Method | Strengths | Limitations |
|---|---|---|
| KS + Bonferroni | Non-parametric; per-feature granularity; well-understood | Conservative with large K |
| MMD (Maximum Mean Discrepancy) | Captures multivariate drift; kernel-based | Less interpretable; single score for all features |
| PSI (Population Stability Index) | Industry standard; easy to explain | Assumes binning; less powerful for continuous features |
| Performance comparison | Direct clinical relevance | Only detects drift that affects performance |
Document your chosen method and justification in the S2 configuration form.
Concept drift proxy at P2
AEGIS detects concept drift indirectly through the P2 condition. P2 is CLINICAL REVIEW, triggered when a metric regresses beyond tolerance tau from the fixed performance reference P^ref. This regression may indicate concept drift indirectly:
IF any_metric degraded > tau from P^ref (fixed performance reference):
→ CLINICAL REVIEW (P2): regression from reference, concept drift may be occurring
This proxy approach avoids the need for labelled data in the drifting dataset (which may not be immediately available in production). Note that major drift is detected separately via A3 ALARM, which fires independently of the P2 condition.
Source
Manuscript Section II-D (drift detection configuration within instantiation parameters) and Equation 2 (Bonferroni-corrected KS test). Drift threshold calibration methodology is described in Supplementary S2.5. Feature selection guidance and the concept drift proxy are discussed in Section II-B-2 (MMM specification). The sepsis feature list and drift progression across iterations are detailed in Section IV-C.