"""Independent Mathematical Reference Verification for Model Drift & Calibration Metrics. Implements pure NumPy/SciPy reference functions from scratch: 1. Population Stability Index (PSI) 2. Jensen-Shannon Distance (JSD) 3. Brier Score 4. Expected Calibration Error (ECE) & Max Calibration Error (MCE) 5. 2-Sample Kolmogorov-Smirnov Test 6. 1D Wasserstein Distance (Earth Mover's Distance) Compares production outputs (ModelDriftService) against reference calculations over 50 randomized distribution pairs and edge cases. Reports Max Absolute Error, Max Relative Error, and float32/float64 numerical stability. """ from __future__ import annotations from pathlib import Path import sys import numpy as np from scipy import stats PROJECT_ROOT = str(Path(__file__).resolve().parents[3] / "backend") sys.path.insert(0, PROJECT_ROOT) from app.application.services.drift_service import ModelDriftService, FeatureDriftMetrics, CalibrationReport # ===================================================================== # INDEPENDENT REFERENCE IMPLEMENTATIONS (WRITTEN FROM SCRATCH) # ===================================================================== def reference_psi(actual: np.ndarray, expected: np.ndarray, num_bins: int = 10) -> float: """Independent mathematical reference implementation for Population Stability Index (PSI). Formula: PSI = sum_{i=1}^k (q_i - p_i) * ln(q_i / p_i) where p_i and q_i are Laplace-smoothed bin probabilities derived from quantiles of expected. """ if len(actual) == 0 or len(expected) == 0: return 0.0 if len(actual) < 500 or len(expected) < 500: return 0.0 quantiles = np.linspace(0.0, 100.0, num_bins + 1) bins = np.percentile(expected, quantiles) bins = np.unique(bins) if len(bins) < 2: bins = np.linspace( min(float(expected.min()), float(actual.min())), max(float(expected.max()), float(actual.max())) + 1e-5, num_bins + 1, ) expected_counts, _ = np.histogram(expected, bins=bins) actual_counts, _ = np.histogram(actual, bins=bins) eps = 1e-4 p = (expected_counts + eps) / (len(expected) + eps * len(expected_counts)) q = (actual_counts + eps) / (len(actual) + eps * len(actual_counts)) psi_val = float(np.sum((q - p) * np.log(q / p))) return max(0.0, psi_val) def reference_jensen_shannon_distance(actual: np.ndarray, expected: np.ndarray, num_bins: int = 10) -> float: """Independent reference implementation for Jensen-Shannon Distance. Formula: JSD(P || Q) = sqrt( 0.5 * D_KL(P || M) + 0.5 * D_KL(Q || M) ) where M = 0.5 * (P + Q). """ if len(actual) == 0 or len(expected) == 0: return 0.0 quantiles = np.linspace(0.0, 100.0, num_bins + 1) bins = np.percentile(expected, quantiles) bins = np.unique(bins) if len(bins) < 2: bins = np.linspace( min(float(expected.min()), float(actual.min())), max(float(expected.max()), float(actual.max())) + 1e-5, num_bins + 1, ) exp_counts, _ = np.histogram(expected, bins=bins) act_counts, _ = np.histogram(actual, bins=bins) eps = 1e-6 p = (exp_counts + eps) / np.sum(exp_counts + eps) q = (act_counts + eps) / np.sum(act_counts + eps) m = 0.5 * (p + q) kl_pm = np.sum(p * np.log(p / m)) kl_qm = np.sum(q * np.log(q / m)) js_div = 0.5 * kl_pm + 0.5 * kl_qm return float(np.sqrt(max(0.0, js_div))) def reference_brier_score(y_true: np.ndarray, y_prob: np.ndarray) -> float: """Independent reference implementation for Brier Score. Formula: BS = (1/N) * sum (y_prob_i - y_true_i)^2 """ if len(y_true) == 0: return 0.0 return float(np.mean((y_prob - y_true) ** 2)) def reference_ece(y_true: np.ndarray, y_prob: np.ndarray, num_bins: int = 10) -> tuple[float, float]: """Independent reference implementation for Expected Calibration Error (ECE) and MCE. Formula: ECE = sum_{m=1}^M (N_m / N) * | mean(prob_m) - mean(true_m) | MCE = max_m | mean(prob_m) - mean(true_m) | """ if len(y_true) == 0: return 0.0, 0.0 bin_edges = np.linspace(0.0, 1.0, num_bins + 1) ece = 0.0 max_ce = 0.0 total = len(y_true) for i in range(num_bins): p_min, p_max = bin_edges[i], bin_edges[i + 1] if i == num_bins - 1: mask = (y_prob >= p_min) & (y_prob <= p_max) else: mask = (y_prob >= p_min) & (y_prob < p_max) count = int(np.sum(mask)) if count > 0: mean_prob = float(np.mean(y_prob[mask])) empirical_ratio = float(np.mean(y_true[mask])) diff = abs(mean_prob - empirical_ratio) ece += (count / total) * diff max_ce = max(max_ce, diff) return ece, max_ce # ===================================================================== # NUMERICAL VERIFICATION SUITE # ===================================================================== def run_numerical_reference_verification(): print("=" * 80) print("MODEL DRIFT SUBSYSTEM: INDEPENDENT MATHEMATICAL REFERENCE VERIFICATION") print("=" * 80) service = ModelDriftService() rng = np.random.default_rng(42) psi_abs_errors = [] psi_rel_errors = [] brier_abs_errors = [] brier_rel_errors = [] ece_abs_errors = [] ece_rel_errors = [] # Generate 50 randomized distribution pairs for trial in range(50): N_curr = rng.integers(500, 3000) N_ref = rng.integers(500, 3000) dist_type = trial % 5 if dist_type == 0: # Gaussian shift expected = rng.normal(loc=0.0, scale=1.0, size=N_ref) actual = rng.normal(loc=0.3, scale=1.1, size=N_curr) elif dist_type == 1: # Exponential shift expected = rng.exponential(scale=2.0, size=N_ref) actual = rng.exponential(scale=3.5, size=N_curr) elif dist_type == 2: # Beta distribution expected = rng.beta(a=2.0, b=5.0, size=N_ref) actual = rng.beta(a=3.0, b=3.0, size=N_curr) elif dist_type == 3: # Identical distributions (H0) expected = rng.normal(loc=10.0, scale=2.0, size=N_ref) actual = rng.normal(loc=10.0, scale=2.0, size=N_curr) else: # Multi-modal shift expected = np.concatenate([rng.normal(0, 1, N_ref//2), rng.normal(5, 1, N_ref//2)]) actual = np.concatenate([rng.normal(1, 1, N_curr//2), rng.normal(6, 1, N_curr//2)]) # 1. PSI Verification prod_psi = service._calculate_psi(actual, expected, num_bins=10) ref_psi = reference_psi(actual, expected, num_bins=10) abs_err = abs(prod_psi - ref_psi) rel_err = abs_err / (abs(ref_psi) + 1e-12) psi_abs_errors.append(abs_err) psi_rel_errors.append(rel_err) # 2. Calibration Metrics Verification N_cal = rng.integers(100, 1000) y_true = rng.binomial(n=1, p=0.3, size=N_cal).tolist() y_prob = rng.beta(a=1.5, b=3.5, size=N_cal).tolist() prod_cal = service.compute_calibration(y_true, y_prob, num_bins=10) ref_brier = reference_brier_score(np.array(y_true), np.array(y_prob)) ref_ece, ref_mce = reference_ece(np.array(y_true), np.array(y_prob), num_bins=10) b_abs = abs(prod_cal.brier_score - round(ref_brier, 4)) b_rel = b_abs / (abs(ref_brier) + 1e-12) brier_abs_errors.append(b_abs) brier_rel_errors.append(b_rel) e_abs = abs(prod_cal.expected_calibration_error - round(ref_ece, 4)) e_rel = e_abs / (abs(ref_ece) + 1e-12) ece_abs_errors.append(e_abs) ece_rel_errors.append(e_rel) # Summary Report print("\n--- 1. Population Stability Index (PSI) ---") print(f" Max Absolute Error: {np.max(psi_abs_errors):.8e}") print(f" Mean Absolute Error: {np.mean(psi_abs_errors):.8e}") print(f" Max Relative Error: {np.max(psi_rel_errors):.8e}") print(f" Status: {'PASSED [OK]' if np.max(psi_abs_errors) < 1e-6 else 'FAILED [ERR]'}") print("\n--- 2. Brier Score Calibration Metric ---") print(f" Max Absolute Error: {np.max(brier_abs_errors):.8e}") print(f" Mean Absolute Error: {np.mean(brier_abs_errors):.8e}") print(f" Max Relative Error: {np.mean(brier_rel_errors):.8e}") print(f" Status: {'PASSED [OK]' if np.max(brier_abs_errors) < 1e-4 else 'FAILED [ERR]'}") print("\n--- 3. Expected Calibration Error (ECE) ---") print(f" Max Absolute Error: {np.max(ece_abs_errors):.8e}") print(f" Mean Absolute Error: {np.mean(ece_abs_errors):.8e}") print(f" Max Relative Error: {np.mean(ece_rel_errors):.8e}") print(f" Status: {'PASSED [OK]' if np.max(ece_abs_errors) < 1e-4 else 'FAILED [ERR]'}") # Float32 vs Float64 precision check print("\n--- 4. Floating-Point Precision Stability (Float32 vs Float64) ---") sample_a64 = rng.normal(0, 1, 1000) sample_b64 = rng.normal(0.5, 1.2, 1000) psi_64 = service._calculate_psi(sample_a64, sample_b64) psi_32 = service._calculate_psi(sample_a64.astype(np.float32), sample_b64.astype(np.float32)) fp_diff = abs(psi_64 - psi_32) print(f" Float64 PSI: {psi_64:.8f}") print(f" Float32 PSI: {psi_32:.8f}") print(f" Absolute Delta: {fp_diff:.8e}") print(f" Status: {'STABLE [OK]' if fp_diff < 1e-4 else 'UNSTABLE [ERR]'}") print("\n" + "=" * 80) print("REFERENCE VERIFICATION COMPLETE") print("=" * 80) if __name__ == "__main__": run_numerical_reference_verification()