4. 缺失时间戳的规则网格重建

为什么需要规则网格

矿山传感器(如风速、CO、顶板压力)常因网关重启、无线丢包或采集器休眠而出现时间戳缺口。若直接对原始记录做滑动平均或差分,时间轴是不等距的,统计结果会被"时间密度"扭曲。规则网格重建的目标是:先确定一个标称采样周期 Δt,生成从起点到终点、步长恒为 Δt 的完整时间索引,再把原始记录按时间对齐到网格上,未命中的网格点标记为缺失。这样后续的插值、连续缺失段检测、频率校验才有统一的时间基准。

关键参数与对齐规则

  • Δt(标称周期):由设备配置或前序频率校验得出,例如 60 s。它必须与原始数据的主间隔一致,否则会把正常采样误判为缺失。
  • 网格起点 t0:取原始序列的最小时间戳,并向下取整到 Δt 的整数倍,保证网格与"整分钟/整小时"对齐,便于跨设备合并。
  • 网格终点 t1:取原始序列的最大时间戳,向上取整到 Δt 的整数倍。
  • 对齐容差 tol:原始时间戳很少精确落在网格点上(存在毫秒级抖动),因此采用"最近网格点 + 容差"匹配,tol 一般取 Δt 的 10%~25%。
  • 冲突处理:若多条原始记录落入同一网格点(抖动导致),保留与网格点时间差最小的一条,其余丢弃并计数。
Δt 的选择直接决定"缺失"的定义。若 Δt 取 30 s 而设备实际 60 s 采样,则每个真实点之间都会插入一个假缺失,后续连续缺失段检测会全线报警。因此 Δt 应来自设备配置或前序频率校验的结论,而不是拍脑袋。
原始不规则时间戳 → 规则网格对齐(Δt = 60 s,tol = 12 s) 原始记录(时间戳有抖动、有缺口) 08:00:03 08:01:01 08:02:58 08:05:02 08:06:00 08:07:59 注意 08:03、08:04 两分钟无记录 → 真实缺口 floor(t0/Δt)·Δt 起,步长 Δt 生成索引;最近网格点匹配,|Δt差| ≤ tol 规则网格(08:00 ~ 08:08,共 9 个格点) 08:00 08:01 08:02 08:03 08:04 08:05 08:06 08:07 08:08 值 3.2 值 3.4 值 3.1 NaN NaN 值 3.6 值 3.5 值 3.7 值 3.9 08:00:03 08:01:01 08:02:58 无记录 无记录 08:05:02 08:06:00 08:07:59 无记录 命中网格点(原始值保留) 新增缺失位(NaN,待后续插值/告警) 网格时间索引 输出:9 行规则索引,其中 7 行有值、2 行 NaN;缺失率 = 2/9 ≈ 22.2%;连续缺失段 = [08:03, 08:04](长度 2) 08:08 格点无原始记录,被标记为尾部缺失,需在后续步骤中决定是否保留(避免把"序列结束"误判为"通信中断")

用 pandas 实现网格重建

下面代码可直接运行,输入是一段带抖动和缺口的示例数据,输出规则网格 DataFrame 与缺失统计。关键点在于:reindex 之前必须先把原始时间戳"吸附"到最近的网格点,否则抖动会让同一格点出现多条记录或全部落空。

import pandas as pd
import numpy as np

# 示例原始数据:时间戳有毫秒抖动,08:03、08:04 缺失
raw = pd.DataFrame({
    "ts": pd.to_datetime([
        "2024-06-01 08:00:03", "2024-06-01 08:01:01",
        "2024-06-01 08:02:58", "2024-06-01 08:05:02",
        "2024-06-01 08:06:00", "2024-06-01 08:07:59",
    ]),
    "value": [3.2, 3.4, 3.1, 3.6, 3.5, 3.7],
})

DELTA = pd.Timedelta(seconds=60)      # 标称周期
TOL   = pd.Timedelta(seconds=12)      # 对齐容差 = 0.2 * DELTA

# 1) 网格边界:向下/向上取整到 Δt 的整数倍
t0 = raw["ts"].min().floor("60s")
t1 = raw["ts"].max().ceil("60s")
grid = pd.date_range(t0, t1, freq=DELTA, name="ts")

# 2) 吸附:每个原始时间戳找最近的网格点
pos = (raw["ts"] - t0) / DELTA
nearest = t0 + (pos.round().astype(int) * DELTA)
offset = (raw["ts"] - nearest).abs()

# 3) 容差过滤 + 冲突去重(保留偏移最小者)
raw = raw.assign(grid_ts=nearest, offset=offset)
raw = raw[raw["offset"] <= TOL]
raw = raw.sort_values("offset").drop_duplicates("grid_ts", keep="first")

# 4) 对齐到规则网格,未命中即为 NaN
aligned = raw.set_index("grid_ts")["value"].reindex(grid)

# 5) 缺失标记与统计
aligned = aligned.rename("value").to_frame()
aligned["is_missing"] = aligned["value"].isna()

print(aligned)
print("缺失率:", aligned["is_missing"].mean().round(4))
print("缺失点数:", int(aligned["is_missing"].sum()), "/", len(aligned))

预期输出(示例数据可直接推出):网格共 9 个点(08:00~08:08),08:03 与 08:04 为 NaN,08:08 因无原始记录也为 NaN,缺失率 3/9 ≈ 0.3333。若把 t1 改为 raw["ts"].max().floor("60s"),则 08:08 不会被纳入网格,缺失率降为 2/9 ≈ 0.2222——这正是"尾部缺失"与"真实中断"需要区分的地方。

容差 tol 不能大于 Δt/2,否则相邻两个网格点会争抢同一条原始记录,导致吸附结果依赖排序而非真实时间。若原始数据抖动超过 Δt/2,说明设备时钟漂移严重,应先做时钟校正,而不是放大容差。

结果判读与验证

检查项通过条件不通过时的含义
网格长度= (t1 - t0)/Δt + 1边界取整逻辑有误
吸附后记录数≤ 原始记录数出现重复吸附,需检查容差与去重
缺失率与设备历史丢包率同量级Δt 选错或容差过小
尾部缺失单独标记,不并入中断段会把序列结束误报为通信故障

完成本步后,aligned 就是一张时间等距、带 is_missing 标记的标准表,可直接交给后续的连续缺失段检测与插值补全使用。