跳转到内容

shift、rolling、resample 与时间对齐

计算机基础 · pandas

前置知识:缺失、重复与类型清洗 · 聚合、groupby 与 transform

本章目录

本章学习目标

  • 先分组排序再计算滞后和窗口
  • 区别观测窗口与时间窗口
  • 检查信号信息时点与缺失交易日

时间计算先保证顺序

时间序列算法通常沿当前行顺序工作,不会自动知道哪一行是昨天。股票面板需要先解析日期、处理重复股票日期,再按股票与日期排序。shift(1) 在同一股票组中取前一条观测的数值,而不是自动取前一个日历日;跨股票直接 shift 会把前一只股票的末值当成后一只股票的前值。

收益率用当前收盘价除以前一条有效定义的价格再减一。若中间交易日缺失,结果覆盖的是两条观测之间的整个区间,不能标成单日收益率;本章列名因此写成 return_observed。要形成日收益序列,应先确定交易日历、补齐索引并决定缺失价格策略,不能把周末和停牌一律当成零收益。

小数据:两种窗口

日期一、二、五、六的价格依次十、十二、十五、二十。第三个观测的 rolling(3) 取十、十二、十五,均值为三十七除以三;按三日时间 rolling('3D') 并采用右闭区间,日期五的窗口是大于日期二且小于等于日期五,只包含十五,均值为十五。两种窗口回答不同问题,结果不相等不是错误。

min_periods 决定至少有多少有效观测才计算结果。rolling(3,min_periods=3) 在前两条和包含缺失价格的不足三值窗口返回缺失。时间窗口的默认最小有效数可能不同,应主动写出要求。closed 控制边界包含,特别是恰好落在左边界的日期,必须用小数据检查。

信号与未来信息

若在当天开盘前交易,当天收盘价还未知,用包含当天收盘的均线生成当天开盘信号会泄漏未来信息。示例先计算含当日收盘的三观测均线,再按股票 shift(1),得到在下一条观测开始前已经可用的信号。是否足够仍取决于真实发布时间、复权处理和执行价格,本课程仅演示基本时点约束。

负向 shift 可以构造未来标签,但标签列不能进入特征。merge_asof 可以将已有历史指标匹配到决策时点,通常使用 direction='backward'、容忍时间差和实体 by;输入必须满足要求的排序。仅按日期精确连接公告数据可能既丢失非交易日公告,也忽视同一天盘后发布的信息。

重采样与对齐

resample 把时间索引按日、周或月划分桶,聚合成交量可求和,价格可取首末值,返回值应说明桶边界和标签时点。股票组内单独重采样,不能把不同股票混成一条价格序列。OHLC 含义是开、高、低、收,不是四个同义的均值。月份频率标记与结束边界在版本间有别,教学代码和文档应核对实际版本。

reindex 到明确日历可以暴露缺失日期,ffill 只能从过去传播值,bfill 可能使用未来值。前向填充不产生真实交易,成交量不应沿用前日;缺少观测的状态需要单独一列。在汇总前,先判断缺失意味着市场休市、无成交还是日志丢失,三者的指标口径不同。

验证与解释窗口结果

脚本同时输出观测收益、三观测均线和滞后信号,断言每只股票第一条前值为空。交互演示切换窗口及滞后,学习者先猜哪些行会变缺失,再看结果。清洗规则只是教学副本保留输入最后一条,并不替代生产的行情版本规则。

检查边界:每组只有一行、日期间隔很大、价格缺失、零价格及同一天重复。窗口的有效样本数应该随结果一起检查,避免用少量观测和完整窗口混比。面试中讲 rolling,必须说清按几条记录还是几天、是否包含当日、怎样处理缺失,以及信号是在交易前还是交易后可用。

版本与复现约定

本课程 pandas 示例以 2.3.3 为验证基线,页面运行记录给出实际版本。pandas 3.x 默认使用 Copy-on-Write:从表中取出的对象发生修改时,不据此假定原表同步变化。跨版本都应使用一次完整的 loc 赋值表达式;需要独立工作表时显式 copy。阅读网上的旧示例时,先核对版本、输入类型和索引,再判断结果能否复现。

交互演示:观察机制

完整示例与实际输出

from pathlib import Path
import pandas as pd
import numpy as np
from _shared import load
ROOT = Path(__file__).resolve().parents[1]
OUT = ROOT / "outputs"
OUT.mkdir(exist_ok=True)
raw=load("panel")
d=raw.drop_duplicates(["symbol","date"],keep="last").copy()
d["date"]=pd.to_datetime(d["date"])
d=d.sort_values(["symbol","date"]).reset_index(drop=True)
d["previous_close"]=d.groupby("symbol")["close"].shift(1)
d["return_observed"]=d["close"]/d["previous_close"]-1
d["mean_3_observations"]=d.groupby("symbol")["close"].transform(lambda s:s.rolling(3,min_periods=3).mean())
d["signal_available_before_today"]=d.groupby("symbol")["mean_3_observations"].shift(1)
first=d.groupby("symbol").head(1)
assert first["previous_close"].isna().all()
assert not d.duplicated(["symbol","date"]).any()
small=pd.Series([10.,12.,15.,20.],index=pd.to_datetime(["2026-01-01","2026-01-02","2026-01-05","2026-01-06"]))
assert small.rolling(3).mean().iloc[2]==37/3
assert small.rolling("3D",closed="right").mean().iloc[2]==15
print("三个观测 / 三个日历日:",small.rolling(3).mean().iloc[2],small.rolling("3D").mean().iloc[2])
print(d.head(8).to_string(index=False))
d.to_csv(OUT / "pd_time_panel.csv",index=False)

实际运行通过CPU Python execution.

三个观测 / 三个日历日: 12.333333333333334 15.0
symbol       date    open   close  volume industry  previous_close  return_observed  mean_3_observations  signal_available_before_today
   AAA 2026-01-01 26.1033 26.3837    6372       科技             NaN              NaN                  NaN                            NaN
   AAA 2026-01-02 26.3837 27.1463    8759       科技         26.3837         0.028904                  NaN                            NaN
   AAA 2026-01-05 27.1463 27.6126    1874       科技         27.1463         0.017177            27.047533                            NaN
   AAA 2026-01-06 27.6126 27.9436    4883       科技         27.6126         0.011987            27.567500                      27.047533
   AAA 2026-01-07 27.9436 28.0324   12345       科技         27.9436         0.003178            27.862867                      27.567500
   AAA 2026-01-08 28.0324 27.8942    2711       科技         28.0324        -0.004930            27.956733                      27.862867
   AAA 2026-01-09 27.8942 28.4592    4291       科技         27.8942         0.020255            28.128600                      27.956733
   AAA 2026-01-12 28.4592 28.4195    8402       科技         28.4592        -0.001395            28.257633                      28.128600

五道自测:先回答,再核对

01 · shift(1) 一定是昨天吗?

查看提示

记录可能缺日期。

查看参考答案

不是,是当前顺序前一条观测;要先分组排序。

02 · rolling(3) 与 rolling('3D') 差别?

查看提示

条数和时间。

查看参考答案

前者最近三观测,后者指定日历时间范围。

03 · 为何开盘信号需要滞后收盘均线?

查看提示

收盘何时可知。

查看参考答案

当日开盘看不到当日收盘,必须使用已知历史值。

04 · bfill 有什么风险?

查看提示

从哪一侧补。

查看参考答案

会从未来记录填到过去,可能造成特征泄漏。

05 · 跨股票直接 rolling 为什么危险?

查看提示

组边界。

查看参考答案

窗口会混入其他股票的记录,需 groupby 后计算。

两项动手任务

任务 1 · 生成按股票排序的观测收益与滞后均线。

提示

先去重,明确观测间隔。

完整参考解答与检查方式
from pathlib import Path
import pandas as pd
import numpy as np
from _shared import load
ROOT = Path(__file__).resolve().parents[1]
OUT = ROOT / "outputs"
OUT.mkdir(exist_ok=True)
raw=load("panel")
d=raw.drop_duplicates(["symbol","date"],keep="last").copy()
d["date"]=pd.to_datetime(d["date"])
d=d.sort_values(["symbol","date"]).reset_index(drop=True)
d["previous_close"]=d.groupby("symbol")["close"].shift(1)
d["return_observed"]=d["close"]/d["previous_close"]-1
d["mean_3_observations"]=d.groupby("symbol")["close"].transform(lambda s:s.rolling(3,min_periods=3).mean())
d["signal_available_before_today"]=d.groupby("symbol")["mean_3_observations"].shift(1)
first=d.groupby("symbol").head(1)
assert first["previous_close"].isna().all()
assert not d.duplicated(["symbol","date"]).any()
small=pd.Series([10.,12.,15.,20.],index=pd.to_datetime(["2026-01-01","2026-01-02","2026-01-05","2026-01-06"]))
assert small.rolling(3).mean().iloc[2]==37/3
assert small.rolling("3D",closed="right").mean().iloc[2]==15
print("三个观测 / 三个日历日:",small.rolling(3).mean().iloc[2],small.rolling("3D").mean().iloc[2])
print(d.head(8).to_string(index=False))
d.to_csv(OUT / "pd_time_panel.csv",index=False)

任务 2 · 把三日记录补成完整日历,保留原始观测标记。

提示

reindex 后不要丢掉 missing 标记。

完整参考解答与检查方式
from pathlib import Path
import pandas as pd
import numpy as np
from _shared import load
ROOT = Path(__file__).resolve().parents[1]
OUT = ROOT / "outputs"
OUT.mkdir(exist_ok=True)
s=pd.Series([10.,12.],index=pd.to_datetime(["2026-01-01","2026-01-03"]))
result=s.reindex(pd.date_range("2026-01-01","2026-01-03",freq="D")).to_frame("close")
result["observed"]=result["close"].notna()
result["history_close"]=result["close"].ffill()
assert result["observed"].tolist()==[True,False,True]
assert result["history_close"].tolist()==[10.,10.,12.]
print(result.to_string())

官方资料与掌握状态

完成两项任务后,按独立完成程度记录。状态会尽量保存在当前浏览器,可导出备份。

动手任务记录

掌握状态与复习

关掉资料独立完成后记 A;提示后完成记 B;需要重学记 C。

记录尽量保存在当前浏览器,建议定期导出备份。