ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

从零搭建你的 A 股量化系统(十五):Pandas 量化必备 30 操作(上)——索引、时间序列、重采样

从零搭建你的 A 股量化系统(十五):Pandas 量化必备 30 操作(上)——索引、时间序列、重采样

系列名:《从零搭建你的 A 股量化系统》| 专栏 S2 Python 工具链 | 第 3 篇 / 共 14 篇
标签:#量化投资 #Pandas #时间序列 #重采样 #Python #数据分析 #零基础 #实战 #A股 #散户

NumPy 是发动机,pandas 是装了发动机的整车。发动机再猛,你也不能天天手搓数组去算"某年某月某只股票涨了多少"“把日线压成月线看趋势”。这些脏活累活,pandas 一脚油门就帮你办了。

这一篇开始,我们用30 个高频操作把 pandas 在量化里最常用、也最容易被新手卡住的本事拆干净。因为一次塞 30 个太干,我拆成上下两篇:

  • 本篇(上):聚焦索引、时间序列、重采样三大块,共15 个操作;
  • 下篇(S2-016):补齐groupby / rolling / shift 与未来函数陷阱,又是15 个

两篇合起来正好30 个量化必备操作。配套脚本src/s2_015_pandas_essentials.py,全程用你仓库里现成的data/510300_daily.csv(沪深300ETF 真实日线)跑通。


📌 你将学到

  • 为什么量化里索引(Index)是第一公民,以及set_index/.loc/.iloc/ 布尔索引到底怎么选;
  • DatetimeIndex这套"把日期当索引"的思维,怎么让你用df['2024']一行就榨出全年数据;
  • 时间序列四大高频动作:按年/月取数、时间段切片、date_range造日期、DateOffset 偏移;
  • 重采样(resample)怎么把日线"压"成周线/月线/季线,并顺手聚合成交量、算月收益;
  • 新手最容易踩的6 个坑(最阴的是df['2024']报错、resample最后一桶不全、read_csv把日期读成字符串),附避雷代码。

📖 前置知识

  • 已按第 1 篇装好ashare环境(参考src/s2_013_detect_env.py确认),并已pip install pandas
  • 看过 S2-014《NumPy 核心》最好——本篇默认你懂"向量化",pandas 底层就是它;
  • 会最基础的 Python:变量、列表、函数。pandas 的 API 名字很直白,不懂统计学也能跟。

操作地图

操作
上(本篇)索引① 读 CSV 吃 BOM ② 设 DatetimeIndex ③ 选列 ④.loc标签取行 ⑤.iloc位置取行 ⑥ 布尔索引 + 索引对齐
上(本篇)时间序列⑦ 按年/月取数 ⑧ 时间段切片 ⑨date_range⑩ 时间差 ⑪ DateOffset 偏移
上(本篇)重采样⑫ 月线收盘 ⑬ 周线 OHLC ⑭ 季度聚合 ⑮ 月收益聚合
下(S2-016)groupby/rolling/shift⑯~⑳groupby全家桶 ㉑rolling滚动 ㉒expandingshift错位 ㉔pct_changeapply㉖ 金叉信号 ㉗ 横截面rankmergeconcat㉚ 缺失值处理

💡 建议:把这张表当书签。上篇学完索引/时间序列/重采样,你就能独立做出"按月看收益、按周看波动"的最基础研究;下篇的rolling/shift才是因子和回测的命门。


一、为什么是 pandas

NumPy 的ndarray只有"行号",没有"名字"。可量化数据天然带日期字段名:2024-09-30 的收盘价、开盘价……你用行号arr[1234]去取,三天后就忘了 1234 是哪天。

pandas 的DataFrame=带标签的二维表:行有索引(通常是日期),列有字段名(open/high/low/close/volume)。好处是——

  • 你不用记行号,直接写df.loc['2024-09-30']
  • 两列相减会自动按日期对齐,不用担心"第一行对第一行"的错配;
  • 时间相关的活(resample重采样、DateOffset偏移)开箱即用。

一句话:NumPy 让你算得快,pandas 让你找得准、读得懂。下面 15 个操作,全是围绕"索引"和"日期"转。


二、索引篇(操作 ① ~ ⑥)

操作① 读 CSV,先吃掉 BOM(量化数据最常见的第一个坑)

A 股数据源(AkShare、Tushare 导出的 CSV)经常带BOM(字节顺序标记)。如果你不处理,第一列名会变成'\ufeffdate',后面set_index('date')直接KeyError

importpandasaspd# encoding='utf-8-sig' 自动吃掉 BOM,量化数据几乎必加df=pd.read_csv("data/510300_daily.csv",encoding="utf-8-sig")print(list(df.columns))# ['date', 'open', 'high', 'low', 'close', 'volume'] ✓ 干净

配套脚本跑出来:

标的: 510300 沪深300ETF 共 1,594 个交易日 区间: 2020-01-02 ~ 2026-07-31 列名: ['open', 'high', 'low', 'close', 'volume'] open high low close volume date 2020-01-02 1.673 1.693 1.672 1.683 627623614.0 2020-01-03 1.686 1.688 1.677 1.680 469673776.0 2020-01-06 1.675 1.692 1.663 1.674 666504783.0 索引类型: DatetimeIndex -> datetime64[ns]

操作② 把日期列设为索引(DatetimeIndex)——时间序列的基石

read_csv读进来时,date只是普通一列字符串。要把它变成"会思考的日期索引",两步:先to_datetime解析,再set_index

df["date"]=pd.to_datetime(df["date"])# 字符串 -> 真正的 Timestampdf=df.set_index("date").sort_index()# 设为索引,并升序排好print(type(df.index).__name__,df.index.dtype)# DatetimeIndex datetime64[ns]

⚠️ 忘记to_datetimeset_index,索引会是"字符串"而不是"日期"——后面df['2024']按年取数会直接失效。这是坑⑤,详见第五节。

操作③ 选列:单列 → Series,多列 → DataFrame

close_series=df["close"]# 单列 -> Series(一维)oc_df=df[["open","close"]]# 多列 -> DataFrame(二维),注意两层方括号
df['close'] 类型: Series 长度: 1594 df[['open','close']] 类型: DataFrame 形状: (1594, 2)

💡 经验法则:单层[]给列名(结果可能是 Series 或 DataFrame),双层[[]]永远返回 DataFrame。写因子时尽量用双层,下游.pipe().assign()才不会因维度突变翻车。

操作④ 按标签取行:.loc['2024-09-30']

索引是日期后,直接用日期字符串当"行号"。比如取出著名的"924 行情"次日:

print(df.loc["2024-09-30"])
open 1.735000e+00 high 1.821000e+00 low 1.686000e+00 close 1.818000e+00 volume 6.370234e+09

操作⑤ 按位置取行:.iloc[0]

想要"第几行"而不是"哪天",用.iloc(按整数位置,从 0 开始):

print(df.iloc[0])# 最早一天 2020-01-02
open 1.673000e+00 high 1.693000e+00 low 1.672000e+00 close 1.683000e+00 volume 6.276236e+08

📌.loc用"标签"(日期)、.iloc用"位置"(第几行)。混用.iloc配日期字符串会报错,新手常栽在这。

操作⑥ 布尔索引 + 索引自动对齐(pandas 最香的特性)

想筛"收盘价大于 2 元的所有交易日",直接上布尔条件:

high=df[df["close"]>2.0]print(f"收盘价 > 2.0 元:{len(high):,}/{len(df):,}天")
收盘价 > 2.0 元的交易日: 432 / 1,594 天 (27.1%)

更妙的是索引自动对齐:两列做运算时,pandas 按"日期"对齐而不是按"行号"。下面这行算"当天开盘到收盘的涨跌幅",完全不用管顺序对不对:

df["intraday_chg"]=(df["close"]-df["open"])/df["open"]
open close intraday_chg date 2020-01-02 1.673 1.683 0.005977 2020-01-03 1.686 1.680 -0.003559 2020-01-06 1.675 1.674 -0.000597

💡 这个"按索引对齐"是 pandas 防错的核心机制。等你下篇做多只股票拼接(concat/merge)时,它会替你挡掉 90% 的"对错行"事故。


三、时间序列篇(操作 ⑦ ~ ⑪)

DatetimeIndex 设好后,时间相关的活变得像说话一样自然。

操作⑦ 按年 / 按月取数(局部字符串索引)

y2024=df.loc["2024"]# 全年m2024_01=df.loc["2024-01"]# 某月print(f"2024 全年交易日:{len(y2024)}天")print(f"2024-01 交易日:{len(m2024_01)}天")
2024 全年交易日: 242 天 2024-01 交易日: 22 天,区间 2024-01-02~2024-01-31 2024-01 收盘: [1.504, 1.5, 1.489, 1.482, 1.465, 1.468, 1.462, 1.469, 1.465, 1.463, 1.472, 1.444, 1.46, 1.463, 1.437, 1.446, 1.465, 1.485, 1.483, 1.472, 1.451, 1.443]

⚠️必须用.loc,不能写df["2024"]——后者会被当成"取名为 2024 的列"而KeyError。这是新手第一坑,第五节展开。

操作⑧ 时间段切片(含两端)

要一段区间,直接'起':'止'两端都包含(和 Python 普通切片不同,这里不"左闭右开"):

q1=df.loc["2024-01-01":"2024-03-31"]print(f"2024 年一季度交易日:{len(q1)}天,区间收盘{q1['close'].min():.3f}~{q1['close'].max():.3f}")
2024 年一季度交易日: 58 天 区间收盘: 1.431 ~ 1.581

操作⑨pd.date_range造连续日期序列

做回测框架、对齐多标的日历时常要自己造日期轴:

r=pd.date_range("2026-01-01","2026-01-10",freq="D")# 连续自然日rb=pd.bdate_range("2026-01-01","2026-01-10")# 仅工作日print([d.date().isoformat()fordinr])print([d.date().isoformat()fordinrb])
freq='D' 连续 10 天: ['2026-01-01', '2026-01-02', '2026-01-03', '2026-01-04', '2026-01-05', '2026-01-06', '2026-01-07', '2026-01-08', '2026-01-09', '2026-01-10'] freq='B'(工作日) 同区间: ['2026-01-01', '2026-01-02', '2026-01-05', '2026-01-06', '2026-01-07', '2026-01-08', '2026-01-09']

📌freq是灵魂:D日、B工作日、W周、ME月末、QE季末、QE-DEC等。它和下面的resample共用同一套频率字符串。

操作⑩ 时间差:自然日 vs 交易日

两个日期相减,得到Timedelta.days拿到天数:

first,last=df.index.min(),df.index.max()print(f"自然日跨度:{(last-first).days}天,实际交易日:{len(df)}天")
首末交易日: 2020-01-02 -> 2026-07-31 自然日跨度: 2402 天 实际交易日: 1594 天 -> 约 242 个交易日/年

💡 记住A 股一年约 242 个交易日(不是 365)。所有"年化"都要乘 242 或 252,别用 365——这是 S1-010 讲年化波动率的底层约定。

操作⑪ DateOffset 偏移:取"N 个自然日/交易日前"

target=pd.Timestamp("2024-09-30")prev_5=target-pd.DateOffset(days=5)# 往前 5 个自然日last_b=df.index[df.index<=target][-1]# 不晚于 target 的最近交易日print("往前5自然日:",prev_5.date(),"| 最近交易日:",last_b.date())
2024-09-30 往前 5 个自然日: 2024-09-25 不晚于 2024-09-30 的最近交易日: 2024-09-30

📌DateOffset(days=5)是"日历偏移";若要"往前 5 个交易日"要用BDay(5)(需from pandas.tseries.offsets import BDay)。做"N 日前的信号"时务必分清,否则会混入周末/节假日。


四、重采样篇(操作 ⑫ ~ ⑮)

resample= 把高频数据按时间频率"降采样"成低频,并指定每个桶怎么聚合。它是把日线变成周/月/季线的唯一正解。

操作⑫ 月线收盘价:resample('ME').last()

monthly_close=df["close"].resample("ME").last()# ME = 月末(Month End)print(monthly_close.tail(6).round(3))
date 2026-02-28 2.079 2026-03-31 1.982 2026-04-30 2.116 2026-05-31 2.153 2026-06-30 2.188 2026-07-31 2.053 Freq: ME

⚠️ 老教程写resample('M'),新版 pandas(≥2.2)会报警告,改用'ME'(月末)/'MS'(月初)。本文全程用新写法。

操作⑬ 周线 OHLC:resample('W').ohlc()

一根 K 线要开高低收,ohlc()一行给齐:

weekly_ohlc=df["close"].resample("W").ohlc()print(weekly_ohlc.tail(4).round(3))
open high low close date 2026-07-12 2.136 2.150 2.106 2.118 2026-07-19 2.086 2.121 2.029 2.029 2026-07-26 2.051 2.102 2.051 2.070 2026-08-02 2.090 2.090 2.035 2.053

操作⑭ 季度聚合多指标:resample('QE').agg({...})

不同列用不同聚合函数,传字典即可:

quarterly=df.resample("QE").agg({"close":"last","volume":"sum"})quarterly["volume"]=(quarterly["volume"]/1e8).round(2)# 成交量转「亿股」quarterly.columns=["季末收盘","季度成交量(亿股)"]print(quarterly.tail(4))
季末收盘 季度成交量(亿股) date 2025-12-31 2.044 459.41 2026-03-31 1.982 943.85 2026-06-30 2.188 630.04 2026-09-30 2.053 336.84

📌 注意最后一行2026-09-30:样本只到 7-31,所以这个"季度"其实只含 7 月数据,是个不完整桶。真实研究里常.iloc[:-1]砍掉最后一桶,或显式判断桶内天数。坑②会讲。

操作⑮ 由日收益聚合月收益:resample('ME').last().pct_change()

把日线收盘价先压成月线,再.pct_change()算"月涨跌幅"——这就是 S5 因子、S8 组合里"月度收益表"的源头:

monthly_ret=df["close"].resample("ME").last().pct_change().dropna()up=(monthly_ret>0).sum()print(f"上涨月:{up}下跌月:{len(monthly_ret)-up}")print((monthly_ret.tail(6)*100).round(2).astype(str).add("%"))
月度收益率序列(共 78 个月,去掉首月) 上涨月: 42 下跌月: 36 date 2026-02-28 0.24% 2026-03-31 -4.67% 2026-04-30 6.76% 2026-05-31 1.75% 2026-06-30 1.63% 2026-07-31 -6.17%

💡 顺便看一眼:近 6 个月 4 涨 2 跌,但单月波动能到 ±6%——这就是为什么要做组合、要控回撤。就这么一行resample,你已经摸到了"收益分布"的门槛。


五、新手最常踩的坑(避雷)

#现象原因解决办法
坑①df['2024']KeyError: '2024'按年/月取数是索引能力,必须走.locdf.loc['2024'],别写df['2024']
坑②resample最后一个月/季数据"缺一半"resample 会补齐到周期边界,末桶只有部分数据展示前.iloc[:-1]砍掉末桶,或过滤桶内天数
坑③df.close取不到列,或取到奇怪东西列名含空格/与 DataFrame 方法重名(如df.count)时属性访问失效永远用df['列名']双层写法,别用df.列名
坑④合并两张表后日期对不上、出现大量NaT一张是"naive"(无时区)、一张是"tz-aware"(有时区)统一用tz_localize(None)去掉时区,或都加同一时区
坑⑤df['2024']取数失效、排序像字符串read_csv把日期读成字符串set_index前忘了to_datetimedf['date'] = pd.to_datetime(df['date'])set_index
坑⑥resample('M')跑出FutureWarning旧频率别名'M'在新版 pandas 已废弃改用'ME'(月末)/'MS'(月初)/'QE'(季末)

坑① 现场演示(最阴的一个,90% 新手第一天就撞):

df=pd.read_csv("data/510300_daily.csv",encoding="utf-8-sig").set_index("date")df.loc["2024"]# ✅ 正确:242 天df["2024"]# ❌ KeyError: '2024'(它去找"叫 2024 的列"了)

坑⑤ 现场演示(为什么必须to_datetime):

raw=pd.read_csv("data/510300_daily.csv",encoding="utf-8-sig").set_index("date")print(raw.index.dtype)# object(字符串!)raw.loc["2024"]# ❌ 字符串索引不支持按年切片clean=raw.reset_index()clean["date"]=pd.to_datetime(clean["date"])clean=clean.set_index("date")print(clean.index.dtype)# datetime64[ns] ✅clean.loc["2024"]# ✅ 242 天

📌保命三连:① 读 CSV 永远encoding='utf-8-sig';②set_index前永远先to_datetime;③ 按年/月取数永远.loc不是[]。把这三条刻进肌肉记忆,pandas 入门的坑你直接跳过一半。


六、本篇小结 & 下篇预告

小结:本篇(上)用真实 A 股日线跑通了15 个量化必备操作,全在"索引 + 时间序列 + 重采样"三块:

  • 索引让你"找得准"——set_index把日期变成索引,.loc按日期取、.iloc按位置取、布尔索引按条件筛,且两列运算按索引自动对齐,天然防错;
  • 时间序列让你"切得动"——df.loc['2024']一行榨全年、'起':'止'含端切片、date_range造日历、DateOffset做偏移;
  • 重采样让你"看得远"——resample('ME'/'W'/'QE')把日线压成月/周/季线,agg({...})多指标同聚合,.pct_change()直接出月度收益。

记住三条保命准则:读 CSV 加utf-8-sig、设索引前先to_datetime、按年取数用.loc

下一篇预告:专栏 S2 的第四篇《Pandas 量化必备 30 操作(下):groupby、rolling、shift 与未来函数陷阱》。你会学到:怎么按"年/股票"分组算指标、rolling滚动窗口算均线、shift错位算收益率——以及为什么shift用错一步,你的回测会"偷看未来"直接变造假。这 15 个操作(⑯~⑳)直接决定你后面因子和回测的真假。准备好ashare环境,我们下篇见。


免责声明:本文所有内容仅用于量化投资技术教学与知识分享,文中涉及的代码示例、跑分数据与实证结果均不构成任何投资建议或个股推荐。投资有风险,入市需谨慎;实际交易前请务必用自己的真实数据充分回测,并充分了解相关风险。

返回列表