3. 舍内温度序列的异常跳变识别与清洗

问题拆解:三类异常在序列上的不同“指纹”

舍内温度以 1 分钟为采样周期连续采集时,原始序列中常见的异常并非同一种形态,清洗策略必须按形态区分,否则会把真实的快速升温(如开启暖风机)误判为尖峰。

  • 短时尖峰:单点或连续 1~2 点偏离邻域,随后立即回到原水平,典型成因为电磁干扰、探头接触抖动。
  • 通信丢包:序列出现缺失点,或网关重试后写入重复值/保持值,表现为“平台段”或时间戳间隔异常。
  • 传感器漂移:长时间缓慢单向偏移,单点变化率很小,但滑动中位数与同舍参考点(或回风温度)的偏差持续扩大。

因此本例采用两级判据:先用滑动中位数抑制孤立尖峰,再用变化率阈值拦截跳变;漂移则通过“中位数残差持续超限”单独标记,不直接删除,而是转为维护工单。

舍内温度序列:异常形态 → 判据 → 处置 ① 短时尖峰 单点越界 邻域中位数 ± 阈值 ② 通信丢包 / 保持值 平台段 时间戳间隔 > 1.5×周期 ③ 传感器漂移 残差持续扩大 参考:回风温度 / 同舍均值 原始序列 x[n] 1 min 采样 · 含缺失 时间戳对齐重采样 滑动中位数 窗口 W=5,取中位数 m[n] 残差 r[n]=x[n]−m[n] 变化率判据 |x[n]−x[n−1]| > 0.8 ℃/min 且 |r[n]| > 0.5 ℃ 处置分流 尖峰→中位数替换 漂移→标记+工单 对环控决策的影响 未清洗:尖峰触发误报警 → 风机频繁启停,舍温波动加大 清洗后:通风等级稳定,仅漂移点进入维护队列

可运行的清洗实现

下面用 Python 标准库完成整条链路,不依赖第三方包,便于直接放到网关侧或边缘盒子上运行。核心是三个函数:重采样对齐、滑动中位数清洗、漂移标记。

from statistics import median

SAMPLE_MIN = 1          # 采样周期,分钟
WIN = 5                 # 滑动中位数窗口(奇数)
RATE_LIMIT = 0.8        # 变化率阈值,℃/min
RESID_LIMIT = 0.5       # 中位数残差阈值,℃
DRIFT_WIN = 30          # 漂移判定连续超限点数
DRIFT_LIMIT = 1.2       # 漂移残差阈值,℃

def resample(raw, sample_min=SAMPLE_MIN):
    """raw: [(timestamp_min, value or None)],按整数分钟对齐,缺失填 None"""
    table = {t: v for t, v in raw}
    if not table:
        return []
    t0, t1 = min(table), max(table)
    return [(t, table.get(t)) for t in range(t0, t1 + 1, sample_min)]

def clean(series):
    """返回清洗后序列与异常记录"""
    values = [v for _, v in series]
    out, events = [], []
    for i, (t, v) in enumerate(series):
        if v is None:
            events.append((t, "丢包", None))
            out.append((t, None))
            continue
        lo, hi = max(0, i - WIN // 2), min(len(values), i + WIN // 2 + 1)
        window = [x for x in values[lo:hi] if x is not None]
        m = median(window) if window else v
        resid = v - m
        prev = next((values[j] for j in range(i - 1, -1, -1)
                     if values[j] is not None), v)
        rate = abs(v - prev)
        if abs(resid) > RESID_LIMIT and rate > RATE_LIMIT:
            events.append((t, "尖峰", round(resid, 2)))
            out.append((t, round(m, 2)))
        else:
            out.append((t, v))
    return out, events

def mark_drift(cleaned, ref):
    """ref: 同舍参考序列(回风温度),返回漂移区间"""
    flags, run = [], 0
    for (t, v), (_, r) in zip(cleaned, ref):
        if v is None or r is None:
            run = 0
            continue
        if abs(v - r) > DRIFT_LIMIT:
            run += 1
            if run >= DRIFT_WIN:
                flags.append(t)
        else:
            run = 0
    return flags

用一段教学示例数据验证判据

构造 20 分钟序列:第 8 分钟注入 +3.0 ℃ 尖峰,第 12~14 分钟模拟丢包(None),第 16 分钟起叠加缓慢漂移。以下数值为按上述参数直接推出的预期结果,用于核对实现是否符合判据。

raw = [(i, 24.0 + 0.05 * i) for i in range(20)]
raw[7] = (7, 27.0)          # 尖峰
raw[11] = (11, None)        # 丢包
raw[12] = (12, None)
raw[13] = (13, None)
for i in range(15, 20):     # 漂移叠加
    raw[i] = (i, raw[i][1] + 0.15 * (i - 14))

series = resample(raw)
cleaned, events = clean(series)
print(events)
# 预期:[(7, '尖峰', 2.9x), (11, '丢包', None), (12, '丢包', None), (13, '丢包', None)]
第 7 点残差约 2.9 ℃、变化率约 2.95 ℃/min,同时越过两条阈值,被中位数替换;第 15 点之后单点变化率仅 0.15 ℃/min,不会触发尖峰判据,但残差会随点数累积,由 mark_drift 在连续 30 点后标记——这正是“尖峰”与“漂移”必须分开处理的原因。

清洗前后对环控决策的对比

指标未清洗清洗后判断依据
尖峰触发报警次数1 次(第 7 分钟)0 次残差与变化率双阈值
通风等级切换因尖峰多切 1 档保持原档位目标舍温偏差 < 0.5 ℃
缺失点处理按 0 或保持值参与均值标记为 None,不参与统计避免拉低舍温均值
漂移识别无连续 30 点残差 > 1.2 ℃ 后标记转维护工单,不删数据
清洗只应修正“可判定为异常”的点。若某段时间舍内确实在快速升温(如暖风机启动),变化率可能短时超过 0.8 ℃/min,此时中位数残差通常不会同步超限,双阈值“与”条件可避免误删真实升温段。上线前建议用一周历史数据回放,统计被替换点数占比,超过 2% 时应先排查传感器与通信链路,而不是放宽阈值。