重采样:resample
本教程共 54 篇 · 第 46 篇 · 更新于 2026-08-11 · 约 8 分钟阅读
本节目标:学会用 resample 把时间序列从一个频率换到另一个频率:高频聚合成低频(降采样)、低频填充到高频(升采样)。
重采样是什么
你有 90 天的日销售数据,老板要看月度汇总;你有每分钟的传感器读数,分析要看每小时均值。把数据从一个时间频率转换到另一个频率,就是重采样(resample)。
重采样的本质是”按时间分组,再聚合”。先把时间切成一段一段(每个自然月、每个自然周),再把每段里的数据合并成一个值。它和 §37 的 groupby 是同一套思想,只是分组键换成了时间。正因如此,resample 的用法和 groupby 很像:先 resample("频率") 切组,再跟聚合方法。
import pandas as pd
import numpy as np
idx = pd.date_range("2024-01-01", periods=90, freq="D")
ts = pd.Series(np.random.randint(100, 500, 90), index=idx)
# 按自然月聚合,每月求和
print(ts.resample("ME").sum())
# 2024-01-31 ...
# 2024-02-29 ...
# 2024-03-31 ...
# Freq: ME, dtype: int64
频率别名和 §45 完全一致:ME 月末、W 每周、QE 季末、h 每小时……按月用 ME,注意不是旧写法 M。
降采样:高频到低频
把高频数据(如每天)聚合成低频(如每周、每月),叫降采样(downsampling)。聚合方法和 groupby 一个套路:
import pandas as pd
import numpy as np
idx = pd.date_range("2024-01-01", periods=90, freq="D")
ts = pd.Series(np.random.randint(100, 500, 90), index=idx)
print(ts.resample("ME").mean()) # 每月平均
print(ts.resample("ME").max()) # 每月最高
print(ts.resample("ME").min()) # 每月最低
print(ts.resample("ME").first()) # 每月第一个值
print(ts.resample("ME").last()) # 每月最后一个值
print(ts.resample("ME").count()) # 每月有多少条数据
print(ts.resample("W").sum()) # 每周求和
金融场景还有 ohlc(),一次给出开盘、最高、最低、收盘四个值:
import pandas as pd
import numpy as np
ts = pd.Series(
np.random.randn(100).cumsum(),
index=pd.date_range("2024-01-01", periods=100, freq="h"),
)
print(ts.resample("D").ohlc())
# open high low close
# 2024-01-01 0.49 0.496863 -1.686207 -1.37358
# ...
label 与 closed:结果挂在区间哪头
降采样把时间切成区间后,每个区间的结果要挂在一个时间标签上。label 决定挂左端还是右端。默认大多数频率是 label="left",比如按周,周一开头的周结果挂在周一;但 ME、QE、YE、W 这些频率默认是右端,月结果挂在月末。
closed 决定区间”左闭右开”还是”右闭左开”。想精确控制,显式写出来:
import pandas as pd
import numpy as np
idx = pd.date_range("2024-01-01", periods=10, freq="D")
ts = pd.Series(np.random.randint(1, 10, 10), index=idx)
# 结果标签用区间右端
print(ts.resample("4D", label="right").sum())
# 2024-01-05 ...
# 2024-01-09 ...
Note初学者可以先忽略 label 和 closed,用默认值。等发现”结果怎么挂到前一天了”这类问题,再回来调这两个参数。
一次算多个指标:agg
和 groupby 一样,resample 结果也能用 agg 一次聚合多个函数,还能对 DataFrame 的不同列用不同函数:
import pandas as pd
import numpy as np
df = pd.DataFrame(
{
"销售额": np.random.randint(100, 500, 60),
"访客数": np.random.randint(1000, 5000, 60),
},
index=pd.date_range("2024-01-01", periods=60, freq="D"),
)
# 每列都算 sum 和 mean
print(df.resample("ME").agg(["sum", "mean"]))
# 不同列用不同聚合
print(df.resample("ME").agg({"销售额": "sum", "访客数": "mean"}))
升采样:低频到高频
升采样(upsampling)方向相反:数据变密了。问题是,一个月一个值,拆到每天,中间的日子没有原始数据——所以升采样必须配合”填充规则”,否则全是 NaN。
import pandas as pd
ts = pd.Series([100, 200, 150], index=pd.date_range("2024-01-01", periods=3, freq="ME"))
# 先看空壳:asfreq 只改变频率,不填值
print(ts.resample("D").asfreq().head(3))
# 2024-01-31 100.0
# 2024-02-01 NaN
# 2024-02-02 NaN
# 向前填充:缺失值用上一个有效值补
print(ts.resample("D").ffill().head(3))
# 2024-01-31 100
# 2024-02-01 100
# 2024-02-02 100
# 向后填充
print(ts.resample("D").bfill().head(3))
# 2024-01-31 100
# 2024-02-01 200
# 2024-02-02 200
三个原始点是 1 月末、2 月末、3 月末(ME 的标签就是月末),升采样到每天后,索引从 1 月 31 日开始。填充方法的语义和 §25 的 ffill/bfill 一致:ffill 用前面的值补,bfill 用后面的值补。想限制连续补多少个,加 limit 参数:
import pandas as pd
ts = pd.Series([100, 200], index=pd.date_range("2024-01-01", periods=2, freq="ME"))
print(ts.resample("D").ffill(limit=2).head(5))
# 2024-01-31 100.0
# 2024-02-01 100.0
# 2024-02-02 100.0
# 2024-02-03 NaN
# 2024-02-04 NaN
日期不在索引上:用 on 参数
索引不是时间,但表里有一列是日期?resample 支持 on="日期列名",直接按那一列重采样:
import pandas as pd
import numpy as np
df = pd.DataFrame(
{
"日期": pd.date_range("2024-01-01", periods=30, freq="D"),
"销量": np.random.randint(1, 50, 30),
}
)
print(df.resample("W", on="日期").sum())
# 日期 销量
# 2024-01-07 ... ...
注意结果索引变成了那一列的时间。多级索引时还能用 level= 指定层级(§22)。
分组后再重采样
重采样可以接在 groupby 后面,先分组,再对每组按时间聚合。比如多个门店的日销售,按门店分组后各自按月汇总:
import pandas as pd
import numpy as np
np.random.seed(1)
df = pd.DataFrame(
{
"门店": np.repeat(["A店", "B店"], 60),
"日期": list(pd.date_range("2024-01-01", periods=60, freq="D")) * 2,
"销售额": np.random.randint(50, 200, 120),
}
)
df = df.set_index("日期")
print(df.groupby("门店").resample("ME").sum())
# 销售额
# 门店 日期
# A店 2024-01-31 ...
# 2024-02-29 ...
# B店 2024-01-31 ...
# 2024-02-29 ...
结果带两层索引:外层是门店,内层是月份。每个门店的月度汇总互不干扰。
看看每组里有什么
Resampler 对象可以像 groupby 一样迭代,逐个查看每个时间组里的原始数据:
import pandas as pd
s = pd.Series(
range(6),
index=pd.to_datetime(["2024-01-01 00:00", "2024-01-01 00:30", "2024-01-01 00:31",
"2024-01-01 01:00", "2024-01-01 03:00", "2024-01-01 03:05"]),
)
for name, group in s.resample("h"):
print(name, "->", len(group), "条")
# 2024-01-01 00:00:00 -> 3 条
# 2024-01-01 01:00:00 -> 1 条
# 2024-01-01 02:00:00 -> 0 条
# 2024-01-01 03:00:00 -> 2 条
注意 02:00 这个小时没有数据,但分组里仍然出现,只是空组。这在检查”哪些时间段缺数据”时非常直观。
asfreq:只换频率不聚合
有些场景你不想聚合,只想把时间轴”加密”——比如日数据补全成小时数据,但不想改变数值。用 asfreq,它只调整频率、不产生新值,空缺处全是 NaN:
import pandas as pd
s = pd.Series([1, 2, 3], index=pd.date_range("2024-01-01", periods=3, freq="D"))
print(s.asfreq("12h"))
# 2024-01-01 00:00:00 1.0
# 2024-01-01 12:00:00 NaN
# 2024-01-02 00:00:00 2.0
# 2024-01-02 12:00:00 NaN
# 2024-01-03 00:00:00 3.0
asfreq 本质上是 reindex(§23)加一个自动生成的日期序列。数据缺日期时(比如没有交易的日子),用 asfreq 补出缺失的时间轴,再配合 ffill/bfill 填值,是处理”日期不连续”问题的标准手法。
origin:调整分箱起点
默认情况下,分箱从”数据所在那天的零点”开始。数据不是整点开始的(比如从 23:30 开始),按 17 分钟分箱就会错位。用 origin 指定分箱起点:
import pandas as pd
import numpy as np
idx = pd.date_range("2024-01-01 23:30:00", periods=10, freq="7min")
ts = pd.Series(np.arange(10) * 3, index=idx)
# 默认从当天零点起分箱
print(ts.resample("17min").sum())
# 2024-01-01 23:14:00 0
# 2024-01-01 23:31:00 9
# 2024-01-01 23:48:00 21
# 2024-01-02 00:05:00 54
# 2024-01-02 00:22:00 51
# 从数据起点起分箱
print(ts.resample("17min", origin="start").sum())
# 2024-01-01 23:30:00 9
# 2024-01-01 23:47:00 21
# 2024-01-02 00:04:00 54
# 2024-01-02 00:21:00 51
origin="start" 从第一条数据开始切,origin="epoch" 从 1970 年零点开始切,也可以传一个具体的 Timestamp 当起点。默认值 "start_day" 是”当天零点”。
小结
重采样的完整套路:数据.resample(频率).聚合()。降采样用聚合函数合并数据(sum、mean、ohlc 等),升采样用 asfreq + 填充规则补数据。频率没变、只想补缺失日期时,也可以用 asfreq 直接换频率(§23 reindex 的另一种形态)。
Tipresample 就是”时间版 groupby”。遇到”按周统计""按月汇总”的需求,先想到它,比 groupby 加手动切日期列省事得多。