18 Matplotlib库基础
18.1 引言可视化的重要性
金融数据可视化:
- 发现模式: 趋势、周期、异常
- 沟通结果: 图表比数字更直观
- 辅助决策: 支撑交易决策
18.2 本章学习目标
通过本章学习,你将能够:
- 说出 Figure(画布)与 Axes(坐标轴)的层次关系,区分
plt.*快捷接口与ax.*面向对象接口,并根据场景选择 - 配置
plt.rcParams实现中文与负号的正常显示,并排查”中文变方框”的三类常见原因 - 绘制折线图、柱状图、散点图、饼图四类基础图形,并设置图例、坐标轴标签、标题、网格与注释
- 读懂并复现平台按揭任务:解释
pmt、ipmt、ppmt三者的关系与numpy_financial的现金流符号约定 - 用
plt.savefig()保存图形,并说明保存与显示的先后顺序
先修:章节 5、章节 6、章节 7(第5–7章 NumPy 数组基础,本章绘图所用的数据均为 NumPy 数组或 Python 列表)
18.3 画布与坐标轴:Matplotlib 的心智模型
在逐行拆解平台代码之前,先建立 Matplotlib 的层次模型,后面所有示例都会反复用到它:
- Figure(画布): 最高层的容器,一张画布承载整幅图,负责整体尺寸、导出(
savefig)等 - Axes(坐标轴): 真正”画图”的对象,每个 Axes 拥有独立的 x 轴、y 轴、刻度、图例与标题;一张画布上可以摆放多个 Axes(子图)
与之对应有两套用法等价的接口:plt.* 快捷接口(pyplot 状态机,自动管理”当前画布/当前坐标轴”,单图快速出图最省事)与 ax.* 面向对象接口(先显式拿到 fig、ax,再调用其方法,多子图与保存复用时对象明确、不会画错地方)。两套接口可以混用,但同一处尽量统一。
# ==================== 导入必要的库 ====================
import matplotlib.pyplot as plt # pyplot模块:提供plt.*快捷接口
import numpy as np # NumPy用于生成数据
# ==================== 准备两行数据 ====================
months = np.arange(1, 13) # 1至12月
fund_a = np.array([1.00, 1.02, 0.99, 1.05, 1.08, 1.06, 1.10, 1.12, 1.09, 1.15, 1.18, 1.20]) # 进取型基金净值
fund_b = np.round(np.linspace(1.00, 1.15, 12), 2) # 稳健型基金净值:缓慢爬升
# ==================== 面向对象接口(推荐) ====================
# subplots()一次返回两个对象:画布fig与坐标轴ax
fig, ax = plt.subplots(figsize=(8, 4)) # 画布宽8英寸、高4英寸,默认包含1个坐标轴
ax.plot(months, fund_a, label='进取型基金') # 在ax上画第一条线
ax.plot(months, fund_b, '--', label='稳健型基金') # 在ax上画第二条线
ax.set_xlabel('月份') # 面向对象接口的轴设置带set_前缀:ax.set_xlabel()
ax.set_ylabel('单位净值') # ax.set_ylabel()
ax.set_title('面向对象接口绘制') # ax.set_title()
ax.legend() # 显示图例
# ==================== 观察层次结构 ====================
print('画布类型:', type(fig).__name__) # Figure:最高层容器
print('坐标轴类型:', type(ax).__name__) # Axes:真正画图的对象
print('画布上坐标轴个数:', len(fig.axes)) # 1
# plt.*接口背后操作的是"当前"坐标轴,可用plt.gca()查看,此处即ax
print('plt.gca()与ax是同一对象:', plt.gca() is ax) # True
plt.tight_layout() # 自动调整布局
plt.show() # 显示图形把上面代码中的 ax.plot 换成 plt.plot、ax.set_xlabel 换成 plt.xlabel,得到的图完全一样——plt.* 总是作用于”当前”坐标轴(plt.gca() 可查看它)。单图快速探索用 plt.* 足够;一旦涉及多子图(如本章”图表类型”示例的 2×2 布局)或画完还要保存、继续修改,显式的 fig/ax 写法对象清晰、不易出错,后续章节也以它为主。
18.4 中文显示与负号
Matplotlib 默认字体 DejaVu Sans 不含中文字形,直接画中文标题会得到一排方框;负号也有独立的坑:默认用 Unicode 减号(U+2212)渲染,而部分中文字体恰恰缺这个字形。两行 rcParams 配置一并解决:
# ==================== 导入必要的库 ====================
import matplotlib.pyplot as plt # Matplotlib绘图库
# ==================== 两项关键配置 ====================
# 第一行:默认字体DejaVu Sans不含中文字形,指定中文字体为黑体SimHei
plt.rcParams['font.sans-serif'] = ['Source Han Serif SC', 'SimHei'] # 规范字体在前,SimHei 仅作后备 # Windows自带黑体;macOS/Linux可换本机已有中文字体
# 第二行:默认负号用Unicode减号U+2212渲染,中文字体常缺该字形
plt.rcParams['axes.unicode_minus'] = False # 改用普通ASCII连字符'-'显示负号
# ==================== 绘制含中文与负值的图 ====================
months = range(1, 7) # 1至6月
profit = [12, -5, 8, -3, 15, -8] # 月度盈亏(万元),含负值
fig, ax = plt.subplots(figsize=(7, 3)) # 创建画布与坐标轴
ax.bar(months, profit, color='steelblue') # 柱状图
ax.axhline(0, color='black', linewidth=0.8) # 0参考线,区分盈亏两色区域
ax.set_title('月度盈亏(检验中文标题与负号显示)') # 中文标题
ax.set_xlabel('月份') # 中文x轴标签
ax.set_ylabel('盈亏(万元)') # 中文y轴标签
# ==================== 查看配置是否生效 ====================
print('font.sans-serif =', plt.rcParams['font.sans-serif']) # 打印当前字体设置
print('axes.unicode_minus =', plt.rcParams['axes.unicode_minus']) # 打印当前负号设置
plt.tight_layout() # 自动调整布局
plt.show() # 显示图形“中文变方框”的三类常见原因与降级办法:
- 本机没有所指定的字体:
SimHei是 Windows 字体,macOS/Linux 常未安装。此时 matplotlib 回退到 DejaVu Sans,控制台打印findfont警告,图上中文渲染为方框。降级办法:把字体名换成本机已有的中文字体(如Microsoft YaHei、Noto Sans CJK SC),或暂时改用英文标签 - 字体名拼写错误: 同样触发
findfont警告,检查引号内的名称是否与系统字体名完全一致 - 负号与中文是两件事: 负号方框由
axes.unicode_minus单独控制,与中文字体设置互不影响
教学平台环境已配置中文字体,以平台渲染为准;本书本地校验环境(peter)未安装中文字体,运行本块时控制台输出正常、图上中文显示为方框并伴随 findfont 警告,属于预期现象而非代码错误。第20、21章的平台代码也都以同样的字体配置开头(设 font.sans-serif),本章平台任务则用等价的 plt.rcParams['font.family'] = 'SimHei'。
18.5 基础绘图
业务背景:等额本息按揭
住房按揭中最常用的还款方式是等额本息:借款人在整个贷款期内每月支付一个固定总额(月供),30年期即360期,每期金额不变。但每期月供内部由两部分构成——当期利息与当期偿还本金,随着剩余本金逐月下降,利息占比越来越小、本金占比越来越大。numpy_financial 库恰好提供了对应的三个函数:pmt 给出每月的月供总额,ipmt 给出某期的利息部分,ppmt 给出某期的本金部分。
任务要求(归纳自块内任务注释与代码):设置中文字体与负号显示参数;导入 numpy_financial;按给定参数(本金 800 万元、年利率 5%、期限 30 年)计算第 360 期偿还的本金部分 ppmt 并用 print 输出(保留 2 位小数);生成 360 期的期数数组,逐期计算本金部分与利息部分;用面向对象接口(fig.add_subplot)在同一坐标轴上绘制三条曲线——每月偿还金额、每月偿还本金、每月偿还利息(均为取负后的正值),设置 x/y 轴标签与图例;将图形保存为 1.png。请将代码原样输入教学平台(注释除外),判定以平台为准。
平台任务(平台原始代码)
以下代码与教学平台任务要求完全一致:
# ⚠️ 平台原始代码 - 请原样输入至教学平台(注释除外),平台才会判定答案正确
# 注:numpy_financial 包本地 peter 环境已安装(1.0.0),教学平台亦已内置
import matplotlib.pyplot as plt # 导入Matplotlib绘图库
plt.rcParams['font.family'] = 'SimHei' # 设置Matplotlib全局参数
plt.rcParams['axes.unicode_minus'] = False # 设置Matplotlib全局参数
import numpy_financial as npf # 导入NumPy数值计算库
import numpy as np # 导入NumPy数值计算库
r=0.05 # 贷款的年利率
n=30 # 贷款的年数
principle=8e6 # 设置期数/数量为8000000
pay_month=npf.ppmt(rate=r/12,per=n*12,nper=n*12,pv=principle,fv=0,when='end') #计算第360期(末期)偿还的本金部分(ppmt)
print('每月的偿还的金额',round(pay_month,2)) # 输出每月的偿还的金额
T_list=np.arange(n*12)+1 #生成一个包含每次还款期限的数组
# 计算每期应还的本金部分
prin_month=npf.ppmt(rate=r/12,per=T_list,nper=n*12,pv=principle,fv=0,when='end')
# 计算每期应还的利息部分
inte_month=npf.ipmt(rate=r/12,per=T_list,nper=n*12,pv=principle,fv=0,when='end')
pay_month_list=pay_month*np.ones_like(prin_month) # 生成与prin_month等长的每月总还款额数组
fig=plt.figure(figsize=(9,6)) # 创建图形画布
ax=fig.add_subplot(111) # 添加单个子图占据整个画布
ax.plot(T_list,-pay_month_list,'r-',label=u'每月偿还金额') # 在子图上绘制曲线
ax.plot(T_list,-prin_month,'m--',label=u'每月偿还本金金额') # 在子图上绘制曲线
ax.plot(T_list,-inte_month,'b--',label=u'每月偿还利息金额') # 在子图上绘制曲线
ax.set_xlabel(u'逐次偿还的期限(月)',fontsize=14) # 设置X轴标签
ax.set_ylabel(u'金额',fontsize=14) # 设置Y轴标签
ax.legend() # 设置图例
plt.savefig("1.png") # 保存图形至文件预期输出(本机 peter 环境实际运行结果——numpy_financial 1.0.0 已可本地实跑并复现下述数值,在线成书本块仍设 eval: false 只回显代码,具体以平台运行结果为准):
- 控制台输出一行:
每月的偿还的金额 -42767.53。负号来自现金流方向约定(还款为付出现金);该值是ppmt(360)即第 360 期偿还的本金部分,量级约 4.28 万元,与真实月供约 4.29 万元(月供口径pmt = -42945.73)非常接近,原因见下方深度解析 - 生成文件
1.png:一张含三条曲线的折线图——一条红色水平直线(“每月偿还金额”,实际画在末期本金 42767.53 元的高度)、一条逐期上升的洋红色虚线(本金)、一条逐期下降的蓝色虚线(利息);任意一期本金与利息之和恒等于月供,两线在 360 期内呈”剪刀差”收敛 - 图上中文(图例、轴标签)在平台环境正常显示;若运行环境未配置中文字体,中文会显示为方框(见上文”中文显示与负号”一节)
18.5.1 代码深度解析
pmt、ipmt、ppmt 三者的关系。对同一笔贷款,任意一期都满足恒等式 ppmt + ipmt = pmt:pmt 是等额本息下不变的月供总额,ipmt 是该期的利息部分,ppmt 是该期归还的本金部分。利息逐期递减、本金逐期递增,但两者之和始终等于月供。
per=n*12 选中哪一期。per 参数指定要计算的期数编号。平台代码中 n=30,故 per=n*12 即 per=360,选中的是第360期(最后一期)。此时剩余本金已非常小,当期利息极少,因此 ppmt(360) 几乎等于整个月供——用平台参数(本金800万元、年利率5%、30年)实算:第360期本金部分 ppmt(360) = -42767.53 元,末期利息 ipmt(360) = -178.20 元,而真正的等额本息月供 npf.pmt(...) = -42945.73 元(约4.29万元)。平台代码把它当作”每月偿还金额”画成水平线,图中该线实际位于末期本金水平(42767.53元)而非真实月供(42945.73元),两者数值接近只是末期”利息几乎还清”的结构性结果。
为什么数值是负的。numpy_financial 沿用现金流方向约定:pv 为正表示”现在收到钱”(借入),则每期还款是”付出去的钱”,符号为负。因此 pmt、ppmt、ipmt 的返回值均为负,平台代码绘图时对序列取负号(如 -prin_month)把它们翻成正的金额再画到图上。
与第54章的口径差异。第54章的同一按揭任务(本金600万元、年利率4.9%、30年)用的是 pmt,即月供口径;本章平台任务用 ppmt(360),即末期本金口径。两章函数选择不同、口径不同,但各自与平台判定答案一致,均无对错之分。
# 注:本块设为 eval: false 仅回显代码;numpy_financial 本地 peter 环境已可运行(下方数值经实跑核实)
# 输出数值已在上方正文给出,并经等额本息公式与360期摊还验算(末期余额归零)核实
# 与平台任务相同的参数:本金800万元,年利率5%,30年
r, n, principle = 0.05, 30, 8e6
monthly = npf.pmt(rate=r/12, nper=n*12, pv=principle, fv=0, when='end') # 月供总额:-42945.73
ppmt_360 = npf.ppmt(rate=r/12, per=n*12, nper=n*12, pv=principle, fv=0, when='end') # 第360期本金:-42767.53
ipmt_360 = npf.ipmt(rate=r/12, per=n*12, nper=n*12, pv=principle, fv=0, when='end') # 第360期利息:-178.20
print('月供总额 pmt =', round(monthly, 2)) # -42945.73
print('第360期本金 ppmt =', round(ppmt_360, 2)) # -42767.53
print('第360期利息 ipmt =', round(ipmt_360, 2)) # -178.20
print('ppmt + ipmt == pmt ?', abs(ppmt_360 + ipmt_360 - monthly) < 1e-6) # True:任意一期本金+利息=月供# =============================================================================
# 题目:按揭贷款本息偿还可视化
# =============================================================================
# 本任务演示如何使用Matplotlib绘制金融时间序列图
# 场景:可视化30年期按揭贷款的本金和利息偿还过程
# ==================== 导入必要的库 ====================
import matplotlib.pyplot as plt # Matplotlib的pyplot模块,提供类似MATLAB的绘图接口
import numpy as np # NumPy数值计算库
# ==================== 按揭贷款参数 ====================
# principal:贷款本金(100万元)
principal = 1000000
# rate:年利率(4.9%,这是典型的商业贷款利率)
rate = 0.049
# years:贷款期限(30年)
years = 30
# ==================== 计算月供(等额本息) ====================
# monthly_rate:月利率 = 年利率 / 12
monthly_rate = rate / 12
# n_months:总月数 = 年数 * 12
n_months = years * 12
# 月供计算公式(等额本息)
# monthly_payment = principal * [r(1+r)^n] / [(1+r)^n - 1]
# 这是标准的按揭贷款月供公式
monthly_payment = principal * (monthly_rate * (1 + monthly_rate)**n_months) / ((1 + monthly_rate)**n_months - 1)
# ==================== 计算每年的本金和利息 ====================
# years_array:年份序列 [1, 2, 3, ..., 30]
years_array = np.arange(1, years + 1)
# balance:剩余本金,初始值为贷款总额
balance = principal
# interest_paid:每年支付的利息列表
interest_paid = []
# principal_paid:每年偿还的本金列表
principal_paid = []
# ==================== 逐年计算本金和利息 ====================
# for循环:遍历每一年
for year in years_array:
# 初始化当年的利息和本金
annual_interest = 0 # 当年累计利息
annual_principal = 0 # 当年累计本金
# 内层循环:遍历当年的12个月
for _ in range(12):
# interest:当月利息 = 剩余本金 * 月利率
interest = balance * monthly_rate
# principal_part:当月本金 = 月供 - 当月利息
principal_part = monthly_payment - interest
# 更新剩余本金:减去当月偿还的本金
balance -= principal_part
# 累加到年度总额
annual_interest += interest
annual_principal += principal_part
# 将年度数据添加到列表
interest_paid.append(annual_interest)
principal_paid.append(annual_principal)
# ==================== 绘图 ====================
# plt.figure():创建画布
# figsize=(10, 6):画布宽度10英寸,高度6英寸
plt.figure(figsize=(10, 6))
# plt.plot():绘制折线图
plt.plot(years_array, interest_paid, label='利息', linewidth=2)
plt.plot(years_array, principal_paid, label='本金', linewidth=2)
# ==================== 设置图表元素 ====================
# plt.xlabel():设置X轴标签
plt.xlabel('年份', fontsize=12)
# plt.ylabel():设置Y轴标签
plt.ylabel('金额(元)', fontsize=12)
# plt.title():设置图表标题
plt.title('按揭贷款30年本金利息分解', fontsize=14)
# plt.legend():显示图例
plt.legend(fontsize=12)
# plt.grid():显示网格线
# alpha=0.3:网格线透明度30%
plt.grid(True, alpha=0.3)
# ==================== 添加注释 ====================
# plt.annotate():在图表上添加文本注释
# xy=(5, interest_paid[4]):箭头指向的坐标(第5年,第5年的利息)
# xytext=(10, interest_paid[4]):文本位置(第10年,第5年的利息高度)
# arrowprops=dict(arrowstyle='->'):箭头样式(单线箭头)
plt.annotate('前期利息占比高', xy=(5, interest_paid[4]), xytext=(10, interest_paid[4]),
arrowprops=dict(arrowstyle='->'))
# ==================== 调整布局并显示 ====================
# plt.tight_layout():自动调整子图间距,避免元素重叠
plt.tight_layout()
# plt.show():显示图表
plt.show()
# ==================== 打印统计信息 ====================
# f-string:格式化字符串
# .2f:保留2位小数
print(f"月供: {monthly_payment:.2f}元")
# 月供 * 月数:总还款额
print(f"总还款: {monthly_payment * n_months:.2f}元")
# 总还款 - 本金:总利息
print(f"总利息: {monthly_payment * n_months - principal:.2f}元")18.6 图表类型
# =============================================================================
# 题目:常用图表类型示例
# =============================================================================
# 本任务演示Matplotlib中常用的四种图表类型
# 应用场景:投资组合数据的多样化展示
# ==================== 导入必要的库 ====================
import matplotlib.pyplot as plt # Matplotlib绘图库
import numpy as np # NumPy数值计算库
# ==================== 创建子图布局 ====================
# plt.subplots(2, 2):创建2行2列的子图布局(共4个图)
# figsize=(12, 10):画布宽度12英寸,高度10英寸
# fig:整个画布对象
# axes:子图数组(2x2的numpy数组)
fig, axes = plt.subplots(2, 2, figsize=(12, 10))
# ==================== 1. 折线图(Line Plot)====================
# np.linspace(0, 10, 100):在[0, 10]区间生成100个等间距点
x = np.linspace(0, 10, 100)
# y = sin(x):计算正弦值
y = np.sin(x)
# axes[0, 0]:第一个子图(左上角)
# .plot():绘制折线图
# linewidth=2:线宽2
axes[0, 0].plot(x, y, linewidth=2)
# .set_title():设置子图标题
axes[0, 0].set_title('折线图')
# .grid():显示网格
axes[0, 0].grid(True)
# ==================== 2. 柱状图(Bar Chart)====================
# categories:类别列表
categories = ['A', 'B', 'C', 'D']
# values:对应的数值
values = [20, 35, 30, 25]
# axes[0, 1]:第二个子图(右上角)
# .bar():绘制柱状图
# color='steelblue':柱子颜色(钢蓝色)
axes[0, 1].bar(categories, values, color='steelblue')
axes[0, 1].set_title('柱状图')
# ==================== 3. 散点图(Scatter Plot)====================
# np.random.seed(42):设置随机种子,确保散点图每次运行结果相同
np.random.seed(42)
# np.random.randn(100):生成100个标准正态分布随机数
x = np.random.randn(100)
y = np.random.randn(100)
# axes[1, 0]:第三个子图(左下角)
# .scatter():绘制散点图
# alpha=0.6:点的透明度60%(重叠部分会变深)
axes[1, 0].scatter(x, y, alpha=0.6)
axes[1, 0].set_title('散点图')
# .set_xlabel():设置X轴标签
axes[1, 0].set_xlabel('X')
# .set_ylabel():设置Y轴标签
axes[1, 0].set_ylabel('Y')
# ==================== 4. 饼图(Pie Chart)====================
# sizes:各部分的大小(占比)
sizes = [30, 20, 25, 25]
# labels:各部分的标签
labels = ['股票', '债券', '现金', '其他']
# axes[1, 1]:第四个子图(右下角)
# .pie():绘制饼图
# labels:各部分的标签
# autopct='%1.1f%%':自动显示百分比(保留1位小数)
axes[1, 1].pie(sizes, labels=labels, autopct='%1.1f%%')
axes[1, 1].set_title('投资组合分布')
# ==================== 调整布局并显示 ====================
# plt.tight_layout():自动调整子图间距
plt.tight_layout()
# plt.show():显示所有子图
plt.show()18.7 本章小结
要点:
- Matplotlib 是层次结构:Figure(画布)是容器,Axes(坐标轴)才是画图对象;
plt.*快捷接口操作”当前”坐标轴,ax.*面向对象接口对象明确,多子图与保存复用时优先使用后者 - 中文与负号靠两行
rcParams配置:font.sans-serif指定中文字体、axes.unicode_minus = False改用 ASCII 减号;中文变方框的根源是当前字体不含中文字形 - 折线图看趋势、柱状图比大小、散点图看关系、饼图看占比;图例、轴标签、标题、网格、注释是图表的五项基本要素
- 等额本息的结构:月供总额不变,内部利息逐期递减、本金逐期递增,任意一期满足
ppmt + ipmt = pmt;numpy_financial按现金流方向取符号,借入为正、还款为负,绘图时常取负翻正 plt.savefig()要在plt.show()之前调用:在 Jupyter/教学平台环境中,show()展示后当前图会被关闭,之后再保存得到的是空图
易错点:
- 本地没有
SimHei等中文字体时,图上中文显示为方框并伴随findfont警告——是环境问题而非代码错误,可换本机已有中文字体或以平台渲染为准 - 多子图循环绘图时用
plt.*接口容易把线画到”当前”坐标轴(而非想要的子图)上,应改用axes[i, j].plot - 直接把
npf.pmt/npf.ppmt的返回值画图会得到负值曲线,忘记取负是平台任务最常见的绘图偏差 plt.subplots(2, 2)返回的是 2×2 的坐标轴数组,用axes[0, 0]索引;只有一个子图时返回的才是单个 Axes 对象- 饼图的
autopct='%1.1f%%'中要写两个%%才能显示百分号,漏写会报格式化错误
18.8 动手与思考
以下练习每题附参考答案(默认折叠)。请先独立完成并写下你的判断,再点开对照,最后上机验证。
输出预测:不运行代码,先写出下面代码的输出结果,再上机检验你的判断。
import matplotlib.pyplot as plt fig, axes = plt.subplots(2, 3) print(type(axes).__name__, axes.shape) print(len(fig.axes)) print(plt.gca() is axes[1, 2])参考答案(先写下你的预测再点开)
解题思路:逐行推演。
plt.subplots(2, 3)一次创建 2 行 3 列共 6 个坐标轴;多子图情形下第二个返回值axes是 NumPy 数组而非单个坐标轴对象,故第 1 个print输出类型名与形状ndarray (2, 3)。fig.axes是画布上全部坐标轴对象的列表,故第 2 个print输出6。plt.gca()返回“当前”坐标轴——subplots按行逐个创建坐标轴,最后创建的是第 2 行第 3 列(右下角)那个,创建完成后它仍是“当前”坐标轴,因此第 3 个print输出True。这段代码只创建画布、未画任何数据,6 个空坐标轴依然会随画布一起生成。# 验证脚本:逐行输出四个表达式的求值结果 import matplotlib.pyplot as plt # 导入pyplot快捷接口 fig, axes = plt.subplots(2, 3) # 创建2行3列共6个坐标轴,axes是2×3数组 print(type(axes).__name__, axes.shape) # axes的类型名与形状 print(len(fig.axes)) # 画布上坐标轴对象的个数 print(plt.gca() is axes[1, 2]) # “当前”坐标轴是否即最后创建的右下角子图预期输出(本机 peter 环境实际运行结果,具体以平台运行结果为准):
ndarray (2, 3) 6 True回扣本章:对应本章小结“易错点”第 4 条——
plt.subplots(2, 3)返回的是 2×3 的坐标轴数组,须用axes[i, j]索引;以及“要点”第 1 条(plt.*总是作用于“当前”坐标轴,plt.gca()可查看它)。概念辨析:
plt.plot(x, y)与ax.plot(x, y)各自的作用对象是什么?为什么在 2×2 子图的循环绘图中推荐ax.*面向对象接口?fig与ax的职责又如何分工(提示:尺寸与savefig归谁管,轴标签与图例归谁管)?参考答案(点开前请先独立完成)
解题思路:逐点作答。第一,
plt.plot(x, y)作用于“当前”坐标轴(即plt.gca()所指的对象),线画到哪根轴取决于此前哪个坐标轴最后被创建或点选;ax.plot(x, y)作用于显式拿到的坐标轴对象ax,落点在代码里一目了然,两套接口画出的图可以完全一样。第二,2×2 子图循环绘图时若沿用plt.*,循环体内每一步都画在同一个“当前”坐标轴上,几条线会全部叠到一处而不是各归各的子图,因此推荐ax.*面向对象接口、按axes[i, j].plot明确落点;多子图与画完还要保存、继续修改的场景,显式的fig/ax对象清晰、不易画错地方。第三,fig与ax分工:fig(画布)负责整幅图层面的事——画布尺寸figsize、子图数量与布局、整幅图导出savefig;ax(坐标轴)负责图内要素——轴标签、标题、刻度与范围、图例、网格、注释。一句话:整幅图归fig管,图内细节归ax管。回扣本章:对应本章小结“要点”第 1 条与“易错点”第 2 条——多子图循环绘图应改用
axes[i, j].plot,避免plt.*把线画到“当前”坐标轴上。变式任务:把本章”基础绘图”一节本地示例(
lst-matplotlib-basic)的逐年本金、利息两条曲线拆到上下两个子图(2 行 1 列)分别绘制,各自设置子图标题与 y 轴标签,并让两个子图共享 x 轴(sharex=True),观察与单图叠画相比哪种更易读。参考答案(点开前请先独立完成)
解题思路:数据口径完全沿用 列表 18.5(本金 100 万元、年利率 4.9%、期限 30 年的等额本息),改用
npf.ppmt/npf.ipmt向量化算出 360 期各月本金与利息(取负翻正),再按reshape(30, 12).sum(axis=1)汇总成逐年序列;绘图改为plt.subplots(2, 1, sharex=True),上图利息、下图本金。共享 x 轴后两个子图的年份刻度严格对齐,同一年份上下对照即可读出两组数值。与单图叠画相比:单图中两线在交叉点(约第 17 年)附近紧贴,谁高谁低难以分辨,且共用一个 y 轴刻度;拆成上下两个子图后各自趋势一目了然,交叉结构反而更容易指认——本例两条曲线量纲相同(均为金额),拆分的收益主要来自“同一位置只读一条线”;当系列更多或量级差异大时,分面共享 x 轴的收益会更明显。# 变式程序:把逐年本金、利息拆到共享x轴的上下两个子图 import numpy_financial as npf # 导入按揭现金流函数库 import numpy as np # 导入NumPy用于数组运算 import matplotlib.pyplot as plt # 导入Matplotlib绘图库 periods = np.arange(1, 361) # 360期期数序列 monthly_principal = -npf.ppmt(rate=0.049/12, per=periods, nper=360, pv=1e6, fv=0, when='end') # 各月偿还本金(取负翻正) monthly_interest = -npf.ipmt(rate=0.049/12, per=periods, nper=360, pv=1e6, fv=0, when='end') # 各月偿还利息(取负翻正) annual_principal = monthly_principal.reshape(30, 12).sum(axis=1) # 按年汇总本金(30年×12月) annual_interest = monthly_interest.reshape(30, 12).sum(axis=1) # 按年汇总利息 years_array = np.arange(1, 31) # 年份序列1至30 fig, axes = plt.subplots(2, 1, figsize=(8, 6), sharex=True) # 上下两个子图共享x轴 axes[0].plot(years_array, annual_interest, color='steelblue') # 上图画逐年偿还利息 axes[0].set_title('逐年偿还利息') # 上图标题 axes[0].set_ylabel('金额(元)') # y轴标签 axes[0].grid(True, alpha=0.3) # 网格线 axes[1].plot(years_array, annual_principal, color='coral') # 下图画逐年偿还本金 axes[1].set_title('逐年偿还本金') # 下图标题 axes[1].set_xlabel('年份') # 共享x轴的标签只在最下方子图设置一次 axes[1].set_ylabel('金额(元)') # y轴标签 axes[1].grid(True, alpha=0.3) # 网格线 plt.tight_layout() # 自动调整布局 plt.savefig('ch18_q3.png') # 保存图形(也可用plt.show()显示) print('月供: {:.2f}元'.format(monthly_principal[0] + monthly_interest[0])) # 月供=任意一期本金+利息 print('第1年: 利息{:.0f}元, 本金{:.0f}元'.format(annual_interest[0], annual_principal[0])) # 首年两口径对照 print('第30年: 利息{:.0f}元, 本金{:.0f}元'.format(annual_interest[-1], annual_principal[-1])) # 末年两口径对照预期输出(本机 peter 环境实际运行结果,图形在线版不执行、以教学平台渲染为准):
月供: 5307.27元 第1年: 利息48666元, 本金15022元 第30年: 利息1659元, 本金62029元图形判读:上子图为一条单调下降的蓝色利息曲线(约 48666 元降至 1659 元),下子图为一条单调上升的橙色本金曲线(约 15022 元升至 62029 元);两条曲线的交叉点(单图叠画时约第 17 年处)在分面后由“上下两图同一年份的高度对比”呈现,年度本金首次超过年度利息的年份更容易指认。
回扣本章:对应本章小结“要点”第 4 条(等额本息月供不变、内部利息逐期递减而本金逐期递增)与“易错点”第 3 条——
npf.pmt/npf.ppmt返回值为负,绘图前须取负翻正。变式任务(平台任务同型改造):仿照平台任务的结构,把参数改为本金 600 万元、年利率 4.9%、期限 20 年,先在纸上推算月供的量级,再在平台上用
numpy_financial验证你的估计,并说明第 240 期(末期)本金与月供的关系。参考答案(点开前请先独立完成)
解题思路:先纸上推算量级。若不计利息,240 期纯本金摊还为 600 万 ÷ 240 = 2.5 万元/期,这是月供的下限;首月利息为 600 万 × (4.9%/12) ≈ 2.45 万元,若把还本全部压到末期的极端口径(每月只付利息、另存 2.5 万本金),月供约为 2.5 万 + 2.45 万 ≈ 4.95 万元,这是首月口径的上限。等额本息下剩余本金逐期下降,全程平均剩余本金略高于初始的一半(利息前置),故月供应介于两者之间且偏向 3.7 万—3.9 万元。实算:月供
pmt = -39266.64元(约 3.93 万元,现金流约定下为负、取负翻正),与月供公式 \(P\cdot\frac{r(1+r)^n}{(1+r)^n-1}\)(r 为月利率、n=240)的验算结果完全一致。第 240 期本金ppmt = -39106.96元、末期利息ipmt = -159.69元,二者之和恰为月供:末期剩余本金已极小、当期利息近乎为零,因此末期本金几乎等于整个月供——这正是本章平台任务(800 万元、5%、30 年)中末期本金 42767.53 元与月供 42945.73 元非常接近的同一结构性结果。# 变式验算:600万元、年利率4.9%、20年等额本息的月供与末期本金、利息 import numpy_financial as npf # 导入按揭现金流函数库(教学平台已内置) loan_rate, loan_years, loan_principal = 0.049, 20, 6e6 # 年利率4.9%、期限20年、本金600万元 monthly_pmt = npf.pmt(rate=loan_rate/12, nper=loan_years*12, pv=loan_principal, fv=0, when='end') # 月供总额(现金流约定下为负) last_principal = npf.ppmt(rate=loan_rate/12, per=loan_years*12, nper=loan_years*12, pv=loan_principal, fv=0, when='end') # 第240期本金部分 last_interest = float(npf.ipmt(rate=loan_rate/12, per=loan_years*12, nper=loan_years*12, pv=loan_principal, fv=0, when='end')) # 第240期利息部分(ipmt返回0维数组,用float取出数值) first_interest = float(npf.ipmt(rate=loan_rate/12, per=1, nper=loan_years*12, pv=loan_principal, fv=0, when='end')) # 第1期利息(首月口径对照) monthly_rate = loan_rate / 12 # 月利率 compound_factor = (1 + monthly_rate) ** (loan_years * 12) # 复利因子(1+r)^n pmt_formula = loan_principal * monthly_rate * compound_factor / (compound_factor - 1) # 等额本息月供公式验算 print('月供总额 pmt = {:.2f} 元'.format(-monthly_pmt)) # 取负翻成正金额输出 print('第240期本金 ppmt = {:.2f} 元'.format(-last_principal)) # 末期本金 print('第240期利息 ipmt = {:.2f} 元'.format(-last_interest)) # 末期利息 print('第1期利息 ipmt = {:.2f} 元'.format(-first_interest)) # 首期利息对照 print('公式验算月供 = {:.2f} 元'.format(pmt_formula)) # 与pmt函数结果对照 print('末期本金+末期利息 = {:.2f} 元'.format(-last_principal - last_interest)) # 验证恒等式ppmt+ipmt=pmt预期输出(本机 peter 环境实际运行结果,具体以平台运行结果为准):
月供总额 pmt = 39266.64 元 第240期本金 ppmt = 39106.96 元 第240期利息 ipmt = 159.69 元 第1期利息 ipmt = 24500.00 元 公式验算月供 = 39266.64 元 末期本金+末期利息 = 39266.64 元注意:以上为本题变式的独立验算代码;列表 18.3 对应平台任务的原始代码块仍须原样输入教学平台,不要用本变式替换。
回扣本章:对应本章小结“要点”第 4 条(任意一期满足
ppmt + ipmt = pmt;numpy_financial按现金流方向取符号,借入为正、还款为负)与“易错点”第 3 条(直接把返回值画图会得到负值曲线)。思考题:为什么
axes.unicode_minus = False能解决负号显示为方框的问题?如果不做任何字体配置,本章哪一类图表(提示:含负值的图)的刻度会首先暴露这个问题?参考答案(点开前请先独立完成)
解题思路:问题根源在字符编码而非字体名拼写。Matplotlib 默认用 Unicode 减号 U+2212 渲染负刻度,而 U+2212 在 DejaVu Sans 与不少中文字体中缺失,字体链上找不到该字形时负号就被渲染为方框;
axes.unicode_minus = False把负号改为普通 ASCII 连字符-,这个字形几乎任何字体都有,于是负刻度显示正常。因此该开关与font.sans-serif的中文字体设置是两件独立的事,中文正常而负号方框、或反过来,都可能出现。若不做任何字体配置,本章“含负值的图”——如“中文显示与负号”一节的月度盈亏柱状图(列表 18.2,profit含 -5、-3、-8)——的 y 轴负刻度会最先暴露方框问题;只画正值的图(如 GDP 柱状图)刻度里没有负号,不会触发。回扣本章:对应本章小结“要点”第 2 条(
font.sans-serif与axes.unicode_minus = False两行配置分别解决中文与负号两类问题),并呼应“中文显示与负号”一节三类常见原因的第 3 条——负号与中文是两件事。