Matplotlib金融图表实战:零歧义、一致性、可追溯的可视化协议

📅 2026/7/13 12:08:12
Matplotlib金融图表实战:零歧义、一致性、可追溯的可视化协议
1. 项目概述为什么一张“能说话”的图比十行代码更有价值在金融数据分析这条路上我踩过最多的坑不是模型跑不起来也不是数据清洗出错而是——图做出来没人看懂。你花三小时调参、两小时写文档最后把一张默认样式的折线图发给风控同事对方扫了一眼就问“这Y轴单位是百万还是十亿那个尖峰是异常值还是真实交易峰值”那一刻你突然意识到图表不是代码的附属品它是数据故事的唯一叙述者。而Matplotlib这个Python生态里最老牌、最扎实的绘图引擎恰恰是那个最常被低估的“叙事导演”。它不炫技但足够可靠它不自动美化却把每一处控制权都交到你手上——前提是你得知道该拧哪颗螺丝。这篇内容就是我过去五年在券商量化部、基金公司投研中把Matplotlib从“能画”变成“会讲”的实战笔记。它不教你怎么画散点图而是告诉你当你要向投资经理解释一只债券的久期风险时如何用背景色暗示市场情绪当你要在季度报告里展示Alpha收益归因时如何让百分比刻度自动带千分位和%符号避免任何歧义当你需要在一张图上同时标出“历史最高价”“当前价格”“压力位”三个关键点时怎么用一条虚线一个箭头一行加粗文字让信息穿透力提升300%。核心关键词“Finance”不是点缀而是所有技巧的锚点——金融数据对精度、可读性、合规性的要求远高于其他领域。比如你绝不能让“1000000”以原始数字形式出现必须是“1,000,000”或“1.0M”你标出的“最高价”必须精确到小数点后两位且单位如USD要与坐标轴格式严格一致。这不是审美偏好而是专业底线。所以这不是一份泛泛的“Matplotlib美化指南”而是一份专为金融从业者定制的“可视化沟通协议”。它适合刚转行做量化分析的程序员也适合需要自己动手做PPT的基金经理助理——只要你需要让数字开口说话它就值得你花20分钟读完。2. 核心设计思路金融图表的三大铁律与Matplotlib的底层逻辑2.1 为什么金融图表不能“随便美”——三条不可妥协的铁律很多新手一上来就想换主题、加阴影、搞3D效果结果图越做越花信息越藏越深。我在中信证券做固收研究时带过几个实习生第一课就是撕掉他们做的“炫酷”K线图。原因很简单金融图表承载的是决策依据不是艺术展览。它必须遵守三条铁律而Matplotlib的设计哲学恰好是为这三条铁律量身打造的。铁律一零歧义原则。金融数据里一个逗号、一个小数点、一个单位符号的缺失都可能引发误判。比如Y轴显示“1000000”没人能立刻反应这是“100万”还是“1000万”X轴时间戳若没标注时区跨时区交易复盘就会出错。Matplotlib的ticker模块之所以重要正是因为它把“格式化”这件事从“事后补救”变成了“源头控制”。你不是在画完图后手动改标签而是在生成坐标轴刻度时就用FuncFormatter或StrMethodFormatter定义好规则——规则一旦写死所有后续图表自动继承杜绝人为疏漏。铁律二上下文一致性原则。一份季度报告里如果收益率曲线用白色背景波动率热力图用灰色背景相关性矩阵又用深蓝背景读者的注意力会被不断打断。我在华夏基金做FOF组合分析时团队强制推行“单色系模板”所有图表统一用seaborn-whitegrid风格主色只用深蓝#004d99和灰蓝#66b2ff辅助色禁用红色避免与下跌信号混淆。Matplotlib的plt.style.use()不是装饰开关而是“品牌一致性协议”。它确保你调用plt.plot()画的任何一张图字体、网格线粗细、刻度长度都完全一致省去每次手动setp()的重复劳动。铁律三可追溯性原则。监管检查时他们不关心你的图漂不漂亮只问“这个峰值点的数据源是什么计算逻辑在哪时间戳是否UTC8”因此所有标注annotation、参考线hlines/vlines都必须绑定真实数据坐标而非像素位置。我见过最危险的操作是有人用plt.text(0.5, 0.8, High)硬编码位置——一旦图表尺寸变化文字就飘到图外。正确的做法是永远用ax.annotate()的xy参数传入数据坐标如xy(datetime(2023,12,1), 105.2)再用xytext微调文字偏移。这样哪怕你把figsize从(12,6)改成(24,12)标注依然精准钉在那个交易日的收盘价上。2.2 Matplotlib的“三层控制模型”为什么直接改plt不够用很多教程教你plt.style.use(ggplot)就完事了但实际工作中你会发现有些地方死活改不过来。这是因为Matplotlib的控制是分层的像洋葱一样剥开第一层全局样式Global Style。plt.style.use()设置的是整个会话的默认值影响plt.plot()、plt.scatter()等顶层函数。但它管不了子图subplot里的细节比如某个特定ax的刻度格式。第二层Axes级控制Axes-level。这才是金融图表的主战场。ax.xaxis.set_major_formatter()、ax.tick_params()这些方法直接操作单个坐标轴对象。它优先级高于全局样式且能针对不同子图定制。比如主图Y轴显示“亿元”右侧副图Y轴显示“BP基点”就必须用ax1.yaxis和ax2.yaxis分别设置。第三层Artist级控制Artist-level。这是最精细的控制直接操作图形元素本身。ax.annotate()返回一个Text对象你可以用text.set_fontweight(bold)单独加粗某一行字plt.hlines()返回LineCollection能用set_linestyle(--)动态切换线型。在制作监管报送图表时我常用这一层给关键数值打“高亮边框”ax.add_patch(Rectangle((x_min, y_min), width, height, fillFalse, edgecolorred, linewidth2))比单纯加粗更醒目。理解这三层你就明白为什么“一键美化”不现实——金融图表需要的是“分层精准干预”。接下来的所有实操都会明确告诉你这一步是在哪一层操作为什么必须在这里动而不是在别处。3. 实操核心从一张默认折线图到专业金融图表的完整改造链3.1 背景与风格为什么seaborn-whitegrid是金融图表的起点先看一张默认的Matplotlib折线图import matplotlib.pyplot as plt import numpy as np import pandas as pd # 模拟国债收益率数据2023年1月-12月 dates pd.date_range(2023-01-01, 2023-12-31, freqD) yields 2.5 0.3 * np.sin(np.arange(len(dates)) * 2 * np.pi / 365) 0.1 * np.random.randn(len(dates)) df pd.DataFrame({date: dates, yield: yields}) plt.figure(figsize(12, 6)) plt.plot(df[date], df[yield]) plt.title(10-Year Treasury Yield (Default)) plt.show()这张图的问题很典型背景纯白无网格线条细得像蛛丝刻度密密麻麻挤在一起标题字号小得要凑近看。它传递的信息是“数据存在但我不在乎你能不能看清”。现在执行第一步改造——应用seaborn-whitegridplt.style.use(seaborn-whitegrid) # 仅此一行全局生效 plt.figure(figsize(12, 6)) plt.plot(df[date], df[yield]) plt.title(10-Year Treasury Yield (seaborn-whitegrid)) plt.show()效果立竿见影背景变成浅灰网格线条变粗刻度间距更舒展。但为什么选它不是因为“好看”而是三个硬核理由网格线是功能不是装饰。seaborn-whitegrid的网格线是浅灰色#e0e0e0粗细适中0.8pt它像一把隐形的尺子让你能快速估算两点间的Y值差。对比dark_background风格黑色背景上的白色网格在打印时会糊成一片而seaborn-whitegrid在PDF、PPT、纸质报告中都清晰可辨。白色背景保障色彩准确性。金融图表常用红绿表示涨跌但很多深色主题会改变人眼对饱和度的感知。在seaborn-whitegrid下red就是标准RGB(255,0,0)green就是RGB(0,128,0)没有色偏。这点在向监管提交彩色打印件时至关重要。字体与间距符合阅读习惯。它的默认字体是DejaVu Sans无衬线体在小字号下依然清晰坐标轴刻度与标签的间距tick_params(pad5)经过优化避免文字重叠。我测试过12种主流样式在200%缩放的PPT里seaborn-whitegrid的刻度标签可读性排名第一。提示plt.style.available会列出所有内置样式但别盲目试。金融场景下fivethirtyeight太花哨bmh网格线太淡classic又太简陋。seaborn-whitegrid是经过市场检验的“安全牌”。如果公司有VI规范可基于它二次定制plt.rcParams.update({font.size: 12, axes.titlesize: 16})。3.2 坐标轴格式化让数字自己“报身份”金融数据的单位、量级、精度必须一眼可辨。Matplotlib的ticker模块就是干这个的。我们分场景拆解场景一金额类数据如基金净值、交易额目标1000000→1,000,000或1.0M原理StrMethodFormatter用Python f-string语法{x:,}自动加千分位FuncFormatter用lambda函数做除法缩放。import matplotlib.ticker as ticker fig, ax plt.subplots(figsize(12, 6)) ax.plot(df[date], df[yield] * 1000000) # 模拟“万元”单位 # 方案A千分位推荐用于精确值 ax.yaxis.set_major_formatter(ticker.StrMethodFormatter({x:,.0f})) ax.set_ylabel(Net Value (CNY)) # 方案B缩写推荐用于大额概览 ax.yaxis.set_major_formatter(ticker.FuncFormatter(lambda x, pos: f{x/1e6:.1f}M)) ax.set_ylabel(Net Value (Million CNY))场景二利率/收益率类数据如国债收益率、信用利差目标2.537→2.54%原理PercentFormatter本质是x*100但需注意它要求输入数据已是小数0.02537而非百分数2.537。很多新手栽在这里。# 错误示范数据是2.537直接用PercentFormatter → 显示253.7% # 正确做法先归一化再格式化 yields_pct df[yield] / 100 # 转为小数 ax.plot(df[date], yields_pct) ax.yaxis.set_major_formatter(ticker.PercentFormatter(xmax1, decimals2)) ax.set_ylabel(Yield (%))场景三时间序列类数据如交易日、持仓周期目标2023-01-01→Jan 2023按月聚合原理DateFormatter配合MonthLocator避免日期拥挤。from matplotlib.dates import DateFormatter, MonthLocator ax.xaxis.set_major_locator(MonthLocator()) # 每月一个主刻度 ax.xaxis.set_major_formatter(DateFormatter(%b %Y)) # 格式Jan 2023 ax.tick_params(axisx, rotation30) # 旋转30度防重叠场景四混合单位如“美元千分位小数点”目标1050000.25→$1,050,000.25原理StrMethodFormatter支持复合格式{x:,.2f}中,是千分位.2f是两位小数。ax.yaxis.set_major_formatter(ticker.StrMethodFormatter(${x:,.2f})) ax.set_ylabel(Price (USD))实操心得我曾在平安资管做信用债分析发现一个致命bug——StrMethodFormatter({x:,.2f})在处理负数时会把负号放在逗号后面如1,000.00-。解决方案是改用FuncFormatterax.yaxis.set_major_formatter(ticker.FuncFormatter( lambda x, pos: f${x:,.2f} if x 0 else f-${abs(x):,.2f} ))3.3 标注与参考线让关键信息“跳出来”而不是“藏起来”金融图表的核心任务是引导读者关注决策点。annotate、hlines、vlines就是你的“视觉指挥棒”。基础标注一个点三要素缺一不可ax.annotate(text, xy, xytext, arrowprops)中xy数据坐标必须如(datetime(2023,12,1), 3.2)xytext文字坐标可选如(datetime(2023,12,15), 3.5)让文字远离数据点arrowprops箭头属性{arrowstyle: -, color: red, lw: 1.5}# 标出2023年最高收益率点 max_idx df[yield].idxmax() max_date df.loc[max_idx, date] max_yield df.loc[max_idx, yield] ax.annotate(fAll-Time High\n{max_yield:.2f}%, xy(max_date, max_yield), xytext(max_date pd.Timedelta(days30), max_yield 0.1), arrowprops{arrowstyle: -, color: darkred, lw: 2}, fontsize12, fontweightbold, bboxdict(boxstyleround,pad0.3, facecolorred, alpha0.1))水平/垂直参考线不只是“画条线”而是“定义区域”plt.hlines(y, xmin, xmax)的xmin/xmax必须用数据坐标而非0-1归一化坐标。否则图表缩放时线会错位。# 画出“政策利率走廊”上下限假设2.25%和2.75% ax.hlines(y2.25, xmindf[date].min(), xmaxdf[date].max(), colorsgreen, linestyles--, lw1.5, labelLower Bound) ax.hlines(y2.75, xmindf[date].min(), xmaxdf[date].max(), colorsred, linestyles--, lw1.5, labelUpper Bound) ax.legend()进阶技巧用ax.axvspan标出“事件窗口”比如标出美联储议息会议前后7天的波动区间# 议息会议日2023-03-22 meeting_date pd.Timestamp(2023-03-22) ax.axvspan(meeting_date - pd.Timedelta(days7), meeting_date pd.Timedelta(days7), alpha0.1, colororange, labelFOMC Window)注意事项所有annotate和hlines的fontsize、color必须与全局样式协调。我习惯把关键标注设为fontweightbold但绝不加大字号如30pt那会破坏图表平衡。12-14pt加粗是专业报告的黄金尺寸。3.4 尺寸、范围与图例控制读者的“视觉动线”一张图的尺寸、坐标轴范围、图例位置决定了读者眼睛怎么移动。金融图表必须“导流”而非“乱流”。尺寸控制figsize不是越大越好plt.subplots(figsize(12,6))是黄金比例2:1适合横版报告。但要注意figsize单位是英寸不是像素。12英寸≈30.5cm在A4纸横向打印时刚好占满宽度。如果图中有大量日期X轴会拥挤。此时宁可加高如(12,8)也不加宽因为纵向空间更易利用。坐标轴范围xlim/ylim是“聚焦镜头”默认plt.ylim()会包含所有数据点包括离群值。但在分析债券利差时一个极端值如-50BP会让整张图压缩成一条线。正确做法是主动裁剪# 利差数据剔除±3倍标准差的离群值 spread df[yield] - benchmark_yield q1, q3 np.percentile(spread, [25, 75]) iqr q3 - q1 lower_bound q1 - 1.5 * iqr upper_bound q3 1.5 * iqr ax.set_ylim(lower_bound, upper_bound) # 主动设定非自动图例定位loc参数的实战选择plt.legend(loc1)右上最常用但有陷阱如果右上角有数据点图例会遮挡。我的经验是locupper left当左上角空旷时如标题下方loccenter right当图右侧有足够空白需配合bbox_to_anchor微调locoutside right终极方案把图例放到图外plt.legend(bbox_to_anchor(1.02, 1), locupper left, borderaxespad0) # 这会把图例放在图右侧不侵占绘图区实操心得图例的markerscale1.5很重要。默认标记太小和文字不成比例。设为1.5后图例里的圆点大小是图中实际散点的1.5倍一眼就能对应上。另外prop{size: 11}必须显式设置否则继承全局字体可能过小。4. 金融场景专项三类高频图表的完整代码模板4.1 收益率曲线图Yield Curve多期限利率的时空对话这是固收分析的基石图。难点在于不同期限1M, 3M, 6M, 1Y...10Y数据点少需平滑Y轴单位是%X轴是期限非时间需自定义刻度。import numpy as np import pandas as pd import matplotlib.pyplot as plt import matplotlib.ticker as ticker # 模拟中国国债收益率曲线2023-12-01 tenors [1M, 3M, 6M, 1Y, 2Y, 3Y, 5Y, 7Y, 10Y, 30Y] yields [1.85, 1.92, 2.05, 2.18, 2.35, 2.48, 2.65, 2.78, 2.85, 3.02] # 单位% plt.style.use(seaborn-whitegrid) fig, ax plt.subplots(figsize(14, 7)) # 绘制曲线用scatterplot保证点线清晰 ax.scatter(range(len(tenors)), yields, s50, cdarkblue, zorder5) ax.plot(range(len(tenors)), yields, o-, colordarkblue, linewidth2.5, markersize6) # X轴自定义期限标签 ax.set_xticks(range(len(tenors))) ax.set_xticklabels(tenors, fontsize12) ax.set_xlabel(Maturity, fontsize13, fontweightbold) # Y轴百分比格式两位小数 ax.yaxis.set_major_formatter(ticker.PercentFormatter(xmax1, decimals2)) ax.set_ylabel(Yield, fontsize13, fontweightbold) # 添加关键标注10Y-1Y利差衡量曲线陡峭度 spread_10y_1y yields[-1] - yields[3] ax.annotate(f10Y-1Y Spread\n{spread_10y_1y:.2f}%, xy(len(tenors)-1, yields[-1]), xytext(len(tenors)-1, yields[-1] 0.15), arrowprops{arrowstyle: -, color: red, lw: 1.5}, fontsize11, fontweightbold, bboxdict(boxstyleround,pad0.2, facecolorred, alpha0.1)) # 网格微调Y轴主网格X轴次网格增强期限感 ax.grid(True, axisy, alpha0.8) ax.grid(True, axisx, whichminor, alpha0.4, linestyle--) ax.minorticks_on() # 标题与布局 ax.set_title(China Government Bond Yield Curve (Dec 1, 2023), fontsize16, fontweightbold, pad20) plt.tight_layout() plt.show()为什么这样设计scatterplot组合scatter确保每个期限点清晰可见s50plot用粗线连接体现“曲线”概念。纯plot会掩盖端点纯scatter缺乏连续感。zorder5确保数据点压在网格线上不被遮挡。ax.minorticks_on()开启次刻度配合whichminor的X轴次网格让“1M-3M-6M”这些短端期限的间隔感更强。利差标注直接计算yields[-1] - yields[3]10Y减1Y数值实时更新避免硬编码。4.2 信用利差热力图Credit Spread Heatmap行业与评级的二维透视这是信用分析的核心工具。难点在于行列标签长行业名、评级颜色映射需突出“高风险区域”且必须有数值标注。import seaborn as sns import numpy as np import pandas as pd # 模拟信用利差数据行业 vs 评级 industries [Banking, Real Estate, Tech, Energy, Consumer] ratings [AAA, AA, AA, A, A, BBB] # 利差单位BP基点 data np.array([ [15, 25, 35, 50, 70, 100], # Banking [45, 65, 85, 110, 140, 180], # Real Estate [20, 30, 40, 55, 75, 110], # Tech [35, 50, 65, 85, 110, 150], # Energy [25, 35, 45, 60, 80, 120] # Consumer ]) # 创建DataFrame df_heat pd.DataFrame(data, indexindustries, columnsratings) plt.style.use(seaborn-whitegrid) fig, ax plt.subplots(figsize(12, 8)) # 使用seaborn绘制热力图比plt.imshow更专业 sns.heatmap(df_heat, annotTrue, # 显示数值 fmtd, # 整数格式 cmapRdYlBu_r, # 红-黄-蓝反向高利差红在上 cbar_kws{label: Credit Spread (BP), shrink: 0.8}, axax) # 设置标签 ax.set_xlabel(Credit Rating, fontsize12, fontweightbold) ax.set_ylabel(Industry, fontsize12, fontweightbold) ax.set_title(Credit Spread by Industry Rating (BP), fontsize14, fontweightbold, pad20) # 旋转X轴标签避免重叠 plt.xticks(rotation45, haright) plt.yticks(rotation0) # 微调布局 plt.tight_layout() plt.show()为什么选seaborn.heatmap而非plt.imshowannotTrue自动在每个格子中心写数值fmtd确保整数BP不用小数。cmapRdYlBu_r_r表示反向让高利差风险高显示为红色符合金融直觉。标准RdYlBu是低值红容易误解。cbar_kws定制色条label明确单位shrink0.8让色条高度匹配图表不突兀。plt.xticks(rotation45)45度是最佳角度既看清标签又不占用过多垂直空间。4.3 多因子归因分析图Multi-Factor Attribution分解Alpha的贡献来源这是量化投资的灵魂图。难点在于多个因子市值、估值、动量贡献值有正有负需用“瀑布图”清晰展示Y轴是百分比需精确到0.01%。import matplotlib.pyplot as plt import numpy as np import pandas as pd # 模拟因子归因结果单位% factors [Market Return, Size Factor, Value Factor, Momentum Factor, Residual Alpha] contributions [8.25, 1.32, -0.45, 0.87, 2.11] plt.style.use(seaborn-whitegrid) fig, ax plt.subplots(figsize(10, 6)) # 绘制瀑布图用barh正负颜色区分 colors [steelblue if x 0 else firebrick for x in contributions] bars ax.barh(factors, contributions, colorcolors, alpha0.8) # 添加数值标签 for i, (bar, contrib) in enumerate(zip(bars, contributions)): # 正数标右边负数标左边 if contrib 0: ax.text(contrib 0.05, i, f{contrib:.2f}%, vacenter, fontsize11, fontweightbold) else: ax.text(contrib - 0.05, i, f{contrib:.2f}%, vacenter, haright, fontsize11, fontweightbold) # Y轴反转顺序让“Market Return”在最上面逻辑起点 ax.invert_yaxis() # X轴百分比格式带正负号 ax.xaxis.set_major_formatter(ticker.PercentFormatter(xmax1, decimals2)) ax.set_xlabel(Contribution to Return (%), fontsize12, fontweightbold) # 添加总Alpha线虚线 total_alpha sum(contributions) ax.axvline(xtotal_alpha, colordarkgreen, linestyle--, linewidth2, labelfTotal Alpha: {total_alpha:.2f}%) ax.legend(loclower right, fontsize10) # 标题 ax.set_title(Factor Attribution Analysis (Q4 2023), fontsize14, fontweightbold, pad20) plt.tight_layout() plt.show()关键设计点ax.invert_yaxis()让市场收益基准在最上方符合“从基准开始逐项加减”的归因逻辑。f{contrib:.2f}%符号强制显示正负号:.2f确保两位小数%单位。ax.axvline()用虚线标出总Alpha颜色darkgreen区别于因子柱状图形成视觉锚点。颜色策略正贡献用steelblue专业、稳重负贡献用firebrick警示、醒目避免用红绿色盲不友好。5. 常见问题与避坑指南那些让我加班到凌晨的Matplotlib Bug5.1 “字体不显示”问题中文、特殊符号、数学公式的终极解法问题现象标题写plt.title(收益率 (%))结果显示方块或plt.ylabel(r$\sigma$)希腊字母σ不渲染。根本原因Matplotlib默认字体DejaVu Sans不支持中文且LaTeX渲染需额外配置。解决方案三步走永久解决中文修改matplotlibrc文件找到路径plt.matplotlib_fname()添加font.sans-serif: SimHei, DejaVu Sans, Bitstream Vera Sans, sans-serif axes.unicode_minus: False # 解决负号显示为方块提示SimHei是Windows黑体Mac用Heiti SCLinux用WenQuanYi Zen Hei。一行写多个备选系统自动 fallback。临时解决代码内plt.rcParams[font.sans-serif] [SimHei, Arial Unicode MS] plt.rcParams[axes.unicode_minus] False数学公式启用LaTeX渲染需系统安装LaTeXplt.rcParams[text.usetex] True # 启用LaTeX plt.title(rVolatility $\sigma$ (Annualized)) # r 原始字符串若不想装LaTeX用mathtext轻量plt.rcParams[mathtext.fontset] stix # 更专业的数学字体 plt.title(rVolatility $\sigma$ (Annualized))5.2 “保存图片模糊/失真”问题矢量图与位图的选择逻辑问题现象plt.savefig(chart.png)PPT里放大后锯齿或plt.savefig(chart.pdf)微信里打不开。真相这不是Matplotlib的Bug而是你没理解输出格式的本质。格式优点缺点适用场景PDF/EPS矢量图无限缩放不失真文件小支持LaTeX部分旧版Office不兼容微信/钉钉预览可能失败学术论文、PDF报告、印刷品PNG通用性强微信/钉钉/PPT完美支持位图放大失真文件较大日常汇报、邮件、即时通讯SVG矢量图网页直接嵌入可编辑文件稍大部分PPT版本支持不佳内部Wiki、网页报告正确做法对外交付客户/监管plt.savefig(chart.pdf, dpi300, bbox_inchestight)内部沟通微信/PPTplt.savefig(chart.png, dpi300, bbox_inchestight)dpi300是印刷标准bbox_inchestight自动裁掉空白边距。实操心得我曾因PNG的dpi设为72默认值导致基金季报里的图被质疑“截图糊弄”。记住dpi只对位图PNG/JPEG有效对PDF无效。PDF的清晰度取决于矢量精度与dpi无关。5.3 “图例覆盖数据”问题自动避让的隐藏技巧问题现象plt.legend()后图例盖住了右上角的关键数据点。解决方案按优先级排序bbox_to_anchorloc精确定位推荐plt.legend(bbox_to_anchor(1.02, 0.98), locupper left, borderaxespad0, frameonTrue) # (1.02, 0.98) 表示图右侧外、顶部下2%完美避开绘图区plt.figlegend()全局图例多子图时用fig.legend(handles, labels, locupper center, bbox_to_anchor(0.5, 1.02), n