ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

深入理解Matplotlib:从核心概念到高频绘图实战与避坑指南

深入理解Matplotlib:从核心概念到高频绘图实战与避坑指南 做了这么多年数据分析跟Matplotlib打交道的次数多得数不清。每次看到有人上来就plt.plot(x, y)然后发现出来的图不是想象那样或者折腾半天中文字体全变方块我就知道多半是没把它的核心逻辑搞明白就开始复制代码了。Matplotlib是Python生态里最基础、最老牌的可视化库它能画折线图、散点图、柱状图、雷达图也能做复杂的子图布局和交互预览几乎所有数据可视化场景都绕不开它。这篇文章打算把Matplotlib的使用方法从头到尾梳理一遍重点放在容易出问题的细节和日常高频功能上不管是刚开始用的小白还是用了挺久但一直靠复制粘贴改图的人应该都能在这里找到一些之前没注意到的关键点。先说清楚Matplotlib有三个核心概念经常把人绕晕figure、axes、axis。很多人以为plt.plot是直接往图上画其实没那么简单。弄清楚这三层关系之后你会发现绝大多数报错和布局问题都能自己想明白不用再到网上到处搜了。这三者的关系我用一个生活化的类比解释一下。把figure想成一张画板它是整个图的最大容器你所有的图都放在这块画板上axes是画板上的一块具体画布这块画布有自己的坐标系和数据区而axis是画布上的坐标轴——就是下边通常带刻度的那条轴和左边竖着的那条轴负责显示刻度和标签。一张画板figure上面可以放多块画布axes每一块画布也都有自己的左右上下轴axis通常说x轴和y轴三维图中还会出z轴。所以plt.plot()的实际逻辑是如果当前没有画板和画布Matplotlib自动给你创建一块画板、一块画布然后在这块画布上画图。如果已经有了它就在当前活动的画布上追加绘制。理解和接口风格也有关系。Matplotlib官方维护了两套画图接口一套是pyplot接口也就是plt.xxx()这种写法很适合交互式环境随手画图很快另一套是面向对象接口先创建figure对象再在它上面创建axes对象然后调用ax.plot()。这两套接口长期被大家混用出了问题往往就是混用闹的。import matplotlib.pyplot as plt # pyplot风格 plt.plot([1, 2, 3], [3, 1, 4]) plt.show() # 面向对象风格 fig, ax plt.subplots() ax.plot([1, 2, 3], [3, 1, 4]) plt.show()这两种方法画出来的图是一样的但面向对象风格更可控。plt.subplots()同时返回画板和画布后续可以在同一个ax上面继续堆叠多个图层也可以创建多个ax并排展示这种能力是pyplot接口比较难实现的。我在实际项目里有一个习惯做探索性分析时用pyplot接口追求快一旦要写可复用的绘图函数或者要出正式图表就全部转成面向对象风格。这一点建议刚开始学的人就直接养成后面写复杂图会少踩很多坑。环境搭建这块很多人以为pip install matplotlib装上就算完事了其实还有两个细节值得注意。第一个细节是虚拟环境问题如果你电脑上同时装了多个Python版本或者用了Anaconda新手经常出现装是装了但导入失败的情况。这个坑几乎都是环境错位造成的。我用pip时会严格确认安装到了当前正在用的那个解释器环境里最稳妥的方式是直接在项目里建一个虚拟环境再装python -m venv venv source venv/bin/activate # Windows下是 venv\Scripts\activate pip install matplotlib第二个细节是版本差异。老版本和新版本在默认样式、字体渲染上差别不小默认字体配置在老版本上尤其麻烦。如果你在Jupyter里运行还需要确认%matplotlib inline是否生效否则可能看不到图。我曾经在远程服务器上跑脚本明明画图代码没有报错就是不显示后来发现是后端没有配置好需要切换到Agg后端保存图片或者配置合适的GUI后端。不过这些属于进阶问题日常本地画图遇到的概率没那么高。中文字体乱码是另一个高频问题。默认情况下Matplotlib的字体配置里没有常见的中文字体所以图里一旦出现中文标签出来的全是方块。根治方案可以写进全局配置一次搞定之后不用再每次关心中文问题。我写常用脚本时都会在本机执行一次import matplotlib.pyplot as plt import matplotlib.font_manager as fm plt.rcParams[font.sans-serif] [SimHei, Microsoft YaHei, PingFang SC, Arial Unicode MS] plt.rcParams[axes.unicode_minus] False第一行指定中文字体第二行解决坐标轴负号显示成方块的问题。上面的列表兼容Windows、macOS和大部分Linux桌面环境。此外还有一个彻底一点的办法直接用字体的绝对路径注册from matplotlib import font_manager # 先找到你系统里中文字体文件所在的路径比如 C:/Windows/Fonts/simhei.ttf font_manager.fontManager.addfont(C:/Windows/Fonts/simhei.ttf) plt.rcParams[font.family] SimHei这个方法强在不需要依赖系统默认配置哪怕你在没有中文字体的服务器上画图只要把字体文件一起带过去注册一下就能正常显示中文。我曾拿这个办法在纯命令行环境里跑出了带中文标签的PNG图片。现在说回日常画图。最常用的就是折线图、柱状图、散点图和雷达图。折线图适合展示连续变化趋势柱状图适合对比分类数据散点图看两个变量之间的关系雷达图展示多个维度的综合表现。我先把折线图和柱状图的基础代码放出来这是两块最基础的积木几乎所有图表都能从它们演变过来。import matplotlib.pyplot as plt import numpy as np x np.arange(1, 13) y1 np.array([5, 7, 8, 9, 12, 15, 18, 17, 14, 10, 8, 6]) y2 np.array([2, 3, 3, 4, 6, 8, 10, 9, 7, 5, 4, 2]) fig, ax plt.subplots(figsize(8, 5)) ax.plot(x, y1, markero, label今年) ax.plot(x, y2, markers, linestyle--, label去年) ax.set_title(月度趋势对比) ax.set_xlabel(月份) ax.set_ylabel(数值) ax.legend() plt.show()柱状图类似的逻辑只要把plot换成bar或barh。注意bar的默认宽度是0.8如果有多组并排柱需要自己控制x轴偏移量不然柱子会全叠在一起fig, ax plt.subplots(figsize(8, 5)) width 0.35 x np.array([1, 2, 3, 4]) v1 np.array([10, 20, 15, 25]) v2 np.array([12, 18, 17, 22]) ax.bar(x - width/2, v1, width, labelA组) ax.bar(x width/2, v2, width, labelB组) ax.set_xticks(x) ax.set_xticklabels([Q1, Q2, Q3, Q4]) ax.legend() plt.show()散点图的热门问题集中在透明度和网格上。当数据点很多时重叠严重直接画出来的图就是一团黑。透明度参数alpha能有效缓解这个问题网格线grid则让读者更容易读出数据大致位置。rng np.random.default_rng(42) x rng.normal(0, 1, 500) y rng.normal(0, 1, 500) * 2 x fig, ax plt.subplots(figsize(7, 6)) scatter ax.scatter( x, y, cy, # 用y值控制颜色 cmapviridis, alpha0.6, # 重点参数控制点透明度 s30 # 点的大小 ) ax.grid(True, linestyle--, linewidth0.6, alpha0.6) ax.set_xlabel(X轴) ax.set_ylabel(Y轴) cbar plt.colorbar(scatter, axax) cbar.set_label(Y值) plt.show()这里cy意味着颜色由数据值决定cmapviridis是常见的颜色映射。透明度0.6对我来说是大量重叠数据点的常用设定太低了看不清整体轮廓太高了又看不出密度分布需要根据点数和重叠程度调节。网格开启后默认的样式有点粗我一般手动改成虚线并且调低透明度观感会干净很多。雷达图在热门搜索里不少见常用于展示多维性能评分比如一个选手的力量、速度、耐力、技巧四个维度的能力值。雷达图的本质是用极坐标来画需要在subplot时指定projectionpolar。这个参数在刚开始用的时候很容易漏掉漏掉之后就会得到一张普通的笛卡尔坐标图完全不是雷达图的样子。import numpy as np import matplotlib.pyplot as plt categories [力量, 速度, 耐力, 技巧, 敏捷] values [80, 65, 90, 75, 70] values np.append(values, values[0]) angles np.linspace(0, 2 * np.pi, len(categories), endpointFalse).tolist() angles np.append(angles, angles[0]) fig, ax plt.subplots(figsize(6, 6), subplot_kw{projection: polar}) ax.plot(angles, values, markero, linewidth2) ax.fill(angles, values, alpha0.25) ax.set_xticks(angles[:-1]) ax.set_xticklabels(categories) plt.show()雷达图的几个注意点第一首尾要闭合所以values和angles都额外append了第一个值第二xticklabels的数量要和类别数量保持一致第三默认极坐标的刻度是从0度开始逆时针排列如果类别顺序和预期不一致可以通过调整起始角度修正。填充的alpha别取太大否则几个维度叠加后会互相遮挡看不清楚。图例是很多人觉得简单但又频繁出错的部分。最基础的用法是在plot、scatter或bar中传入label参数最后调ax.legend()。但如果只有一条曲线时忘记加label或者多条曲线都没加labellegend会出现各种意外。真正常见的麻烦是图例位置和内容不理想这时候用legend的loc参数和handles参数可以解决大多数问题。fig, ax plt.subplots() l1, ax.plot([1, 2, 3], [1, 2, 3], labelline A) l2, ax.plot([1, 2, 3], [3, 2, 1], labelline B) ax.legend(handles[l1, l2], locupper left, frameonFalse)loc可填best、upper right、lower center之类的方位串也可以填数值坐标。frameonFalse能去掉图例的边框让图看起来更简洁。如果图例项不是直接从当前axes里的绘图对象来的handles参数还可以手动构造一个特殊的图例对象比如我要在图上加一条示意线但不想真的画数据时就可以用Line2D做一个假曲线加进图例。遇到图例内容重叠在数据上的情况不要盲目把图例挪到某个边角有时候调整legend的位置到外侧更合理。方式是把legend的bbox_to_anchor参数配成大于1的数值比如bbox_to_anchor(1.05, 1)这样图例会放到axes外面配合plt.tight_layout()可以避免图表被裁切。网格和刻度的精细控制属于让图显得专业的关键环节。Matplotlib默认在每个坐标轴主刻度位置显示标签但我们经常需要手动指定刻度位置让坐标轴显示得更舒服。用得最多的是set_xticks和set_xticklabels前者管刻度线位置后者管刻度标签文字。还有一个好用但经常被忽略的是MultipleLocator可以按照固定间隔自动排刻度from matplotlib.ticker import MultipleLocator fig, ax plt.subplots(figsize(8, 5)) x np.linspace(0, 10, 100) y np.sin(x) ax.plot(x, y) ax.xaxis.set_major_locator(MultipleLocator(1)) # 每隔1个单位显示刻度 ax.yaxis.set_major_locator(MultipleLocator(0.2)) # 每隔0.2个单位显示刻度 ax.grid(whichmajor, linestyle-, alpha0.5) ax.grid(whichminor, linestyle--, alpha0.3) plt.show()网格分主刻度网格major和次刻度网格minor两者可以分别设置样式。为了让刻度标签更清晰还可以用ax.tick_params(axisx, rotation45)来旋转刻度文字这个在处理长标签时特别实用。颜色系统是另一大块。Matplotlib内置了命名颜色、颜色序列和Colormap三套体系。命名颜色就是red、blue、steelblue这类带名字的颜色适合快速测试颜色序列主要用于多个系列自动轮换比如plt.rcParams[axes.prop_cycle]控制了新绘图对象在不指定颜色时自动使用的颜色循环Colormap则是从数值到颜色的映射适合散点图、热力图等高连续数据的场景。命名颜色和Colormap混用时会遇到一个问题同一个图里既有离散分类又有连续数据如果都用同一个Colormap反而会误导读者。我的习惯是分类数据用颜色序列中的离散颜色连续数据用viridis或plasma这样的顺序色带必要时把离散颜色和连续色带分开配置。若想自己定义Colormap可以用LinearSegmentedColormapfrom matplotlib.colors import LinearSegmentedColormap colors [#2a4d6e, #e08e45, #d14334] custom_cmap LinearSegmentedColormap.from_list(my_cmap, colors)这段代码会把三个颜色按渐变连起来变成一个有序色带。自定义Colormap在品牌图、论文图、报告图里很常见因为默认的viridis虽然科学但不一定符合视觉风格。一个完整的图表还要考虑整体样式。Matplotlib内置了几套样式用plt.style.use()可以一键切换比如ggplot、seaborn-v0_8-whitegrid、default。我在写正式项目时通常直接把seaborn-v0_8-whitegrid作为基础样式再叠加自己的字体和颜色设置出来的图比纯默认样式好很多。注意不同版本下seaborn样式的名称有差异老版本里叫seaborn-whitegrid新版本带上了-v0_8这种前缀按版本适配即可。样式的使用要在画图之前写例如plt.style.use(seaborn-v0_8-whitegrid) plt.rcParams.update({ font.size: 10, axes.labelsize: 12, axes.titlesize: 14, legend.fontsize: 10 })这些全局参数可以集中放到项目头部保证所有图风格统一。我维护的绘图脚本里几乎都有一份这样的全局配置改一处全项目有效不用每张图重复设置。保存图片这块最经典的错误是直接右键截图然后贴到报告里导致图很模糊。Matplotlib的savefig支持dpi参数控清晰度一般来说论文配图建议dpi300PPT演示可以200左右网页用100也够了。除了dpi保存时还有两个容易忽略的地方bbox_inchestight会自动裁剪空白边界transparentTrue能输出透明背景很多汇报模板需要透明背景图。fig.savefig( output.png, dpi300, bbox_inchestight, facecolorwhite )bbox_inchestight这个参数值得一提它做的是根据figure内容智能调整边框把多余白边裁掉。如果图例被放到了axes外面使用这个参数后就能保证图例不die。注意加了transparentTrue时facecolor会被忽略如果想既要透明又要白底不要同时配置。最后聊聊那些高频报错和布局问题。我在实际使用中遇到最多的问题大概有三类。第一类是图形重叠尤其是多个子图之间标题或标签互相挤压解决方法是加plt.tight_layout()它会自动调整子图间距如果还不够可以用plt.subplots_adjust(hspace0.3, wspace0.3)手动调整子图之间的高度和宽度距离。第二类是KeyError: xxx这种与rcParams相关的错误多半是参数名写错了或者Matplotlib版本太老不支持某个新参数。我只能说升级到新版本并且多看官方文档的rcParams列表错误原因基本都能对上。第三类是中文乱码的变体——中文字体设置当前有效但换了一个电脑之后又变成方块这是因为字体列表里第一个字体在另一台机器上不存在所以要把多个平台常见字体都写在fallback列表里尽量让配好的脚本跨机器可用。关于多子图的布局我这里多写一点。很多人一开始不知道怎么在一个figure里放多张图一通乱写最后只有一张图生效。逻辑很简单plt.subplots(nrows, ncols)生成一个由axes组成的数组然后按索引访问每个axes。fig, axes plt.subplots(2, 2, figsize(10, 8)) axes[0, 0].plot([1, 2, 3], [1, 4, 9]) axes[0, 1].bar([A, B, C], [3, 7, 5]) axes[1, 0].scatter([1, 2, 3], [2, 4, 8]) axes[1, 1].plot([3, 2, 1], [1, 3, 2])返回的axes是二维数组所以索引用[行号, 列号]。如果只要一行子图返回的是一维数组。这个差异经常让人报错写axes[0, 1]时一维数组会报错需要改成axes[1]。另外2x2布局不一定要全部填满留空的地方可以用axes[1, 1].set_visible(False)或fig.delaxes(axes[1, 1])隐藏掉让布局更自然。除了基础布局gridspec可以实现更灵活的跨行列布局。这属于进阶用法但如果你的报表里需要做那种上面一个大图、下面两个小图的排版直接用gridspec是最舒服的。from matplotlib.gridspec import GridSpec fig plt.figure(figsize(9, 7)) gs GridSpec(2, 2, figurefig, height_ratios[2, 1]) ax1 fig.add_subplot(gs[0, :]) # 上面一整行 ax2 fig.add_subplot(gs[1, 0]) # 左下 ax3 fig.add_subplot(gs[1, 1]) # 右下height_ratios控制了上下两行的高度比例gs[0, :]表示第一行跨越所有列这种写法在需要主次分明的图里效果很好。我经常用这种结构做数据概览页上面一张大趋势图下面左侧分布图、右侧相关性图一张A4图能承载大量信息又不显得拥挤。Matplotlib这套体系除了画静态图也能做简单的交互功能比如用mplcursors库在图上悬停显示数据点信息或者用widgets模块画滑块按钮实现动态筛选。不过这些大多数场景用不上真正核心的仍然是把基础画图能力打扎实。我个人在实际操作中最深的体会是Matplotlib的官方文档和示例库其实就是最好的资料但它给出的示例偏向演示API凡是涉及放在真实项目里怎么排版、字体怎么统一、图标怎么美观这类工程化问题官方文档反而不太说。多维护一套自己的绘图配置模块把字体、颜色、默认大小、常用样式统一封装才是长期提高效率的做法。每接到一个新的画图任务只需要关注数据不用重复设置样式。另外补充一个小技巧写脚本的时候建议先构造几个简单的样例数据快速跑通流程再套真实数据。这样遇到问题很容易判断是数据问题还是画图代码问题。很多人在真实数据上画不出图最后发现是数据里有缺失值没处理报错信息还没看懂就已经浪费了半天。处理缺失值最直接的办法是df.dropna()或对数据进行插值画图前先把数据清洗干净能省下大量Debug时间。真要说还有什么想分享的那就是别一次画太复杂的图。刚开始学的时候总想把所有信息塞进一张图里面板五个坐标系、图例十多个、颜色花里胡哨结果图出来信息过载谁看了都头疼。好的可视化是让读者在一秒内就抓住重点不是展示你掌握了多少API。这一点在Python里很多库都能做到但Matplotlib给了你完全的自由度也把设计责任交到了你手上。就写到这吧希望这篇东西能让你少走一点那些我当年绕过的弯路。
返回列表