55  绘制曲线图

本章定位(复习与补充训练层):本章对应正课第18章《Matplotlib库基础》(章节 18,曲线绘制与同一按揭任务的另一种口径)以及第20章《常用图形绘制》(章节 20)中”时间序列用折线图”的选型要点,并与第49章《数据框可视化》(章节 50)互为补充训练(本章为分项版),用于复习、补缺与额外练习。建议先不看讲解,直接尝试下方平台任务,再对照解析补弱项。本章不属于必修主线。先做本章『动手与思考』第 1 题与平台任务自测,通过即可跳过本章。

55.1 引言曲线图的力量

曲线图(Line Plot)是最基础也是最重要的数据可视化形式之一,在金融领域帮助我们观察趋势、发现季节性与周期模式、比较不同资产的表现并监控关键指标的变化。

55.2 本章学习目标

通过本章复习,你将能够:

  1. 不查资料复述 plt.plot 的常用参数(颜色-线型格式串、lwlabel)以及 xlabel/ylabel/title/legend/grid 的设置顺序,并据此复原平台任务的绘图骨架
  2. 解释等额本息与等额本金两种还款规则下”月供、本金、利息”三条曲线的形态差异及其成因,并说明 pmtppmtipmt 的符号约定
  3. 根据数据特点在单轴多线、双Y轴(twinx)、对数坐标(semilogy)、填充区间(fill_between)与堆叠面积图(stackplot)中作出选择,并说出各自的适用场景与误导风险
  4. rolling 计算移动平均线,标注金叉与死叉,并解释移动平均信号的滞后性

先修:章节 18章节 6(第18章的绘图基础与第6章的数组运算,本章平台任务中的 arangelinspaceones_like 均由 NumPy 提供)。

55.3 曲线图的数学基础

曲线图本质上是函数的可视化:

\[ y = f(x) \]

在金融时间序列中:

  • 自变量 \(x\): 时间 \(t\)
  • 因变量 \(y\): 价格、收益率、成交量等

连续与离散:

  • 理论上,价格是离散的(仅在交易时刻有定义)
  • 但为了可视化,我们用线段连接相邻点,形成连续曲线
  • 这种线性插值假设价格在交易间隔内平滑变化

55.4 基础曲线图单条线

任务要求(归纳自块内任务注释与代码):按给定按揭参数(本金 600 万元、年利率 4.9%、期限 30 年)用 pmt 计算等额本息月供,取负并保留 2 位小数后用 print 输出;生成 360 期的期数数组,用 ppmt/ipmt 逐期计算本金与利息部分;在同一坐标轴绘制三条曲线——每月偿还金额、每月偿还本金、每月偿还利息(均取负翻正),设置 X/Y 轴标签、图例与网格,将图形保存为 1.png。请将代码原样输入教学平台(注释除外),判定以平台为准。

平台任务1(平台原始代码)

以下代码与教学平台任务要求完全一致:

列表 55.1: 平台原始代码 (1)
# 注:numpy_financial包已安装于本地peter环境(1.0.0),平台亦已内置
# ⚠️ 平台原始代码 - 请原样输入至教学平台(注释除外),平台才会判定答案正确
import numpy as np  # 导入NumPy数值计算库
import pandas as pd  # 导入Pandas数据分析库
import matplotlib.pyplot as plt    #导入matplotlab的子模块pyplot
import  numpy_financial as nmf  # 导入NumPy库
plt.rcParams["font.sans-serif"] = ["SimHei"]  # 设置Matplotlib全局参数

r = 0.049 #贷款的年利率
n = 30   #贷款的期限(年)
principle = 6e6 #贷款的本金
pay_month = nmf.pmt(rate=r/12,nper=n*12,pv=principle,fv=0,when="end")  # 计算等额本息月供金额
print("在等额本息规则下丁先生每月偿还的金额",-round(pay_month,2))  # 输出在等额本息规则下丁先生每月偿还的金额
t = np.arange(n*12)+1  #生成一个包含每次还款期限长度的数组
principle_pay_month = nmf.ppmt(rate=r/12,per=t,nper=n*12,pv=principle,fv=0,when="end") #计算每月偿还的本金额
interest_pay_month = nmf.ipmt(rate=r/12,per=t,nper=n*12,pv=principle,fv=0,when="end")  #计算每月偿还的利息额
pay_month_array = pay_month*np.ones_like(principle_pay_month) #创建一个每月偿还的数组
plt.figure(figsize=(9,6))  # 创建图形画布
plt.plot(t,-pay_month_array,"r-",label=u"每月偿还金额",lw=2.5)  # 绘制折线图
plt.plot(t,-principle_pay_month,"m--",label=u"每月偿还本金金额",lw=2.5)  # 绘制折线图
plt.plot(t,-interest_pay_month,"b--",label =u"每月产股韩利息金额",lw=2.5)  # 绘制折线图
plt.xticks(fontsize=14)  # 设置X轴刻度标签
plt.xlabel(u"逐次偿还的期限(月)",fontsize=14)  # 设置X轴标签
plt.yticks(fontsize=13)  # 设置Y轴刻度标签
plt.ylabel(u"金额",fontsize=14)  # 设置Y轴标签
plt.title(u"等额本息还款规则下每月偿还的金额以及本金额与利息额")  # 设置图表标题
plt.legend(loc=0,fontsize=13)  # 添加图例
plt.grid()  # 显示网格线
plt.savefig("1.png")  # 保存图形至文件

预期输出(本机实际运行结果;以平台运行结果为准):

在等额本息规则下丁先生每月偿还的金额 31843.6

此外无其他控制台输出,并生成文件 1.png:一条红色的”每月偿还金额”水平直线(恒为 31,843.6 元)、一条逐期上升的洋红虚线(每月本金,自首月 7,343.60 元升至末期 31,714.10 元)、一条逐期下降的蓝色虚线(每月利息,自首月 24,500.00 元降至末期 129.50 元),任意一期两条虚线之和恒等于月供 31,843.60 元。本地环境缺少 SimHei 字体时图上中文显示为方框并伴随 findfont 警告,属环境问题而非代码错误。

注:上图第3条曲线的图例字符串 u"每月产股韩利息金额" 是平台原始题面中的乱码,正确含义是”每月偿还利息金额”;因平台按原始代码判定,请原样输入,本地自主练习时写作 u"每月偿还利息金额" 即可。

另外,第18章的同一按揭贷款任务用 ppmt(rate=r/12,per=n*12,...) 计算每月偿还金额(其口径是末期的本金部分),而本章用 pmt 计算等额本息月供,两章口径不同、各自正确。

任务要求(归纳自块内任务注释与代码):用 linspace 生成 2%~8% 共 100 档贷款利率,逐档计算 30 年期、600 万元本金的等额本息月供;绘制月供随利率上升的红色曲线,并用蓝色圆点标出实际利率 4.9% 对应的月供(图例注明”贷款利率4.9%的每月偿还金额”),设置轴标签、图例与网格,保存为 2.png。请将代码原样输入教学平台(注释除外),判定以平台为准。

平台任务2(平台原始代码)

以下代码与教学平台任务要求完全一致:

列表 55.2: 平台原始代码 (2)
# 注:numpy_financial包已安装于本地peter环境(1.0.0),平台亦已内置
# ⚠️ 平台原始代码 - 请原样输入至教学平台(注释除外),平台才会判定答案正确
import numpy as np  # 导入NumPy数值计算库
import matplotlib.pyplot as plt  #导入matplotlab的子模块pyplot
import numpy_financial as nmf  # 导入NumPy库
plt.rcParams["font.sans-serif"] = ["SimHei"]  # 设置Matplotlib全局参数

r = 0.049 #贷款的年利率
n = 30   #贷款的期限(年)
principle = 6e6 #贷款的本金

r_list = np.linspace(0.02,0.08,100) #生成贷款利率的一个数组
pay_month = nmf.pmt(rate=r/12,nper=n*12,pv=principle,fv=0,when="end")  # 计算等额本息月供金额
pay_month_list = nmf.pmt(rate=r_list/12,nper=n*12,pv=principle,fv=0,when="end") #计算不同贷款利率条件下的每月偿还本息之和
plt.figure(figsize=(9,6))  # 创建图形画布
plt.plot(r_list,-pay_month_list,"r-",label=u"每月偿还金额",lw=2.5)  # 绘制折线图
plt.plot(r,-pay_month,"bo",label=u"贷款利率4.9%的每月偿还金额")  # 绘制折线图
plt.xticks(fontsize=14)  # 设置X轴刻度标签
plt.xlabel(u"贷款利率",fontsize=14)  # 设置X轴标签
plt.yticks(fontsize=14)  # 设置Y轴刻度标签
plt.ylabel(u"金额",fontsize=14,rotation=90)  # 设置Y轴标签
plt.legend(loc=0,fontsize=13)  # 添加图例
plt.grid()  # 显示网格线
plt.savefig("2.png")  # 保存图形至文件

预期输出(本机实际运行结果;以平台运行结果为准):本块无控制台输出,生成文件 2.png——一条随利率上升而上升的红色月供曲线(2%~8% 共 100 个点)与 4.9% 处的蓝色圆点标记;圆点的纵坐标与任务一输出的 31843.6 一致,可互为校验。

任务要求(归纳自块内任务注释与代码):按等额本金规则计算 360 期还款——每月偿还本金固定为 600 万/360,每月利息按剩余本金以月利率循环重算;在同一坐标轴绘制三条曲线——每月还款总额、每月偿还本金(黄色虚线,水平)、每月偿还利息(青色虚线,递减),设置轴标签、标题、图例与网格,保存为 3.png。请将代码原样输入教学平台(注释除外),判定以平台为准。

平台任务3(平台原始代码)

以下代码与教学平台任务要求完全一致:

列表 55.3: 平台原始代码 (3)
# 注:numpy_financial包已安装于本地peter环境(1.0.0),平台亦已内置
# ⚠️ 平台原始代码 - 请原样输入至教学平台(注释除外),平台才会判定答案正确
import numpy as np  # 导入NumPy数值计算库
import matplotlib.pyplot as plt #导入matplotlab的子模块pyplot
import numpy_financial as nmf  # 导入NumPy库
plt.rcParams["font.sans-serif"] = ["SimHei"]  # 设置Matplotlib全局参数

r = 0.049 #贷款的年利率
n = 30  #贷款的期限(年)
principle = 6e6 #贷款的本金
t = np.arange(n*12)+1 #生成一个包含每次还款期限长度的数组
prin_month = principle/(n*12) #计算等额本金还款规则下的每月本金还款额
prin_month_array = np.ones(n*12)*prin_month #生成一个每月本金还款额的数组
int_month_array = np.zeros_like(prin_month_array) #生成存放每月利息还款额的初始数组
for i in np.arange(n*12):  # 遍历np.arange(n*12)中的每个i
 int_month_array[i] = (principle-i*prin_month)*r/12 #计算逐月支付的利息额
pay_total_month = prin_month_array+int_month_array #j计算等额本金还款规则下的每月还款总额
plt.figure(figsize=(9,6))  # 创建图形画布
plt.plot(t,pay_total_month,"m-",label=u"每月偿还金额",lw=2.5)  # 绘制折线图
plt.plot(t,prin_month_array,"y--",label=u"每月偿还本金额",lw=2.5)  # 绘制折线图
plt.plot(t,int_month_array,"c--",label =u"每月偿还利息额",lw=2.5)  # 绘制折线图
plt.xticks(fontsize=14)  # 设置X轴刻度标签
plt.xlabel(u"逐次偿还的期限(月)",fontsize=14)  # 设置X轴标签
plt.yticks(fontsize=14)  # 设置Y轴刻度标签
plt.ylabel(u"金额",fontsize=14,rotation=90)  # 设置Y轴标签
plt.title(u"等额本金还款规则下每月偿还的金额以及本金额与利息额",fontsize=14)  # 设置图表标题
plt.legend(loc=0,fontsize=13)  # 添加图例
plt.grid()  # 显示网格线
plt.savefig("3.png")  # 保存图形至文件

预期输出(本机实际运行结果;以平台运行结果为准):本块无控制台输出,生成文件 3.png——洋红色的”每月偿还金额”曲线自首月 41,166.67 元逐期线性递减至末期 16,734.72 元,黄色虚线的每月本金恒为 16,666.67 元(600 万/360),青色虚线的每月利息自首月 24,500.00 元递减至末期 68.06 元;首月月供 41,166.67 元高于等额本息的 31,843.6 元,末期则低于它,这正是等额本金”月供逐期递减”的形态。

列表 55.4
# =============================================================================
# 题目:基础曲线图——贵州茅台股价走势
# =============================================================================
# 本任务演示如何使用Matplotlib绘制基础曲线图,展示单条时间序列数据
# 应用场景:可视化单个资产的股价走势

# ==================== 导入必要的库 ====================
import matplotlib.pyplot as plt  # Matplotlib的pyplot接口,提供类似MATLAB的绘图API
import pandas as pd                 # Pandas数据分析库,用于数据处理
import numpy as np                  # NumPy数值计算库

# ==================== 设置中文字体支持 ====================
# Matplotlib默认不支持中文显示,需要手动设置中文字体
# plt.rcParams是一个全局配置字典,用于控制绘图的各种属性
plt.rcParams['font.sans-serif'] = ['Source Han Serif SC', 'SimHei']  # 规范字体在前,SimHei 仅作后备  # 设置默认字体为黑体(SimHei),确保中文正常显示
plt.rcParams['axes.unicode_minus'] = False    # 解决负号'-'显示为方块的问题
# 这两行设置确保图表中的中文文字和负号都能正确显示

# ==================== 创建示例数据 ====================
# 场景:模拟贵州茅台30个交易日的股价数据
# pd.date_range()生成日期序列
# '2024-01-01':起始日期
# periods=30:生成30个日期(30个交易日)
dates = pd.date_range('2024-01-01', periods=30)

# 使用列表推导式生成模拟股价数据
# 基础价格1850元
# i * 2:每天上涨2元(模拟上涨趋势)
# np.random.randn() * 10:添加随机波动,标准差为10元
np.random.seed(42)  # 设置随机种子,确保每次运行结果相同
prices = [1850 + i * 2 + np.random.randn() * 10 for i in range(30)]
# prices是一个包含30个价格的列表,模拟真实股价的随机波动

# 创建DataFrame便于数据管理
# DataFrame是Pandas的核心数据结构,类似Excel表格
df = pd.DataFrame({
    '日期': dates,      # 日期列,作为X轴
    '收盘价': prices    # 收盘价列,作为Y轴
})

# ==================== 绘制基础曲线图 ====================
# plt.figure()创建一个新的图形窗口
# figsize=(12, 6):图形大小为12英寸宽×6英寸高
# 这个比例适合展示时间序列数据,宽度较大以容纳更多时间点
plt.figure(figsize=(12, 6))

# plt.plot()绘制折线图(曲线图)
# df['日期']:X轴数据(时间序列)
# df['收盘价']:Y轴数据(价格)
# linewidth=2:线条宽度为2,使折线更清晰可见
# color='#2E86AB':线条颜色,使用十六进制颜色代码(深蓝色)
plt.plot(df['日期'], df['收盘价'], linewidth=2, color='#2E86AB')

# ==================== 设置图表装饰 ====================
# plt.title()设置图表标题
# fontsize=16:标题字号为16(较大,突出显示)
# fontweight='bold':标题文字加粗
plt.title('贵州茅台股价走势', fontsize=16, fontweight='bold')

# plt.xlabel()设置X轴标签
# fontsize=12:标签字号为12
plt.xlabel('日期', fontsize=12)

# plt.ylabel()设置Y轴标签
plt.ylabel('收盘价(元)', fontsize=12)

# plt.grid()添加网格线,帮助读取数值
# True:显示网格线
# alpha=0.3:网格线透明度为0.3(使网格不显眼,不干扰数据)
plt.grid(True, alpha=0.3)

# plt.xticks()设置X轴刻度标签的显示方式
# rotation=45:刻度标签旋转45度(防止日期文字重叠)
plt.xticks(rotation=45)

# plt.tight_layout()自动调整布局,防止元素重叠(如标题被截断)
plt.tight_layout()

# plt.show()将图形显示出来
plt.show()

# ==================== 输出数据统计信息 ====================
print('数据统计:')
# df['收盘价'].describe()计算收盘价的描述性统计量
# 包括:计数、均值、标准差、最小值、四分位数、最大值
print(df['收盘价'].describe())

代码解析:

  1. 中文字体设置:

    plt.rcParams['font.sans-serif'] = ['SimHei']
    plt.rcParams['axes.unicode_minus'] = False
    • SimHei:黑体,确保中文正常显示
    • unicode_minus:解决负号显示问题
  2. plot函数参数:

    • linewidth:线宽,数值越大线越粗
    • color:颜色,可以用十六进制、RGB、英文名称
    • linestyle:线型,'-'(实线)、'--'(虚线)、':'(点线)
  3. 图表装饰:

    • title:标题
    • xlabel/ylabel:轴标签
    • grid:网格线
    • tight_layout:自动调整布局,避免元素重叠

55.5 多条曲线对比

列表 55.5
# =============================================================================
# 题目:多条曲线对比——不同股票价格走势
# =============================================================================
# 本任务演示如何在同一图表中绘制多条曲线,对比不同资产的表现

# ==================== 创建多只股票数据 ====================
# 场景:模拟三只股票30个交易日的价格数据
# 使用之前生成的dates作为时间序列
np.random.seed(42)  # 设置随机种子,确保每次运行结果相同
stocks_data = pd.DataFrame({
    '日期': dates,
    # 贵州茅台:高价股(1850元起),每天上涨1.5元,标准差8元
    '贵州茅台': [1850 + i * 1.5 + np.random.randn() * 8 for i in range(30)],
    # 五粮液:中价股(220元起),每天上涨0.8元,标准差5元
    '五粮液': [220 + i * 0.8 + np.random.randn() * 5 for i in range(30)],
    # 招商银行:低价股(45元起),每天上涨0.3元,标准差2元
    '招商银行': [45 + i * 0.3 + np.random.randn() * 2 for i in range(30)]
})

# ==================== 绘制多条曲线 ====================
plt.figure(figsize=(12, 6))

# 绘制第一条曲线:贵州茅台(红色)
# label参数设置图例标签,将在图例中显示
plt.plot(stocks_data['日期'], stocks_data['贵州茅台'],
         label='贵州茅台', linewidth=2, color='#E3120B')

# 绘制第二条曲线:五粮液(青色)
plt.plot(stocks_data['日期'], stocks_data['五粮液'],
         label='五粮液', linewidth=2, color='#008080')

# 绘制第三条曲线:招商银行(深灰色)
plt.plot(stocks_data['日期'], stocks_data['招商银行'],
         label='招商银行', linewidth=2, color='#2C3E50')

# ==================== 设置图表装饰 ====================
plt.title('不同股票价格走势对比', fontsize=16, fontweight='bold')
plt.xlabel('日期', fontsize=12)
plt.ylabel('收盘价(元)', fontsize=12)

# plt.legend()显示图例
# loc='best':自动选择最佳位置(避免遮挡数据)
# fontsize=11:图例字号为11
plt.legend(loc='best', fontsize=11)

plt.grid(True, alpha=0.3)
plt.xticks(rotation=45)
plt.tight_layout()
plt.show()

# ==================== 输出各股票统计信息 ====================
print('各股票统计信息:')
# describe()对每只股票分别计算统计量
print(stocks_data.describe())

视觉层次设计:

当绘制多条曲线时,需要考虑:

  1. 颜色选择:使用对比度高、易于区分的颜色
  2. 线宽:主要数据用较粗的线,次要数据用较细的线
  3. 图例位置:loc='best'自动选择最佳位置
  4. Y轴范围:如果数据量级差异大,考虑用双Y轴归一化

55.6 双Y轴图

列表 55.6
# =============================================================================
# 题目:双Y轴图——价格与成交量
# =============================================================================
# 本任务演示如何创建双Y轴图表,同时展示量价关系
# 应用场景:分析价格与成交量、收益率与波动率等不同量级的数据

# ==================== 创建价格和成交量数据 ====================
np.random.seed(42)  # 设置随机种子,确保每次运行结果相同
df_dual = pd.DataFrame({
    '日期': dates,
    # 收盘价:45元起,每天上涨0.5元,标准差3元
    '收盘价': [45 + i * 0.5 + np.random.randn() * 3 for i in range(30)],
    # 成交量:5000手起,每天增加100手,标准差500手
    # 成交量与价格的量级不同(价格是几十,成交量是几千),需要双Y轴
    '成交量': [5000 + i * 100 + np.random.randn() * 500 for i in range(30)]
})

# ==================== 创建图表和轴对象 ====================
# plt.subplots()创建图形和轴对象
# fig:图形对象,包含整个图形
# ax1:左Y轴对象(第一个轴)
# figsize=(12, 6):图形大小
fig, ax1 = plt.subplots(figsize=(12, 6))

# ==================== 绘制左Y轴(价格) ====================
color1 = '#E3120B'  # 红色,用于价格线

# ax1.set_xlabel()设置X轴标签
ax1.set_xlabel('日期', fontsize=12)

# ax1.set_ylabel()设置左Y轴标签
# color=color1:标签颜色与线条颜色一致,便于识别
ax1.set_ylabel('收盘价(元)', color=color1, fontsize=12)

# ax1.plot()在左Y轴上绘制数据
# color=color1:线条颜色
# linewidth=2:线宽
# label='收盘价':图例标签
line1 = ax1.plot(df_dual['日期'], df_dual['收盘价'],
                 color=color1, linewidth=2, label='收盘价')

# ax1.tick_params()设置刻度标签样式
# axis='y':只设置Y轴刻度
# labelcolor=color1:Y轴刻度标签颜色与线条颜色一致
ax1.tick_params(axis='y', labelcolor=color1)

# ax1.grid()只在左Y轴添加网格线
ax1.grid(True, alpha=0.3)

# ==================== 创建共享X轴的右Y轴 ====================
# ax1.twinx()创建一个共享X轴的新Y轴(右Y轴)
# 这样两个Y轴对应同一个X轴(时间轴)
ax2 = ax1.twinx()

# ==================== 绘制右Y轴(成交量) ====================
color2 = '#008080'  # 青色,用于成交量线

# ax2.set_ylabel()设置右Y轴标签
ax2.set_ylabel('成交量(手)', color=color2, fontsize=12)

# ax2.plot()在右Y轴上绘制数据
# linestyle='--':使用虚线,与实线的价格线区分
line2 = ax2.plot(df_dual['日期'], df_dual['成交量'],
                 color=color2, linewidth=2, linestyle='--', label='成交量')

# ax2.tick_params()设置右Y轴刻度标签样式
ax2.tick_params(axis='y', labelcolor=color2)

# ==================== 合并图例 ====================
# 因为ax1和ax2各有图例,需要合并成一个统一的图例
# lines = line1 + line2:合并两条线
lines = line1 + line2
# [l.get_label() for l in lines]:提取每条线的标签
labels = [l.get_label() for l in lines]
# ax1.legend()在左Y轴上显示合并后的图例
# lines:线条对象列表
# labels:标签列表
ax1.legend(lines, labels, loc='best', fontsize=11)

# ==================== 设置标题并显示 ====================
plt.title('价格与成交量走势', fontsize=16, fontweight='bold')
plt.tight_layout()
plt.show()

双Y轴的使用场景:

  1. 量价关系:价格与成交量
  2. 收益率与波动率:收益与风险
  3. 价格与指标:价格与移动平均线、RSI等技术指标
  4. 不同量级数据:股价(元)与市值(万亿元)

注意事项:

  • 双Y轴可能误导读者,需谨慎使用
  • 明确标注哪个轴对应哪条线
  • 考虑用子图(subplots)替代,更清晰

55.7 填充区域图

列表 55.7
# =============================================================================
# 题目:填充区域图——价格区间
# =============================================================================
# 本任务演示如何绘制填充区域图,展示预测区间或置信区间
# 应用场景:布林带、价格预测区间、期权盈亏区间

# ==================== 创建带上下限的数据 ====================
np.random.seed(42)  # 设置随机种子,确保每次运行结果相同
df_area = pd.DataFrame({
    '日期': dates,
    # 价格:100元起,每天上涨1元,标准差3元
    '价格': [100 + i + np.random.randn() * 3 for i in range(30)],
    # 上限:105元起,每天上涨1元,标准差2元(价格上方5元)
    '上限': [105 + i + np.random.randn() * 2 for i in range(30)],
    # 下限:95元起,每天上涨1元,标准差2元(价格下方5元)
    '下限': [95 + i + np.random.randn() * 2 for i in range(30)]
})

# ==================== 绘制填充区域图 ====================
plt.figure(figsize=(12, 6))

# 绘制实际价格曲线(深灰色)
plt.plot(df_area['日期'], df_area['价格'],
         label='实际价格', linewidth=2, color='#2C3E50')

# plt.fill_between()填充两条曲线之间的区域
# df_area['日期']:X轴数据
# df_area['下限']:区域下边界
# df_area['上限']:区域上边界
# alpha=0.3:透明度0.3(使填充区域不遮挡网格线)
# color='#008080':填充颜色(青色)
# label='预测区间':图例标签
plt.fill_between(df_area['日期'], df_area['下限'], df_area['上限'],
                 alpha=0.3, color='#008080', label='预测区间')

# ==================== 设置图表装饰 ====================
plt.title('价格走势与预测区间', fontsize=16, fontweight='bold')
plt.xlabel('日期', fontsize=12)
plt.ylabel('价格(元)', fontsize=12)
plt.legend(loc='best', fontsize=11)
plt.grid(True, alpha=0.3)
plt.xticks(rotation=45)
plt.tight_layout()
plt.show()

# ==================== 计算区间宽度统计 ====================
print('区间宽度统计:')
# 区间宽度 = 上限 - 下限
df_area['区间宽度'] = df_area['上限'] - df_area['下限']
# describe()计算区间宽度的统计量
print(df_area['区间宽度'].describe())

金融应用:

  1. 置信区间:统计模型预测的置信区间
  2. 布林带:移动平均线 ± 标准差
  3. 期权策略:期权到期时的盈亏区间
  4. 风险管理:VaR(在险价值)的区间表示

fill_between参数:

  • alpha:透明度(0-1),越小越透明
  • color:填充颜色
  • where:条件填充,只填充满足条件的区域
  • interpolate:在边界处插值,使填充更平滑

55.8 堆叠面积图

列表 55.8
# =============================================================================
# 题目:堆叠面积图——投资组合构成
# =============================================================================
# 本任务演示如何绘制堆叠面积图,展示各部分随时间的变化及总体趋势
# 应用场景:投资组合资产配置、公司收入构成、市场份额变化

# ==================== 创建投资组合数据 ====================
np.random.seed(42)  # 设置随机种子,确保每次运行结果相同
df_portfolio = pd.DataFrame({
    '日期': pd.date_range('2024-01-01', periods=12),  # 12个月
    # 股票:100万元起,每月增长5万元,标准差3万元
    '股票': [100 + i*5 + np.random.randn()*3 for i in range(12)],
    # 债券:80万元起,每月增长2万元,标准差2万元
    '债券': [80 + i*2 + np.random.randn()*2 for i in range(12)],
    # 现金:20万元起,每月增长0.5万元,标准差1万元
    '现金': [20 + i*0.5 + np.random.randn()*1 for i in range(12)]
})

# ==================== 绘制堆叠面积图 ====================
plt.figure(figsize=(12, 6))

# plt.stackplot()绘制堆叠面积图
# df_portfolio['日期']:X轴数据(时间)
# 接下来的三个参数是要堆叠的Y值序列
# df_portfolio['股票']:第一层(最底层)
# df_portfolio['债券']:第二层(堆叠在股票之上)
# df_portfolio['现金']:第三层(最顶层)
# alpha=0.8:透明度0.8(使区域不太透明)
# colors:颜色列表,分别对应三个序列
# labels:标签列表,用于图例
plt.stackplot(df_portfolio['日期'],
              df_portfolio['股票'],
              df_portfolio['债券'],
              df_portfolio['现金'],
              alpha=0.8,
              colors=['#E3120B', '#008080', '#F0A700'],
              labels=['股票', '债券', '现金'])

# ==================== 设置图表装饰 ====================
plt.title('投资组合资产配置变化', fontsize=16, fontweight='bold')
plt.xlabel('日期', fontsize=12)
plt.ylabel('资产价值(万元)', fontsize=12)

# loc='upper left':图例位置在左上角
plt.legend(loc='upper left', fontsize=11)

# axis='y':只显示Y轴网格线(水平网格线)
plt.grid(True, alpha=0.3, axis='y')
plt.xticks(rotation=45)
plt.tight_layout()
plt.show()

# ==================== 计算各资产占比 ====================
print('各资产占比:')
# total:每月的资产总值(股票+债券+现金)
# sum(axis=1):按行求和(横向求和)
total = df_portfolio[['股票', '债券', '现金']].sum(axis=1)

# 计算最新月份(最后一行)各资产占比
for asset in ['股票', '债券', '现金']:
    # df_portfolio[asset].iloc[-1]:获取该资产的最后一行值(最新值)
    # total.iloc[-1]:获取总资产的最后一行值
    ratio = df_portfolio[asset].iloc[-1] / total.iloc[-1]
    # f'{asset}: {ratio:.1%}':格式化输出,百分比保留1位小数
    print(f'{asset}: {ratio:.1%}')

堆叠图的数学含义:

\[ y_{\text{total}} = y_1 + y_2 + \cdots + y_n \]

每层的实际值:

\[ y_{\text{layer }i} = \sum_{j=1}^i y_j \]

金融应用:

  • 资产配置:展示股票、债券、现金的配置变化
  • 收入构成:主营业务收入、其他业务收入
  • 市场份额:不同公司在市场中的份额变化

55.9 对数坐标轴

列表 55.9
# =============================================================================
# 题目:对数Y轴——复利增长可视化
# =============================================================================
# 本任务演示如何使用对数坐标轴展示指数增长数据
# 应用场景:复利增长、投资回报、跨越数量级的数据

# ==================== 创建复利增长数据 ====================
# years:年份序列,从2000年到2023年
years = np.arange(2000, 2024)

initial_value = 100      # 初始值:100元
growth_rate = 0.15       # 年化增长率:15%
# values:使用复利公式计算每年的价值
# (1 + growth_rate) ** (years - 2000):复利因子
# 例如:2000年:(1.15)^0 = 1,价值100元
#       2001年:(1.15)^1 = 1.15,价值115元
#       2002年:(1.15)^2 = 1.3225,价值132.25元
values = initial_value * (1 + growth_rate) ** (years - 2000)

# ==================== 创建对比:线性vs对数 ====================
# plt.subplots(1, 2, figsize=(15, 6))创建1行2列的子图布局
# fig:图形对象
# (ax1, ax2):两个轴对象的元组
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(15, 6))

# ==================== 线性坐标(左图) ====================
# ax1.plot()在第一个子图绘制
ax1.plot(years, values, linewidth=2, color='#E3120B')
ax1.set_title('线性坐标', fontsize=14, fontweight='bold')
ax1.set_xlabel('年份', fontsize=12)
ax1.set_ylabel('价值(元)', fontsize=12)
ax1.grid(True, alpha=0.3)

# ==================== 对数坐标(右图) ====================
# ax2.semilogy()绘制Y轴为对数坐标的图表
# semilogy = semi-logarithmic y-axis(Y轴对数化)
# 在对数坐标下,指数增长呈直线
ax2.semilogy(years, values, linewidth=2, color='#008080')
ax2.set_title('对数坐标', fontsize=14, fontweight='bold')
ax2.set_xlabel('年份', fontsize=12)
# Y轴标签注明"对数",提醒读者
ax2.set_ylabel('价值(元,对数)', fontsize=12)
ax2.grid(True, alpha=0.3)

plt.tight_layout()
plt.show()

# ==================== 输出复利增长分析 ====================
print('复利增长分析:')
print(f'初始值: {initial_value}元')
print(f'年化增长率: {growth_rate:.1%}')
print(f'2023年价值: {values[-1]:.2f}元')
# 计算总增长倍数
print(f'总增长倍数: {values[-1]/initial_value:.1f}倍')

对数坐标的金融意义:

线性坐标:展示绝对值变化 对数坐标:展示相对变化(百分比)

在金融中:

  • 复利效应:对数坐标下,恒定增长率呈直线
  • 收益率比较:不同起始点的资产可以直接比较斜率
  • 长期趋势:更适合展示跨越数量级的数据

数学原理:

\[ \ln(y) = \ln(A \cdot e^{rt}) = \ln(A) + rt \]

在半对数图上,指数增长呈直线,斜率即为增长率 \(r\)

55.10 金融应用移动平均线

列表 55.10
# =============================================================================
# 题目:移动平均线——技术分析基础
# =============================================================================
# 本任务演示如何绘制移动平均线,识别金叉和死叉信号
# 应用场景:技术分析、趋势跟踪、交易信号生成

# ==================== 创建价格数据 ====================
# np.random.seed(42)设置随机种子,确保每次运行结果相同
np.random.seed(42)

# df_price:模拟60个交易日的股价数据
df_price = pd.DataFrame({
    '日期': pd.date_range('2024-01-01', periods=60),
    # 收盘价:100元起
    # np.random.normal(0.5, 2, 60)生成60个正态分布随机数(均值0.5,标准差2)
    # np.cumsum()计算累积和,模拟价格的随机游走
    '收盘价': 100 + np.cumsum(np.random.normal(0.5, 2, 60))
})

# ==================== 计算移动平均线 ====================
# .rolling(window=5).mean():5日移动平均线
# rolling(5):创建一个5日的滚动窗口
# mean():计算窗口内数据的均值
df_price['MA5'] = df_price['收盘价'].rolling(window=5).mean()

# 20日移动平均线(中期趋势线)
df_price['MA20'] = df_price['收盘价'].rolling(window=20).mean()

# 60日移动平均线(长期趋势线)
df_price['MA60'] = df_price['收盘价'].rolling(window=60).mean()

# ==================== 绘制移动平均线 ====================
plt.figure(figsize=(14, 7))

# 绘制收盘价(深灰色,半透明)
plt.plot(df_price['日期'], df_price['收盘价'],
         label='收盘价', linewidth=1.5, color='#2C3E50', alpha=0.7)

# 绘制MA5(红色,短期均线)
plt.plot(df_price['日期'], df_price['MA5'],
         label='MA5', linewidth=1.5, color='#E3120B')

# 绘制MA20(青色,中期均线)
plt.plot(df_price['日期'], df_price['MA20'],
         label='MA20', linewidth=1.5, color='#008080')

# 绘制MA60(黄色,长期均线,线宽加粗)
plt.plot(df_price['日期'], df_price['MA60'],
         label='MA60', linewidth=2, color='#F0A700')

# ==================== 标注金叉和死叉 ====================
# 金叉:短期均线上穿长期均线(买入信号)
# (MA5 > MA20):当前MA5大于MA20
# .shift(1):将数据下移一行,获取前一日的值
# (MA5.shift(1) <= MA20.shift(1)):前一日的MA5小于等于MA20
# 两个条件都满足时,表示MA5刚刚上穿MA20
golden_cross = (df_price['MA5'] > df_price['MA20']) & (df_price['MA5'].shift(1) <= df_price['MA20'].shift(1))

# 死叉:短期均线下穿长期均线(卖出信号)
death_cross = (df_price['MA5'] < df_price['MA20']) & (df_price['MA5'].shift(1) >= df_price['MA20'].shift(1))

# plt.scatter()绘制金叉点(红色向上三角)
# df_price['日期'][golden_cross]:金叉发生的日期
# df_price['MA5'][golden_cross]:金叉点的MA5值
# color='red':红色
# s=100:散点大小为100
# marker='^':向上三角符号
# label='金叉':图例标签
# zorder=5:图层顺序为5(确保散点显示在线条上方)
plt.scatter(df_price['日期'][golden_cross], df_price['MA5'][golden_cross],
           color='red', s=100, marker='^', label='金叉', zorder=5)

# plt.scatter()绘制死叉点(绿色向下三角)
plt.scatter(df_price['日期'][death_cross], df_price['MA5'][death_cross],
           color='green', s=100, marker='v', label='死叉', zorder=5)

# ==================== 设置图表装饰 ====================
plt.title('移动平均线与交叉信号', fontsize=16, fontweight='bold')
plt.xlabel('日期', fontsize=12)
plt.ylabel('价格(元)', fontsize=12)
plt.legend(loc='best', fontsize=11)
plt.grid(True, alpha=0.3)
plt.xticks(rotation=45)
plt.tight_layout()
plt.show()

# ==================== 输出移动平均线统计 ====================
print('移动平均线统计:')
print(df_price[['收盘价', 'MA5', 'MA20', 'MA60']].describe())

移动平均线的金融理论:

简单移动平均(Simple Moving Average, SMA):

\[ \text{SMA}_t = \frac{1}{n}\sum_{i=0}^{n-1} P_{t-i} \]

交易信号:

  • 金叉(Golden Cross):短期均线上穿长期均线,买入信号
  • 死叉(Death Cross):短期均线下穿长期均线,卖出信号

局限性:

  • 滞后性:基于历史价格,信号滞后
  • 震荡市场:在横盘震荡中频繁产生错误信号
  • 参数敏感性:不同周期效果差异大

55.11 本章小结

要点:

  • 曲线图是函数 \(y=f(x)\) 的可视化:金融时间序列中以时间为自变量、价格为因变量,相邻点之间默认用线段相连
  • plt.plot(t, y, 'r--', label=..., lw=2.5) 用格式串同时指定颜色与线型;label 配合 plt.legend(loc=0, fontsize=13) 生成图例,loc=0 表示自动选择最佳位置
  • 等额本息:月供固定,内部利息逐期递减、本金逐期递增,任意一期满足 ppmt + ipmt = pmt;等额本金:每月本金固定,利息按剩余本金逐月重算,月供总额逐期递减
  • numpy_financial 按现金流方向取符号,还款额返回负值,绘图时常取负翻正(如 -pay_month-interest_pay_month)
  • 双Y轴用 ax2 = ax1.twinx() 共享X轴,可同图展示价格与成交量等不同量纲的序列,但两个坐标轴的图例需用 lines = line1 + line2 手动合并
  • 对数坐标(semilogy)下恒定增长率的复利曲线呈直线,斜率对应增长率,适合展示跨越数量级的数据
  • fill_between 填充上下限形成区间带(预测区间、布林带);stackplot 堆叠展示构成,每层高度是前几层的累计值

易错点:

  • 平台任务1第三条曲线的图例字符串是题面乱码(正确含义为”每月偿还利息金额”),平台按原始代码判定,本地自主练习时应写正确字符串
  • 第18章的同一按揭任务用 ppmt(rate=r/12, per=n*12, ...) 计算末期本金口径,本章用 pmt 计算月供口径,两章口径不同、各自正确,不要相互”改错”
  • 忘记对 pmt/ppmt/ipmt 的返回值取负,画出的曲线会整体落在X轴下方
  • 等额本金任务的利息需按剩余本金逐月循环计算,直接改用 ipmt 就变成了等额本息口径
  • plt.savefig() 应在 plt.show() 之前调用,否则本地保存出的是空白图片
  • 多条曲线只写 label 而忘记调用 plt.legend(),图例不会出现;fontsize 参数多数时候属于 legend/xticks 等函数而非 plot 本身

55.12 动手与思考

以下练习每题附参考答案(默认折叠)。请先独立完成并写下你的判断,再点开对照,最后上机验证。

  1. 自测回忆:不看正文,写出下列格式字符串各自对应的颜色与线型:'r-''m--''b--''y--''c--';并回答 lwlabelloc=0fontsize 分别应在哪个函数中传入。

    参考答案(点开前请先独立完成)

    解题思路:格式串由”颜色字符 + 线型字符”拼成——'r-' 红色实线,'m--' 品红色虚线,'b--' 蓝色虚线,'y--' 黄色虚线,'c--' 青色虚线。参数归属:lw(线宽)与 label(图例条目文本)在 plt.plot() 等绘图函数中传入;loc=0plt.legend() 中传入(0 表示自动选择最佳位置);fontsize 多数时候属于 plt.legend()plt.xticks()plt.yticks() 等标注类函数,而非 plot 本身。

    回扣主线:格式串与图例参数的归属见第 章节 18 章与本章”要点”第 2 条。

  2. 输出预测:不运行代码,先写出下面代码的输出结果,再上机检验你的判断。

    import numpy as np
    t = np.arange(6) + 1
    pay = np.ones_like(t) * 30000.0
    print(t)
    print(pay)
    print(len(np.linspace(0.02, 0.08, 100)))

    参考答案(先写下你的预测再点开)

    解题思路np.arange(6) 生成 0 至 5,加 1 后为 1 至 6(期数编号);np.ones_like(t) 生成与 t 同形状的全 1 整数数组,乘 30000.0 后变为浮点型 30000.0(月供为常数序列);np.linspace(0.02, 0.08, 100) 在 2% 至 8% 之间取 100 个等差点,长度为 100。

    # 验证脚本:期数序列、常数月供序列与利率网格长度
    import numpy as np  # 导入NumPy库
    t = np.arange(6) + 1  # 1至6的期数编号
    pay = np.ones_like(t) * 30000.0  # 同形状全1数组乘常数得等额月供序列
    print(t)  # 输出期数
    print(pay)  # 输出月供序列
    print(len(np.linspace(0.02, 0.08, 100)))  # 利率网格的点数

    预期输出(本机 peter 环境实际运行结果,具体以平台运行结果为准):

    [1 2 3 4 5 6]
    [30000. 30000. 30000. 30000. 30000. 30000.]
    100

    回扣主线arange/linspace/ones_like 的行为见第 章节 5 章(经本章平台任务复用)。

  3. 思考题:等额本息与等额本金两种规则下,“每月还款总额”曲线一条水平、一条递减。请从”每月偿还本金是否固定、利息按什么基数计算”出发解释这一差异,并说明为什么两条”每月偿还利息”曲线都是递减的。

    参考答案(点开前请先独立完成)

    解题思路:等额本息的定义是”每月还款总额固定”,把每月月供拆开看,其中本金部分逐月递增、利息部分逐月递减、两者之和恒定——利率息按剩余本金计提,随还款推进剩余本金下降,利息逐月变少,月供中腾出的份额自动补到本金上。等额本金是”每月偿还本金固定”,利息同样按剩余本金计提,剩余本金线性下降使利息近似线性递减,于是月供总额(固定本金 + 递减利息)逐月递减。两条利息曲线都递减的共同根源:利息恒等于”剩余本金 × 月利率”,而两种规则下剩余本金都单调下降,只是下降路径不同——等额本金线性下降,等额本息先慢后快。

    回扣主线:两种还款规则的口径对照见第 章节 18 章同一按揭任务与本章”要点”第 3 条。

  4. 变式任务(平台任务同型改造):仿照平台任务2的利率敏感性分析,把横轴从贷款利率改为贷款期限——本金100万元、年利率4.9%、期限从5年到30年取整数年,绘制等额本息月供随期限变化的曲线,并用一个圆点标出30年期的月供;观察曲线下降速度为什么越来越慢(凹向如何)。

    参考答案(点开前请先独立完成)

    解题思路:用 npf.pmt 逐个期限计算月供并取负翻正。实测:5 年期月供 18825.45 元,30 年期 5307.27 元;期限从 5 年延到 10 年月供减少 8267.71 元,而从 25 年延到 30 年只减少 480.52 元——下降速度越来越慢,曲线递减且下凸(凹函数,二阶导大于 0)。原因:期限拉长把固定本金摊到更多期,每期本金份额的边际缩减随期数增多而变薄,而利息部分趋近一个稳定占比,月供随期限趋于一个渐近下限而不是降到零。

    # 变式脚本:月供对期限的敏感性曲线
    import matplotlib.pyplot as plt  # 导入绘图库
    import numpy as np  # 导入NumPy库
    import numpy_financial as npf  # 导入金融计算库
    rate_annual = 0.049  # 年利率4.9%
    principal = 1000000.0  # 贷款本金100万元
    terms = np.arange(5, 31)  # 期限5至30年取整数年
    pay_by_term = -npf.pmt(rate_annual / 12, terms * 12, principal)  # 各期限月供(取负翻正)
    print(round(pay_by_term[0], 2), round(pay_by_term[-1], 2))  # 5年期与30年期月供
    print(round(pay_by_term[0] - pay_by_term[5], 2), round(pay_by_term[20] - pay_by_term[25], 2))  # 两段边际降幅对照
    plt.plot(terms, pay_by_term / 10000, 'b--', lw=2, label='等额本息月供')  # 月供折算万元后画曲线
    plt.scatter([30], [pay_by_term[-1] / 10000], color='red', s=80, zorder=5, label='30年期月供')  # 圆点标注30年期
    plt.xlabel('贷款期限(年)')  # X轴标签
    plt.ylabel('月供(万元)')  # Y轴标签
    plt.legend()  # 显示图例
    plt.savefig('pay_by_term.png')  # 保存图形

    预期输出(本机 peter 环境实际运行结果,具体以平台运行结果为准):

    18825.45 5307.27
    8267.71 480.52

    图形:曲线自左上(5 年期约 1.88 万元/月)向右下单调递减,前段陡峭、后段平缓,红色圆点落在曲线右端(30 年期约 0.53 万元/月),整体下凸。

    注意:以上为变式代码;列表 55.2 的平台任务仍须按原始代码原样输入教学平台。

    回扣主线:利率敏感性分析的结构与 pmt 取负翻正的口径见本章平台任务 2 解析与第 章节 18 章按揭例。

  5. 变式任务:把”金融应用:移动平均线”一节的双均线从MA5与MA20改为MA10与MA60,重新统计金叉、死叉的个数与位置,并讨论窗口变长后信号数量与滞后性如何变化。

    参考答案(点开前请先独立完成)

    解题思路:先看数据约束——该节的模拟样本只有 60 个交易日,rolling(60) 只在最后 1 个位置窗口才装满,MA60 全程只有 1 个有效值;而交叉的判定要求相邻两天两条均线同时有效,因此 MA10/MA60 在 60 日样本上金叉、死叉均为 0 个(对照:同一样本的 MA5/MA20 有 2 个金叉,位于索引 33、55,1 个死叉,位于索引 39)。把样本延长到 120 日(同一随机种子,路径是 60 日版本的延长)后再统计:MA5/MA20 增至 3 个金叉(索引 33、55、107)与 2 个死叉(索引 39、103),MA10/MA60 仍为 0——本模拟路径带上升趋势,MA10 自 MA60 首个有效期起一直位于其上方,从未回落穿越。结论:窗口越长,均线越平滑、对短期波动的响应越迟钝,交叉信号数量通常越少、滞后越强,甚至在单边趋势样本中整段无信号;同时长窗口要求更长的事前数据,否则起始段大面积为 NaN。

    # 变式脚本:两组均线窗口的交叉统计对照
    import pandas as pd  # 导入pandas库
    import numpy as np  # 导入NumPy库
    for n_days in (60, 120):  # 原样本60日与其延长版120日
        np.random.seed(42)  # 与正文同一随机种子,路径前段一致
        close_s = pd.Series(100 + np.cumsum(np.random.normal(0.5, 2, n_days)))  # 模拟收盘价
        ma5, ma20 = close_s.rolling(5).mean(), close_s.rolling(20).mean()  # 短中期均线
        ma10, ma60 = close_s.rolling(10).mean(), close_s.rolling(60).mean()  # 变式均线组
        up52 = (ma5 > ma20) & (ma5.shift(1) <= ma20.shift(1))  # MA5上穿MA20
        down52 = (ma5 < ma20) & (ma5.shift(1) >= ma20.shift(1))  # MA5下穿MA20
        up1060 = (ma10 > ma60) & (ma10.shift(1) <= ma60.shift(1))  # MA10上穿MA60
        down1060 = (ma10 < ma60) & (ma10.shift(1) >= ma60.shift(1))  # MA10下穿MA60
        print(n_days, 'MA5/MA20金叉', np.flatnonzero(up52.to_numpy()).tolist(), '死叉', np.flatnonzero(down52.to_numpy()).tolist())  # 输出交叉位置
        print(n_days, 'MA10/MA60金叉', np.flatnonzero(up1060.to_numpy()).tolist(), '死叉', np.flatnonzero(down1060.to_numpy()).tolist())  # 输出交叉位置

    预期输出(本机 peter 环境实际运行结果,具体以平台运行结果为准):

    60 MA5/MA20金叉 [33, 55] 死叉 [39]
    60 MA10/MA60金叉 [] 死叉 []
    120 MA5/MA20金叉 [33, 55, 107] 死叉 [39, 103]
    120 MA10/MA60金叉 [] 死叉 []

    回扣主线:双均线交叉的判定写法见本章”金融应用:移动平均线”一节(列表 55.10),均线口径另见第 章节 20 章。