3. 时间戳重复记录的识别与去重策略

重复时间戳从哪里来,为什么不能直接 drop_duplicates

矿山传感器数据在采集端重传、网关缓存补发、边缘节点时钟回拨或人工补录时,常会出现“同一传感器 + 同一时间戳”对应多条记录。这类重复与“整行完全相同”的重复不同:时间戳相同,但数值可能不同(例如重传时携带了修正后的值)。因此去重前必须先按 (sensor_id, timestamp) 分组识别冲突,再按业务语义选择保留策略,而不是对整行做 drop_duplicates()。

本例使用一台提升机振动传感器 VIB-01 的示例数据,构造 3 组重复时间戳,分别演示首条、末条、均值三种策略,并对比去重后同一统计量(振动 RMS 均值)的差异。

VIB-01 重复时间戳识别 → 三种保留策略 → 统计影响 原始记录(按时间排序) timestamp vib_rms 标记 08:00:002.10 08:00:102.35 08:00:202.28A1 08:00:202.52A2 08:00:302.41 08:00:402.66B1 08:00:402.58B2 08:00:402.61B3 08:00:502.44 08:01:002.30C1 08:01:002.36C2 按 (sensor_id, ts) 分组 08:00:20 → 2 条 08:00:40 → 3 条 08:01:00 → 2 条 其余时间戳 → 1 条 重复组数:3 重复记录数:4 去重后行数:8 冲突判定: 同键不同值 → 需策略决策 不可整行去重 策略一:保留首条 08:00:20 → 2.28 08:00:40 → 2.66 08:01:00 → 2.30 策略二:保留末条 08:00:20 → 2.52 08:00:40 → 2.61 08:01:00 → 2.36 策略三:取均值 08:00:20 → 2.40 08:00:40 → 2.617 08:01:00 → 2.33 去重后 RMS 均值 首条:2.390 末条:2.435 均值:2.412 原始(含重复):2.410 末条与首条差 0.045 ≈ 1.9%(示例值) 均值策略最接近 原始整体水平 末条适合“重传即修正” 首条适合“先到为准” 均值适合“多值融合” 策略选择须写入清洗日志

识别重复:先分组计数,再定位冲突

用 groupby 对 (sensor_id, timestamp) 计数,size() > 1 的组即为重复时间戳。进一步检查组内 vib_rms 是否唯一,可区分“完全重复”和“值冲突”两类,后者才需要策略决策。

import pandas as pd

raw = pd.DataFrame({
    "sensor_id": ["VIB-01"] * 11,
    "timestamp": pd.to_datetime([
        "2024-06-01 08:00:00", "2024-06-01 08:00:10", "2024-06-01 08:00:20",
        "2024-06-01 08:00:20", "2024-06-01 08:00:30", "2024-06-01 08:00:40",
        "2024-06-01 08:00:40", "2024-06-01 08:00:40", "2024-06-01 08:00:50",
        "2024-06-01 08:01:00", "2024-06-01 08:01:00",
    ]),
    "vib_rms": [2.10, 2.35, 2.28, 2.52, 2.41, 2.66, 2.58, 2.61, 2.44, 2.30, 2.36],
})

key = ["sensor_id", "timestamp"]
grp = raw.groupby(key)["vib_rms"]
dup_report = pd.DataFrame({
    "count": grp.size(),
    "n_unique": grp.nunique(),
}).query("count > 1")
print(dup_report)
#                      count  n_unique
# sensor_id timestamp
# VIB-01    2024-06-01 08:00:20      2         2
# VIB-01    2024-06-01 08:00:40      3         3
# VIB-01    2024-06-01 08:01:00      2         2

三组重复的 n_unique 均等于 count,说明都是“值冲突”而非完全重复,必须按策略处理。

三种保留策略的实现

关键点:drop_duplicates 的 keep 参数只能保留首/末条,无法做均值;均值策略需先聚合再重建索引。三种实现都应以 (sensor_id, timestamp) 为键,并保持时间索引有序。

def dedup_first(df):
    return df.drop_duplicates(subset=key, keep="first")

def dedup_last(df):
    return df.drop_duplicates(subset=key, keep="last")

def dedup_mean(df):
    return (df.groupby(key, as_index=False)["vib_rms"]
              .mean()
              .sort_values(key))

for name, fn in [("first", dedup_first), ("last", dedup_last), ("mean", dedup_mean)]:
    out = fn(raw).set_index("timestamp")
    print(name, "rows=", len(out), "rms_mean=%.3f" % out["vib_rms"].mean())
# first rows= 8 rms_mean=2.390
# last  rows= 8 rms_mean=2.435
# mean  rows= 8 rms_mean=2.412
三种策略去重后行数一致(8 行),差异只体现在重复时间戳处的取值。因此“行数对不对”不能作为去重正确性的唯一判据,必须核对重复键上的取值是否符合策略预期。

策略对后续统计的影响与选择依据

示例数据中去重后 RMS 均值:首条 2.390、末条 2.435、均值 2.412,原始含重复为 2.410。末条与首条相差约 0.045(约 1.9%),在趋势分析或阈值报警统计中可能改变结论。选择依据应来自数据产生机制:

  • 保留末条:适用于网关重传携带修正值、后到记录更可信的场景,是补发型重复的常用默认。
  • 保留首条:适用于“先到为准”、后续重复为无效补发的场景,需确认采集端时间顺序可靠。
  • 取均值:适用于多次采样代表同一时刻真实水平、无单条更可信的情况,可降低单点噪声,但会平滑掉突变。

无论选哪种,都应在清洗日志中记录:重复组数、被丢弃记录数、所用策略、策略前后关键统计量,便于回溯。若同一批数据中重复原因混杂(既有重传又有补录),建议先按来源字段分组再分别应用策略,而不是全局一刀切。

验证清单

检查项通过条件
重复键是否清零去重后 groupby(key).size().max() == 1
行数是否符合预期去重后行数 = 原始行数 − 被丢弃重复记录数
取值是否符合策略抽查重复时间戳,首条/末条/均值与原始组内值一致
时间顺序是否保持索引单调递增,无因聚合导致的乱序
统计影响是否留痕日志记录策略前后 RMS 均值等关键指标