Matplotlib绘图全指南:思路、实战与高级定制技巧

发布时间:2026/10/5 11:59:59
Matplotlib绘图全指南:思路、实战与高级定制技巧 1. 先想清楚再动手Matplotlib制图的整体思路Matplotlib是Python生态里最老牌、也最“耐撕”的绘图库。你可能见过它画出那种论文里的精致曲线也见过它偶尔吐出一张丑到崩溃的默认蓝橙色图。我用了快十年最大的体会是很多人画图难看不是代码水平不行而是压根没想清楚这张图要干嘛。所以这一节先不聊API聊怎么在写plt.plot()之前把思路捋顺。1.1 这张图到底要表达什么先选图表类型再写代码我见过太多人在Excel里画饼图在Matplotlib里也画饼图完全不过脑子。选图表类型的标准只有一个——你想让读者在3秒内看到什么。要展示一个变量随时间的变化折线图几乎是唯一合理的选择要对比几个类别的数值大小柱状图更直接要看分布直方图或者箱线图。这听起来像废话但许多新手卡在“不知道用什么图”本质上是没有把问题翻译成“比较”还是“趋势”还是“构成”。举例来说同样是2026年某市每天的确诊病例数据如果你关心“峰值在什么时候出现”那就用折线图把日期放到x轴如果你关心“哪个区的总量最大”那就换个柱状图如果你关心“新增病例的分布是否偏态”那就画直方图。同一个数据源可以派生出完全不同的三张图没有一张是错的只是问题不同。所以动手前先问自己一句我要向谁传达什么结论这句话定不下来后面的颜色、线宽、图例全是白搭。在Matplotlib里面选图表类型其实是在选对应的函数plot()画线bar()和barh()画柱scatter()画散点hist()画直方图imshow()画热力图。函数名就是动作先把动作定下来再考虑参数。1.2 认准两个核心对象figure和axes不搞混Matplotlib最劝退新手的就是plt这样的“状态机写法”和fig, ax plt.subplots()这样的“对象写法”混着用。我统一建议从第一天开始就用对象写法。plt.plot()这种用法虽然少敲几个字但一旦画子图、多坐标系、或者动态更新图像就会变得极其别扭。简单说一张图就是一个figure画布画布上可以有多个axes坐标系。坐标系才是真正画曲线、放刻度、贴标签的地方。理解了这个层级关系你就明白为什么很多人说“给图加标题”让人晕——plt.title()设置的是当前坐标系标题而fig.suptitle()才是画布总标题。我个人习惯是能用ax.xxx()就用ax.xxx()少碰全局状态。这样代码拿到任何项目里都能移植不会因为别人调用了plt.figure()就把你的布局打乱。import matplotlib.pyplot as plt import numpy as np x np.linspace(0, 10, 100) y np.sin(x) fig, ax plt.subplots(figsize(8, 4)) ax.plot(x, y, color#C44E52, linewidth2) ax.set_xlabel(时间 (s)) ax.set_ylabel(振幅) ax.set_title(正弦波示例) plt.show()这个例子看起来简单但包含了我强调的两个要点显式创建fig和ax通过ax调用绘图方法。后面所有的高级操作都是在这个框架上做加法。1.3 全局配置与中文字体一开始就堵住坑Matplotlib刚装好的默认样式说实话是不适合直接拿去给报告用的。那一圈白边、偏小的字号、还有中文直接变方框的问题几乎每个人都会踩。我建议在项目入口处配置一次rcParams后面所有图都跟着受益。import matplotlib as mpl mpl.rcParams.update({ font.sans-serif: [SimHei], axes.unicode_minus: False, font.size: 11, axes.titlesize: 13, axes.labelsize: 11, axes.spines.top: False, axes.spines.right: False, figure.dpi: 150, savefig.dpi: 300, })axes.unicode_minus这个参数往往被忽略如果不设成False坐标轴的负号会显示成乱码。figure.dpi和savefig.dpi分开设置也值得养成习惯——屏幕上预览不需要太高但保存成图片时尽量用300dpi打印或者放进论文里都不会糊。另外还有一个小技巧如果系统里有好看的字体比如“微软雅黑”或者“PingFang SC”可以把字体名字加进sans-serif列表第一位效果比SimHei更细腻。字体问题我在后面专门有一节再展开这里先提个醒。2. 从零画出一张能用的图基础图表实战有了全局配置以后就到了动手画图的环节。这节我会用折线图、柱状图和子图布局三个案例把最常见的场景走一遍。我不打算堆砌所有参数只挑那些真正会改变图表表达效果的关键配置。2.1 折线图与日期刻度处理别让x轴变成乱码数字折线图最容易翻车的地方不是线本身而是坐标轴的刻度。特别是横轴是日期的时候如果不做处理Matplotlib会把日期当成一串数字刻度标签挤成一团或者压根显示不出来。我试过最稳妥的做法是使用matplotlib.dates模块它提供了对日期刻度的完整支持。比如你有每天的确诊病例数据存放在一个pandas.DataFrame里日期列是datetime64类型那么可以这样画import pandas as pd import matplotlib.dates as mdates # 假设 df 有 date 和 cases 两列 df pd.DataFrame({ date: pd.date_range(2026-09-01, periods60, freqD), cases: np.random.poisson(lam30, size60) }) fig, ax plt.subplots(figsize(10, 5)) ax.plot(df[date], df[cases], markero, markersize4, linewidth1.8) # 关键自动选择日期刻度并格式化 ax.xaxis.set_major_locator(mdates.WeekdayLocator(interval1)) ax.xaxis.set_major_formatter(mdates.DateFormatter(%m-%d)) ax.xaxis.set_minor_locator(mdates.DayLocator()) fig.autofmt_xdate() plt.show()fig.autofmt_xdate()会旋转日期标签避免重叠。这里我用了WeekdayLocator让主刻度每周出现一次DayLocator让次刻度每天一个位置。如果数据跨度大可以换MonthLocator月份用%Y-%m格式显示。记住一句话日期刻度问题不是靠肉眼数位数解决的而是靠显式指定locator和formatter。画折线时还有两个细节我很看重。第一个是marker数据点少的时候比如每周一个空心的marker比实心的更耐看还能用markersize控制大小。第二个是linewidth默认的1.0在截图压缩后会显得很细我一般设到1.8或2.0。这些参数都不是必须的但它们决定了图放大缩小后的观感。2.2 柱状图与误差条把“对比”画得诚实柱状图的核心是“对比”因此每一根柱子的宽度、颜色、间距都要为对比服务。Matplotlib的bar()函数有几个参数常被忽略width控制柱宽alpha控制透明度edgecolor控制描边颜色。做横向对比时我给柱子设置不同的颜色但颜色数量不要超过类别数量否则读者会误以为颜色本身有含义。下面是带误差条的分组柱状图示例这类图在实验数据对比里非常常见# 两组数据每组4个类别 categories [项目A, 项目B, 项目C, 项目D] group1 [12, 18, 9, 15] group1_err [0.8, 1.2, 0.6, 1.0] group2 [16, 11, 14, 10] group2_err [0.9, 0.7, 1.1, 0.5] x np.arange(len(categories)) width 0.35 fig, ax plt.subplots(figsize(9, 5)) rects1 ax.bar(x - width/2, group1, width, yerrgroup1_err, label实验组, capsize3, color#3B7DD8) rects2 ax.bar(x width/2, group2, width, yerrgroup2_err, label对照组, capsize3, color#F7B32B) ax.set_xticks(x) ax.set_xticklabels(categories) ax.set_ylabel(测量值) ax.legend() plt.show()capsize3给误差条加了个小横线这个细节很容易被漏掉但不加的话误差条上端是一根细线视觉上像没画完。另外我用width0.35而不是默认的0.8这样两组柱子之间不会紧密靠在一起留出一点空隙反而更易读。柱状图有两个经验法则第一如果柱子是代表“计数”或“构成”不要添加多余的颜色渐变第二横轴的类别顺序要有意义不要按字母排列而要按数值大小排列除非类别本身有自然顺序。这个规矩在写报告时尤其管用。2.3 多子图布局从“一张图”到“一张大图”实际项目中很少只画一张孤立的图。你可能需要把趋势图、分布图、对比图拼在一张画布上方便读者对照。Matplotlib的subplots()直接指定行列数是最快的方案。fig, axes plt.subplots(2, 2, figsize(10, 8)) axes[0, 0].plot(x, np.sin(x)) axes[0, 0].set_title(正弦) axes[0, 1].plot(x, np.cos(x), color#C44E52) axes[0, 1].set_title(余弦) axes[1, 0].hist(np.random.randn(1000), bins30, color#2E86AB) axes[1, 0].set_title(直方图) axes[1, 1].scatter(x, np.exp(x/10), s20, alpha0.6) axes[1, 1].set_title(散点) plt.tight_layout() plt.show()plt.tight_layout()在我心里是高频救命函数。没有它子图的标题和标签经常被切掉保存出来的图四周严重缩水。你也可以用constrained_layoutTrue替代但我更习惯在最后调tight_layout()它更直观。如果你需要子图宽度或高度不同就用gridspec_kw参数。比如左侧子图占30%宽、右侧占70%宽fig, axes plt.subplots(1, 2, figsize(10, 4), gridspec_kw{width_ratios: [1, 3]})这个写法在画“主图放大图”或者“总览局部细节”时很实用。子图布局的本质是把多个独立的axes放进同一个figure每一个axes都是独立画布可以独立设置标题、刻度、颜色。理解这一点之后你就不会被“如何共享坐标轴”“如何让子图对齐”这些问题困住了。3. 让图“讲清楚事”高级定制技巧基础图表能画出来只是第一步。真正拉开差距的是图表的“可读性”和信息层级。这一节讲颜色、标注、坐标轴刻度三项它们分别解决“什么突出”“哪里要说明”“数值范围怎么看”这三个问题。3.1 颜色与色板管理控制在三种颜色以内Matplotlib默认的颜色循环虽然辨识度还行但放在一起并不高级。很多配色看起来廉价就是因为颜色之间没有明确的主次关系。我的经验是一张信息图里最多用三种主色其余的交给灰度。强调关键序列用高饱和色辅助序列用低饱和或灰色背景尽量保持白色。颜色参数这样用color接受十六进制字符串如#C44E52这比用red更可控。多个序列循环时用plt.cm里的离散色板比如plt.cm.Set2.colors。连续数据的热力图用plt.cm.viridis不要用jetjet的视觉效果虽鲜艳但映射上有歧义专业场合不推荐。这里有一段用色板的实际案例画多条曲线并配图例colors plt.cm.Set2.colors[:4] for i, col in enumerate(colors): ax.plot(x, np.sin(x i*np.pi/4), colorcol, labelfsin(x{i}π/4))还有一个很容易被忽略的“负空间”概念白色和留白也是配色的一部分。Matplotlib的默认边框用了四周都是黑色的盒子我通常去掉上、右两条边框可以在rcParams里设置axes.spines.top/right为False整张图立刻显得干净许多。对比一下同一条曲线在不同边框下的观感你会发现去边框是成本最低的美化手段。3.2 标注、注释与文字排版让图会说话图表里最值钱的信息往往不在坐标轴上而在标注里。峰值是多少、拐点在哪里、几个序列之间的差异有多大这些结论如果只靠读者肉眼去读就是失败的可视化。ax.annotate()是专门干这个的。比如你要在一张折线图上标注峰值点peak_x df[date][25] peak_y df[cases][25] ax.annotate(峰值: 78例, xy(peak_x, peak_y), xytext(peak_x pd.Timedelta(days5), peak_y 15), arrowpropsdict(arrowstyle-, color#333333), fontsize10)xy是目标数据点的坐标xytext是文字标注的位置arrowprops定义箭头样式。这里需要注意如果x轴是日期时间类型xytext里的偏移量也要用pd.Timedelta否则偏移就是错误的。除了annotate我还常用ax.text()在图表的空白区域加注释说明比如“该时间段内数据中断”之类的提示。text()的参数看着简单但有一个致命的坑默认坐标是数据坐标而你想把文字固定在图的角落时应该用transformax.transAxes这样坐标变成了0到1的相对位置。比如ax.text(0.95, 0.05, 数据截止到9月底, transformax.transAxes, haright, vabottom, color#888888, fontsize9)这个技巧在批量出图时特别有用不管数据范围怎么变文字永远待在图的右下角。关于文字排版我的建议是“克制”。不要在一张图上放超过三处自由注释注释多了读者不知道该看哪里。文字是辅助不是主角。如果一张图需要写一大段解释才能看懂那说明图本身表达不清应该回去改图表类型而不是补注释。3.3 坐标轴刻度精调从默认值到数值区间坐标轴刻度是图表中读者和数据的接口它决定了图的数值上下文。Matplotlib的自动刻度通常表现不错但在对比图表、放大局部、或者突出显示某个区间时就得手动干预了。先看一个简单但高频的场景x轴的范围要留出边距不要让曲线的第一个点和最后一个点贴到图边框上。ax.set_xlim(x.min() - 0.5, x.max() 0.5) ax.set_ylim(-1.5, 1.5)如果想让y轴刻度从0开始并且按照每0.25一个刻度显示用import matplotlib.ticker as mticker ax.yaxis.set_major_locator(mticker.MultipleLocator(0.25)) ax.yaxis.set_major_formatter(mticker.FormatStrFormatter(%.2f))很多人不知道刻度的间隔还能这样控制默认的AutoLocator虽然自动化但不保证能给出符合人类阅读习惯的步长。MultipleLocator(0.25)的意思是刻度落在0.25的整数倍位置格式化显示两位小数。比如你要画百分比数据可以设置FuncFormatter把0.25显示成“25%”ax.yaxis.set_major_formatter(mticker.FuncFormatter(lambda x, pos: f{int(x*100)}%))还有一种常见需求是隐藏刻度线、只保留刻度标签尤其适合横轴是类别的柱状图ax.tick_params(axisx, length0)坐标轴过多的刻度线会让图显得杂乱特别是当刻度标签已经足够表达含义时刻度线完全可以去掉。这个选择纯粹是审美层面没有对错但一旦你意识到刻度线是可以移除的你的图就开始有了个人风格。4. 图表保存与发布字体、矢量格式与常见坑画图的过程再精彩最终交付的是一张图片文件。保存图片这个环节看起来简单实际操作中的问题密度极高。中文字体变方块、dpi太低、边缘被裁剪、背景色变成透明这些问题我都在不同项目里遇见过。4.1 保存图片的格式选择PNG、PDF还是SVGMatplotlib保存图片时用plt.savefig()但很多人把格式等同于扩展名忽略了不同格式的适用场景。如果你要发在网页上用PNG如果你要把图放进学术论文或矢量打印件用PDF或者SVG如果图片要导入Word或者PPT最好用高清PNG因为很多办公软件对SVG的支持并不好。fig.savefig(output.png, dpi300, bbox_inchestight)bbox_inchestight会自动裁剪掉图周围多余的空白让图片内容完整且紧凑。这个参数我每次都会用除非你有意想让图在文档中占据一个固定的宽高比。PDF格式在LaTeX文档中非常好用因为它是矢量图缩放多少倍都不会失真。还有一个冷门但实用的参数是facecolor。默认情况下PNG背景是白色但如果你把图表嵌进深色背景的PPT页面就会看到一块明显的白边这时可以这样保存fig.savefig(output.png, facecolor#F7F7F7)甚至可以配合transparentTrue保存透明背景图适合放在数据大屏上。不过透明背景有一个隐患有时会让字体渲染发虚建议能不用就不用。4.2 中文字体缺失问题从根源上解决“中文全变方块”是Matplotlib新手第一大痛点。这个问题的根源是Matplotlib默认字体没有中文字形。网上流传的解决方法是修改rcParams指定中文字体但这只在一台机器上有效代码换到别的电脑可能又出问题。我这里给一套更稳的方案。首先检测系统里有哪些字体可用import matplotlib.font_manager as fm fonts [f.name for f in fm.fontManager.ttflist] print(SimHei in fonts, Microsoft YaHei in fonts, Noto Sans CJK SC in fonts)然后动态选择第一个可用的中文字体import os def setup_cjk_font(): candidates [Microsoft YaHei, SimHei, Noto Sans CJK SC, PingFang SC, WenQuanYi Micro Hei] available {f.name for f in fm.fontManager.ttflist} for name in candidates: if name in available: plt.rcParams[font.family] [sans-serif] plt.rcParams[font.sans-serif] [name] plt.rcParams[font.sans-serif] break plt.rcParams[axes.unicode_minus] False把这段函数放到项目的公共工具模块里每次画图前调用一下比手改rcParams可靠得多。如果服务器里一个中文字体都没有那就干脆用英文标签画图不要在图上显示中文。硬要显示中文的话需要手动下载字体文件并注册有点折腾不是每个项目都值得。4.3 清晰度与图片尺寸放大看依然清晰图片清晰度主要取决于两个因素像素尺寸和dpi。figsize单位是英寸dpi是每英寸像素点数。一张figsize(8, 4)的图以300dpi保存输出图片是2400×1200像素在绝大多数屏幕上都能看清。这里有个常见误解如果先用plt.show()显示窗口再手动右键保存图片得到的清晰度可能和直接savefig完全不同。因为交互窗口默认dpi是100而你在代码里设置的savefig.dpi只对保存操作生效。所以我的习惯是展示和保存分开屏幕上快速预览用plt.show()最终交付一律用savefig显式指定参数。fig, ax plt.subplots(figsize(8, 4)) # ... 画图 ... fig.savefig(report.png, dpi300, bbox_inchestight)如果把图嵌入LaTeX文档需要控制宽度可以先用bbox_inchestight导出然后查看裁剪后的实际尺寸再在LaTeX里用\includegraphics[width0.8\textwidth]缩放。矢量格式完全不用担心缩放失真所以强烈建议学术论文使用PDF格式。还有一个小巧思当你的图表包含非常多的数据点时PNG文件体积会很大但SVG文件可能很小而且任意缩放。反过来如果你的图上有大量文字标注SVG文件反而可能比PNG大。根据场景灵活选择多产出一个svg目录版本往往能救急。5. 实战复盘一张疫情折线图是如何完成的前面讲的都是散点知识这一节我串成一个完整案例用公开的每日新增病例数据画一张信息丰富的折线图。这个案例来自我实际做过的一个数据分析项目虽然数据是模拟的但处理流程完全一致。5.1 从数据到图表的完整链条数据清洗、聚合、绘图假设有一份CSV文件记录每天的新增病例数列包括date和new_cases。画图前数据至少要经过两步处理把日期字符串转换成datetime64类型并按日期排序。很多人忽略排序导致折线图上的线条左右乱窜看起来像心电图。import pandas as pd df pd.read_csv(covid_daily.csv) df[date] pd.to_datetime(df[date]) df df.sort_values(date)接下来如果要画“7日移动平均”通常的做法是新生成一列。移动平均的价值是平滑掉周末效应和检测量波动让趋势更清晰。我用的是rolling(window7, centerTrue).mean()centerTrue让平均值在滑动窗口的正中心这样曲线不会整体向右偏移。df[rolling_7] df[new_cases].rolling(7, centerTrue).mean()然后开始画图fig, ax plt.subplots(figsize(12, 6)) ax.bar(df[date], df[new_cases], width0.9, color#D3E0EA, label每日新增) ax.plot(df[date], df[rolling_7], color#C44E52, linewidth2.2, label7日移动平均) ax.xaxis.set_major_locator(mdates.MonthLocator()) ax.xaxis.set_major_formatter(mdates.DateFormatter(%b)) ax.set_ylabel(新增病例数) ax.set_title(每日新增病例与7日移动平均) ax.legend(frameonFalse) fig.autofmt_xdate() fig.savefig(covid_trend.png, dpi300, bbox_inchestight) plt.show()柱状图用浅灰色展示原始数据折线用深红色展示移动平均信息层级一下出来了读者先看到红色的趋势线再看到背后的灰色波动。柱宽width0.9让柱子之间几乎没有空隙更能体现“连续时间序列”的感觉。月份刻度用%b格式显示简洁明了。legend(frameonFalse)去掉图例边框让画面更干净。这一步做完了一张能直接放进日报的图就出来了。整个流程中数据清洗占的时间其实比绘图多但绘图脚本的质量决定了后续改图成本。我强烈建议把图表绘制封装成函数参数只有df和输出路径这样换一天的数据重新跑一次就能得到一张新图不用手工调整任何东西。5.2 排错实录我踩过又爬出来的坑画图过程中最常见的三个报错和异常我都遇到过现在写出来给后来人省点时间。第一个是日期刻度重叠。如果你把MonthLocator改成WeekdayLocator而数据跨了半年那刻度标签就会密到互相覆盖。解决办法一个是增大figsize另一个是像之前那样调用fig.autofmt_xdate()旋转标签。如果旋转后还重叠可以进一步设置ax.xaxis.set_tick_params(rotation45)手动控制角度。第二个是中文乱码。有段时间我在Linux服务器上跑脚本本地Windows好好的一部署到服务器就全变方块。后来发现服务器没有微软雅黑字体于是才有了上面那段setup_cjk_font()函数。现在我的所有项目代码都会先调用这个函数省了很多半夜被运维喊起来改图的痛苦。第三个是图被裁剪或者大片空白。通常都是savefig时忘了bbox_inchestight。如果图内容明明很大保存出来却被切了检查一下你是不是在savefig之前调用了plt.tight_layout()这两个操作并不冲突但顺序错了会产生奇怪的结果。还有一次我为了在图上叠加一个普查人口金字塔用了双x轴结果两套刻度的精度不同柱子对不齐。后来改用ax.secondary_xaxis()解决这个API是在Matplotlib 3.1之后引入的比当年自己造轮子画双坐标轴简单得多。我的看法是双坐标轴能不用就不用它经常误导读者对量级的判断但真到需要的时候优先找新的官方API别去抄旧博客里的老司机代码。5.3 常见问题速查表30秒定位问题我整理了一张表按异常现象、常见原因、解决方案三列来查基本覆盖日常绘图90%的问题。异常现象常见原因解决方案中文显示为方块系统缺少中文字体调用动态选择中文字体的函数或安装字体坐标轴负号显示乱码axes.unicode_minus为默认True在rcParams中设置axes.unicode_minus: False保存图片模糊dpi太低或用了交互窗口手动保存savefig(dpi300)图片四周白边过大未裁剪空白使用bbox_inchestight子图标题与标签重叠没调用自动布局使用plt.tight_layout()或constrained_layout日期刻度挤在一起刻度太密或标签方向不对调locator并autofmt_xdate()图例莫名其妙多出来好多行重复绘制了相同曲线给每条曲线设置唯一label只调用一次legend柱状图柱子底部悬空原本就是缩放的视图set_ylim(bottom0)让柱子从0开始线型变成一点一点断断续续数据中有NaN或inf用dropna()清掉数据再画保存的PDF字体找不到系统没安装对应postscript字体检查字体或改用系统自带PDF引擎这张表是我个人项目里的“急刹车”清单每次出图出问题先对着表看一遍绝大多数都能找到答案。现在回想这些年跟Matplotlib打交道的经历最深的体会是“先有审美再有代码”。教程满天飞能画出图的人很多能画出一眼看清结论的图的人不多。我自己也不是一开始就会就是每天画、每个项目画、每次复盘画法哪里别扭慢慢才摸到门道。如果你也打算深入学习Matplotlib我的建议很简单先画十张风格不同的图再用这篇文章里的配置把它们统一起来最后强迫自己只用三种颜色讲清一个故事。等你能做到这一步Matplotlib对你来说就不再是文档里的函数名而是一支趁手的笔。