Refactor ModelMetrics to utilize DriftAnalysis for drift detection - Replaced ModelAnalysis with DriftAnalysis in the ModelMetrics class to enhance drift detection capabilities. - Updated method signatures and documentation to reflect the changes in target_name and return values. - Adjusted data handling to ensure compatibility with the new analysis methods and improved clarity in the drift metrics dataframe preparation.
4.5 KiB
Specification: sientia_model — Jensen–Shannon drift (DriftAnalysis)
This document describes what sientia_model.analytics.drift_analysis.DriftAnalysis should change so downstream consumers (e.g. Laborious calculate_drift → Postgres drift_metrics.value NOT NULL) no longer receive NaN for Jensen–Shannon on valid finite data.
Scope
- File:
sientia_model/analytics/drift_analysis.py - Method:
_jensen_shannon_distance(self, ref: np.ndarray, cur: np.ndarray, bins: int = 20) -> float - Callers:
detect_univariate_driftuses this for thejensen_shannonmethod; results are written tovaluein the drift dataframe.
Problem
The current implementation:
- Builds bin edges from
refonly:np.histogram(ref, bins=bins, density=True). - Builds the chunk histogram with
density=Trueon the same edges:np.histogram(cur, bins=edges, density=True).
When every value in cur falls outside the closed support implied by those edges (typical case: production chunk drifted above the reference max or below the reference min), NumPy yields all-zero counts for cur. With density=True, normalization does 0/0, producing NaN for the whole histogram, which propagates to float('nan') in detect_univariate_drift → SQL NULL where value is NOT NULL.
This appears in Laborious when:
model_config.targetexcludes the main target column from univariate features, so another feature (e.g.sensor_2) is compared chunk-by-chunk against the full reference series for that feature.- Reference and current ranges do not overlap for some chunks (strong drift or different scaling).
Other methods (kolmogorov_smirnov, wasserstein) do not use this histogram+density path, so they can stay finite while Jensen–Shannon alone becomes null.
Required behavior
- Finite output for finite
refandcurafter removing non-finite values, whenever both sides have at least one usable sample. - Explicit handling of out-of-range chunk mass: probability mass from
curthat does not fall into any bin defined fromrefmust still be represented (so the chunk distribution sums to 1), analogous to NannyML’s continuous JS approach (tail / “leftover” mass). - Missing values: drop
NaNfromrefandcurbefore computing. If either side is empty after that, returnfloat('nan')(callers may filter or map; schema may still forbid null — product decision outside this spec).
Recommended algorithm (replace current body)
ref = np.asarray(ref, float); cur = np.asarray(cur, float).ref = ref[~np.isnan(ref)]; cur = cur[~np.isnan(cur)].- If
ref.size == 0orcur.size == 0: returnfloat('nan'). hist_ref, edges = np.histogram(ref, bins=bins)— counts, notdensity=True.p = hist_ref.astype(float) / ref.size(reference bin probabilities).hist_cur, _ = np.histogram(cur, bins=edges);q = hist_cur.astype(float) / cur.size.leftover = 1.0 - float(np.sum(q)). Ifleftover > 1e-15(tolerance for float noise), appendleftovertoqand0.0topso both remain proper discrete distributions over the same extended support.- Apply small smoothing (existing module constant
EPSILONis fine): addEPSILONtopandq, renormalize each to sum 1. m = 0.5 * (p + q); compute symmetric JS via KL terms as today, e.g.inner = 0.5 * (sum(p*log(p/m)) + sum(q*log(q/m))).- Return
sqrt(max(inner, 0.0))to guard against tiny negativeinnerfrom floating-point error.
Non-goals / notes
- Numerical parity with the old
density=Trueimplementation is not required; parity with NannyML or scipy JS is desirable but optional. The priority is finite, interpretable drift when the chunk is outside the reference histogram range. - Multivariate drift in the same file is unchanged by this spec.
- Tests in
sientia_modelshould cover: (a) chunk entirely above reference max, (b) entirely below reference min, (c) overlapping range, (d)reforcurall-NaN after cleaning.
Reference (external)
- NannyML continuous JS uses count-based bin probabilities and a leftover mass bin; see
ContinuousJensenShannonDistanceinnannyml/drift/univariate/methods.py(_calculate,leftover = 1 - np.sum(...)). - Historical NaN-in-reference issue: NannyML#339 / #340 (orthogonal to out-of-range mass, but relevant for input cleaning).