查看全课大纲(第 41 / 52 节)
  1. 1.量化分析基本概念:从咖啡馆到华尔街
  2. 2.量化交易员学习路线:从零基础到策略开发
  3. 3.量化策略生命周期:数据分析的五大流程
  4. 4.量化数据类型:数值型与类别型数据
  5. 5.量化沙盒搭建(1):Python与Anaconda安装
  6. 6.量化沙盒搭建(2):Jupyter Notebook与库配置
  7. 7.本地多源数据:量化历史回测的基石
  8. 8.TXT数据流操作:处理金融交易日志
  9. 9.JSON数据流操作:解析股票行情API响应
  10. 10.CSV行情文件操作:读取与存储历史K线
  11. 11.Excel报表操作:清洗财报与财务指标
  12. 12.量化数据库:SQL常用查询语法
  13. 13.量化数据库:Python连接与写入
  14. 14.量化多表连接:关联股票信息与行情
  15. 15.实战:小象咖啡店营业数据与个股财务指标的多源数据分析
  16. 16.量化数据采集:爬虫工作原理与合规
  17. 17.量化爬虫核心:URL管理与去重策略
  18. 18.量化网页下载:防反爬与请求头伪装
  19. 19.网页解析(1):使用XPath定位财经新闻
  20. 20.网页解析(2):使用CSS选择器与ItemLoader
  21. 21.Scrapy框架:构建工业级量化爬虫
  22. 22.Scrapy实战(1):新建项目与数据模型定义
  23. 23.Scrapy实战(2):编写财经爬虫逻辑
  24. 24.Scrapy实战(3):启用数据管道清洗行情
  25. 25.Scrapy实战(4):防反爬中间件配置
  26. 26.实战:构建每日热门个股行情自动采集系统
  27. 27.NumPy与SciPy:加速量化矩阵计算
  28. 28.NumPy数组:构建股票价格矩阵
  29. 29.数组切片:提取特定股票与交易日数据
  30. 30.NumPy常用方法:计算均值与波动率
  31. 31.向量化计算:摆脱缓慢的For循环
  32. 32.通用函数ufunc:计算对数收益率与累计收益
  33. 33.实战:股票组合资产收益率与协方差矩阵计算
  34. 34.Pandas Series:管理单只股票收盘价
  35. 35.Pandas DataFrame:构建多只股票行情面板
  36. 36.Pandas Index:时间序列索引与对齐
  37. 37.DataFrame索引操作:loc与iloc定位行情
  38. 38.索引总结:量化多字段筛选与定位
  39. 39.运算与对齐:合并不同交易日的数据
  40. 40.Pandas map:对股票代码与状态做映射
  41. 41.Pandas apply:滚动计算技术指标
  42. 42.排序操作:按市盈率与收益率对股票排名
  43. 43.数据清洗:填充与剔除停牌股票缺失值
  44. 44.数据清洗:剔除重复报价与异常交易数据
  45. 45.常用统计:量化最大回撤与分位数风险评估
  46. 46.极值定位:寻找历史最高价与最低价的发生日
  47. 47.风险与累计收益:计算年化波动率与累计回报
  48. 48.实战:小象咖啡店营业数据与热门个股时间序列清洗分析
  49. 49.Series索引操作:按位置与标签访问行情数据
  50. 50.文件读写操作:CSV行情数据导入导出
  51. 51.数据清洗:替换异常报价数据
  52. 52.常用统计:sum/mean/median/count聚合指标

Pandas apply:滚动计算技术指标

约 15 分钟

Pandas 函数应用进阶:apply 轴向计算与全表映射(原 applymap)

小象实战讲义 · 量化交易Python入门:从咖啡馆到华尔街

上一节我们用 Series.map 处理了单列的值映射。这一节要解决的是更常见的批量计算场景:一张持仓表里多列之间要交叉运算、整张行情表的每个单元格要统一格式��。这正是 apply 与 map(原课讲授的 applymap)出场的地方。学完本节,你能清晰判断「什么时候用 map、什么时候用 apply」,并写出在现代 pandas 环境里可直接运行的批量计算代码。

💡 核心导读

  • 三个函数应用 API 的适用对象:Series.map 只作用于单列;DataFrame.apply 沿指定轴(行或列)批量计算;DataFrame.map 作用于整张表的所有单元格。
  • apply 的轴向语义:axis=1 表示沿行横向计算(多列交叉运算,如收盘价 × 持仓量 = 市值),axis=0 表示沿列纵向聚合。
  • 一次重要的版本变更:原课讲授的 applymap 在 pandas 3.0 中已被移除,官方统一为 DataFrame.map。本讲义一律使用新写法,并说明如何迁移旧代码。
  • 全部代码锁定现代环境实跑(Python 3.14 + pandas 3.0),输出可直接核对。

1. 三个函数应用 API 的定位边界

Pandas 提供了三层递进的函数应用能力,先厘清各自的适用范围:

┌────────────────────────────────────────────────────────────┐
│  函数应用 API 选区(量化数据处理场景)                     │
├──────────────┬─────────────────────────────────────────────┤
│ Series.map   │ 仅单列:元素级的值替换 / 映射               │
│ DataFrame.apply │ 沿 axis=0(列)或 axis=1(行)批量计算    │
│ DataFrame.map│ 整张表:对每个单元格做元素级变换            │
└──────────────┴─────────────────────────────────────────────┘

其中 apply 的关键在于轴向:axis=1(沿行)会把每一行当作一个 Series 传入函数,适合做「多列交叉」的计算(收盘价 × 持仓量 = 持仓市值);axis=0(沿列,默认)会把每一列当作 Series 传入,适合做列级聚合(如求均值、极差)。

下面在一张持仓快照上同时演示这两种轴向:

import pandas as pd

# 某量化组合的持仓与行情快照(硬编码小样例,输出确定)
positions = pd.DataFrame({
    "stock_code": ["600519", "000858", "601318", "600036"],
    "stock_name": ["贵州茅台", "五粮液", "中国平安", "招商银行"],
    "close": [1682.0, 142.5, 48.6, 36.2],
    "quantity": [100, 2000, 5000, 8000],
})

# axis=1:沿行横向计算——每一行以 Series 传入,用于多列交叉运算
positions["market_value"] = positions.apply(
    lambda row: row["close"] * row["quantity"], axis=1
)
print("持仓市值(axis=1 逐行计算):")
print(positions[["stock_name", "close", "quantity", "market_value"]])
持仓市值(axis=1 逐行计算):
  stock_name   close  quantity  market_value
0       贵州茅台  1682.0       100      168200.0
1        五粮液   142.5      2000      285000.0
2       中国平安    48.6      5000      243000.0
3       招商银行    36.2      8000      289600.0

注意这里的语义:函数体里用列名 row["close"]、row["quantity"] 取数,必须配 axis=1。如果误写成 axis=0,每一列会被当成一个 Series 传入,row["close"] 立刻抛 KeyError——这是量化数据处理里最常见的轴向错误。

再看 axis=0 的列级聚合:

import pandas as pd

# 三只标的的日收益率(硬编码小样例,输出确定)
returns = pd.DataFrame({
    "600519": [0.012, -0.008, 0.005, 0.011],
    "000858": [-0.015, 0.006, 0.009, -0.004],
    "601318": [0.003, 0.002, -0.006, 0.007],
})

# axis=0:沿列纵向聚合——对每只标的分别求平均日收益率
print("各标的平均日收益率(axis=0 列级聚合):")
print(returns.apply(lambda col: col.mean()))
各标的平均日收益率(axis=0 列级聚合):
600519    0.0050
000858   -0.0010
601318    0.0015
dtype: float64

把两者对照起来看:axis=1 的结果长度等于行数(每行一个值),axis=0 的结果长度等于列数(每列一个值)。判断口诀:看你要的结果是「每行一个」还是「每列一个」。


2. 全表元素级映射:DataFrame.map(原 applymap)

量化数据里常见的另一类需求是格式统一,比如把收益率矩阵里的小数统一转成百分比字符串。原课使用的 DataFrame.applymap 正是完成这件事的经典函数,但在 pandas 3.0 中 applymap 已被移除,官方给出的等价写法是 DataFrame.map——语义完全一致,只是方法名与 Series.map 统一了。因此本讲义一律使用 DataFrame.map。

import pandas as pd

# 收益率矩阵:3 只标的 × 4 个交易日(硬编码小样例,输出确定)
returns = pd.DataFrame({
    "600519": [0.012, -0.008, 0.005, 0.011],
    "000858": [-0.015, 0.006, 0.009, -0.004],
    "601318": [0.003, 0.002, -0.006, 0.007],
})

# DataFrame.map:pandas 3.0 起取代已移除的 applymap,逐单元格变换
pct = returns.map(lambda x: f"{x * 100:.2f}%")
print("逐单元格转百分比(DataFrame.map):")
print(pct)

# Series.map:仅单列,按字典做值映射
s = pd.Series(["A股", "港股", "A股", "美股"])
print("\n市场分类编码(Series.map):")
print(s.map({"A股": "A", "港股": "H", "美股": "US"}))
逐单元格转百分比(DataFrame.map):
   600519  000858  601318
0   1.20%  -1.50%   0.30%
1  -0.80%   0.60%   0.20%
2   0.50%   0.90%  -0.60%
3   1.10%  -0.40%   0.70%

市场分类编码(Series.map):
0     A
1     H
2     A
3    US
dtype: str

DataFrame.map 的关键特点是逐单元格、无视列名:它把函数依次作用到每一个格子,函数只关心「这个值是什么」,不关心「它在哪一列」。上面把小数收益率转成带百分号的字符串,就是它的典型用途。

版本提示:若你在旧教程或原课视频中看到 df.applymap(...),只需把方法名替换为 df.map(...) 即可,参数与返回行为不变。这是 pandas 3.0 的一次统一命名变更,不影响数据分析思路。


3. 三者的选择经验与常见坑

实际工作中按下面的顺序判断即可:

  • 只处理一列的值映射(如把「市场分类」换成代码)→ 用 Series.map;
  • 需要沿行或沿列把多列组合成一个结果(如按行算持仓市值、按列算平均收益)→ 用 DataFrame.apply;
  • 需要对整张表的每个单元格做同一个轻量变换(转百分比、统一格式化)→ 用 DataFrame.map。

因为 DataFrame.map 无视列名,对混合类型表格要格外小心:如果表里既有数值列又有文本列,函数必须能同时处理两种类型,否则会抛异常。下面的写法用类型判断做防护,这在因子权重表这类混合表里很常见:

import pandas as pd

# 因子权重表:权重列是小数,因子名列是文本(硬编码小样例)
weights = pd.DataFrame({
    "factor": ["momentum", "value", "quality"],
    "weight": [0.35, 0.45, 0.20],
})

# DataFrame.map 对全表逐元素生效,故需按类型分支处理
formatted = weights.map(
    lambda v: f"{v:.2f}" if isinstance(v, float) else v
)
print("因子权重表格式化结果:")
print(formatted)
因子权重表格式化结果:
     factor weight
0  momentum   0.35
1     value   0.45
2   quality   0.20

两个常见坑:

  1. 轴向写反:axis=1 与 axis=0 的语义完全不同。报错时第一件事就是检查轴向——尤其是函数体里用列名取数却传了 axis=0,会直接 KeyError。
  2. 性能预期:apply / map 本质上是把 Python 函数逐元素或逐切片地作用上去,属于 Python 级循环,适合「逻辑复杂但数据量不大」的场景。当计算本身可以用 NumPy 向量化表达时(例如整列统一加减乘除),应优先用向量化写法——在百万行级别的日频数据上,两者速度差别非常明显。

📝 动手练一练

  1. 概念辨析题:请用一句话分别说明 Series.map、DataFrame.apply、DataFrame.map 的适用对象。

    👉 点击查看参考答案

    参考答案:

    • Series.map:只作用于单个 Series(一列),做元素级的值替换或映射;
    • DataFrame.apply:作用于 DataFrame,沿 axis=0(按列)或 axis=1(按行)批量计算;
    • DataFrame.map:作用于整张 DataFrame,对每一个单元格做元素级变换(即原 applymap)。
  2. 编程练习题:给定 df = pd.DataFrame({'a': [1, 2, 3], 'b': [4, 5, 6]}),请用 apply 沿 axis=1 新增一列 row_sum,值为每行 a 与 b 之和。

    👉 点击查看参考答案

    参考答案:

    import pandas as pd
    
    df = pd.DataFrame({'a': [1, 2, 3], 'b': [4, 5, 6]})
    df['row_sum'] = df.apply(lambda row: row['a'] + row['b'], axis=1)
    print(df)
a  b  row_sum
0  1  4        5
1  2  5        7
2  3  6        9
  1. 迁移练习题:原课用 applymap 完成了「把权重矩阵的小数统一格式化为两位小数字符串」。请写出 pandas 3.0 下的等价写法,并说明需要改动方法名的原因。

    👉 点击查看参考答案

    参考答案:

    import pandas as pd
    
    weights = pd.DataFrame({"w1": [0.1234, 0.5678], "w2": [0.9, 0.1]})
    print(weights.map(lambda v: f"{v:.2f}"))
         w1    w2

0 0.12 0.90 1 0.57 0.10

原因:`DataFrame.applymap` 在 pandas 3.0 中已被移除,官方将其更名为 `DataFrame.map`,语义与参数完全不变,因此只需把方法名 `applymap` 换成 `map` 即可,**不要再使用 `applymap`**。

</details>

---

## 本章小结

- 掌握了 `DataFrame.apply` 的 `axis=0`(列级聚合)与 `axis=1`(行级交叉计算)两种轴向用法,并记住判断口诀:**要「每行一个结果」用 `axis=1`,要「每列一个结果」用 `axis=0`**;
- 掌握了 `DataFrame.map` 对整张表逐元素变换的方法,并知晓它是 **pandas 3.0 中 `applymap` 的替代写法**(原 `applymap` 已移除,练习与小结中均不应再出现该名称);
- 厘清了 `map`、`apply`、`map`(原 `applymap`)三者的选择边界与版本差异。

### 📋 行动清单

- [ ] 在本地执行本节代码,观察 `axis=0` 与 `axis=1` 两种输出的差异。
- [ ] 找一个旧教程里的 `applymap` 例子(含练习题),把它改写成 `DataFrame.map` 并跑通。
- [ ] 检查自己手上的量化项目里是否还有 `applymap` 调用,一并迁移。

—— 小象教研组