跳转至

第 7 课 NumPy、pandas 和 Matplotlib 分别解决什么问题

三个名字容易让初学者误以为又要学习三门语言。它们都是 Python 工具包,分别擅长整组数值计算、带行列名称的表格处理和图形检查。本课用同一份四天行情贯穿全部示例。

适合谁会读函数、列表和字典的读者
前置知识列表、字典、函数、导入和报错阅读
建议用时150 分钟,可以分两次完成
本课成果建立四天行情表,计算相邻收益并画出价格曲线

Python 自带列表为什么还不够方便

第 4 课已经用列表保存一组价格。列表灵活,可以同时装数字、文字和其他容器,适合学习程序结构。量化研究还要处理几十万行数值、按日期筛选表格、计算整列变化并画图,全部手写循环会产生很多重复代码,也更容易连错索引。

NumPy、pandas 和 Matplotlib 接住了三类常见工作。NumPy 处理形状整齐的数值数组,pandas 在数值之上增加行列名称、日期和缺失值,Matplotlib 把选定的列画成曲线、柱形或散点。它们可以互相传递数据,职责仍然不同。

Python、NumPy、pandas 与 Matplotlib 在量化研究中的分工

图中每个工具都对应一种问题。Python 负责程序规则,NumPy 负责成组数值,pandas 负责表格关系,Matplotlib 负责把异常形状显示出来。

flowchart LR
    accTitle: 四个工具怎样接成一条研究流程
    accDescr: Python 组织判断和函数,NumPy 执行成组数值计算,pandas 保存日期与字段关系,Matplotlib 把选定结果画出来供人检查

    a[Python<br/>变量、条件、循环、函数] --> b[NumPy<br/>统一类型的数值数组]
    b --> c[pandas<br/>带日期与列名的表格]
    c --> d[Matplotlib<br/>把结果画出来]
    d --> e[回到表格<br/>定位异常日期]

工具包不会决定收益该从收盘算到收盘,还是从下一个开盘开始。它们也不知道复权价格何时可见、停牌日怎样处理。金融定义和时间约束仍由研究者写清楚,工具只负责执行。

NumPy 怎样让一组数字同时运算

Python 列表乘以 2 会把列表内容重复两遍,NumPy 数组乘以 2 会让每个数值分别乘以 2。原因在于列表主要保存一组通用对象,NumPy 数组通常要求统一数值类型,并按规则排列在内存中,因此能够进行批量运算。

下面代码先导入 NumPy,并按社区习惯使用简称 npnp.array 把四个收盘价转换成数组。代码运行以前先预测结果,每项价格乘 2 应得到 20.0、20.4、20.2 和 20.8,平均价格则是四项相加再除以 4。

import numpy as np

prices = np.array([10.0, 10.2, 10.1, 10.4])
doubled = prices * 2
average_price = prices.mean()

print(doubled)
print(average_price)

输出数组为 [20. 20.4 20.2 20.8],平均价格为 10.175。显示中的 20. 表示浮点数 20.0。prices * 2 没有写循环,NumPy 会把乘法应用到全部元素,这种对整组数据执行同一计算的方式常被称为向量化

向量化通常更快,也更接近数学公式,不过它仍然依赖位置对应。两个长度相同的数组可以顺利相除,即使一个按周一到周四排列,另一个按周二到周五排列,程序也不会知道日期错了。NumPy 负责数值形状,日期含义需要研究者另外维护。

用两个切片计算相邻收益

四个价格会产生三段相邻收益。我们可以用切片把前三项作为旧价格,把后三项作为新价格,两组位置一一对应。开始写代码以前先列出配对关系,10.0 对 10.2,10.2 对 10.1,10.1 对 10.4。

previous = prices[:-1]
current = prices[1:]
returns = current / previous - 1

print(previous)
print(current)
print(returns)

prices[:-1] 从开头取到最后一项以前,得到前三项;prices[1:] 从索引 1 取到末尾,得到后三项。两组长度都是 3,位置相除后得到约 2%、负 0.98% 和 2.97%。保留并打印 previouscurrent,能让读者确认相邻配对,避免只看最终结果。

NumPy 数组有两个重要属性。shape 描述每个维度多长,本例一维数组形状为 (4,)dtype 描述元素使用哪种数据类型,本例通常是浮点数。机器学习常接收“样本数乘特征数”的二维数组,方向颠倒以后程序可能把特征当成样本,所以每次交给模型以前都应查看形状。

print(prices.shape)
print(prices.dtype)

pandas 为什么给数值加上行列名称

NumPy 数组能高效计算,单靠位置很难说明每列是什么、每行属于哪天。pandas 的 DataFrame 可以理解为一张带行名和列名的二维表,Series 则是一列带索引的数据。它们会把数值与标签放在一起,更适合行情和财务数据。

下面把第 4 课的列表套字典写法交给 DataFrame。每个字典是一行,字典键会成为列名。代码出现以前先看输入,四行都包含日期、开盘和收盘,程序应该得到四行三列的表。

import pandas as pd

rows = [
    {"date": "2026-01-05", "open": 10.0, "close": 10.2},
    {"date": "2026-01-06", "open": 10.2, "close": 10.1},
    {"date": "2026-01-07", "open": 10.1, "close": 10.4},
    {"date": "2026-01-08", "open": 10.4, "close": 10.6},
]

prices = pd.DataFrame(rows)
print(prices)

输出表的每一行是一日记录,dateopenclose 是三列。左侧还会显示 0 到 3 的行索引,这是 pandas 为表格提供的默认位置标签。索引与普通列表位置有相似之处,却还会参与后面的对齐和合并。

可以用 prices.shape 查看表格形状,结果应为 (4, 3),表示四行三列。prices.columns 会列出真实列名,prices.dtypes 会显示每列类型。刚读入一份陌生数据时,先看形状、列名、类型和前几行,比直接计算收益更安全。

print(prices.shape)
print(prices.columns)
print(prices.dtypes)
print(prices.head())

单层和双层方括号分别选择什么

从 DataFrame 选择一列时,在单层方括号里写列名,结果是一条 Series。选择多列时,把列名列表放进外层方括号,形成双层方括号,结果仍然是一张 DataFrame。

close_series = prices["close"]
small_table = prices[["date", "close"]]

print(type(close_series))
print(type(small_table))

第一项类型是 pandas Series,第二项类型是 pandas DataFrame。两者都能保存索引,维度不同会影响后面方法的返回形状。初学者遇到“为什么这一列没有 columns”时,先查看 typeshape,通常就能分清当前拿到的是一维 Series 还是二维表格。

给表格增加新列时,把新列名写在等号左边,等号右边提供长度与表格行数相同的数据。下面先手算每天收盘减开盘,再把结果保存为 intraday_change

prices["intraday_change"] = prices["close"] - prices["open"]
print(prices[["date", "open", "close", "intraday_change"]])

第一天结果为 0.2,第二天为负 0.1,第三天为 0.3,第四天为 0.2。代码利用同一行索引把两列相减,结果自动对应回原行。若两列来自不同表,索引是否一致就需要额外检查。

筛选表格为什么分成判断和取行两步

想保留收盘价高于开盘价的日期,先比较两列。比较结果是一条与表格同样长的布尔 Series,每行保存真或假。随后用这条布尔条件选择原表中的行。

is_up_day = prices["close"] > prices["open"]
up_days = prices[is_up_day]

print(is_up_day)
print(up_days)

第二天收盘低于开盘,对应条件为假,所以不会出现在 up_days 中。把判断和筛选拆成两个变量,可以先核对真假序列,再查看最终表格。写成一行 prices[prices["close"] > prices["open"]] 结果相同,第一次学习时拆开更清楚。

打开 pandas 表格探索器

筛选以后修改数据时,要知道结果可能是原表的视图或副本。研究初稿可以明确写 .copy(),表示后续修改只针对新表,减少“修改有没有写回原表”的含糊。

up_days = prices[is_up_day].copy()

日期文字为什么要转换成日期类型

当前 date 列仍然是文字。文字知道字符顺序,却不了解年月日、月份长度和交易时区。排序、计算日期间隔或按月汇总以前,应使用 pd.to_datetime 转成日期时间类型,再按日期排序。

prices["date"] = pd.to_datetime(prices["date"])
prices = prices.sort_values("date")

print(prices["date"].dtype)
print(prices[["date", "close"]])

代码先转换类型,再按照日期升序重新排列整行。必须排序整张表,不能只把日期列单独排序,否则价格会与日期分离。真实数据还要检查时区和交易所日历,同一个时间戳在上海、纽约和世界协调时间中可能指向不同交易阶段。

日期转换失败时,可能出现 NaT,它表示缺失的日期时间。应定位原始文字并检查格式,不要为了让排序通过而删除全部异常行。异常日期可能揭示表头混入数据、编码错误或数据供应商字段变化。

pct_change 怎样计算相邻收益

pandas 的 pct_change 会在当前行与前一行之间计算比例变化。它只按照当前行顺序工作,所以必须先确认日期已经升序排列。四天价格会得到四行结果,其中第一天没有更早价格,无法计算收益。

prices["return"] = prices["close"].pct_change()
print(prices[["date", "close", "return"]])

第一行显示 NaN,它是缺少可用数值的标记。这里的缺失有明确原因,第一天没有前一日价格。它不等于零收益,填成零会声称当天确实经历了一段没有涨跌的持有,而事实是我们不知道这一段。

pct_change 的结果应与 NumPy 切片手算一致。可以选择第二、三、四行,与前面约 2%、负 0.98% 和 2.97% 比较。工具方法减少了代码长度,无法免除小样例检查。

为了看清内部动作,可以把一行拆开。shift(1) 会把上一行收盘价移动到当前行旁边,随后当前价除以前价并减一。并排显示这三列,日期关系会更加直观。

prices["previous_close"] = prices["close"].shift(1)
prices["return_by_hand"] = (
    prices["close"] / prices["previous_close"] - 1
)

print(prices[["date", "previous_close", "close", "return_by_hand"]])

return_by_hand 应与 return 相同。把移动后的前价保留下来,会让后面的策略代码更容易审查;确认逻辑稳定以后,可以减少不需要的中间列。

pandas 索引为什么会自动对齐

pandas 的 Series 同时保存值和索引。两列相加时,它会先寻找相同索引,再对相同位置执行运算。下面两个 Series 分别覆盖周一周二和周二周三,只有周二同时存在。

left = pd.Series([10, 20], index=["周一", "周二"])
right = pd.Series([1, 2], index=["周二", "周三"])

print(left + right)

周二结果为 21,周一和周三缺少另一边,结果为 NaN。自动对齐能避免单纯按位置把不同日期相加,也会在索引类型不一致、时区不同或存在重复值时产生意外缺失。合并两张表以前,应检查索引是否唯一、日期类型是否一致;合并以后还要核对行数和缺失比例。

merge 成功只说明语法与字段存在,不能证明连接键正确。若同一天有多只股票,只按日期合并会产生多对多组合,行数可能突然增加。后面的面板数据课程会要求同时使用日期和股票代码。

Matplotlib 为什么只是检查入口

表格能够精确查看某一行,图形更容易暴露价格跳点、日期断档和长期趋势。Matplotlib 通常使用简称 plt。开始画图以前,我们已经拥有按日期排序的 prices 表,横轴使用日期,纵轴使用收盘价。

import matplotlib.pyplot as plt

plt.plot(prices["date"], prices["close"], marker="o")
plt.title("Teaching close prices")
plt.xlabel("Date")
plt.ylabel("Price")
plt.grid(alpha=0.3)
plt.show()

plot 决定横纵数据,marker="o" 在每个交易日画出一个点,后面几行补充标题、坐标名称和辅助网格,show 最后显示图形。标题使用英文只为避免部分本地环境缺少中文字库,网页正文仍用中文解释。

图形出现断崖时,应回到表格定位日期,查看原始价格、复权口径和数据来源。曲线平滑也不能证明数据正确,错误排序、重复日期和缺失填补可能仍画出一条顺眼的线。图形负责提出检查问题,表格和来源记录负责回答。

一张研究图应写清横轴时间、纵轴单位和数据口径。为了好看截断纵轴、加入平滑或使用双轴,都会改变读者感受,必须在图注中说明。画图函数最好读取已经计算好的列,不在内部悄悄重算策略,否则表格与图可能来自两套规则。

把三个工具接成一次可检查的小研究

现在把本课步骤连起来。输入是四天虚构行情,目标是转换日期、排序、计算相邻收益、筛选日内上涨日期,并画出收盘曲线。代码较长,所以先列出预期。表格应有四行,第一行收益缺失,第二到四行收益与手算相同,日内上涨日应排除第二天。

import pandas as pd
import matplotlib.pyplot as plt

rows = [
    {"date": "2026-01-05", "open": 10.0, "close": 10.2},
    {"date": "2026-01-06", "open": 10.2, "close": 10.1},
    {"date": "2026-01-07", "open": 10.1, "close": 10.4},
    {"date": "2026-01-08", "open": 10.4, "close": 10.6},
]

prices = pd.DataFrame(rows)
prices["date"] = pd.to_datetime(prices["date"])
prices = prices.sort_values("date")
prices["return"] = prices["close"].pct_change()

up_days = prices[prices["close"] > prices["open"]].copy()

assert len(prices) == 4
assert prices["date"].is_monotonic_increasing
assert prices["return"].isna().sum() == 1

print(prices)
print(up_days[["date", "open", "close"]])

plt.plot(prices["date"], prices["close"], marker="o")
plt.xlabel("Date")
plt.ylabel("Price")
plt.show()

代码可以按四段阅读。导入区取得工具,数据区建立表格,处理区转换和计算,检查与输出区验证行数、日期顺序和缺失数量。每一段都能单独运行并查看中间结果。若某处不符合预期,就停在该段排查,不要继续把错误传到图表。

这仍然不是完整策略。程序没有根据收益产生信号,也没有成交、仓位和成本。下一课会第一次阅读完整的均线策略代码,在那里把“数据、指标、信号、持仓和收益”连接起来。

容易弄错的地方

看起来省事的做法 它会怎样改变结果 更可靠的检查
不排序便计算 pct_change 函数按错误行顺序连接日期 转换日期,排序后检查单调性
把所有 NaN 填成零 未知值被伪造成真实零值 先解释每个缺失的来源
混用 Series 与 DataFrame 一维和二维对象返回形状不同 查看 typeshape 和列名
相加两列时忽略索引 pandas 会按标签而非单纯位置配对 并排查看索引与缺失结果
merge 成功便相信合并正确 错误键可能让行数增加或丢失 合并前后核对行数、唯一性和空值
图形看起来正常便相信数据 多种错误仍能画出平滑曲线 回到表格与原始来源定位日期

一分钟自测

prices[prices["close"] > prices["open"]] 分成哪两个动作?为什么第一行相邻收益会是 NaN

查看答案 内部比较先得到每行真假的布尔 Series,外层方括号再保留条件为真的行。第一行没有更早收盘价,无法形成相邻收益,所以应保留缺失标记。

本课术语

词语 现在应该怎样理解
NumPy 处理统一类型数值数组的 Python 工具包
数组 具有明确形状和统一数据类型的一组数值
向量化 对整组数值应用同一种运算
pandas 处理带索引和列名表格的 Python 工具包
DataFrame pandas 中带行列结构的二维表格
Series pandas 中带索引的一维数据
NaN 当前位置缺少可用数值的标记
Matplotlib 把数值和表格列画成图形的工具包

课后练习

1 选择工具

需要按日期筛选行情并保留三列时,优先使用哪个包?若只想让十万个数同时乘 2,又优先考虑哪个包?

提示比较带列名表格与统一数值数组的分工。
答案按日期筛选带列名表格优先使用 pandas,整组统一数值计算优先使用 NumPy。

2 解释缺失值

为什么 pct_change 第一行不能直接解释为零收益?

提示收益率需要当前价格与前一价格。
答案第一行没有前一价格,结果未知。零收益会声称价格确实没有变化,两种含义不同。

3 找出时间风险

行情顺序为周三、周一、周二,直接运行 pct_change 会发生什么?

提示函数只读取当前行与上一行,不知道真实日期。
答案它会按错误行顺序连接价格,得到没有时间意义的收益。应先把日期转成日期类型并排序整表。

4 动手修改

在完整示例中增加日内变化列 intraday_change,并筛选变化大于 0.15 元的行。

提示先用收盘减开盘建立新列,再用新列产生布尔条件。
答案写 `prices["intraday_change"] = prices["close"] - prices["open"]`,随后使用 `prices[prices["intraday_change"] > 0.15]`。

5 开放思考

为什么量化研究通常同时保留代码、表格和图,而不只看收益曲线?

评价要点回答应说明代码记录规则,表格保留具体日期与字段,图形暴露整体形状;三者需要互相核对,图形无法单独证明时间、口径和数据正确。

下一课会阅读第一段完整策略代码。我们会先写自然语言规则,再逐行连接数据、指标、信号、持仓和收益。继续阅读第 8 课 逐行读懂均线策略