如何对超过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_id | timestamp | temp |
|---|---|---|
| 1 | 10 | 22.1 |
| 1 | 10 | 22.1 |
| 1 | 10 | NaN |
| 1 | 10 | 23.0 |
| 2 | 10 | 19.5 |
| 2 | 10 | 19.5 |
传感器1在10:03的缺失值距离10
,可以填充。10,因此保留NaN,不能从10。
组内首次有效读数之前的缺失值也会保留,因为这些行没有可用的历史读数。
超过1.2亿行时,重点检查内存和排序
这种写法移除了逐组 Python 回调,但仍需要多次扫描数据,并创建整列中间结果。它是否满足流水线要求,取决于分组数量、数据类型、排序状态和可用内存,不能仅凭写法保证具体耗时。
以1.2亿行为例,一个每行8字节的数组,其数据区约占0.96 GB,尚未计入索引、分组信息和其他对象的开销。排序、时间差计算、填充和最终赋值期间,都可能同时存在多个大型数组。del 可以减少后续操作仍需持有的对象,但不保证进程占用的内存立即归还给操作系统。
实际处理时可以按以下顺序优化:
- 确认数据是否已经有序。只要每个传感器内部按时间升序排列,就可以跳过示例中的全表排序;不同传感器的行不一定要连续。
- 尽量只携带必要列。如果原表很宽,可以先在
sensor_id、timestamp、temp及必要的行标识上计算,再按行标识回写结果。 - 内存不足时分区处理。优先按
sensor_id分区,让同一传感器的数据进入同一分区,并保证分区内的时间顺序。
如果只能按时间分块,需要跨块保存每个传感器最近一次原始有效读数的值和时间。每个块单独运行 ffill() 会丢失块边界之前的有效读数,产生错误结果。
如果同一传感器存在相同时间戳,还应先确定这些行的处理顺序。同一时刻的有效读数是否可以用于填充其他记录,需要由业务规则明确。
是否需要 merge_asof 或 Numba
merge_asof 可以通过 by='sensor_id'、direction='backward' 和 tolerance=pd.Timedelta(minutes=5),为目标记录寻找最近的历史有效读数。但它需要构造有效读数表、满足连接的排序要求,并处理连接结果和原行的对应关系。对于同一张表中的单列填充,分组填充加掩码更直接。
如果 pandas 方案的内存或执行时间仍不满足要求,可以考虑 Numba 编译循环。在数据已按传感器和时间排列的前提下,循环只需维护当前传感器最近一次原始有效读数及其时间,遇到缺失值时检查时间差。这里仍然有循环,但不再逐行执行 Python 代码。是否更快,需要用实际数据测试,并单独考虑首次编译开销。
备注:内容仅供参考。
