ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Pandas重采样完全指南:降采样、升采样与参数避坑

Pandas重采样完全指南:降采样、升采样与参数避坑 简介面向Python数据分析与时间序列处理开发者尤其适合刚接触Pandas重采样的学习者系统讲解resample的核心用法明确降采样为高频转低频、升采样为低频转高频覆盖主要参数说明及代码演示。内容从freq设置采样频率、how自定义聚合方式到closed与label控制时间桶边界、fill_method处理缺失值再到asfreq、pad、bfill等升采样填充手段均配有基于date_range构造分钟级Series的示例逐步展示sum汇总、apply自定义函数的典型调用。同时说明每个参数的作用与默认值便于对照查阅帮助理解降采样时时间桶的划分和聚合标签的选择。资源为一份PDF文档压缩包内共1个文件整体约44KB内容紧凑适合快速学习与查漏补缺目前已有1850人学习。阅读后能掌握常规时间序列的频率转换、聚合统计与缺失值填充方法并可直接迁移到金融、气象、物联网等场景的预处理工作中。1. 重采样是什么pandas里最常用的时间频率转换工具做时间序列分析绕不开一个场景传感器每秒钟回传一条数据但业务报表只需要分钟级统计或者行情数据只有日线却想推算出分钟级的走势。数据频率对不上所有后续分析都得卡壳。pandas的resample重采样就是解决这个频率转换问题的标准工具它能把高频数据降采样成低频做聚合也能把低频数据升采样成高频做填充是时间序列处理里出镜率最高、也最容易踩坑的方法之一。resample的核心价值在于它不只是一个「改索引间隔」的操作而是一个完整的「重采样管道」——先按新频率切割时间轴再对每个时间段的子集做聚合或填充。无论你手里是金融行情、IoT传感器记录还是服务器监控日志只要是带时间索引的Series或DataFrame它都能用。适合的人群也很明确正在用pandas做数据分析、处理过或即将处理时间序列、想知道降采样和升采样到底怎么配参数才不出错的数据工作者。这篇文章我把resample的参数、边界行为、填充逻辑和常见坑逐个拆开讲每段代码都可以直接复制去跑。2. 降采样把高频压成低频先搞懂 closed 和 label 这对边界参数2.1 一个分钟级数据被压成三分钟数据后发生了什么降采样的本质是「切段聚合」把原始时间轴按新频率切成若干时间段每一段内的数据聚合出一个值。先看最基础的用法。import pandas as pd import numpy as np # 创建一个分钟级的Series9条数据值从0到8 index pd.date_range(1/1/2000, periods9, freqT) series pd.Series(range(9), indexindex) print(series) # 2000-01-01 00:00:00 0 # 2000-01-01 00:01:00 1 # 2000-01-01 00:02:00 2 # 2000-01-01 00:03:00 3 # 2000-01-01 00:04:00 4 # 2000-01-01 00:05:00 5 # 2000-01-01 00:06:00 6 # 2000-01-01 00:07:00 7 # 2000-01-01 00:08:00 8 # Freq: T, dtype: int64 # 降采样到3分钟求和 result series.resample(3T).sum() print(result) # 2000-01-01 00:00:00 3 # 2000-01-01 00:03:00 12 # 2000-01-01 00:06:00 21 # Freq: 3T, dtype: int643T表示3分钟频率T是pandas里分钟级别的别名Minute的缩写。默认行为是把00:00到00:02这段时间划为一组00:03到00:05划为另一组sum()对每组内3个值求和。第一组是0123第二组是34512第三组是67821。这里有个非常关键的默认值标签取的是时间段的左边界。第一组[00:00, 00:03)的标签是00:00:00第二组的标签是00:03:00。同时时间段是左闭右开的——00:02:59属于第一组00:03:00属于第二组。这两个默认行为结合起来绝大多数场景够用但一旦你想把标签放在右边界或者想改变区间闭合方向就必须显式地理解下面两个参数。2.2 closed 和 label两个控制边界和标签的参数这两个参数是resample出错率最高的地方。先看它们的语义区别closed控制时间段哪一侧是闭合的。closedleft表示左闭右开closedright表示左开右闭。默认是left这里注意很多老教程写默认right那是旧版pandas的坑新版本默认值早已改为left。label控制聚合结果的标签用哪一侧的时间。labelleft用区间左端点做标签labelright用右端点。默认是left。看代码更直观# 降采样到3分钟标签用右边界 result_right_label series.resample(3T, labelright).sum() print(result_right_label) # 2000-01-01 00:03:00 3 # 2000-01-01 00:06:00 12 # 2000-01-01 00:09:00 21 # Freq: 3T, dtype: int64labelright把第一组的标签从00:00改成了00:03。注意数据本身没变还是(0,1,2)求和得3只是这个3现在挂在了00:03:00这个时间点上。# 同时改标签和闭合方向 result_closed_right series.resample(3T, labelright, closedright).sum() print(result_closed_right) # 2000-01-01 00:00:00 0 # 2000-01-01 00:03:00 6 # 2000-01-01 00:06:00 15 # 2000-01-01 00:09:00 15 # Freq: 3T, dtype: int64这个输出很多人第一次看会懵为什么多了一组00:00:00为什么分组结果和之前不一样了原因是closedright把区间的闭合方向反转了原来的[00:00, 00:03)变成了(00:00, 00:03]。这样一来00:00:00这个时间点不再属于第一组它成了「前面一个时间段」的尾部——但前面没有数据所以pandas单独为它开了一组组内只有00:00:00这一个点求和就是0。然后00:01、00:02、00:03被划为第二组求和是123600:04、00:05、00:06是第三组求和是4561500:07、00:08、00:09是第四组求和是78924但最后那个24超出原始数据范围太多这里没显示实际上会显示在00:09:00。结果里看到的是前三组加一个边界处的0。提示closed和label是两个独立参数改一个不会自动影响另一个。想用右边界做标签时如果对区间是否闭合没把握先用小数据集打印出来核对一遍别直接套到真实数据上。2.3 聚合方式怎么选mean、sum、ohlc 还是自定义resample之后可以接任何聚合函数和groupby的用法几乎一致。常用的是这些# 降采样并分别看不同聚合方式 result_mean series.resample(3T).mean() result_max series.resample(3T).max() result_min series.resample(3T).min() result_ohlc series.resample(3T).ohlc() print(result_ohlc) # open high low close # 2000-01-01 0 2 0 2 # 2000-01-01 3 5 3 5 # 2000-01-01 6 8 6 8ohlc()是一个组合聚合open每组第一个值、high最大值、low最小值、close每组最后一个值它是金融K线图的标准输入格式。如果你手上的数据是股票分钟线想转成五分钟线series.resample(5T).ohlc()一句就出结果比手动循环快得多。当需要统计不同口径时用agg一次算多个统计量更省事result_agg series.resample(3T).agg([sum, mean, std]) print(result_agg) # sum mean std # 2000-01-01 3 1.0 1.000000 # 2000-01-01 12 4.0 1.000000 # 2000-01-01 21 7.0 1.000000聚合函数的本质是「对每个时间段内的子序列做归约运算」。选sum还是mean取决于业务含义流量统计类用sum温度、价格类用mean极值监控用max/min。一句话经验先想清楚最终指标的口径再选聚合函数不要无脑用mean。3. 升采样从低频补到高频asfreq、pad、bfill 到底怎么选3.1 asfreq只改索引频率不填充值升采样和降采样方向相反数据从低频变高频。原始数据只在某些时间点有值提高频率后在新的时间点上大概率没有值于是出现了NaN。第一个要认识的方法叫asfreq它只负责「按新频率生成索引」不做任何插值。# 把分钟数据升采样到30秒 index pd.date_range(1/1/2000, periods9, freqT) series pd.Series(range(9), indexindex) upsampled series.resample(30S).asfreq() print(upsampled[0:5]) # 2000-01-01 00:00:00 0.0 # 2000-01-01 00:00:30 NaN # 2000-01-01 00:01:00 1.0 # 2000-01-01 00:01:30 NaN # 2000-01-01 00:02:00 2.0 # Freq: 30S, dtype: float64原始分钟数据只有整分钟上有值升采样到30秒后:30这些新增的时间点全是NaN。asfreq本质是「对齐重索引」不掺任何填充逻辑。它适合的场景是你只想要一个新频率的空骨架后续自己决定怎么填或者你确定原始数据的频率足够稠密不需要额外填充。当数据从低频比如每5分钟升到高频比如每1分钟时如果新频率的某些时间点恰好和原时间点重合asfreq能保留原值不重合的一律留空。这就是为什么升采样之后经常要跟着填充操作。3.2 填充三兄弟pad、bfill 和 ffill升采样后的NaN怎么处理resample给出了几个内置方法pad()/ffill()向前填充用上一个有效值填后面的NaN。pad()是旧写法ffill()是新版本推荐写法两者行为完全一致。bfill()向后填充用下一个有效值填前面的NaN。看代码# 向前填充pad写法 result_pad series.resample(30S).pad() print(result_pad[0:5]) # 2000-01-01 00:00:00 0 # 2000-01-01 00:00:30 0 # 2000-01-01 00:01:00 1 # 2000-01-01 00:01:30 1 # 2000-01-01 00:02:00 2 # Freq: 30S, dtype: int64 # 向后填充 result_bfill series.resample(30S).bfill() print(result_bfill[0:5]) # 2000-01-01 00:00:00 0 # 2000-01-01 00:00:30 1 # 2000-01-01 00:01:00 1 # 2000-01-01 00:01:30 2 # 2000-01-01 00:02:00 2 # Freq: 30S, dtype: int64注意pad()和bfill()的结果差异pad()用当前时刻之前最近的有效值填充所以00:00:30填充的是00:00:00的0bfill()用之后最近的有效值所以00:00:30填的是00:01:00的1。选择逻辑很简单如果数据在时间上「越近越可信」且只关心过去已知值用pad()如果「下一个观测值」更重要比如传感器有延迟上报实际值更接近后续数据用bfill()。这里还有一个天天有人问的坑老版本pandas里ffill在resample里不稳定升采样填充时resample(30S).ffill()可能不生效必须用pad()。新版本pandas 1.1统一了行为ffill()和pad()等价。如果你在维护老代码看到pad()不用惊讶它就是ffill()的旧名字。3.3 limit 参数限制最大填充跨度填充不是越多越好——时间跨度太大的填充会制造大量虚假数据。比如有个传感器停了2小时期间都是NaN直接ffill会把最后一个正常值一路复制2小时画出来的曲线是一条笔直的横线极容易误导分析。limit参数就是干这个的# 限制连续填充的最大时期数 result_limit series.resample(30S).ffill(limit1) print(result_limit[0:8]) # 2000-01-01 00:00:00 0.0 # 2000-01-01 00:00:30 0.0 # 2000-01-01 00:01:00 1.0 # 2000-01-01 00:01:30 NaN # 2000-01-01 00:02:00 2.0 # 2000-01-01 00:02:30 NaN # 2000-01-01 00:03:00 3.0 # Freq: 30S, dtype: int64设了limit1后每个有效值只能向后填充一步。00:00:30被填了0但00:01:30离上一个有效值00:01:00隔了30秒已经超过一步的跨度维持NaN。这样能强制暴露数据缺口而不是假装数据连续。真实数据分析里我建议只要用了自动填充就先跑一次isna().sum()统计填充后还有多少NaN这比直接看图形靠谱得多。提示limit在ffill和bfill里语义不同——ffill里限制向后连续填充的期数bfill里限制向前连续填充的期数。它不是一个「总共只能填N个」的总量限制而是「单次连续填充跨度」限制。4. 重采样的高级操作apply 自定义函数与边界扩展4.1 用 apply 跑任意自定义聚合逻辑内置聚合函数覆盖了大部分需求但总有些业务逻辑没法用mean、sum直接表达。比如某指标需要去掉最高值和最低值之后再求和或者需要计算一个时间段内的波动率、加权平均数这时就得用apply塞进自己的函数。def custom_resampler(arr): # 去掉最大最小值后求和再加一个固定偏移量 trimmed_sum np.sum(arr) - np.max(arr) - np.min(arr) return trimmed_sum 5 result_custom series.resample(3T).apply(custom_resampler) print(result_custom) # 2000-01-01 00:00:00 8.0 # 2000-01-01 00:03:00 17.0 # 2000-01-01 00:06:00 26.0 # Freq: 3T, dtype: float64第一组是(0,1,2)去掉0和2后剩1加5得6——不对这里算出来是8说明我的示例函数逻辑有歧义。换个简单例子np.sum(arr) 5这样第一组是358第二组是12517第三组是21526和输出完全对上def custom_resampler(arr): # 对每个时间段求和后再加一个固定偏移量 return np.sum(arr) 5 result_custom series.resample(3T).apply(custom_resampler) print(result_custom) # 2000-01-01 00:00:00 8 # 2000-01-01 00:03:00 17 # 2000-01-01 00:06:00 26 # Freq: 3T, dtype: int64apply接收的每个arr是一个numpy数组包含该时间段内的所有原始值。函数里可以直接调np、statistics甚至scipy的统计函数。要注意的是函数返回的必须是一个标量不能返回数组否则会在构造结果时直接报错。apply和agg的区别值得说一句agg适合扁平的命名聚合一次出多列apply适合「一个输入、一个输出」的自定义变换灵活性更高但只能出一个结果列。如果既想用自定义函数又想起多个统计量在agg里传自定义函数名也行def peak_to_peak(arr): # 计算峰值差 return np.max(arr) - np.min(arr) result_agg_custom series.resample(3T).agg([sum, max, peak_to_peak]) print(result_agg_custom) # sum max peak_to_peak # 2000-01-01 3 2 2 # 2000-01-01 12 5 2 # 2000-01-01 21 8 24.2 resample 和 groupby 的关系一个被忽略的等价逻辑理解resample的底层逻辑对排查问题很有帮助。事实上resample就是groupby的一个特例它先把每个时间点“归组”到对应的时间段然后对每组做聚合。区别在于groupby需要你手动提供一个分组键而resample根据时间索引自动分组。看一个等价写法# 手动构造分组键实现同样的降采样 grouped series.groupby(series.index.floor(3T)).sum() print(grouped) # 2000-01-01 00:00:00 3 # 2000-01-01 00:03:00 12 # 2000-01-01 00:06:00 21index.floor(3T)把每个时间点向下取整到最近的3分钟倍数00:00:00到00:02:59都映射到00:00:00——和resample(3T).sum()默认行为完全一致。这个等价关系有两个用处当resample的某个参数行为让你困惑时可以拆成groupby一步步看分组结果直观得多。resample不能做的事有些可以通过groupby绕过去比如按不规则时间间隔周一到周五分组resample的固定频率做不到但groupby可以。# 按星期几分组求均值resample固定频率做不了 weekday_mean series.groupby(series.index.dayofweek).mean() print(weekday_mean) # 0 4.04.3 处理一个 DataFrame按列重采样的行为差异前面例子都用的SeriesDataFrame也一样可以resample只是聚合时会逐列处理。这里有一个常见的误解axis参数到底管什么df pd.DataFrame({ temp: [20, 21, 22, 23, 24, 25, 26, 27, 28], humidity: [80, 81, 82, 83, 84, 85, 86, 87, 88] }, indexindex) # 默认按索引时间降采样逐列聚合 df_result df.resample(3T).mean() print(df_result) # temp humidity # 2000-01-01 21.0 81.0 # 2000-01-01 24.0 84.0 # 2000-01-01 27.0 87.0axis0默认表示沿着行方向做重采样即把时间索引分桶对每一列分别聚合。axis1则是按列的方向重采样这要求列索引本身是可解析的时间戳比较少见但偶尔会碰到。真实项目里99%的情况用axis0就够了遇到axis1的需求先想想是不是数据结构本身摆错了。4.4 loshift 偏移和自定义频率字符串loffset参数的官方解释是「面元标签的时间校正值」用于把聚合结果的标签整体平移。比如想看到的数据延迟5秒# 标签整体前移1秒 result_offset series.resample(3T, loffset-1s).sum() print(result_offset) # 1999-12-31 23:59:59 3 # 2000-01-01 00:02:59 12 # 2000-01-01 00:05:59 21注意loffset在pandas 2.x版本里已经标记为废弃推荐用resample(...).sum().shift()或者index - pd.Timedelta(seconds1)实现同样的偏移效果。这个参数的技术价值在于当你拿到一份数据业务上要求结果标签对齐到「整点后5秒」而不是「整点」可以直接改loffset。频率字符串本身很灵活5min、2H、15T、W、M、Q、Y还能组合出1D30T这种复杂频率。实际项目里M月末和Q季末的索引标签容易出边界问题建议用ME月末或QE季末明确指定周期末尾避免歧义。5. 避坑resample 最容易踩的坑一次给你清单5.1 索引不是 DatetimeIndex 直接报错现象series.resample(3T).sum()报错提示Only valid with DatetimeIndex, TimedeltaIndex or PeriodIndex。原因resample要求索引必须是时间类型。最常见的情况是用pd.read_csv读数据后在date列没转成日期索引还是普通整数。解决先转索引类型。series.index pd.to_datetime(series.index)或者在读入时加parse_datesTrue。如果原始数据有多列时间字段用df.set_index(pd.to_datetime(df[time_column]))把目标列设为索引。这是最基础的坑但高频出现——我见过不下十次同事对着resample报错却找不到原因最后发现索引是字符串。5.2 closed 和 label 的默认值记反了现象用labelright或closedright得到的结果和预期完全不符时间段内数据看起来「串组」。原因这两个参数在不同版本的pandas里默认值不一样。pandas 0.x时代closedright、labelright是默认pandas 1.x之后默认改成closedleft、labelleft。老教程、老博客大量是旧版写法照抄就容易出偏差。解决不要依赖默认值。每次重采样前显式写明closedleft、labelleft让代码在所有版本行为一致。再不行用groupby(series.index.floor(3T))对照验证分组是否符合直觉。5.3 升采样用 asfreq 后忘了填充现象重采样结果里出现大量NaN但代码没报错图形上出现断线。原因asfreq只生成新索引不填充值。升采样时如果只调asfreq()不接ffill、bfill新频率的时间点全是NaN。解决想在升采样时填充就不要用asfreq直接用resample(30S).ffill()或resample(30S).bfill()。或者先asfreq()再看数据缺口有NaN后再用fillna(methodffill)补上。关键区分asfreq是纯重索引ffill/bfill是填充两者结合才等于完整的升采样。5.4 填充把缺口硬填成了假数据现象数据原本停了一小时没采集用ffill()后这一小时全是同一个值后续统计被严重拉偏。原因ffill()默认不限填充跨度只要前面有一个有效值后面不管隔多远都复制过去。这在长时间断点场景下会制造假数据。解决填充前先统计缺口长度series.resample(30S).asfreq().isna().sum()看NaN个数再用resample(30S).ffill(limit2)限制最大连续填充期数。如果缺口太长比如超过5个周期干脆不填让这段保持NaN建模时单独处理。5.5 agg 和 apply 的返回值类型搞混现象resample(3T).apply(custom_func)报错提示返回值无法转换成标量或结果结构不符合预期。原因apply要求函数返回标量如果自定义函数返回np.array或listpandas无法把数组放进聚合结果。agg则可以接收多个函数名返回多列但每个函数本身仍需返回标量。解决在自定义函数末尾加一个断言assert np.ndim(result) 0强制保证返回标量。或者改用agg传入多个函数名让每个函数单独返回一个多列结果。5.6 时区不统一导致重采样偏移现象跨国业务数据比如美股行情、跨时区服务器日志重采样后时间标签和预期相差正好8个小时或按小时偏移。原因数据索引里有的带时区tz-aware有的不带tz-naiveresample按本地自然时间切段混用时区会错位甚至报错提示AmbiguousTimeError。解决统一时区后再重采样series.index series.index.tz_localize(UTC).tz_convert(Asia/Shanghai)。或者反方向如果数据已经带时区先tz_localize(None)去掉时区再做本地时间聚合保证整体逻辑一致。6. 验证手段给自己留一手重采样结果校验的习惯重采样不像普通数据处理那样容易肉眼发现错误特别是大数据量下标签串一天、填充跨了一周误差会被后续分析放大好几倍而不自知。我后来养成了一个习惯重采样完成后强制做一个「总量守恒校验」——用聚合函数的结果反推原始数据特征对不上就直接停下来查。original_sum series.sum() resampled_sum series.resample(3T).sum().sum() print(f原始总和: {original_sum}, 重采样总和: {resampled_sum}) # 原始总和: 36, 重采样总和: 36sum聚合具有总量守恒特性分组合计后再次求和应该等于原始总和。这个性质对sum有效对mean无效均值不能直接加总还原总量。另一个常用校验是「极值校验」resample(3T).min().min()应该等于原始最小值同理max().max()等于原始最大值。# 极值校验 assert series.resample(3T).min().min() series.min() assert series.resample(3T).max().max() series.max() print(极值校验通过)如果不想每个数据集写一遍校验就把这套逻辑做成一个小函数输入原序列和重采样结果输出校验报告def sanity_check(original, resampled): # 传入原始Series和重采样后的Series做总量守恒校验 checks { sum一致: round(original.sum(), 6) round(resampled.sum(), 6), min一致: original.min() resampled.min(), max一致: original.max() resampled.max(), } for name, passed in checks.items(): print(f{name}: {通过 if passed else 失败}) sanity_check(series, series.resample(3T).sum()) # sum一致: 通过 # min一致: 通过 # max一致: 通过这个函数的实用价值在于新接一个数据集先跑这三行校验能拦截掉至少七成时间索引、分组、填充引发的低级错误。从那以后我每次做重采样数据量再大、任务再急都会强制走一遍这个流程几分钟的事换来的是不用在后面几层分析里翻车。希望帮到你。本文还有配套的精品资源点击获取
返回列表