4. 特征值分布监控:均值、方差、分位数、空值率的实时计算

特征分布监控,说白了就是盯着数据有没有「变味」。

我见过太多案例:模型上线时 AUC 0.85,两周后掉到 0.6。查来查去,发现某个特征的均值从 0.5 漂到了 0.8。你说模型能不崩吗?

所以这一章,我们重点聊四个核心指标——均值、方差、分位数、空值率。它们怎么在实时流里算,怎么设阈值,怎么报警。

4.1 为什么是这四个指标?

先问个问题:一个特征「变了」,到底变在哪?

  • 均值:整体水平偏移。比如用户年龄均值从 30 变成 35,说明人群结构变了。
  • 方差:离散程度变化。方差突然变小,可能数据被截断;突然变大,可能有异常值混入。
  • 分位数:分布形状的细节。均值没变,但 P95 飙升,说明尾部出现极端值。
  • 空值率:数据完整性。空值率从 1% 跳到 20%,上游大概率挂了。

这四个指标组合起来,基本能覆盖 90% 的分布异常场景。我在项目中遇到过,只监控均值是不够的——有一次均值完全正常,但 P99 翻了三倍,最后发现是某个传感器间歇性故障。

核心原则:不要只盯一个指标。均值 + 分位数 + 空值率,三管齐下。

4.2 实时计算的挑战

离线算这些指标很简单——一个 SQL 搞定。但实时流里,数据是一条一条来的,你不能等全量数据到齐再算。

难点在哪?

  • 均值:可以用增量公式,维护 count 和 sum 就行。
  • 方差:需要维护 count、sum、sum_sq,用 Welford 算法。
  • 分位数:这个最头疼。精确分位数需要排序,实时流里不可能。
  • 空值率:相对简单,维护 null_count 和 total_count 即可。

嗯,这里要注意——分位数是实时监控里最大的坑。我早期踩过这个雷,直接用精确计算,结果窗口数据一多,延迟直接飙到秒级。

4.3 增量计算方案

4.3.1 均值与方差的增量更新

均值和方差有成熟的增量公式。我习惯用 Welford 算法,数值稳定性好,适合流式场景。

// 增量更新均值和方差
class StreamingStats {
    private long count = 0;
    private double mean = 0.0;
    private double m2 = 0.0;  // 用于方差计算

    public void update(double value) {
        count++;
        double delta = value - mean;
        mean += delta / count;
        double delta2 = value - mean;
        m2 += delta * delta2;
    }

    public double getMean() {
        return mean;
    }

    public double getVariance() {
        return count > 1 ? m2 / (count - 1) : 0.0;
    }

    public double getStddev() {
        return Math.sqrt(getVariance());
    }
}

这个算法的好处是:只维护三个变量(count, mean, m2),O(1) 空间,O(1) 时间。每条数据进来,更新一次就行。

小技巧:如果特征值范围很大(比如收入字段),建议用 double 而不是 float,避免精度丢失。我在金融项目里吃过这个亏,float 算方差,累计误差到了 10% 级别。

4.3.2 分位数的近似计算

实时分位数,业界主流方案是 T-Digest 或 GK 算法。我个人偏好 T-Digest,因为它对尾部数据更敏感,适合监控场景。

T-Digest 的核心思路:

  • 把数据分成多个「桶」,每个桶维护均值和计数。
  • 桶的大小不固定——数据密集的地方桶小,稀疏的地方桶大。
  • 查询分位数时,根据桶的分布做插值。
// T-Digest 使用示例(Java)
TDigest digest = TDigest.createMergingDigest(100);  // compression=100

// 每条数据进来
digest.add(value);

// 查询分位数
double p50 = digest.quantile(0.5);
double p95 = digest.quantile(0.95);
double p99 = digest.quantile(0.99);

compression 参数控制精度和内存的平衡。100 的 compression,误差通常在 1% 以内,内存消耗几百 KB。够用了。

注意:T-Digest 不是精确算法。如果业务要求精确分位数(比如金融合规),你需要用窗口全量排序,但要做好延迟和资源预算。我曾经在支付场景被迫用精确分位数,窗口大小只能设 1000 条,再多就扛不住。

4.3.3 空值率的实时统计

空值率相对简单。维护两个计数器:

class NullRateTracker {
    private long total = 0;
    private long nullCount = 0;

    public void record(boolean isNull) {
        total++;
        if (isNull) {
            nullCount++;
        }
    }

    public double getNullRate() {
        return total > 0 ? (double) nullCount / total : 0.0;
    }
}

但要注意窗口语义——你是算滑动窗口内的空值率,还是从启动至今的累计空值率?我建议用滑动窗口,因为累计值对突变不敏感。

4.4 窗口设计与阈值策略

实时监控离不开窗口。常见的窗口类型:

窗口类型 说明 适用场景
滑动窗口 固定时间长度,持续滑动 均值、方差、空值率
计数窗口 固定数据条数,满条滑动 分位数(精确计算时)
会话窗口 根据数据间隔划分 用户行为特征
累计窗口 从启动至今 基线校准

我个人习惯:滑动窗口 + 计数窗口组合。滑动窗口设 1 小时,计数窗口设 10000 条,哪个先触发就用哪个。这样既能保证时效性,又能避免数据稀疏时的统计偏差。

4.5 阈值怎么设?

阈值设得太松,漏报;设得太紧,误报满天飞。我总结了一套经验:

  • 均值:用历史均值的 ±3σ 作为阈值。如果特征本身有周期性,按小时/天分别建模。
  • 方差:用历史方差的倍数。我常用 2x 和 5x 两个级别——2x 告警,5x 紧急。
  • 分位数:P95/P99 用值。比如「响应时间 P99 > 500ms」直接告警。
  • 空值率:用阈值。比如空值率 > 5% 告警,> 20% 紧急。
避坑指南:阈值不要一成不变。我经历过一个教训——某个特征在业务大促期间均值自然上浮 20%,但阈值没调,告警刷了一整屏。后来我加了「动态基线」,根据历史同期数据自动调整阈值。

4.6 知识体系总览

下面这张图,把本章的核心逻辑串起来了:

特征值分布监控知识体系 均值 方差 分位数 空值率 实时计算方案 Welford 增量算法 | T-Digest 近似分位数 | 滑动窗口计数器 窗口策略:滑动窗口 + 计数窗口组合 窗口大小:1小时 / 10000条,谁先触发用谁 阈值策略:动态基线 + 多级告警 均值±3σ | 方差2x/5x | 分位数值 | 空值率5%/20% 核心原则:组合监控,避免单一指标盲区

4.7 落地建议

最后给几个实操建议:

  • 先跑基线:上线前至少收集 7 天的历史数据,算好均值、方差、分位数的正常范围。
  • 分级告警:WARN 级别发钉钉/企微,CRITICAL 级别直接电话。别让工程师在凌晨三点被无关告警吵醒。
  • 定期复盘:每周看一次告警记录,调整阈值。数据分布是会变的,阈值也要跟着迭代。
我的习惯:每个特征单独配一个监控配置,包括窗口大小、阈值、告警级别。别搞「一刀切」——不同特征的分布差异很大,统一阈值只会让你陷入告警疲劳。

嗯,特征分布监控这块,说难不难,说简单也不简单。核心就两点:选对指标,算对窗口。剩下的,就是不断调优阈值的过程了。