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) 时间。每条数据进来,更新一次就行。
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。够用了。
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% 紧急。
4.6 知识体系总览
下面这张图,把本章的核心逻辑串起来了:
4.7 落地建议
最后给几个实操建议:
- 先跑基线:上线前至少收集 7 天的历史数据,算好均值、方差、分位数的正常范围。
- 分级告警:WARN 级别发钉钉/企微,CRITICAL 级别直接电话。别让工程师在凌晨三点被无关告警吵醒。
- 定期复盘:每周看一次告警记录,调整阈值。数据分布是会变的,阈值也要跟着迭代。
嗯,特征分布监控这块,说难不难,说简单也不简单。核心就两点:选对指标,算对窗口。剩下的,就是不断调优阈值的过程了。