第 7 课 NumPy、pandas 和 Matplotlib 分别解决什么问题¶
三个名字容易让初学者误以为又要学习三门语言。它们都是 Python 工具包,分别擅长整组数值计算、带行列名称的表格处理和图形检查。本课用同一份四天行情贯穿全部示例。
Python 自带列表为什么还不够方便¶
第 4 课已经用列表保存一组价格。列表灵活,可以同时装数字、文字和其他容器,适合学习程序结构。量化研究还要处理几十万行数值、按日期筛选表格、计算整列变化并画图,全部手写循环会产生很多重复代码,也更容易连错索引。
NumPy、pandas 和 Matplotlib 接住了三类常见工作。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,并按社区习惯使用简称 np。np.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%。保留并打印 previous 与 current,能让读者确认相邻配对,避免只看最终结果。
NumPy 数组有两个重要属性。shape 描述每个维度多长,本例一维数组形状为 (4,);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)
输出表的每一行是一日记录,date、open 和 close 是三列。左侧还会显示 0 到 3 的行索引,这是 pandas 为表格提供的默认位置标签。索引与普通列表位置有相似之处,却还会参与后面的对齐和合并。
可以用 prices.shape 查看表格形状,结果应为 (4, 3),表示四行三列。prices.columns 会列出真实列名,prices.dtypes 会显示每列类型。刚读入一份陌生数据时,先看形状、列名、类型和前几行,比直接计算收益更安全。
单层和双层方括号分别选择什么¶
从 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”时,先查看 type 和 shape,通常就能分清当前拿到的是一维 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"]] 结果相同,第一次学习时拆开更清楚。
筛选以后修改数据时,要知道结果可能是原表的视图或副本。研究初稿可以明确写 .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 会在当前行与前一行之间计算比例变化。它只按照当前行顺序工作,所以必须先确认日期已经升序排列。四天价格会得到四行结果,其中第一天没有更早价格,无法计算收益。
第一行显示 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 | 一维和二维对象返回形状不同 | 查看 type、shape 和列名 |
| 相加两列时忽略索引 | 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 课 逐行读懂均线策略。