PAYATHON 2026

如何对超过1.2亿行数据按组执行带5分钟时限的 ffill?

数据阿明

可以用两次分组 ffill() 分别传播最近一次原始有效读数的时间和数值,再用时间差掩码将超过5分钟的填充值恢复为 NaN。整个过程不需要 groupby().apply() 或 Python 逐组循环。

这里按“超过5分钟才失效”处理,恰好5分钟时允许填充。如果要求严格小于5分钟,将代码中的 .le() 改成 .lt()。

为什么 ffill(limit=1) 不符合要求

limit 限制的是连续填充的缺失行数,而不是时间长度。传感器的采样间隔不固定时,填充一行可能跨越几秒,也可能跨越十几分钟。

时间差应始终从最近一次原始有效读数算起。填充出来的值不能作为新的计时起点,否则连续缺失的数据可能不断延长有效期。

用 pandas 分组填充并检查时间差

实现有以下前提:

  • timestamp 已是 datetime 类型,且没有 NaT。
  • sensor_id 没有缺失值。
  • 每个传感器的数据按时间升序排列。
import pandas as pd

max_gap = pd.Timedelta(minutes=5)

# 如果每个传感器内部已经按时间升序排列,可省略排序。
# 此操作会改变结果的行顺序。
df = df.sort_values(['sensor_id', 'timestamp'])

# 必须在修改 temp 之前识别原始有效读数。
valid = df['temp'].notna()

# 只有原始有效读数所在行才保留时间,然后在组内向前传播。
last_valid_ts = (
    df['timestamp']
    .where(valid)
    .groupby(df['sensor_id'], sort=False, observed=True)
    .ffill()
)

elapsed = df['timestamp'] - last_valid_ts
within_gap = elapsed.le(max_gap)

# 先释放不再需要的大型中间对象。
del valid, last_valid_ts, elapsed

# 传播数值,再按距最近原始有效读数的时间筛选。
filled = (
    df['temp']
    .groupby(df['sensor_id'], sort=False, observed=True)
    .ffill()
)

df['temp'] = filled.where(within_gap)

del filled, within_gap

sort=False 只控制是否对分组键排序,不能代替传感器内部的时间排序。observed=True 用于分类类型分组键,避免引入未出现的类别组合。

对于示例数据,结果为:

sensor_idtimestamptemp
110
22.1
110
22.1
110
NaN
110
23.0
210
19.5
210
19.5

传感器1在10:03的缺失值距离10

,可以填充。10
,因此保留 NaN,不能从10
。

组内首次有效读数之前的缺失值也会保留,因为这些行没有可用的历史读数。

超过1.2亿行时,重点检查内存和排序

这种写法移除了逐组 Python 回调,但仍需要多次扫描数据,并创建整列中间结果。它是否满足流水线要求,取决于分组数量、数据类型、排序状态和可用内存,不能仅凭写法保证具体耗时。

以1.2亿行为例,一个每行8字节的数组,其数据区约占0.96 GB,尚未计入索引、分组信息和其他对象的开销。排序、时间差计算、填充和最终赋值期间,都可能同时存在多个大型数组。del 可以减少后续操作仍需持有的对象,但不保证进程占用的内存立即归还给操作系统。

实际处理时可以按以下顺序优化:

  1. 确认数据是否已经有序。只要每个传感器内部按时间升序排列,就可以跳过示例中的全表排序;不同传感器的行不一定要连续。
  2. 尽量只携带必要列。如果原表很宽,可以先在 sensor_id、timestamp、temp 及必要的行标识上计算,再按行标识回写结果。
  3. 内存不足时分区处理。优先按 sensor_id 分区,让同一传感器的数据进入同一分区,并保证分区内的时间顺序。

如果只能按时间分块,需要跨块保存每个传感器最近一次原始有效读数的值和时间。每个块单独运行 ffill() 会丢失块边界之前的有效读数,产生错误结果。

如果同一传感器存在相同时间戳,还应先确定这些行的处理顺序。同一时刻的有效读数是否可以用于填充其他记录,需要由业务规则明确。

是否需要 merge_asof 或 Numba

merge_asof 可以通过 by='sensor_id'、direction='backward' 和 tolerance=pd.Timedelta(minutes=5),为目标记录寻找最近的历史有效读数。但它需要构造有效读数表、满足连接的排序要求,并处理连接结果和原行的对应关系。对于同一张表中的单列填充,分组填充加掩码更直接。

如果 pandas 方案的内存或执行时间仍不满足要求,可以考虑 Numba 编译循环。在数据已按传感器和时间排列的前提下,循环只需维护当前传感器最近一次原始有效读数及其时间,遇到缺失值时检查时间差。这里仍然有循环,但不再逐行执行 Python 代码。是否更快,需要用实际数据测试,并单独考虑首次编译开销。

备注:内容仅供参考。