时间序列分析的第一步往往是把数据按时间顺序排列,但现实中传感器离线、节假日无交易、日志采集失败都会造成日期断档。如果直接用 Pandas 读取这类数据,得到的 DataFrame 很可能缺少中间某些日期对应的行。缺失行并不代表数据本身有 NaN,而是压根没有那一行。补全这些缺失行是计算滚动均值、对齐多张表、绘制连续曲线之前的必要步骤。Pandas 提供了几种高效方法,核心思路都是先构造一个完整的时间索引,再让原数据向这个索引对齐。

一、用 date_range 和 reindex 补齐连续日期
最直观的补全方式是先生成一个完整的日期范围,再让原始 DataFrame 向这个范围对齐。Pandas 中的 pd.date_range() 可以根据起始日期、结束日期和频率生成一个 DatetimeIndex。例如按天生成从 2024-01-01 到 2024-01-05 的连续索引,只需要指定 freq='D'。之后调用 reindex(),原 DataFrame 会按新索引重新排列,原本不存在的日期会以 NaN 形式出现在对应行。整个过程不会修改原数据,而是返回一个新的 DataFrame。
下面这段代码演示了一个典型的补全场景:原始数据只有三天,缺少 2024-01-02 和 2024-01-04 两行。通过 date_range 与 reindex 的组合,可以快速得到连续五天的完整记录。
import pandas as pd
# 原始 DataFrame,日期列缺少 2024-01-02 和 2024-01-04
df = pd.DataFrame({
'date': ['2024-01-01', '2024-01-03', '2024-01-05'],
'sales': [120, 150, 180]
})
df['date'] = pd.to_datetime(df['date'])
df = df.set_index('date')
# 以天为频率生成完整日期范围
full_index = pd.date_range(start='2024-01-01', end='2024-01-05', freq='D')
df_full = df.reindex(full_index)
print(df_full)
输出结果中,2024-01-02 和 2024-01-04 两行的 sales 列会自动变成 NaN。这个特性非常重要,因为它把“没有数据”的状态保留了下来,方便后续判断哪些是真实缺失、哪些是补出来的空行。如果原始 DataFrame 有多个数值列,reindex 会为所有列统一补齐缺失行,不会只处理某一列。
使用 date_range 时需要注意频率字符串的大小写:'D' 表示日历日,'H' 表示小时,'T' 或 'min' 表示分钟,而 'd' 是无效的。另外,如果原始索引中存在重复的时间戳,reindex 会直接抛出 ValueError,因此补全之前最好先检查并去重。可以通过 df.index.is_unique 快速判断,若存在重复则用 df[~df.index.duplicated(keep='first')] 保留第一条记录。
二、asfreq 与 resample 的差异及选择
很多人在补全时间序列时会误用 resample。实际上,resample 的核心是重采样,它必须配合聚合函数使用,比如 .mean()、.sum() 或者 .ohlc()。如果只是想让数据对齐到固定频率,同时保留原始数值,asfreq 才是更合适的工具。asfreq 只做频率转换,不做任何聚合,它会按指定频率生成新的时间索引,并返回原数据中与这些时间点对应的值。对于原数据中不存在的时间点,asfreq 默认为其填充 NaN。
下面是一个对比示例。原始数据的时间戳不是整点,直接调用 asfreq('h') 会把非整点的数据丢弃,因为那些时间点不在小时频率上。而 resample('h').asfreq() 会先按小时分箱,再取每个箱子内的最后一个值,这样虽然也会生成小时索引,但原始数据不会被完全丢弃。
import pandas as pd
rng = pd.to_datetime(['2024-01-01 10:00', '2024-01-01 12:30', '2024-01-01 15:45'])
df = pd.DataFrame({'value': [1, 2, 3]}, index=rng)
# asfreq 只保留整点,12:30 和 15:45 的数据会丢失
df_asfreq = df.asfreq('h')
print(df_asfreq)
# resample 升采样到小时,保留每个小时区间内的最后一个值
df_resampled = df.resample('h').asfreq()
print(df_resampled)
对于日频数据补全这类场景,asfreq('D') 内部会做类似 date_range 加 reindex 的操作,但实现更高效,代码也更简洁。例如 df.asfreq('D', method='ffill') 可以在补全缺失行的同时执行前向填充,一步完成两个动作。需要注意的是,asfreq 的 method 参数只在升采样时有效,也就是新频率比原频率更高、时间点更多的情况。如果原数据本身就是高频,降采样到低频应该使用 resample 配合聚合函数,避免损失信息。
总结一下:补全缺失行用 asfreq 或 reindex,聚合降采样用 resample。如果把 resample 当成补全工具,结果通常会丢掉原始数据中的细粒度信息,因此选择方法前要先明确自己到底是要“对齐补全”还是“聚合降频”。
三、分组时间序列的缺失行补全
实际业务中经常遇到多个独立时间序列混在一个 DataFrame 里的情况,比如多个传感器的读数、多只股票的日线数据。每个序列的起止日期可能不同,缺失位置也不一样。此时不能直接对整个 DataFrame 调用 asfreq,否则会把不同序列的日期混在一起,导致错误对齐。正确做法是先按分组键分组,再对每个组单独补全。
Pandas 提供了两种分组补全思路。第一种是利用 groupby 加 resample,这要求先把时间列设置为索引。第二种是自定义一个补全函数,在 groupby.apply 中为每个组生成对应的完整日期范围,再用 reindex 补齐。下面代码演示了这两种方法。
import pandas as pd
df = pd.DataFrame({
'sensor_id': ['A', 'A', 'A', 'B', 'B'],
'time': pd.to_datetime(['2024-01-01', '2024-01-03', '2024-01-05',
'2024-01-02', '2024-01-04']),
'reading': [1.0, 3.0, 5.0, 2.0, 4.0]
})
# 方法一:groupby + resample + asfreq
df = df.set_index('time')
df_full1 = df.groupby('sensor_id').resample('D').asfreq().reset_index()
print(df_full1)
# 方法二:groupby + apply 自定义补全,更灵活
def fill_missing(group):
full_range = pd.date_range(start=group.index.min(), end=group.index.max(), freq='D')
return group.reindex(full_range)
df_full2 = df.groupby('sensor_id').apply(fill_missing).reset_index()
print(df_full2)
两种方法都能得到每个传感器按天连续的记录。区别在于方法一完全依赖 resample 的默认行为,代码更紧凑;方法二可以灵活控制日期范围,比如使用全局统一的起止日期,而不是每个组各自的最小最大值。如果希望所有序列对齐到同一个时间轴上,可以在 fill_missing 中使用外部传入的 full_range,而不是从组内计算。
补全之后通常会出现大量 NaN,这时需要决定如何填充。如果缺失行对应的是设备确实没有上报数据,使用 ffill 前向填充可以保持最近一次读数的连续性,但要注意它可能掩盖真实的信号变化。使用 bfill 后向填充则可能引入未来信息,在时间序列建模时会导致数据泄漏。更稳健的做法是先保留 NaN,结合业务规则判断,或者使用 interpolate 做线性插值。另外,建议添加一个辅助列标记哪些行是补出来的,例如 df_full1['is_filled'] = df_full1['reading'].isna(),这样后续分析时可以区分真实观测和填充值。
四、性能与常见坑
当数据量较大时,补全操作的内存与时间开销不可忽略。生成一个从 2010 年到 2025 年的分钟级索引大约会创建 700 多万个时间点,如果 DataFrame 包含多个浮点列,内存占用可能达到几百 MB。此时应优先使用 asfreq 而不是手动 date_range 加 reindex,因为 asfreq 在内部对频率对齐做了优化。同时,不要在循环里逐行补数据,而应一次性生成完整索引后整体复制。
另一个高频的坑是重复时间戳。如果原始数据中同一个时间点出现多次,直接调用 reindex 或 asfreq 都可能报错。处理方式是先按时间排序,再用 keep='last' 或 keep='first' 去重。对于时区问题,建议统一使用带时区的 DatetimeIndex,否则不同时区的数据在合并时可能产生意外偏差。
import pandas as pd
# 假设 df 已经按时间列索引,且可能存在重复时间戳
if not df.index.is_unique:
df = df[~df.index.duplicated(keep='first')]
# 补全缺失行并标记哪些是填充出来的
df_full = df.asfreq('D')
df_full['was_missing'] = df_full.isna().any(axis=1)
还需要注意,补全后的 DataFrame 行数可能远大于原始数据。如果只是为了绘图,可以临时补全;如果用于存储,应评估是否真的需要保留所有 NaN 行。在某些分析中,保留稀疏数据反而能节省空间并保留真实的信息密度。最后,不要忘记 asfreq 返回的是新对象,原始 DataFrame 不会被修改,除非显式赋值或用 inplace=True。
掌握这些方法后,面对时间序列缺失行问题时就能快速选择合适工具:简单补全用 asfreq,需要自定义日期范围用 date_range 加 reindex,分组场景用 groupby 配合 resample 或 apply。关键是理解每步操作的索引变化,这样才能在数据对齐、性能和控制填充行为之间找到平衡。

