1. resample是什么先搞清楚它到底解决什么问题如果你的日常工作里接触过时间序列数据一定有这种经历拿到了秒级或者分钟级的交易记录想要按天统计一下总销售额或者拿到了每日的温度数据发现跟季度分析需要的口径对不上。这时候把数据从一个时间频率转换到另一个时间频率就是重采样。而Python里pandas包提供的resample()函数就是专门干这个活的。resample()本质上做的事情可以理解成“按时间去分组再处理”。它可以做两类操作一类是降采样也就是把高频率数据聚合到低频率比如把分钟数据汇总成小时、天、周、月另一类是升采样也就是把低频率数据扩展到高频率比如把月度数据填充成每天都有值。你可能觉得这不就是groupby换个玩法吗没错原理上确实类似但resample()是专门针对时间维度的它理解时间索引的特殊性比如月份的末尾、周末的跳转、季度的边界这些都是普通groupby很难优雅处理的。我在实际项目里用resample最多的场景有这么几类金融行情数据的K线重采样把Tick数据转成分钟线或日线传感器日志的降采样把1秒一条的数据聚合成5分钟一条用于存储和报表还有业务数据的同环比计算需要先把指标对齐到统一的月度或周度口径。可以说凡是和时间粒度有关系的ETL十有八九会用到它。这个函数适合谁来学我觉得只要你在用pandas处理带时间戳的数据就应该掌握。不管你是刚入门的数据分析新人还是已经在写生产管线的数据工程师resample都是绕不开的基础工具。下面我把从基础语法到实战坑点一条一条拆开讲。2. 动手前的关键准备索引必须是可解析的时间类型先说一个最常见的坑报了错提示Only valid with DatetimeIndex, TimedeltaIndex or PeriodIndex, but got an instance of RangeIndex这基本就是索引没有转成时间类型。resample的工作机制是先按时间索引去切段再对每个时间段做聚合。所以DataFrame的索引必须是pandas能识别的时间类型具体来说就是DatetimeIndex、TimedeltaIndex或者PeriodIndex。如果你的数据里时间是一个普通列必须先把它设成索引。示例代码大概是这样的import pandas as pd df pd.DataFrame({ price: [100, 102, 101, 105, 107], }, index[2024-01-01 09:30, 2024-01-01 09:31, 2024-01-01 09:35, 2024-01-01 09:40, 2024-01-01 09:45]) # 查看索引类型 print(df.index) # 输出 Index([2024-01-01 09:30, ...], dtypeobject)object类型索引没法直接resample用pd.to_datetime()转换df.index pd.to_datetime(df.index) # 或者更标准的方式读文件时直接指定 # df pd.read_csv(data.csv, parse_dates[time], index_coltime)转换完之后df.index就变成DatetimeIndex了这时才能执行resample()。还有一类数据的时间是分散在多列里的比如年、月、日各占一列用pd.to_datetime()将多列合并成时间序列再设索引即可df pd.DataFrame({ year: [2024, 2024, 2024], month: [1, 1, 1], day: [1, 2, 3], value: [10, 20, 30] }) df[date] pd.to_datetime(df[[year, month, day]]) df df.set_index(date)这套“把普通列变成时间索引”的操作是resample的敲门砖。我见过不少同事在这块卡了很久整了半天代码发现错误一直出在索引类型上。提示read_csv读文件的时候直接在参数里用parse_dates[time列名]和index_coltime列名一步到位能省掉后续转换的麻烦。3. 核心语法与参数你真正需要记住的是这几个resample的标准调用方式很简洁df.resample(rule, axis0, closedleft, labelleft, onNone, levelNone, originstart_day)rule是必填的它决定你要重采样到什么频率比如T代表分钟、H代表小时、D代表天、W代表周、M代表月末、Q代表季度末、Y代表年末。其他参数里实际项目中最常用的是closed、label、on这三个。closed和label这两个参数经常放在一起说因为它们都跟窗口边界有关closed指定每个时间段哪一侧是闭区间。降采样时左闭右开还是右闭左开对边界取值影响很大。label指定聚合结果用时间段的哪个边界来标记是左边时间还是右边时间。举一个最简单的例子把分钟数据按小时聚合df.resample(H, closedleft, labelleft).sum()如果按小时聚合默认closedleft, labelleft意思就是[09:00, 10:00)这个小时内09:00到09:59的数据汇总后结果标记在09:00上。你可能觉得这两个参数平时不用管但在金融数据里K线的开高低收往往需要把某个边界值归入前一根还是后一根K线这时候closed和label不正确结果会差一根K线。比如按日线收盘价如果闭区间设置错了可能会把当天的最后一笔成交漏掉或者把次日凌晨的数据并进来这种错误是失之毫厘谬以千里。on参数的作用是当你有时间戳列但不想动索引时直接指定这个列作为重采样依据。比如索引不是时间但有一列交易时间就可以这样写df.resample(D, on交易时间).sum()level参数则用于多层索引如果你的索引是(股票代码, 交易日期)这种结构用level指定在哪个层级做时间重采样。另外有个比较新的origin参数可以控制重采样窗口的对齐起点。默认情况下按天重采样就是自然日的零点到零点但有的业务场景自定义零点比如期货夜盘从晚上21点开始算一个交易日那么可以把origin设为21:00:00让窗口从晚上9点开始切。3.1 频率字符串速查表频率字符串含义典型场景T/min分钟行情数据、监控日志H小时小时级报表D自然日日报W每周默认周日起周报W-MON从周一开始的周业务周统计M月末月报MS月初月度起始对齐Q季度末季报Y年末年报10T10分钟高频聚合2H2小时细粒度时段分析4. 降采样实操把高频率数据汇总成低频率降采样就是数据量由多变少这是resample最常见的用法。调用resample()之后紧接着要挂一个聚合方法比如sum()、mean()、max()、min()、first()、last()、ohlc()等。4.1 单列聚合与多列聚合先准备一份模拟的分钟级销售数据import pandas as pd import numpy as np time_range pd.date_range(2024-03-01, 2024-03-07 23:59, freq5T) df pd.DataFrame({ amount: np.random.randint(100, 500, sizelen(time_range)), orders: np.random.randint(5, 50, sizelen(time_range)) }, indextime_range)按天求总销售额daily df.resample(D).sum()这里sum()会对所有数值列各算各的amount求总和orders求总和一步到位。如果想不同列用不同聚合方式可以配合agg()使用daily df.resample(D).agg({ amount: sum, orders: [sum, mean] })这种写法在生产报表里非常实用既能看到总量又能看到平均客单价相关指标。多列多聚合函数会生成多层列索引后续可以用daily.columns [amount_sum, orders_sum, orders_mean]把列名拍平。4.2 金融K线的神器ohlc如果是做行情相关的工作resample(D).ohlc()可以直接生成开盘、最高、最低、收盘四列这是标准Candlestick数据。这在手动处理之前得写一长串groupby逻辑现在一行搞定daily_kline df.resample(D).ohlc()泛化到任意分钟级别频率也一样比如5分钟K线df.resample(5T).ohlc()4.3 自定义聚合逻辑内置方法不够用的时候写个lambda或者普通函数传给apply()def peak_ratio(s): return (s - s.min()) / (s.max() - s.min() 1e-9) df.resample(D)[amount].apply(peak_ratio)apply()接收的是每个时间窗口内的Series注意别在函数里做太重的循环窗口多的时候性能会下降。4.4 降采样后索引标签的坑默认按天聚合标签是每一天的日期按周聚合默认标签是每个周的周日日期。这在报表上可能不符合业务习惯比如国内很多企业喜欢本周最后的工作日作为周标签。可以用labelright调整或直接对结果索引再做一次转换。就我的经验来说降采样后的数据最好在聚合之后立刻确认索引的边界对不对尤其是closed参数的具体定义与实际数据范围是否符合预期。别看教科书上说默认值就够了真实行业数据往往边界条件复杂。注意降采样时如果某列有缺失值sum()默认会把缺失当成0而mean()会跳过缺失。如果你不希望缺失值被“自动填0”先用dropna()或fillna()处理。5. 升采样实操从稀疏到稠密的填充策略升采样和降采样完全相反是把低频率数据填充成高频率数据。这里没有“聚合”的概念只有“填空”的问题。假设我们有月度销售额monthly pd.DataFrame({ sales: [100, 150, 130], }, indexpd.to_datetime([2024-01-31, 2024-02-29, 2024-03-31]))想扩成每天都有值直接resample(D)会得到一堆NaN因为原始数据在每个月中只有一天有值。这时需要填充策略# 用上一个有效值填充等价于业务里的“沿用上期” daily_ffill monthly.resample(D).ffill() # 用下一个有效值填充 daily_bfill monthly.resample(D).bfill()如果你希望月末值代表整个月的状态用ffill是合理的如果你想模拟“月底才产生数据、下月初才生效”这种业务bfill更贴合。比ffill()更精细的是interpolate()插值法。例如把季度数据插成月度序列使用线性插值quarterly pd.DataFrame({ gdp: [100, 110, 125], }, indexpd.to_datetime([2024-03-31, 2024-06-30, 2024-09-30])) monthly_interp quarterly.resample(M).interpolate(methodlinear)interpolate()支持多种方法线性插值、时间插值、样条插值等。对于时间序列用methodtime会按实际时间间隔比例插值比单纯按位置插值更准确。5.1 升采样时是否用聚合方法可能有人会疑惑为什么升采样还能用sum()确实可以但它不会把数值变大只是会把原始有值那个时间点的数据填充到窗口里其他时间点还是NaN所以升采样时一般不用聚合函数而是用ffill、bfill、interpolate。还有一种场景是升采样加聚合结合比如从“每天总量”重采样到“每小时总量”希望把当天的值平均摊到每个小时然后求和等于原值。这个思路不能用简单填充需要先展开再切分# 先把日值按当天的小时数重复再按小时重采样 df_hourly df_daily.resample(H).ffill() / 24 df_hourly_sum df_hourly.resample(D).sum()回到日粒度时总量正好等于原来的值。这种“降基展开再聚合”的技巧在预算分摊、费用拆分字段时很实用。提示升采样生成的NaN一定不能忽视。往数据库里写之前最好确认这些NaN是有意保留还是需要用0填充否则后面报表统计会出现“为什么这周数据凭空少了一段”的诡异问题。6. 进阶玩法按周、月、季度以及自定义分组对齐resample真正体现功力的是各种非标准时间频率的对齐方式。因为自然日、自然周和业务周往往不是一回事。6.1 按业务周聚合很多公司用的是周一到周日作为业务周不是默认的周日到周六。频率字符串W-MON表示以周一作为周边界df.resample(W-MON).sum()这周的所有数据会汇总后索引标记为那个周一的日期。如果不想让索引落在周一而想落在周日再加labelright。6.2 按周几聚合或者按一天中的时段有类需求是按工作日和周末对比或者按一周中的某一天来聚合。resample也能干但思路稍微绕一下# 按周几看平均销量 df.groupby(df.index.dayofweek).mean()严格来说这不是resample而是groupby特定时间属性。不过善用index.dayofweek、index.hour这些访问器能把很多“周期性分析”做得干净利落。6.3 多索引股票数据的重采样如果你在同时处理多只股票的数据索引是(code, time)这样的MultiIndex直接resample会报错或者切错层级。必须先groupby(level0)再重采样df.groupby(level0).resample(D).last()这样得到的是每只股票每个交易日的最后一条数据相当于日线收盘序列。注意重采样会默认把被分组的层级保留得到的结果索引有code和date两层后面需要reset_index或直接处理这两层索引。6.4 财务口径的Q端问题季度频率Q默认的窗口结束日是12月31日、3月31日、6月30日、9月30日这正好对应自然季度一般可以直接用。但如果公司的财年不是自然年起始比如从4月1日开始那么用freqQ-MAR或者Q-APR来调整季度锚定月份df.resample(Q-MAR).sum()这种参数设定在上市公司财报处理中非常常见不掌握的话很容易把季度数据切错。7. 常见问题与排查技巧实录7.1 索引是object不是DatetimeIndex报错信息里如果出现DatetimeIndex和RangeIndex的字眼就往索引类型上检查。用df.index pd.to_datetime(df.index)转换即可。也有一种情况是日期字符串格式不统一比如有的行是2024/01/01有的行是2024-01-01to_datetime默认能自动识别绝大多数常见格式但遇到完全分不清的格式会抛异常这时可以用format参数指定df.index pd.to_datetime(df.index, format%Y-%m-%d %H:%M:%S)7.2 聚合结果全是NaN这个一般发生在resample的频率比数据本身频率更高的时候或者原始数据里全为空。先检查原始数据有效值数量再检查频率字符串是否正确。比如数据本身是日频的你写了resample(H)出来的当然是大部分NaN。这种情况不是bug是逻辑写错了。7.3 边界数据归属不对比如按日聚合总感觉00:00前后的数据被分到了前一天或者23:59的数据跑到了第二天。这就需要检查closed参数。closedleft是默认左闭右开closedright是右闭左开。如果数据的时间戳精确到秒而你不知道某笔交易的落地时间是在边界内还是边界外宁可用边界值排除法——开一个一小时的窗口做小样本来验证。7.4 时区问题导致结果乱了如果数据里带时区信息比如UTC时间而你的业务现场用的是北京时间重采样前最好先转换df.index df.index.tz_convert(Asia/Shanghai)或者读取时直接指定时区df pd.read_csv(data.csv, parse_dates[time], index_coltime) df.index df.index.tz_localize(UTC).tz_convert(Asia/Shanghai)忽略时区的结果往往是原来每天数据从00点到24点看起来正常实际上跨天统计差了好几个小时。7.5 resample之后的索引重置重采样之后结果是一个以时间频率为索引的DataFrame。如果你想把时间变回普通列用reset_index()daily df.resample(D).sum().reset_index()这在写SQL或导出Excel时很常用因为数据库表一般不喜欢把主键时间放在index里。7.6 性能优化建议resample本身不是特别重但在大数据量下也会变慢。几个经验先把不需要的列删掉再resample会快很多如果只是求一列聚合不要对整份DataFrame做resample只对Series做时间索引提前排序无序索引会导致额外开销对于超大数据集可以用pd.Grouper(freqD)配合groupby在需要多列分组时更灵活。8. 一些我在项目中养成的经验习惯最后聊几个我在实际项目里的体会供你参考。第一写完resample之后一定打印出结果的索引头和尾值。我不止一次觉得逻辑天衣无缝结果输出一看日期从1号跳到了31号中间少了一截。原因往往是数据源本身就缺了某几天然后被resample“悄悄”补了出来或者跳过去了。确认开头、中间、结尾三步走基本能挡住80%的重采样错误。第二频率字符串尽量用标准缩写并且写清楚是T还是min。旧版本里T和min等价但代码审查时如果混用新人容易看懵。我的习惯是统一用min可读性更好。第三遇到复杂的重采样需求先画个示意图把窗口边界标出来再写代码。比如“每30分钟的订单量但窗口从整点开始还是从半点开始”这种需求用文字描述总会有歧义画图沟通一次代码基本不会写错。尤其是origin参数用图去理解窗口对齐远比看文档效率高。如果你后续还想继续深入建议研究一下pd.Grouper和resample的配合使用尤其是在分子分母指标拆解以及多个字段口径对齐的场景里它们能帮你把一个复杂口径拆成一组清晰的小步骤。掌握了这些之后时间序列的数据处理基本就没什么能难住你的了。