Quality Monitoring › Code Implementation

Michael BrenndoerferFebruary 14, 202617 min read
Language AI HandbookAll chapters
Part 65 of 67Production Systems
Chapter 498Quality Monitoring › Code Implementation
Reading section 7 of 10 · Code Implementation · 17 minSection 7 of 10 · 17 min

Book contents

Section 7 of 10 · 17 min read

Quality Monitoring › Code ImplementationOne section · Sections in this chapter7 / 10 ↓
Reading tools Definitions & inline links

Choose your expertise level to adjust how many terms are explained. Beginners see more tooltips, experts see fewer to maintain reading flow. Hover over underlined terms for instant definitions.

Article links

Make inline references clickable

This section implements a practical quality monitoring system covering metric computation, drift detection, and alert generation. We use synthetic data to simulate a production deployment where quality degrades gradually over time, matching the pattern you would see after an input distribution shift or a slow knowledge staleness effect.

Setup and Imports

In[5]:
Code
import numpy as np

# Set reproducible seed
rng = np.random.default_rng(42)

Simulating a Production Quality Stream

We simulate 500 time steps representing a deployment that runs normally for the first 200 steps, then gradually degrades. The degradation follows a linear ramp, mimicking the kind of slow input distribution shift that is common in production but hard to notice without monitoring.

In[6]:
Code
def simulate_quality_stream(n_steps=500, drift_start=200, rng=None):
    """
    Simulate a stream of quality metric values with a gradual drift.
    Returns bleu_scores and bertscore_values as numpy arrays.
    """
    if rng is None:
        rng = np.random.default_rng(0)

    bleu_scores = []
    bertscore_values = []

    for t in range(n_steps):
        if t < drift_start:
            # Stable production quality
            bleu = rng.normal(loc=0.42, scale=0.05)
            bert = rng.normal(loc=0.85, scale=0.03)
        else:
            # Gradual degradation after drift_start
            drift_magnitude = (t - drift_start) / (n_steps - drift_start)
            bleu = rng.normal(loc=0.42 - 0.15 * drift_magnitude, scale=0.06)
            bert = rng.normal(loc=0.85 - 0.08 * drift_magnitude, scale=0.04)

        bleu_scores.append(max(0.0, min(1.0, bleu)))
        bertscore_values.append(max(0.0, min(1.0, bert)))

    return np.array(bleu_scores), np.array(bertscore_values)


bleu_scores, bertscore_values = simulate_quality_stream(
    n_steps=500, drift_start=200, rng=rng
)
Out[7]:
Console
Total time steps: 500
Pre-drift BLEU  : mean=0.4211, std=0.0493
Post-drift BLEU : mean=0.3425, std=0.0721
Mean BLEU drop  : 0.0786

The simulated stream shows a clear drop in mean BLEU after step 200, while the standard deviation remains similar. This is a classic location shift: the model's outputs are less similar to references on average, but the output-to-output variability is unchanged. A monitoring system that tracks only the variance of outputs would miss this entirely, which is why mean-sensitive detectors like CUSUM are important alongside variance-sensitive ones.

Visualizing the Quality Stream

Out[8]:
Visualization
Two-panel line plot of BLEU and BERTScore over 500 time steps with rolling mean and drift start marker.
Simulated quality metric stream for BLEU (top) and BERTScore (bottom) over 500 time steps. Both metrics are stable for the first 200 steps before drifting downward. The rolling 20-step mean (solid line) reveals the trend clearly against the noisy raw values, illustrating why smoothing is essential for practical monitoring dashboards.

The raw metric values are noisy enough that step-by-step comparisons would be misleading: many individual post-drift values are higher than many individual pre-drift values simply due to sampling variability. The rolling mean makes the trend clearly visible. In a production monitoring dashboard, the rolling mean is the signal you monitor; the raw values are useful for diagnostics but not for threshold-based alerting.

KS Test for Distribution Shift

We implement a sliding-window KS test that compares a reference window to the most recent window of the same size. When the KS statistic exceeds a threshold and the p-value falls below a significance level, the test reports drift. The reference window is fixed at the initial 100 observations; the current window slides forward one step at a time.

In[9]:
Code
def sliding_window_ks_test(scores, ref_size=100, window_size=50, alpha=0.05):
    """
    Apply a two-sample KS test between a fixed reference window and
    a sliding current window. Returns detected drift points.
    """
    reference = scores[:ref_size]
    drift_points = []
    ks_stats = []
    p_values = []

    for start in range(ref_size, len(scores) - window_size + 1):
        current = scores[start : start + window_size]
        ks_stat, p_val = stats.ks_2samp(reference, current)
        ks_stats.append(ks_stat)
        p_values.append(p_val)
        if p_val < alpha:
            drift_points.append(start + window_size // 2)

    return np.array(ks_stats), np.array(p_values), drift_points


ks_stats, p_values, bleu_drift_points = sliding_window_ks_test(
    bleu_scores, ref_size=100, window_size=50, alpha=0.05
)
Out[10]:
Console
KS test results (BLEU, alpha=0.05):
  Reference window size : 100 steps
  Current window size   : 50 steps
  First drift detected  : step 253
  Total drift detections: 223
  Max KS statistic      : 0.8500
  Min p-value observed  : 3.50e-25

The KS test detects drift shortly after step 200, where the true degradation begins. The first detection slightly lags the actual drift start because the test requires observing a full current window before accumulating sufficient evidence. The lag is approximately window_size / 2 steps in expectation, which for our 50-step window means a typical lag of about 25 steps.

CUSUM Sequential Detector

The CUSUM detector is more sensitive to gradual drift than the windowed KS test because it processes each observation immediately rather than waiting for a full window. Every new metric value is incorporated into the running statistic, so the detector can alarm within a handful of steps of the drift starting rather than waiting for a full window to fill.

In[11]:
Code
def cusum_detector(scores, target_mean, k=0.01, h=5.0):
    """
    Cumulative sum control chart for detecting downward drift.
    k: allowance (sensitivity), h: threshold (specificity).
    Returns alarm time indices and the CUSUM statistic series.
    """
    s_minus = 0.0  # cumulative sum for downward drift
    alarms = []
    s_values = []

    for t, x in enumerate(scores):
        s_minus = max(0.0, s_minus + (target_mean - x - k))
        s_values.append(s_minus)
        if s_minus > h:
            alarms.append(t)
            # Reset after alarm to detect subsequent drift episodes
            s_minus = 0.0

    return alarms, np.array(s_values)


# Use pre-drift mean as the target
target_mean = bleu_scores[:200].mean()
cusum_alarms, cusum_values = cusum_detector(
    bleu_scores, target_mean=target_mean, k=0.01, h=5.0
)
Out[12]:
Console
CUSUM detector results (BLEU):
  Target mean           : 0.4211
  Allowance k           : 0.01
  Threshold h           : 5.0
  First alarm           : step 351
  Total alarms raised   : 4

Visualizing Drift Detection

Out[13]:
Visualization
Two-panel plot showing KS p-values on log scale and CUSUM statistic over time, both detecting drift near step 200.
Comparison of the KS test (top) and CUSUM detector (bottom) applied to the BLEU metric stream. The KS test p-value drops sharply below the alpha=0.05 threshold shortly after the true drift begins at step 200, and the CUSUM statistic climbs above the threshold h=5.0 as drift accumulates. Both detectors confirm the same event, giving corroboration that reduces false alarm risk in practice.

The CUSUM plot reveals an important behavioral property: after the alarm fires and the statistic resets to zero, it climbs again quickly because the degradation is ongoing. This produces a series of alarms rather than a single alarm, which in practice would drive escalating alert severity or automated rollback. The KS test, by contrast, saturates at very low p-values once drift is fully established; the p-value provides a measure of evidence strength rather than a count of events.

Population Stability Index

We compute PSI to quantify how much the distribution has shifted between the reference period and the current period, binning BLEU scores into 10 buckets. PSI gives a scalar summary that is easy to threshold for alerting: below 0.1 is stable, 0.1 to 0.25 warrants investigation, above 0.25 requires action.

In[14]:
Code
def compute_psi(reference, current, n_bins=10, epsilon=1e-6):
    """
    Population Stability Index between two distributions.
    Bins are defined by quantiles of the reference distribution.
    """
    display_edges = np.quantile(reference, np.linspace(0, 1, n_bins + 1))
    bin_edges = display_edges.copy()
    # Capture values beyond the reference range in the two tail bins.
    bin_edges[0] = -np.inf
    bin_edges[-1] = np.inf

    ref_counts, _ = np.histogram(reference, bins=bin_edges)
    curr_counts, _ = np.histogram(current, bins=bin_edges)

    ref_pct = ref_counts / ref_counts.sum()
    curr_pct = curr_counts / curr_counts.sum()

    # Replace zeros to avoid log(0)
    ref_pct = np.where(ref_pct == 0, epsilon, ref_pct)
    curr_pct = np.where(curr_pct == 0, epsilon, curr_pct)

    psi = np.sum((curr_pct - ref_pct) * np.log(curr_pct / ref_pct))
    return psi, ref_pct, curr_pct, display_edges


reference_window = bleu_scores[:200]
current_window = bleu_scores[350:]  # Late-stage degraded period
psi_value, ref_pct, curr_pct, bin_edges = compute_psi(
    reference_window, current_window
)
Out[15]:
Console
PSI Results (BLEU: reference vs. current window):
  PSI value     : 4.7114
  Interpretation: Major shift -- action required
Out[16]:
Visualization
Grouped bar chart of BLEU score bin proportions for reference and current windows, showing leftward shift.
PSI bin comparison between the reference window (first 200 steps) and the degraded current window (last 150 steps). Because the bin boundaries are reference quantiles, the reference bars are approximately equal by construction. The current distribution concentrates strongly in the lowest BLEU bins and leaves less mass in the higher bins, producing the large PSI value shown in the title.

The grouped bar chart makes the shift concrete. The reference bars sit near 0.10 because the ten bins were defined from reference quantiles, so each contains roughly one tenth of the reference observations. The current window piles probability into the lowest bins and leaves much less in the higher bins. This is exactly the signature of quality degradation: the model still produces some good outputs, but the proportion of low-quality outputs has increased substantially.

Alert Engine

We build a multi-metric alert engine that consumes metric streams and fires alerts when the rolling mean deviates from the baseline by more than a specified number of standard deviations. The engine supports initializing any number of named metrics, updating them one observation at a time, and maintaining a history of all alerts for post-hoc analysis.

In[17]:
Code
class QualityAlertEngine:
    """
    Multi-metric alert engine with rolling statistics and threshold levels.
    """

    def __init__(self, window_size=50, warn_sigma=2.0, crit_sigma=3.0):
        self.window_size = window_size
        self.warn_sigma = warn_sigma
        self.crit_sigma = crit_sigma
        self.history = {}
        self.alerts = []

    def initialize_metric(self, name, baseline_values):
        self.history[name] = {
            "baseline_mean": float(np.mean(baseline_values)),
            "baseline_std": float(np.std(baseline_values)),
            "window": deque(maxlen=self.window_size),
        }

    def update(self, t, metrics: dict):
        fired = []
        for name, value in metrics.items():
            if name not in self.history:
                continue

            info = self.history[name]
            info["window"].append(value)

            if len(info["window"]) < self.window_size:
                continue

            current_mean = np.mean(info["window"])
            deviation = (info["baseline_mean"] - current_mean) / (
                info["baseline_std"] + 1e-9
            )

            if deviation >= self.crit_sigma:
                level = "CRITICAL"
            elif deviation >= self.warn_sigma:
                level = "WARNING"
            else:
                level = None

            if level:
                fired.append(
                    {
                        "t": t,
                        "metric": name,
                        "level": level,
                        "current_mean": round(current_mean, 4),
                        "baseline_mean": round(info["baseline_mean"], 4),
                        "deviation_sigma": round(deviation, 2),
                    }
                )

        self.alerts.extend(fired)
        return fired


engine = QualityAlertEngine(window_size=50, warn_sigma=2.0, crit_sigma=3.0)
baseline = bleu_scores[:200]
engine.initialize_metric("bleu", baseline)
engine.initialize_metric("bertscore", bertscore_values[:200])

for t in range(200, len(bleu_scores)):
    engine.update(t, {"bleu": bleu_scores[t], "bertscore": bertscore_values[t]})
Out[18]:
Console
Alert Engine Summary:
  Total alerts    : 136
  WARNING alerts  : 136
  CRITICAL alerts : 0

  First WARNING (t=418):
    Metric         : bleu
    Current mean   : 0.3218
    Baseline mean  : 0.4211
    Deviation      : 2.01 sigma
Out[19]:
Visualization
Line plot of rolling BLEU mean with shaded warning and critical zones, warning event markers, no critical event markers, and the drift start.
Quality alert timeline showing the rolling BLEU mean and its sigma-based alert zones. Warning events are placed within the 2-to-3-sigma band after the rolling mean crosses the warning threshold. No BLEU critical events occur in this simulation because the rolling mean remains above the 3-sigma threshold; the empty critical count demonstrates that severity is not escalated without sufficient evidence.

The alert timeline shows the intended separation between severity levels. Warning alerts appear once the rolling mean dips into the 2-to-3-sigma band. The BLEU signal does not cross the 3-sigma threshold in this run, so no critical BLEU alerts fire. That is useful behavior rather than a missing result: the on-call team receives early notice without the system overstating the severity of the observed degradation.

Regression Detection with A/B Test

When deploying a new model version, we need to determine whether any observed quality difference is statistically significant rather than random noise. We implement a one-sided Mann-Whitney U test, which asks whether the treatment (new model) is stochastically less than the control (current model). The Mann-Whitney test is preferred over a t-test for quality metrics because it makes no distributional assumptions.

In[20]:
Code
def ab_regression_test(control_scores, treatment_scores, alpha=0.05):
    """
    Test whether treatment (new model) regresses on a quality metric
    compared to control (current model).
    Returns a summary dict with test statistics and verdict.
    """
    # One-sided Mann-Whitney U test: is treatment stochastically less than control?
    stat, p_value = stats.mannwhitneyu(
        treatment_scores, control_scores, alternative="less"
    )
    regressed = p_value < alpha
    effect_size = treatment_scores.mean() - control_scores.mean()

    return {
        "control_mean": float(control_scores.mean()),
        "treatment_mean": float(treatment_scores.mean()),
        "effect_size": float(effect_size),
        "u_statistic": float(stat),
        "p_value": float(p_value),
        "alpha": alpha,
        "regression_detected": bool(regressed),
    }


# Scenario A: new model version with genuine regression
control = rng.normal(loc=0.42, scale=0.05, size=500)
regressed_treatment = rng.normal(loc=0.38, scale=0.05, size=500)

# Scenario B: new model version without regression (noise only)
stable_treatment = rng.normal(loc=0.42, scale=0.05, size=500)

result_regressed = ab_regression_test(control, regressed_treatment)
result_stable = ab_regression_test(control, stable_treatment)
Out[21]:
Console

Scenario A (genuine regression):
  Control mean   : 0.4198
  Treatment mean : 0.3721
  Effect size    : -0.0477
  p-value        : 0.0000
  Verdict        : REGRESSION DETECTED

Scenario B (no regression):
  Control mean   : 0.4198
  Treatment mean : 0.4199
  Effect size    : +0.0001
  p-value        : 0.5062
  Verdict        : No significant regression
Out[22]:
Visualization
Two-panel density histogram comparing control and treatment BLEU distributions for genuine regression and no-regression cases.
Distribution comparison for the A/B regression test scenarios, using a filled control histogram and an outlined treatment histogram with common bin edges in each panel. Scenario A (left) shows a clearly leftward-shifted treatment distribution, with the treatment mean about 0.04 BLEU points below the control. The one-sided Mann-Whitney test detects a significant regression. Scenario B (right) shows near-identical distributions drawn from the same generating process and no significant regression.

The test correctly identifies Scenario A as a regression and Scenario B as acceptable noise. The 500 observations per arm provide ample separation for the simulated 0.04-point effect. In practice, the required sample size depends on the metric variance, target effect size, significance level, and desired statistical power, so it should be determined with a power analysis rather than treated as a fixed minimum.

Key Parameters

The key parameters for a quality monitoring system are:

  • reference window size: The period used to establish baseline statistics. Longer windows produce more stable baselines but are slower to adapt to deliberate model updates. Typical values range from two to four weeks of production data.
  • detection window size: The rolling window over which current statistics are computed and compared to the baseline. Smaller windows detect drift sooner but are noisier; larger windows are more stable but introduce detection lag.
  • KS test alpha: The significance threshold for rejecting the null hypothesis of equal distributions. Common choices are 0.01 or 0.05. Lower alpha reduces false alarms at the cost of slower detection.
  • CUSUM k (allowance): Controls sensitivity to small shifts. Set to half the minimum shift magnitude you want to detect.
  • CUSUM h (threshold): Controls the false alarm rate. Higher values mean fewer false alarms but slower detection. Tune by simulation against historical data.
  • alert sigma thresholds: The number of standard deviations from the baseline mean that trigger warning (typically 2 sigma) and critical (typically 3 sigma) alerts.
  • PSI bin count: The number of bins used to compute PSI. More bins capture finer distributional differences but require larger sample sizes for stable estimates. Ten to twenty bins is typical.