在生态监测、工业物联网和设备运维系统中,异常数据往往具有不同表现形式。部分异常十分直观,例如温度瞬间跳变、通信延迟突然增加、传感器读数连续归零。还有一些异常隐藏得更深,单个指标仍处于合理范围,多个指标组合后却偏离了设备长期形成的运行规律。
面对这些情况,工程系统通常会结合统计检测与机器学习检测。**统计方法计算开销较小,适合快速识别短期波动;机器学习方法能够同时分析多个特征,适合发现复杂的异常组合。**滚动 MAD 与孤立森林分别代表这两类思路,前者观察当前数据相对于近期历史的变化,后者判断当前样本在整体数据分布中的孤立程度。
一、时序异常检测的基本思路
时序数据具有连续到达、分布变化和指标相关等特点,固定阈值很难适应全部运行环境。以温度为例,将超过 35℃的数据直接判定为异常,在夏季可能产生大量误报,在冬季又可能错过尚未达到阈值的明显升温。更合理的方式是根据近期历史数据建立动态基线,再判断当前数据是否偏离正常范围。

滚动均值、滚动标准差和滚动 MAD 都可以建立动态基线 。其中,滚动 MAD 对极端值更加稳定,适合处理容易混入异常点的传感器数据。当系统同时采集温度、湿度、电压、风速和通信延迟时,仅对每个指标分别检测仍然不够。某条数据可能在每个维度上都没有越界,它们共同出现时却十分少见,此时就需要引入孤立森林等多维异常检测模型。
二、滚动MAD建立局部基线
1. 滚动窗口与中位数
滚动 MAD 可以理解为一个不断向前移动的观察窗口。假设窗口长度为 20,当第 21 条数据到达时,系统使用前 20 条数据计算中位数和 MAD,再判断当前数据与局部基线之间的距离。下一条数据到达后,窗口向前移动一位,基线也会随近期数据同步更新。

中位数用于描述窗口内数据的中心位置,它不容易被极端值拉动。对于本身就需要识别异常值的任务,这种稳定性十分重要。
2. MAD分数的计算
MAD 表示窗口内各个数据与中位数之间绝对距离的中位数。
bash
MAD = median(|xi - median(x)|)
得到 MAD 后,可以进一步计算当前数据的异常分数。
bash
MAD_score = 0.6745 × (x - median) / (MAD + ε)
其中,ε 是一个很小的数,用于避免 MAD 等于零时出现除零问题。当分数绝对值超过设定阈值时,当前数据就可以被标记为异常。3.5 是较常见的初始阈值,真实项目仍需结合采样频率、传感器精度和误报情况进行调整。
3. 滚动MAD的适用范围
滚动 MAD 特别适合识别突增、突降和短时间偏移。它的计算过程清晰,系统能够直接给出当前值、窗口中位数和 MAD 分数,便于定位异常来源。它主要描述单个指标在时间方向上的变化,对于多个指标之间隐藏的联合关系,仍需要其他方法补充。
三、孤立森林识别多维异常
1. 随机选择特征与分割点
孤立森林是一种无监督异常检测算法 。模型训练时不需要提前标注正常样本和异常样本,它会不断随机切分数据,并观察每个样本需要经历多少次切分才能被单独分离。模型由多棵孤立树组成,每棵树都会独立执行这一过程。
假设一条监测数据包含温度、湿度和通信延迟三个特征。某棵树开始构建时,会随机选择一个特征,例如温度。当前节点中的温度范围为 20℃到 40℃,模型会在这个范围内随机选择一个分割点,例如 32℃,再将数据划分为两个子集 。【孤立树-随机切分数据】
bash
temperature < 32
temperature >= 32
下一次切分可能继续选择温度,也可能改为湿度或通信延迟。每次选择的特征和分割点都带有随机性,数据会在不断切分中逐渐进入更小的子集,最终落到不同叶节点中。
2. 路径长度与异常程度
假设某个节点中的温度数据如下。
bash
27.1 27.3 27.5 27.8 28.0 46.0
如果随机分割点为 35℃,46℃会直接进入一个单独分支,其余数据仍然聚集在另一个分支中。46℃只经历一次切分就被隔离,因此路径长度很短。正常数据彼此接近,需要更多次切分才能逐个分开,路径长度通常更长。

单棵树的判断存在偶然性,某个正常点也可能恰好被较早分离。孤立森林会建立多棵树,再计算同一个样本在不同树中的平均路径长度 。如果某个数据点在大量随机切分中都能较快被隔离,它通常具有更高的异常概率。
四、两种方法处理不同类型的异常
滚动 MAD 关注当前数据与近期历史之间的差异,孤立森林关注当前样本在多维数据空间中的稀有程度。两者观察数据的角度不同,适合处理的异常类型也有所区别。
例如某设备的温度长期处于 25℃到 32℃之间,湿度长期处于 70%到 90%之间。某一时刻温度为 30℃,湿度为 88%,两个数值单独看都没有超出正常范围,滚动 MAD 可能不会触发异常。如果历史数据中高温通常伴随较低湿度,那么这一组合就可能十分少见。孤立森林同时分析温度和湿度后,可以发现该样本偏离了主要数据群体。
五、滚动MAD与孤立森林的协同方式
在实时监测系统中,可以让滚动 MAD 和孤立森林并行处理同一条数据 。滚动 MAD 输出局部异常结果,孤立森林输出多维异常结果,系统再根据两个模块的判断进行分级。
| 滚动MAD | 孤立森林 | 检测结果 |
|---|---|---|
| 正常 | 正常 | 正常 |
| 异常 | 正常 | 局部异常 |
| 正常 | 异常 | 组合异常 |
| 异常 | 异常 | 高置信度异常 |
只有滚动 MAD 触发时,数据可能发生了短期突变,但整体组合仍接近历史分布。只有孤立森林触发时,单个指标没有明显偏离,多个特征组合后形成了稀有状态。两个模块同时触发时,可以提高告警等级,并优先进入人工复核或自动拦截流程。

先使用滚动 MAD 筛选数据,再将候选样本交给孤立森林复核,也是一种可行方案。这种串行方式能够减少模型调用次数,但容易遗漏单变量正常、多变量组合异常的数据。对于服务端运行的监测系统,并行检测通常能够保留更完整的异常信息。
六、孤立森林需要补充时间特征
孤立森林本身不会自动理解数据的时间顺序。直接输入当前温度、湿度和通信延迟时,模型只能观察当前状态,无法知道温度是否突然升高,也无法判断传感器是否长时间保持同一个数值。
在时序项目中,通常需要主动构造当前值、相邻差值、窗口均值偏差、窗口标准差、连续重复次数和数据到达延迟等特征。假设当前温度为 31℃,这个数值本身可能处于正常范围,但它相对上一分钟升高了 8℃。
bash
temperature = 31
temperature_diff = 8
加入差值特征后,孤立森林可以同时观察当前状态和变化速度。原始值描述当前水平,差值描述瞬时变化,窗口偏差描述局部漂移,窗口标准差描述近期波动。经过这些特征补充后,模型会更适合处理连续到达的监测数据。
七、简化的Python实现
下面的代码使用滚动 MAD 检测单变量突变,并使用孤立森林分析多维数据和变化特征。代码构造了温度、湿度和通信延迟三类数据,同时注入温度突增、组合异常和延迟异常。
python
import numpy as np
import pandas as pd
from sklearn.ensemble import IsolationForest
def rolling_mad_score(series, window=20, eps=1e-9):
"""
使用当前数据之前的历史窗口计算滚动 MAD 分数。
"""
history = series.shift(1)
median = history.rolling(
window=window,
min_periods=window
).median()
mad = history.rolling(
window=window,
min_periods=window
).apply(
lambda values: np.median(
np.abs(values - np.median(values))
),
raw=True
)
return 0.6745 * (series - median) / (mad + eps)
rng = np.random.default_rng(42)
sample_count = 500
data = pd.DataFrame({
"temperature": rng.normal(28, 1, sample_count),
"humidity": rng.normal(80, 3, sample_count),
"latency": rng.normal(0.2, 0.03, sample_count)
})
# 注入三类异常
data.loc[350, "temperature"] = 40
data.loc[420, ["temperature", "humidity"]] = [31, 90]
data.loc[460, "latency"] = 2.5
sensor_columns = [
"temperature",
"humidity",
"latency"
]
# 计算滚动 MAD
for column in sensor_columns:
data[f"{column}_mad"] = rolling_mad_score(
data[column]
)
mad_columns = [
f"{column}_mad"
for column in sensor_columns
]
data["mad_flag"] = (
data[mad_columns]
.abs()
.max(axis=1)
> 3.5
)
# 构造相邻差值特征
for column in sensor_columns:
data[f"{column}_diff"] = data[column].diff()
feature_columns = [
"temperature",
"humidity",
"latency",
"temperature_diff",
"humidity_diff",
"latency_diff"
]
valid_data = data[feature_columns].dropna()
# 使用前 300 条有效数据训练孤立森林
model = IsolationForest(
n_estimators=200,
contamination=0.02,
random_state=42
)
model.fit(valid_data.iloc[:300])
# 保存孤立森林检测结果
data["if_flag"] = False
data.loc[valid_data.index, "if_flag"] = (
model.predict(valid_data) == -1
)
# 融合两个模块的结果
data["result"] = "正常"
data.loc[
data["mad_flag"] & ~data["if_flag"],
"result"
] = "局部异常"
data.loc[
~data["mad_flag"] & data["if_flag"],
"result"
] = "组合异常"
data.loc[
data["mad_flag"] & data["if_flag"],
"result"
] = "高置信度异常"
print(
data.loc[
[350, 420, 460],
[
"temperature",
"humidity",
"latency",
"mad_flag",
"if_flag",
"result"
]
]
)
这段实现分别保留了滚动 MAD 和孤立森林的检测结果。前者提供局部偏差证据,后者提供多维分布证据。实际系统还可以在两个模块之前加入物理边界规则,直接处理湿度超过 100%、时间戳倒退、设备编号缺失等明确错误,从而形成规则检测、统计检测和机器学习检测相互补充的异常识别结构。