ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

Matplotlib.pyplot从入门到顺手:避坑指南与多子图布局详解

Matplotlib.pyplot从入门到顺手:避坑指南与多子图布局详解 绘图的坑我帮你踩过了Matplotlib.pyplot 从入门到顺手我见过太多人第一次跑数据可视化卡在module matplotlib has no attribute pyplot这类报错上或者装了半天库import matplotlib.pyplot as plt依然红一片。说实话作为一个用 Python 做数据分析、平时出图表比写文档还勤快的人我太清楚 Matplotlib 这套东西对新手有多劝退了——文档不算友好默认样式又丑网上教程东一榔头西一棒子真正能落地解决问题的干货反而分散。所以这篇我就把 Matplotlib.pyplot 这套绘画体系从安装、核心概念、常用图表的实操写法到“六张图合在一个图里”这种多子图布局、中文乱码和样式美化这些高频痛点一次性串起来讲。不管你是刚接触数据可视化的学生还是工作中被各种报表逼着画图的职场人这篇文章都能让你少走弯路照着手敲就能跑出图来。1. 先把基础夯实matplotlib 的安装与“面向对象”思维1.1 安装那点事别让环境劝退你matplotlib 的安装本身不算复杂但很多人恰恰死在这一步。常见的pip install matplotlib报错多半是网络源太慢、Python 版本太老或者当前环境压根没激活对。我习惯在装任何 Python 包之前先敲一句python -m pip install --upgrade pip把 pip 自己先升到最新然后再装 matplotlibpython -m pip install matplotlib -i https://pypi.tuna.tsinghua.edu.cn/simple这里用的是清华镜像源国内下载速度快非常多。如果你用的是 Anaconda 发行版那更省事conda 环境里一般自带 matplotlib直接在终端里输入conda install matplotlib很多新手会遇到“明明装了却还是ModuleNotFoundError: No module named matplotlib”这种十有八九是环境混了。比如你在终端里pip install装了一个然后在 Jupyter Notebook 或者 IDE 里用的是另一个 Python 解释器。排查方法很简单在报错的那个环境里重新跑一下安装命令或者直接在代码里打印import sys print(sys.executable)看看当前解释器路径再去命令行里pip show matplotlib确认包的安装路径两者能对上就基本没问题。这个坑我踩了不止一次尤其是换了新电脑、装了多个 Python 版本的时候特别容易中招。1.2 pyplot 是什么它和你画的图是什么关系import matplotlib.pyplot as plt里的pyplot说白了是 matplotlib 提供的一个面向过程的接口模块。它像一个“画板管家”你调用一次plt.plot()它就在当前画布上画一笔你调用plt.show()它就把画布弹出来给你看。这个机制类似你在纸上先画草图草稿不用留最后成品满意了再精心装裱。但真正让我从“会用”到“用明白”的是理解了 Matplotlib 的面向对象接口。简单理解Figure整张画布/纸张所有元素都贴在它上面。Axes画布上的一个绘图区域也就是一个“子图”你实际看到的数据、坐标轴、刻度都是 Axes 的属性。Axis就是 X 轴、Y 轴本身控制刻度、标签等细节。刚开始用plt.plot(x, y)这种写法时你以为在往“全局画布”上画东西其实 pyplot 在背后自动帮你创建了一个 Figure 和一个 Axes。等你要在一个图里放六个子图的时候只有把 Figure 和 Axes 的概念搞明白才能随心所欲地排兵布阵。所以后面讲子图布局之前我会先带你把这个模型吃透。2. 从零画出一张图折线图与散点图实战2.1 你的第一张折线图代码背后发生了什么折线图大概是 matplotlib 里最常用、也最被低估的图表。很多人用plt.plot(x, y)画完就拉倒但里面的参数值得玩味。我先给出一个最朴素的版本import matplotlib.pyplot as plt import numpy as np x np.linspace(0, 10, 100) y np.sin(x) plt.plot(x, y) plt.title(y sin(x)) plt.xlabel(x) plt.ylabel(y) plt.grid(True) plt.show()这段代码会画出一条正弦波。但你可能好奇np.linspace(0, 10, 100)干了什么它生成从 0 到 10 之间均匀分布的 100 个点。因为正弦函数是连续曲线你不可能真的画“无限”个点用有限点连成折线就足够平滑了。100 个点在 0~10 这个区间上非常平滑根本看不出是折线如果只给 10 个点曲线就会呈现明显的锯齿感。plt.plot(x, y)里面还有很多好用的参数。比如你想把线的颜色改成红色线型改成虚线点用圆形标记plt.plot(x, y, colorred, linestyle--, markero, markersize4)这里的marker控制坐标点上的标记样式markersize控制标记大小。我一般这么记plot函数有通用的样式参数还有对应的缩写写法比如r--o就代表红色虚线加圆形标记。缩写在快速画图时很高效但代码可读性会下降合作项目里我会写成完整参数名方便别人看懂。2.2 散点图当数据凌乱时更需要被看见散点图的出场场景往往是“两个变量之间有没有相关性”。比如分析身高和体重的关系、广告投入和销售额的关系用散点图一眼就能看出趋势。x np.random.randn(200) y np.random.randn(200) * 0.5 x * 0.3 plt.scatter(x, y, alpha0.6, s30, csteelblue) plt.title(Scatter Plot Demo) plt.xlabel(X) plt.ylabel(Y) plt.show()这里alpha0.6是透明度。当散点特别多、重叠特别严重的时候不加透明度你会看到一团黑加了透明度就能看到分布密度差异。s是点的大小c是颜色。这些参数看似简单但真正决定一张散点图好不好看、能不能传递信息的往往是这些细节。我自己的习惯是散点图尽量配合趋势线一起用。直接用np.polyfit拟合一条一次函数再叠加画到图上比单纯丢一堆点上去更有说服力。import numpy as np coeff np.polyfit(x, y, deg1) trend np.polyval(coeff, x) plt.scatter(x, y, alpha0.6, s30, csteelblue, labeldata) plt.plot(x, trend, colorred, linewidth2, labeltrend) plt.legend() plt.show()np.polyfit的作用就是做最小二乘拟合deg1表示一次线性拟合返回的是斜率与截距组成的数组。再用np.polyval把拟合系数代回去算出每个 x 对应的拟合值。这样图上有原始点、有趋势线、有图例一眼就能跟老板交代“这两列数据到底有没有关系”。3. 柱状图与饼图业务汇报里的“发言担当”3.1 柱状图别忽略基准线这个细节柱状图在业务汇报里几乎是刚需。比如你手里有三个城市的销售额数据想直观比大小cities [Beijing, Shanghai, Guangzhou] sales [120, 165, 90] plt.bar(cities, sales, color[#4C72B0, #DD8452, #55A868]) plt.ylabel(Sales (万元)) plt.title(City Sales Comparison) plt.show()plt.bar第一个参数是分类轴的位置第二个是高度也就是数值大小。颜色我习惯用 matplotlib 内置的配色 cycler不用自己调得太费神这几个十六进制色值是来自 seaborn 风格的颜色看起来比默认的蓝橙绿和谐不少。柱状图有一个常被忽略但很关键的参数bottom。如果你想做“堆积柱状图”比如一个季度里线上销售额和线下销售额分别多少叠加起来看总量这个参数就非常有用import numpy as np quarters [Q1, Q2, Q3] online [30, 42, 38] offline [50, 48, 45] plt.bar(quarters, online, labelOnline) plt.bar(quarters, offline, bottomonline, labelOffline) plt.ylabel(Sales) plt.legend() plt.show()我踩过一个坑堆积柱状图的bottom参数如果没对齐好两个分类的数据会“错位叠加”看起来特别奇怪。比如online有 3 个数据、offline有 3 个数据必须确保bottom里的序列长度和当前柱子的序列长度一致。如果长度不一致matplotlib 不会报错但你的图会悄悄画错地方而且不仔细看根本发现不了。3.2 饼图三个容易翻车的坑饼图是一个看着简单、实际很容易翻车的图。小问题在于标签重叠大问题在于信息表达不清。一张饼图超过 5 个类别我建议你换个图表类型或者把次要类别合并成一个“其他”。一个相对稳的写法labels [A, B, C, D] sizes [45, 30, 15, 10] colors [#1f77b4, #ff7f0e, #2ca02c, #d62728] explode (0, 0.1, 0, 0) # 只有第二个扇形突出 plt.pie(sizes, labelslabels, colorscolors, autopct%1.1f%%, startangle90, explodeexplode, shadowFalse) plt.axis(equal) plt.show()autopct%1.1f%%控制扇形内显示的百分比格式保留一位小数。startangle90是可以转饼图起始角度我习惯从 90 度开始这样第一块扇形在正上方读起来更顺。explode是让某一块“炸开”适合强调重点类别。但有一类问题很隐蔽plt.pie默认会自动按数据顺序从逆时针方向排列扇形如果数据本身不是按大小排的你可能想手动调整顺序。我一般先给数据排序再传进去否则图上一团乱麻用户看半天也不知道哪个占比大。另一个坑是plt.axis(equal)千万别漏不写的话饼图会被拉成椭圆视觉上比例全错了。4. 六张图合在一个图上子图布局的精髓4.1 用 plt.subplots 直接切分画布很多热搜词会指向“matplotlib 六张图合在一个图上”这其实是多子图布局问题。最推荐的方式是plt.subplots()它比plt.subplot()更 Pythonic而且返回的fig, axes结构让你可以分别控制每个子图。fig, axes plt.subplots(2, 3, figsize(12, 8))这行代码生成了一个 2 行 3 列的画布网格共 6 个子图。figsize控制整张图的宽高单位是英寸。2 行 3 列在我实践下来是六张图里最舒服的排列横向 3 张不会太拥挤纵向 2 行也能保证高度合理。如果你把figsize设成 (6, 4)每个子图会非常局促标签和标题互相挤压图基本没法看。接下来向每个子图填充内容import numpy as np x np.linspace(0, 10, 100) fig, axes plt.subplots(2, 3, figsize(12, 8)) axes[0, 0].plot(x, np.sin(x)) axes[0, 0].set_title(sin(x)) axes[0, 1].plot(x, np.cos(x)) axes[0, 1].set_title(cos(x)) axes[0, 2].plot(x, np.tan(x)) axes[0, 2].set_ylim(-5, 5) axes[0, 2].set_title(tan(x)) axes[1, 0].plot(x, x**2) axes[1, 0].set_title(x^2) axes[1, 1].plot(x, np.exp(x)) axes[1, 1].set_title(exp(x)) axes[1, 2].plot(x, np.log(x 1)) axes[1, 2].set_title(log(x1)) plt.tight_layout() plt.show()注意axes是一个二维数组axes[0, 0]是第一行第一列axes[0, 1]是第一行第二列依此类推。不熟悉的人总是把axes[0]当成第一个子图用着用着就错了。知道它是二维结构以后你就把每个axes[i, j]当作一个独立的“小画布”调用它的plot()、set_title()等方法来绘制内容就行。真正常见的错误是画完六个子图后图与图之间贴得太紧标题和刻度标签都挤在一起。解决办法就是加上plt.tight_layout()这一行它会让 matplotlib 自动调整子图间距效果立竿见影。我在所有多子图布局里都会加这一句基本上不会翻车。4.2 更灵活的方式add_subplot 与 GridSpecsubplots虽好但它只能画规则的均匀网格。如果我想让第一个子图横跨两列或者某个子图更高一些就得用add_subplot或GridSpec了。add_subplot适合“手动划分但仍在网格内”的场景。比如我想让左边一个大图右边上下两个小图fig plt.figure(figsize(10, 6)) ax1 fig.add_subplot(1, 2, 1) ax2 fig.add_subplot(2, 2, 2) ax3 fig.add_subplot(2, 2, 4) ax1.plot(x, np.sin(x)) ax1.set_title(Left Large Plot) ax2.plot(x, np.cos(x), tab:orange) ax2.set_title(Top Right) ax3.plot(x, np.exp(x / 3), tab:green) ax3.set_title(Bottom Right) plt.tight_layout() plt.show()fig.add_subplot(1, 2, 1)表示把画布划分成 1 行 2 列选中第 1 个位置。fig.add_subplot(2, 2, 2)表示划分成 2 行 2 列选中第 2 个位置。这样左右区域在网格里错开就能实现“左边一个大图右边上下两个小图”的效果。GridSpec更强大支持自定义每个子图跨度的精细控制。它做复杂仪表盘时非常好用但多数人日常用subplots就够了。我建议先把subplots练熟碰到需求超出“均匀网格”的范围时再上GridSpec去攻这样学起来更平滑。5. 中文乱码与外观美化别让图被“丑”拦住5.1 中文显示问题的两条出路matplotlib 默认字体不包含中文字符所以一画图中文就变成一个个小方框非常影响美观。以前我都是用rcParams把所有字体全局改成宋体或者黑体plt.rcParams[font.sans-serif] [SimHei] plt.rcParams[axes.unicode_minus] False第一行是把默认无衬线字体改为黑体第二行是让负号正常显示。设完这个中文标题、标签都不会乱码。这个做法的缺点是如果你换一台电脑或者生成的图片交给别人用他自己的环境跑字体不存在就会失效。所以更稳妥的办法是直接指定字体路径import matplotlib.pyplot as plt from matplotlib import font_manager font_path /System/Library/Fonts/PingFang.ttc font_prop font_manager.FontProperties(fnamefont_path) plt.rcParams[font.family] font_prop.get_name()font_manager.FontProperties可以直接加载指定路径下的字体文件不依赖系统是否安装。fname参数就是字体文件路径你可以找到系统里任一一个中文字体把路径往这一填全局字体就变了。这个方式在公司服务器、或者多人协作时最稳因为它把“这台机器有没有这个字体”的依赖彻底绕开了。5.2 调整配色、坐标轴和网格线的实战技巧默认的 matplotlib 样式确实很“工程风”蓝底、白刻度、灰边框放在论文里没问题但放在大屏演示或者自有博客上就差点意思。我常用的策略是直接用内置的style主题plt.style.use(seaborn-v0_8-whitegrid)不同 matplotlib 版本可用的 style 名稍有差异比如老版本是seaborn新版本可能是seaborn-v0_8-whitegrid。如果use时报错可以先执行print(plt.style.available)把所有可用的 style 名列出来挑一个你喜欢的。我试过ggplot、fivethirtyeight、seaborn-v0_8-darkgrid最终用得最多的是带网格的seaborn-v0_8-whitegrid原因很简单它对数据点的位置阅读非常友好又不会像深色主题那样“喧宾夺主”。坐标轴视角的调整也很技巧。比如画正切函数默认 y 轴范围会拉到正负无穷图完全没法看所以我上面才加了set_ylim(-5, 5)。用一个日志量级的数据我还会用set_xscale(log)。用对坐标轴尺度要比单纯换颜色更能解决“图看不清楚”的问题。plt.grid(True)是打开网格线alpha0.3是让网格变淡不至于抢了数据本身的风头。这些都是细节但正是这些细节决定了你的图是“能用”还是“好看”。6. 猜你想问Matplotlib 高频报错与实用排查6.1 加载不出图、看不见图、图一闪而过“代码运行了没有报错但图没弹出来”是一个高频问题。如果你在终端脚本里调用plt.show()后图一闪而过大概率是脚本执行完就退出了窗口也跟着关了。我一般会在脚本结尾加一句plt.show() input(Press Enter to exit...)让窗口等你敲回车再关闭。如果你在 Jupyter Notebook 里图像显示不出来通常是魔法命令没加在 cell 最上面加%matplotlib inline%matplotlib inline是让 matplotlib 图像直接内嵌到 Jupyter 的输出区域里而不是弹出独立窗口。头一次用 Jupyter 没加这句图就会“跑丢”但其实它被输出到底层了不立即可见特别容易让人摸不着头脑。还有一个容易被忽略的点如果你在服务器上跑代码没有图形界面plt.show()就什么也不会显示。这时候你需要用fig.savefig(output.png, dpi150, bbox_inchestight)把图直接保存成文件再下载到本地看。dpi控制分辨率bbox_inchestight会裁掉多余的留白让图片更紧凑。这个组合我基本天天用尤其批量出图的时候全部通过savefig落盘效率极高。6.2 图例、刻度、以及保存时被裁剪的坑图例的位置loc是一个容易翻车的细节。默认是右上角best但你有时候希望固定图例位置比如locupper left或者loclower right。当数据点很多、图例和曲线重叠在一起时我会加frameonFalse去掉图例边框或者稍微调小图例文字透明度这样整体看起来更干净。刻度标签过多也是麻烦。如果 x 轴有 50 个类别全部显示出来就会叠成黑黑一团。我常用plt.xticks(rotation45, haright)让标签旋转 45 度避免互相遮挡haright表示右对齐这样旋转后的文字看着更整齐。如果你的数据是时间序列直接像下面这样让 matplotlib 自动挑选刻度间距通常比手动指定更聪明fig.autofmt_xdate()保存图片被裁剪是另一个常见的坑。如果你代码里写了plt.savefig(myplot.png)但标题或者某个标签被切了半个那就是你没有设置bbox_inchestight。我在生成任何交付用图时都会写plt.savefig(myplot.png, dpi200, bbox_inchestight)dpi200保证放大后不糊bbox_inchestight保证所有元素完整落进图片。这条经验是我被朋友提醒过之后才长记性的现在它已经写进了我的代码备忘录。6.3 性能问题绘制太多点卡死怎么办当你的数据量非常大比如有几百万个散点直接用plt.scatter画matplotlib 会卡到你怀疑人生。scatter的设计目标是支持每个点不同大小、不同颜色这些额外负担在大数据量下会被无限放大。这个时候我一般换用plt.plot加上标记来画散点图x np.random.randn(1000000) y np.random.randn(1000000) # 快很多 plt.plot(x, y, ., markersize1, alpha0.3).表示用单个像素点作为标记markersize1进一步缩小点的大小。实测下来百万级数据的绘制速度能提升好几个数量级。如果数据量再大还可以用np.histogram2d先做密度统计然后用imshow画热力图这才是正解。可视化不是硬画而是用合适的图表达合适的数据密度。7. 数据可视化的“好习惯”与扩展方向7.1 一个万能的绘图模板直接抄作业用到现在我自己总结了一套模板基本能满足 90% 的日常需求。你可以直接复制改改import matplotlib.pyplot as plt import numpy as np # 全局设置 plt.rcParams[figure.dpi] 120 plt.rcParams[font.sans-serif] [SimHei] plt.rcParams[axes.unicode_minus] False fig, ax plt.subplots(figsize(8, 5)) # 数据准备 x np.linspace(0, 10, 100) y1 np.sin(x) y2 np.cos(x) # 绘图 ax.plot(x, y1, linewidth2, labelsin(x)) ax.plot(x, y2, linewidth2, labelcos(x), linestyle--) ax.set_xlabel(x) ax.set_ylabel(y) ax.set_title(Sin and Cos Curves) ax.legend(locupper right) ax.grid(True, alpha0.3) plt.tight_layout() plt.savefig(sin_cos.png, dpi150, bbox_inchestight) plt.show()figure.dpi是控制整个图片输出分辨率的全局参数这个模板里设成 120 是我反复权衡后的值观察窗口清晰、保存文件体积也合理。你换成figsize(10, 6)就能得到更宽的长宽比。整套模板拆解下来无非就是“设置全局→准备数据→选择 Axes 绘图→标注细节→保存显示”思路清晰后任何图都只是变这几个环节。7.2 接下来说说往 Pandas 和 Seaborn 方向的自然延伸matplotlib 只是基础绘图库真正的生产力提升来自和 Pandas、Seaborn 的结合。Pandas 的 DataFrame 直接提供.plot()方法底层就是调用 matplotlibimport pandas as pd df pd.DataFrame({x: x, sin: np.sin(x), cos: np.cos(x)}) df.plot(xx, y[sin, cos], figsize(8, 5)) plt.show()df.plot(xx, y[sin, cos])会自动生成图例并对 DataFrame 里的列名做映射比逐个传递数组要省事不少。碰到数据探索阶段我基本都是这个流程先df.describe()看统计口径再用df.plot()快速画一遍粗图最后才用 matplotlib 精细调整样式。Seaborn 更高级它是基于 matplotlib 的封装画统计图特别方便import seaborn as sns tips sns.load_dataset(tips) sns.scatterplot(datatips, xtotal_bill, ytip, huetime)sns.scatterplot的hue参数可以按另一个类别字段自动着色并且自动添加图例。这个功能用纯 matplotlib 去写会非常繁琐但用 Seaborn 一行就搞定了。如果你做数据分析报告强烈建议先学 matplotlib 打底再学 Seaborn 提效。7.3 用动画展示数据变化其实没那么玄乎matplotlib 的 animation 模块我一开始觉得特别复杂但只要理解“每一帧重新绘制一次”这个本质就发现也挺顺手。比如想让一条正弦曲线动起来可以这样做import matplotlib.animation as animation fig, ax plt.subplots() x np.linspace(0, 2 * np.pi, 100) line, ax.plot(x, np.sin(x)) def update(frame): line.set_ydata(np.sin(x frame / 10)) return line, ani animation.FuncAnimation(fig, update, frames100, interval50) plt.show()关键在于line.set_ydata()只更新 y 数据而不是从头plot一次这样性能才扛得住。FuncAnimation会反复调用update函数每次传一个frame参数你就可以用这个帧编号来控制数据的变化。interval50表示每 50 毫秒更新一帧这个速度播放起来比较顺滑不会太焦急也不会卡顿。动画很适合做时序数据的展示比如传感器数值的实时曲线回放或者模拟数据的变化过程。虽然日常静态图已经能覆盖绝大多数场景但掌握了 animation你会发现述职汇报里放一段动态变化图效果比干巴巴的静态图好太多。8. 回顾与一个实用建议我自己最早接触 matplotlib 时也走了一些弯路。那时候什么都是从网上抄代码抄是抄会了但始终没有理解“Figure-Axes”这一对核心概念导致一碰到复杂布局就两眼一抹黑。后来我强迫自己每画一张图都先问一句“我现在是在操纵 Figure 还是 Axes”这个习惯帮我节省了大量时间。如果你想在这个基础上继续深入我建议你把官方文档里的 Canvas 示例挨个跑一遍特别是subplots和GridSpec的组合用法。另一点是留意apply_autoscale、set_xlim、set_aspect这些坐标轴细节它们解决的虽然是“小问题”但在真实工作中正是这些小问题决定了你的图表是让人眼前一亮还是让人皱眉头。画图这件事和写代码一样多练才有手感。打开你的编辑器先跑通上面的模板再试着把里面的数据换成你自己的一步步来你很快就能把 matplotlib.pyplot 用成顺手的小工具。
返回列表