1. 从零开始理解Matplotlib的绘图逻辑
1.1 先搞清楚Figure、Axes和plt的关系
很多小白学Matplotlib容易卡在第一步:搞不清plt.plot()、plt.figure()、plt.subplots()之间到底是什么关系。其实这背后有一套非常清晰的逻辑,搞懂了它,后面画图就是水到渠成的事。
我用一个生活化的类比来解释:做菜的时候,你需要厨房(Figure)、锅(Axes)、以及炒菜的动作(plot)。
- Figure是整个画布,相当于整个厨房空间。它最大,承载所有东西。
- Axes是画布上的一个绘图区域,相当于厨房里的那口锅。在一张画布上可以放很多口锅(比如1行2列的子图),每口锅单独画自己的内容。
- plt.plot()这类函数就是炒菜的动作,你得指定在哪口锅里炒。
传统的plt.plot()其实是所谓的“状态机”模式,它会自动找到当前活跃的Axes,画上去。但当你开始画子图,或者需要精确控制多个图时,这种隐式的状态管理会让你晕头转向。所以我的建议是:从第一篇教程开始,就使用显式的方式——fig, ax = plt.subplots(),而不是直接plt.plot()。
刚开始学,你可能会觉得plt.plot(x, y)好像更简单,但一旦代码行数超过50行,图的数量超过1个,你会体会到这套显式逻辑带给你的掌控感。我在实际项目里画几十个图,靠的就是这套规范的面向对象接口。
所以,这篇教程里的所有示例,我都用统一的结构:
import matplotlib.pyplot as plt import numpy as np # 创建画布和坐标系 fig, ax = plt.subplots(figsize=(8, 5)) # 在ax上绘制内容 ax.plot(x, y) # 显示图形 plt.show()这样做一开始就建立正确的使用习惯,后续你去看官方文档、开源项目代码时也不会发怵。
1.2 为什么Matplotlib的地位至今无法被替代
市面上Python可视化的库很多,比如Seaborn、Plotly、Bokeh,为什么我还要专门花一整篇教程来写Matplotlib?
我的实际感受是:Matplotlib是地基,其他库都是在这片地基上盖的楼。Seaborn封装的底层就是Matplotlib,Plotly虽然交互性强,但是静态图表的精细控制能力远不如Matplotlib。
Matplotlib拥有极强的控制力。不管是科研论文里的严谨图表,还是日常分析中的快速探索性画图,它都能胜任。它能控制的细节包括但不限于:坐标轴刻度方向、标签的字体族、网格线的线型、图例的位置精确到某个坐标点。这种“刀耕火种级”的自由度,是其他库很难给的。
更重要的是,Matplotlib对中文字体的支持,虽然配置过程麻烦,但一旦配好了,出来的图非常稳定。我在网络上经常看到有人抱怨乱码问题,其实那都是第一次配置没配好,配好之后,后面所有的项目都可以复用。
既然系列教程已经出了第一篇,相信你已经画出了第一张图,认识了plot、show、savefig。这篇教程的第二篇,我们把基础夯实,把绘图的核心环节拆开揉碎讲清楚,让你不仅会用,还能懂得为什么这么做。
2. 小白必会的绘图核心流程拆解
2.1 数据准备阶段的两大关键习惯
说句实话,我看到很多教程一上来就讲各种绘图API,却忽略了数据准备。但在真实场景中,画图前的数据整理时间往往占整个流程的70%。
绘图数据的本质是“序列”。x轴一个序列,y轴一个序列。这两个序列必须长度一致,元素一一对应。flask如果你从数据库或Excel里读取数据,通常得到的是DataFrame,你需要先做筛选、排序、去重,才能变成绘图可用的序列。
我建议小白在画图之前,务必养成两个习惯:
第一个习惯:先打印数据形状。
print(len(x_data), len(y_data)) # 或者 print(x_data.shape, y_data.shape)这个动作只需要一秒钟,但能避免80%的无效报错。Matplotlib报错很多时候就是长度不匹配。
第二个习惯:用变量名代表数据语义,不要写魔法常量。
# 不推荐 ax.plot([1, 2, 3, 4], [2, 4, 1, 5]) # 推荐 days = np.array(['周一', '周二', '周三', '周四']) temperatures = np.array([22, 25, 19, 28]) ax.plot(days, temperatures)这样做的意义在于代码可读性和可维护性。几个月之后翻看脚本,你还能知道自己在画什么。
另外,如果你用的是numpy数组,画图前要注意是否有NaN值。NaN会在图线上造成断点,看似异常其实背后是数据缺失。可以用np.isnan()配合索引定位清洗。
# 删除包含NaN的值 mask = ~np.isnan(y_data) x_clean = x_data[mask] y_clean = y_data[mask]2.2 创建画布时figsize和dpi到底怎么选
fig, ax = plt.subplots(figsize=(8, 5), dpi=100)这行代码里,figsize和dpi是小白的第一个拦路虎。
我遇到很多同学问:为什么我的图保存出来文字很小、图片很模糊?答案就藏在这两个参数里。
figsize是画布的长和宽,单位是英寸(inch)。dpi是每英寸包含的点数(dots per inch)。最终图片的像素尺寸 = 宽度(英寸)× dpi。比如figsize=(8, 5),dpi=100,那图片就是 800×500 像素。
- 日常屏幕查看:
figsize=(8, 5)或(10, 6),dpi=100 就够了。 - 论文或报告打印:建议
figsize=(12, 8),dpi=300,这样插入Word或LaTeX后才不会糊。 - 网页展示:可以适当小巧,
figsize=(6, 4),dpi=80~100。
还有一个容易被忽略的点:长宽比例决定图形的视觉说服力。我见过太多人直接把默认的方形图丢在汇报里,其实你完全可以根据数据变化趋势来调整。如果数据随时间缓慢变化,用宽扁的图(12, 4)会更好展示过程;如果比较各组之间的差异,用接近正方形(7, 6)会更均衡。
还有一个小技巧,如果图形内容只有一条折线,不要用太大的画布,否则线会显得很细,视觉上不饱满。线条的粗细、画布的大小、坐标轴的刻度密度,这三者是互相配合的。
2.3 plot函数的核心参数逐个说清楚
ax.plot()是出镜率最高的函数,但参数非常多。小白一开始只需要掌握三个核心参数:color(颜色)、linestyle(线型)、marker(数据点标记样式)。
ax.plot(x, y, color='crimson', linestyle='--', marker='o', linewidth=2, markersize=6)颜色的取值有三种常用格式:
- 颜色名称字符串,比如
'red'、'blue'、'crimson'。 - 十六进制颜色码,比如
'#FF4500',精细控制颜色时非常有用。 - 灰度值,比如
'0.2',介于黑与白之间。
线型常用的有:'-'实线、'--'虚线、'-.'点划线、':'点线。选择线型的逻辑是:区分不同数据系列时,先改颜色;如果颜色不足以区分(比如黑白打印),再改线型。
标记常用的有:'o'圆点、's'正方形、'^'三角形、'D'菱形。标记的作用是标识离散的数据点,配合markersize控制大小。
我总结一个实战原则:连续型数据用实线,预测或对比数据用虚线,离散采样点加标记。这样你画出来的图,别人第一眼就能读出数据的属性。
还有alpha参数,控制透明度,0到1,适合重叠数据较多的场景。两个数据系列重叠严重时,调低alpha可以看清底层趋势。
2.4 显示和保存图形的正确姿势
画完图之后,plt.show()和fig.savefig()是两种出口。它们的使用场景完全不同。
plt.show()用于交互环境,比如Jupyter Notebook里运行或者命令行执行脚本。它的作用是渲染图形到屏幕上供你查看。注意,show()之后,画布的状态会重置,如果窗口被关闭,你不能再往图里添加内容。
fig.savefig()用于把图形保存为文件,比如PNG、JPG、SVG、PDF。
fig.savefig('输出图表.png', dpi=200, bbox_inches='tight')这里有个我踩过很多次的坑:如果你先调用了plt.show(),再调用savefig(),保存下来的图片可能是一片空白。因为show()已经消费了画布。正确的方式是:先savefig(),再show()。
关于bbox_inches='tight':它的作用是裁掉图片四周多余的空白,让图的边框尽量贴合画布内容。如果你不设置,保存出来的图四周往往有大片留白,插入文档里显得不协调。我日常画图基本都加这个参数。
保存格式也有讲究:
- 提交到毕业论文或学术期刊:保存
.pdf或.svg矢量图,文字怎么放大都不糊。 - 网页直接展示:保存
.png,透明度支持好且体积适中。 - 尽量不要用
.jpg,因为jpeg压缩会导致文字边缘出现噪点,不美观。
3. 让图表具备“表达力”的细节定制
3.1 标题、轴标签和图例的正确写法
Matplotlib画图的默认状态是什么?是没标题、没坐标轴标签、没图例。很多小白画完图直接丢给人看,别人根本不知道横轴是什么、纵轴是什么。图表是信息传达的载体,缺少这些基本元素,等于写了一篇文章没有段落标题。
ax.set_title('2024年各季度销售额趋势')设置图表标题。ax.set_xlabel('月份')、ax.set_ylabel('销售额(万元)')设置坐标轴标签。ax.legend()显示图例。
图例的写法有讲究。最简单的方式是在plot时指定label:
ax.plot(month, sales_a, label='A产品') ax.plot(month, sales_b, label='B产品') ax.legend(loc='best')loc参数控制图例位置:'upper right'、'upper left'、'lower left'、'best'自动选择不遮挡数据的位置。我通常还会加一句frameon=False去掉图例边框,视觉上更干净,避免边框线干扰数据读取。
如果你画的图数据系列比较多,图例重叠,还有一种方案是把图例放在图外:
ax.legend(loc='upper left', bbox_to_anchor=(1.02, 1), borderaxespad=0)这个方法可以让图例出现在画布右侧外部,适合图内空间不够的情况。
有一个细节需要注意:x轴是分类数据(比如周一、周二)时,plot会把它们当作字符串处理,轴刻度会均匀分布;但要注意标签可能会重叠,尤其数量过多时。这时候有几种处理方式,初学者知道旋转标签就够了:
plt.xticks(rotation=45)或者,直接用ax.tick_params(axis='x', rotation=45)。
3.2 坐标轴范围的设置逻辑
默认情况下,Matplotlib会自动计算坐标轴的上下限,把画出的数据完整包裹住。但自动的范围有时并不理想。
我举一个典型场景:你想画一个从0到10的指数增长曲线,默认可能会把y轴范围压缩得让曲线填满整个图。但你想强调“起点在0”,那就得手动指定范围。
ax.set_xlim(0, 12) # x轴范围 ax.set_ylim(0, 100) # y轴范围手动设置范围时要谨慎:范围过窄会截断数据线,让观者产生误解;范围过宽则会压缩数据差异的视觉表现。比如员工绩效都在85~95分之间,如果你的轴从0到100,大家看起来都差不多,图表传递不出差距。此时把y轴设为80到100,差异立刻显现。
不过也要提醒一句:在学术或诚实呈现数据的原则下,y轴截断(也就是不从0开始)要注意标注清楚,否则有误导倾向。
还有ax.set_aspect('equal')可以控制坐标轴比例相同,适合画几何图形或需要保持形状比例的场景,比如地图坐标点分布。散点图中的点位分布,如果横纵比例不一致,看起来聚集方向会被扭曲。
3.3 网格、边框和坐标轴刻度的精细控制
一张“专业感”很强的图和一般图表的区别,往往体现在细节。网格线就是其中之一。
ax.grid(True, linestyle='--', alpha=0.6)默认的网格是实线,黑色,显得特别生硬。我习惯用浅灰色虚线,alpha调低,看起来是淡淡的背景参考线。这样既不干扰数据,又能辅助读数。
关于刻度,ax.tick_params()是控制刻度外观的统一入口,包括刻度方向、长短、标签字号。
ax.tick_params(axis='both', labelsize=10, direction='in')direction='in'是让刻度线朝图形内侧,这是很多科研论文的标准风格。坦白说,日常汇报用不着这个细节,但做正规图表时很加分。
除了主刻度,还有次要刻度set_minor_locator,适合画对数坐标时必须的精细网格。对数坐标在分析数量级跨越很大的数据时非常常用,比如收入分布、地震震级、人口增长。
ax.set_xscale('log') ax.set_yscale('log')一旦用了对数坐标,刻度标签的读法完全变了,小白看到非线性的刻度不要慌,你只需要关心数据的相对比例而不是绝对差值。
3.4 字体和中文显示的终极解法
Matplotlib默认字体渲染不出中文字符,这是它在国内使用者中最有名的槽点。网上有各种教程,但很多写得不清不楚。我直接给出我长期稳定使用的方法。
第一步,查看当前系统里可用的中文字体:
import matplotlib.font_manager as fm # 查看所有可用字体 for font in fm.fontManager.ttflist: if 'SimHei' in font.name or 'Microsoft' in font.name: print(font.name)第二步,全局设置字体:
plt.rcParams['font.sans-serif'] = ['SimHei'] # 用黑体 plt.rcParams['axes.unicode_minus'] = False # 解决负号显示为方块的问题axes.unicode_minus这个配置非常关键。中文字体里没有专门的负号字符,默认渲染时负号会变成方块。记得一定设成False。
如果你用Linux系统,可能没有SimHei,那可以安装中文字体包,或者指定为'Noto Sans CJK SC',然后同样用rcParams去设置。还有一种更稳妥的方法:在代码里指定字体文件路径。
from matplotlib import font_manager import matplotlib.pyplot as plt # 指定系统中文字体文件路径 font_path = '/usr/share/fonts/truetype/droid/DroidSansFallbackFull.ttf' prop = font_manager.FontProperties(fname=font_path) plt.rcParams['font.family'] = prop.get_name()这种方式不依赖系统已经注册的字体名称,直接把字体文件塞进去,成功率更高。我曾在各种不同的系统里装Matplotlib,这一直是我推荐新手的备选方案。
4. 多种绘图类型的选型与实战
4.1 散点图、直方图、柱状图该怎么选
Matplotlib除了折线图,还有三类最常用的基础图:散点图、直方图、柱状图。很多小白搞不清它们的使用场景,我给出一张选型思路表:
| 图表类型 | 适用场景 | 对应函数 |
|---|---|---|
| 折线图 | 连续时间序列、趋势变化 | ax.plot() |
| 散点图 | 两变量的相关关系、分布轮廓 | ax.scatter() |
| 直方图 | 数据分布的频率统计 | ax.hist() |
| 柱状图 | 类别对比、离散数据 | ax.bar() |
核心逻辑就一句话:横轴是时间,用折线;横轴是数值变量,先考虑散点;只看单个变量的分布,用直方图;要对比品类大小,用柱状图。
我见过有人用柱状图画连续时间序列数据,硬生生把平滑的趋势画成一截一截的,这就是搞混了数据类型。
4.2 散点图的实际操作与透明度的妙用
散点图最容易出现的问题是“点太多、叠在一起看不出密度”。比如你有两万个样本点,所有点挤成一团黑色。这时候alpha参数就是救星。
x = np.random.randn(10000) y = np.random.randn(10000) fig, ax = plt.subplots(figsize=(8, 6)) ax.scatter(x, y, s=5, alpha=0.5, c='steelblue')s=5是控制点的大小,alpha=0.5让点半透明,重叠的区域颜色更深,这样就能看出来中心密度更高。这是分析大样本分布时非常实用的技巧。
还可以在散点图中加入颜色映射,也就是用c参数传入第三个维度的数值,配合cmap='viridis',做出类似热力分布的效果。
sc = ax.scatter(x, y, c=z, cmap='viridis', s=10) fig.colorbar(sc) # 添加颜色条说明viridis是Matplotlib默认的色图,色觉障碍者也能分辨。如果是为了严谨呈现数据,一定要用默认的viridis,不要自己调花里胡哨的颜色。
4.3 直方图的参数细节:bins和density
直方图最容易出错的地方是bins的设置。bins是箱子数量,决定直方图的分组粒度。
ax.hist(data, bins=30, density=True, edgecolor='white')bins过小,分布细节被抹平;过大,图形会非常破碎,噪声太多。我的一般做法是先试bins=20,再根据形状增减。数据量大于一万时,可以适当把bins增加到50甚至100。
density=True的效果是把直方图的y轴变成频率密度而不是简单计数。这样处理的好处是:当两组数据样本量不同时,可以用density模式放在同一张图上比较分布形态,而不受样本量影响。
4.4 柱状图的美化与数据标签
柱状图在业务汇报里最常用。默认的柱子灰灰的,没有信息标注,能力展现不出来。我一般会给每个柱子加上数值标签。
categories = ['A部门', 'B部门', 'C部门', 'D部门'] values = [15, 22, 18, 27] fig, ax = plt.subplots(figsize=(8, 5)) bars = ax.bar(categories, values, color=['#5790FC', '#F89C20', '#E42536', '#964A8B']) # 在柱顶添加数据标签 for bar in bars: height = bar.get_height() ax.text(bar.get_x() + bar.get_width()/2., height + 0.5, f'{height}', ha='center', va='bottom', fontsize=12)bar.get_x()和bar.get_width()是柱子几何属性的读取方法,在柱子的正上方的位置定位文本,再偏移0.5个单位防止文字贴在柱子顶端。
另外,如果分类多于10个,横向柱状图的表现力更好。用ax.barh(),标签在y轴,数据条在x轴方向延伸,阅读起来更轻松。
5. 子图布局与多图对比的实用技巧
5.1 用subplots创建多子图的基本套路
在分析报告中,把多个相关图表放在同一个画布上,能强烈提升信息密度。plt.subplots(nrows, ncols)的返回值是画布和一个二维的axes数组。
fig, axes = plt.subplots(2, 2, figsize=(12, 10)) # axes 是一个 2x2 的数组然后你可以分别选择某个位置来画图:
axes[0, 0].plot(x1, y1) axes[0, 1].scatter(x2, y2) axes[1, 0].hist(data1) axes[1, 1].bar(categories, values)这个模式极其清晰,就好比你面前摆了一张4格的盘子,你往每个格子里放对应的菜。
还有一个小技巧:如果只需要快速展开一行的多个图,可以用plt.subplots(1, 3),这时候axes是一维数组,直接用axes[0]、axes[1]、axes[2]索引即可。
5.2 用共享坐标轴保持图间比例一致
多子图对比时,最怕的是坐标系范围不一致,导致视觉对比失真。比如左图y轴是0到5,右图y轴是0到50,旁人看柱子的高低还以为是趋势差异,实际上只是刻度不同。
解决方案是使用sharex和sharey参数:
fig, axes = plt.subplots(1, 2, figsize=(12, 5), sharey=True)这样两个子图会共用y轴,一个图调整范围另一个也跟着调,对比起来客观准确。在展示多个实验组的同一指标时,强烈建议开启sharex或sharey。
5.3 子图之间间距的调优方案
多个子图挤在一起,标签互相重叠,观感很差。此时可以用fig.tight_layout()自动调整子图的间距,也可以手动调plt.subplots_adjust。
fig.tight_layout() # 一行代码解决大部分间距问题tight_layout是自动计算每个子图合适的位置,非常省心。如果有图例被截断的情况,增加rect参数:
fig.tight_layout(rect=[0, 0, 0.9, 1]) # 右侧留出10%空间这些细节,虽然图上不起眼,但发布于报告或博客时,整体观感会完全不一样。
6. 常见问题与排查技巧实录
6.1 中文乱码和负号显示的问题
这是小白最常见的问题,几乎人人都遇到。中文乱码的原因很简单:Matplotlib默认字体不支持汉字。解决分两步,第一检查系统可用字体,第二在代码里全局设置。我再次强调那一行关键的:
plt.rcParams['axes.unicode_minus'] = False如果你设了中文字体,但没有设上面这行,负号会显示成方块,而且很难察觉原因,因为它只是一个小方块,被忽略掉。
6.2 明明画了图但export出来是空白
plt.show()之后再调用savefig,或者忘记调用savefig直接show,都可能产生空白图。正确顺序是先保存再显示。
还有另一个场景:在使用pycharm运行脚本时,show()会弹出窗口并阻塞进程,如果不手动关闭窗口,代码不会继续往下走。如果你是在写自动化生成图表的脚本,千万不要在循环里调用show()。只要写savefig就好。
6.3 图例不显示label的问题
很多小白画了带label的线,但调用ax.legend()后图例却是空的。常见原因是,legend()被调用在了plot之前。图例的信息是从当前ax中已有的带label的元素收集的,顺序很重要,一定保证plot在前,legend在后。
还有一种情况:图例显示了一堆“_child”条目,这是因为代码是在循环里不正确地传了label。解决办法是在plot时显式指定label,或者在循环后调用ax.legend([series_names])手动传入标签列表。
6.4 坐标轴刻度变成科学计数法的处理
当数据量跨越好几个数量级时,Matplotlib自动把刻度显示成1e6这样的形式。某些场景下不友好,比如展示人口数据时,阅读者更习惯看到“5,000,000”而不是“5×10^6”。
这时可以用:
ax.ticklabel_format(style='plain', axis='y')或者用自定义格式:
from matplotlib.ticker import FuncFormatter ax.yaxis.set_major_formatter(FuncFormatter(lambda x, p: format(int(x), ',')))最后分享一下我自己的制图流程。每次需要画一个新图,我不会一上来就敲代码。先问自己三个问题:横轴是什么类型的数据?要强调对比还是趋势?最终呈现载体是PPT还是论文?想清楚这三个,再打开编辑器,公式一套流程走下来,基本上图的质量不会差。绘图本就不是一步到位的艺术,先用默认形式画出来,再逐步迭代样式,这比一开始就追求完美更实际。你反复调出来的图,才是真正符合你需求的图。