首页 / Pandas 入门教程 / 重采样:resample

Pandas 入门教程

重采样:resample

本教程共 54 篇 · 第 46 篇 · 更新于 2026-08-11 · 约 8 分钟阅读

PandasPandas 入门教程resample重采样降采样升采样

本节目标:学会用 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",比如按周,周一开头的周结果挂在周一;但 MEQEYEW 这些频率默认是右端,月结果挂在月末。

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 的另一种形态)。

Tip

resample 就是”时间版 groupby”。遇到”按周统计""按月汇总”的需求,先想到它,比 groupby 加手动切日期列省事得多。