1. 为什么我始终推荐“matplotlib打底、seaborn精修”这套组合
做了几年的数据分析和可视化,但凡有朋友问我Python画图该学哪个库,我都会给同一个答案:先把matplotlib用熟,再把seaborn当“快捷方式”来用,两个一起配合,基本能覆盖日常工作里九成以上的图表需求。
这个建议不是拍脑袋。matplotlib是Python生态里最底层的绘图库之一,几乎所有其他可视化库的底层渲染都建立在它之上。它足够灵活,但灵活的另一面是繁琐:一个简单的柱状图,从坐标轴、刻度、边框到图例,零零碎碎要设置的东西不少。而seaborn是基于matplotlib封装出来的统计绘图库,它的核心价值在于“用一行代码画出统计图表”,并且自带一套比matplotlib默认样式好看得多的主题配色。对新手来说,seaborn上手非常快;对老手来说,seaborn的统计 API 能极大减少重复劳动。
但需要注意的是,seaborn并未替代matplotlib,它只是帮你把matplotlib的很多底层细节做了封装和美化。所以,你在seaborn里画完图之后,如果想要调节坐标轴名称、修改图例位置、加注释、调整子图布局,最终还是得回到matplotlib的API层去操作。真正成熟的用法,是把seaborn当成“快速绘图入口”,把matplotlib当成“精细化调整层”,两者叠加使用。
这篇文章的目标读者有两类。一类是刚接触数据可视化的小白,你想知道这两个库分别能做什么、该怎么选;另一类是有一定经验、但经常在画图时被各种细节卡住的同学,比如坐标轴刻度格式、中文乱码、图例重复、配色不协调等常见问题,我会把这些实操中容易踩的坑一并整理出来。内容不堆砌理论,全部是可复现的代码思路,你可以直接拿自己手头的数据试。
2. 分清两兄弟的分工:matplotlib解决“能画”,seaborn解决“画得快且好看”
先直观感受下两个库的差异。假设我们有一份模拟的某电商平台一周销售数据,包含日期、销售额、订单数、商品类目四个字段。用matplotlib从零画一张简单折线图,你可能需要这样写:
import matplotlib.pyplot as plt fig, ax = plt.subplots(figsize=(8, 4)) ax.plot(data["日期"], data["销售额"], marker="o", linestyle="-", color="#4C72B0") ax.set_xlabel("日期") ax.set_ylabel("销售额(元)") ax.set_title("一周销售额趋势", pad=12) ax.grid(True, linestyle="--", alpha=0.3) ax.tick_params(labelsize=10) plt.xticks(rotation=45) plt.tight_layout() plt.show()需要留意的地方不少:fig, ax = plt.subplots()代表你创建了一个画布fig和一个坐标区ax;所有元素都要通过ax.set_xxx一个一个手动添加;坐标轴标签、标题、网格、刻度旋转、边框间距全部靠手工调。这对新手来说很容易漏掉某一步,但优点是每一步你都有绝对的控制权。
2.1 seaborn是如何“偷懒”的
同样的折线图,如果使用seaborn的lineplot函数:
import seaborn as sns sns.set_theme(style="whitegrid") sns.lineplot(data=data, x="日期", y="销售额", marker="o")两行核心代码,背景网格、默认配色、坐标轴自适应已经都处理好了。如果你新增需求,想按不同类目分别绘制多条趋势线,只需要增加一个hue参数:
sns.lineplot(data=data, x="日期", y="销售额", hue="类目", marker="o")一条线变成多条线,颜色自动区分,图例自动生成。这就是seaborn在统计图表场景下的核心优势:它能自动帮你完成“分组聚合再绘图”的整个流程。在matplotlib里,你得先groupby,再循环画多条线,还要手动处理图例标签。
2.2 选择逻辑
坦白讲,日常需求里两者并没有绝对的边界,但我的选择逻辑是这样的:如果只是快速摸下数据分布、画个内部报表、做个探索性分析,直接seaborn,几行代码就出图,效率很高;如果需要做对外展示、精确控制图表每个像素,或者seaborn没有对应的高级图形(例如复杂的多面板组合、自定义不等宽布局、极坐标系图),就回到matplotlib手动搭建。
更关键的是,不要把seaborn和matplotlib当成两套割裂的工具。seaborn画完返回的其实也是matplotlib的axes对象。我经常这样操作:
ax = sns.boxplot(data=data, x="类目", y="销售额") ax.set_title("各品类销售额分布", fontsize=14, pad=12) ax.axhline(data["销售额"].mean(), linestyle="--", color="gray", linewidth=1)sns.boxplot生成坐标区后,后续的标题、参考线、坐标轴调整全走matplotlib的老路子,两个库就这样自然衔接。
3. 环境准备与一份可复用的数据造数方案
写图之前先解决环境。我建议使用最新的稳定版本,而不是最高版本。多个项目里出现过seaborn新版升级后部分统计接口行为变化的情况,所以升级前最好先看下更新日志。
pip install matplotlib seaborn pandas numpy如果你用的是Anaconda或者其他科学计算发行版,这三个库通常已经预装。装完在Python或者Jupyter里验证下:
import matplotlib import seaborn as sns print("matplotlib:", matplotlib.__version__) print("seaborn:", sns.__version__)3.1 Jupyter里显示图的两种方式
Jupyter Notebook里画图,有两种方式让图表直接显示在单元格下方,但很多人会混淆:
%matplotlib inline上面这行是魔法指令,让matplotlib内嵌显示在Notebook里,不需要写plt.show()也能显示。还有一种更现代的方式是:
%config InlineBackend.figure_format = "retina"这行设置图片按Retina分辨率渲染,在Mac或者高分屏上图表文字边缘更清晰,同样需要和%matplotlib inline配合使用。如果你用的是Jupyter Lab,推荐使用%matplotlib widget,它可以生成可交互的浮动图窗,缩放和悬停查看数值都很方便。
注意:
%matplotlib widget需要安装ipympl,直接用pip install ipympl即可。
数据方面,为了方便后续示例演示,我准备生成一份完全虚构的“某便利店30天销售数据”,包含日期、销售额、订单数、天气情况、是否促销五个字段:
import numpy as np import pandas as pd from datetime import datetime, timedelta rng = np.random.default_rng(2024) base_date = datetime(2024, 5, 1) dates = [base_date + timedelta(days=i) for i in range(30)] sales = rng.normal(12000, 2500, size=30).clip(6000, 20000).round(2) orders = (sales / rng.normal(80, 15, size=30)).clip(80, 260).round(0) weather = rng.choice(["晴", "多云", "小雨"], size=30) promotion = rng.choice([0, 1], size=30, p=[0.7, 0.3]) data = pd.DataFrame({ "日期": dates, "销售额": sales, "订单数": orders, "天气": weather, "促销": promotion }) print(data.head())用随机种子保证每次运行结果一致,这样你复现代码时,看到的图表和我下面描述的一致,方便对照。
4. matplotlib从“能看”到“专业”的五个细节
很多人用matplotlib画图,功能上没问题,但图就是不“高级”。问题通常集中在细节:字体、间距、配色、刻度、信息密度。我挑几个高频场景展开说一下。
4.1 中文字体问题的根治方案
matplotlib默认字体不包含中文字符,直接绘图会出现方块乱码。网上最常见的方法是plt.rcParams["font.sans-serif"] = ["SimHei"],但不同操作系统可用字体不一样,换台电脑就得重配。我的做法是做一个统一初始化函数:
import matplotlib.pyplot as plt def set_global_font(): # 常见的跨平台中文字体候选 font_candidates = [ "Microsoft YaHei", # Windows "PingFang SC", # macOS "SimHei", # Windows 黑体 "WenQuanYi Micro Hei" # Linux ] from matplotlib import font_manager available = {f.name for f in font_manager.fontManager.ttflist} for font in font_candidates: if font in available: plt.rcParams["font.sans-serif"] = [font] plt.rcParams["axes.unicode_minus"] = False return font return None注意axes.unicode_minus = False这一行不能省,否则坐标轴上负号会显示为方块。项目中如果经常画图,建议把这个设置放进一个公共配置模块,每次绘图前调用一次。
4.2 用rcParams统一全局风格
如果想给所有图表统一风格,不要在每个绘图函数里重复设置一堆参数,直接改rcParams:
plt.rcParams.update({ "figure.dpi": 100, "savefig.dpi": 300, "axes.spines.top": False, "axes.spines.right": False, "axes.titlesize": 13, "axes.labelsize": 11, "xtick.labelsize": 10, "ytick.labelsize": 10, "legend.frameon": False, "figure.autolayout": True, })axes.spines.top/right设置为False可以去掉顶部和右边的边框线,图表立刻显得干净很多;“留白”本身就是专业感的来源。
4.3 子图布局与其背后的设计逻辑
多子图是matplotlib的强项。最常用的写法有两种。plt.subplots(nrows=2, ncols=2)适合均等网格,但如果想要第一行是宽图、第二行是两个小图的复合布局,推荐用gridspec:
import matplotlib.gridspec as gridspec fig = plt.figure(figsize=(10, 7)) gs = gridspec.GridSpec(2, 2, height_ratios=[1.2, 1], width_ratios=[1, 1], hspace=0.35, wspace=0.25) ax1 = fig.add_subplot(gs[0, :]) # 第一行整行 ax2 = fig.add_subplot(gs[1, 0]) # 左下 ax3 = fig.add_subplot(gs[1, 1]) # 右下height_ratios控制行高比例,width_ratios控制列宽比例。实际项目中,每张子图的x轴经常是同一个日期字段,这种时候我通常先建一个大坐标区,然后对子坐标区做sharex=ax1的处理:
fig = plt.figure(figsize=(10, 6)) ax1 = fig.add_subplot(2, 1, 1) ax2 = fig.add_subplot(2, 1, 2, sharex=ax1)共享x轴后,上下两张图横坐标范围完全一致,并且只需要在底部子图设置x轴标签,上方子图的x轴刻度会自动隐藏,版面更紧凑,看起来也专业很多。
4.4 刻度格式化:让坐标轴说人话
画金额或者数量相关的图,坐标轴上的大数字经常需要格式化。比如销售额是“12345.67”元,直接放在y轴上既不美观也难读。用FuncFormatter可以轻松转成“1.23万”或“12.3K”:
from matplotlib.ticker import FuncFormatter def fmt_money(x, _): if x >= 10000: return f"{x / 10000:.1f}万" return f"{x:.0f}" ax.yaxis.set_major_formatter(FuncFormatter(fmt_money))日期轴上的刻度也要注意。如果日期跨了好几个月,matplotlib默认可能每个点都显示一个标签,密密麻麻挤在一起。一个简单的方法是设置MaxNLocator:
from matplotlib.ticker import MaxNLocator ax.xaxis.set_major_locator(MaxNLocator(nbins=6))强制x轴最多显示6个刻度,视觉效果会清爽很多。
4.5 保存图的几个细节
保存图片时最常犯的错误是:明明屏幕上看着清晰,保存到本地却模糊。原因在于savefig的默认dpi和屏幕dpi不同。推荐:
fig.savefig("output.png", dpi=300, bbox_inches="tight", facecolor="white")bbox_inches="tight"会自动裁剪掉图表周围多余白边,防止标题或轴标签被截断。如果做文档报告,建议直接存PDF矢量图,非高清不糊:
fig.savefig("output.pdf", bbox_inches="tight")5. seaborn统计图的正确打开方式:从分布探索到多变量关系
seaborn的价值在于统计分析,我挑四个最常用、最能体现它优势的图型来说。
5.1 直方图与核密度估计组合
单变量分布是探索数据的起点。seaborn的histplot可以在一张图里同时展示直方图和核密度曲线。对销售数据里的“订单数”字段分布情况:
import seaborn as sns sns.set_theme(style="whitegrid") ax = sns.histplot(data["订单数"], kde=True, bins=15, color="#4C72B0") ax.set_title("订单数分布:直方图 + 核密度估计", pad=12)如果设置kde=True,才会同时画出KDE曲线;如果不加这个参数,就只有直方图。比起distplot(旧版函数,不少教程还在用,但已经废弃),新版本统一用histplot,别再用旧API了。
5.2 箱线图与分类散点的互补
箱线图适合观察不同类别的分布对比。比如比较不同天气下销售额的差异:
sns.boxplot(data=data, x="天气", y="销售额", palette="Set2")但箱线图会隐藏数据的“形状”,看不出数据点具体聚在哪个区间。我习惯在箱线图上方叠加一层抖动散点,这样既能看到整体分布形态,又能保留每个样本的细节:
sns.boxplot(data=data, x="天气", y="销售额", palette="Set2", width=0.5) sns.stripplot(data=data, x="天气", y="销售额", color="black", alpha=0.3, jitter=True)真实项目里,只要数据量在几百以内,叠加散点都能增加不少信息量。数据量特别大的时候,stripplot会变成一团黑线,建议改用sns.violinplot直接看密度分布,或者使用sns.boxenplot,它能更好地展示大数据量的分位数细节。
5.3 热力图:相关性矩阵与缺失值矩阵
热力图是我用得最频繁的可视化图形。特别是拿到一份新的数据表,第一件事就是画相关性矩阵:
corr = data[["销售额", "订单数", "促销"]].corr() sns.heatmap(corr, annot=True, cmap="RdBu_r", center=0, fmt=".2f", square=True, linewidths=0.5, cbar_kws={"shrink": 0.8})几个参数值得说下:annot=True在格子里显示相关系数数值;fmt=".2f"控制显示小数位;center=0让0值落在色带正中间,正负相关性用红蓝区分更直观;square=True让热力图单元格为正方形,视觉更舒服;cbar_kws={"shrink": 0.8}缩短色带长度,避免色带太大抢风头。
除了相关性矩阵,热力图还有一个很容易被忽略的用法:画缺失值分布图。如果你的数据里有空值,用sns.heatmap(data.isnull(), cbar=False)一下子就能看清哪些字段在哪些行有缺失,是很实用的数据清洗工具。
5.4 多变量关系:pairplot和FacetGrid
先看pairplot,它能在一张图里展示所有数值型变量两两之间的散点关系,对角线位置放单变量分布:
sns.pairplot(data, vars=["销售额", "订单数", "促销"], hue="天气", diag_kind="kde", corner=True)corner=True只显示下三角部分,去掉重复对称的上三角,在大数据量时可以显著提速,图也更好看。hue还会自动根据分组给散点和分布曲线着色,一张图能看出非常多信息。
FacetGrid是更灵活的分面工具,适合“按某个分类变量拆分数据、分别绘图、最后拼在一个大图里”的场景。比如想看不同天气条件下销售额和订单数的关系,可以把画布按天气拆成三块:
g = sns.FacetGrid(data, col="天气", height=3.5, sharex=False) g.map_dataframe(sns.scatterplot, x="订单数", y="销售额", hue="促销", alpha=0.7) g.add_legend()这段代码会自动创建三个子图,分别对应“晴”“多云”“小雨”,每张图里都用不同颜色区分是否促销。sharex=False的意思是让每个子图的x轴范围独立缩放,防止某个分组的数据过窄被压扁。
6. seaborn的高级定制:从统计图无缝切换到底层控制
很多人用seaborn画完图之后,不知道怎么继续调节细节。其实方法很简单:seaborn的所有绘图函数都返回一个Axes对象,拿到这个对象后,所有matplotlib坐标轴操作方法都能照常使用。
6.1 调整配色
seaborn有一个很容易被忽略的灵活功能:可以直接传matplotlib的色带或者自定义颜色序列给palette参数。比如要突出促销组和非促销组的对比,可以指定一个显眼的亮色序列:
custom_palette = ["#E4572E", "#3B7EA1"] sns.boxplot(data=data, x="天气", y="销售额", palette=custom_palette, width=0.5)这里的两个十六进制颜色,一个橙色一个蓝色,形成互补色对比。不用记复杂的色带API,只要是matplotlib能识别的颜色格式,seaborn基本都支持。
6.2 在seaborn图上叠加自定义元素
假设我们已经用histplot画出了销售额分布,现在想用一条参考线标出均值位置,并添加文本注释:
ax = sns.histplot(data["销售额"], kde=True, bins=20, color="#4C72B0") mean_sale = data["销售额"].mean() ax.axvline(mean_sale, color="#C0392B", linestyle="--", linewidth=1.5) ax.text(mean_sale + 300, ax.get_ylim()[1] * 0.9, f"均值:{mean_sale:.0f}", color="#C0392B")ax.axvline是matplotlib画垂直线的方法,ax.text用于添加文字,位置由ax.get_ylim()动态计算,而不是硬编码坐标。这种做法在汇报场景中很实用,一张图上同时展示分布形态和关键统计量,信息量立刻提升。
6.3 组合图表的构建思路
最常用的组合图需求是“柱状图 + 折线图双轴”,比如展示销售额柱状图的同时,叠加一条订单量折线。seaborn没有直接的“双轴图”函数,但我们可以利用两个库的协同来完成:
fig, ax1 = plt.subplots(figsize=(10, 5)) sns.barplot(data=data, x="日期", y="销售额", ax=ax1, color="#9ECAE1", alpha=0.6, label="销售额") ax2 = ax1.twinx() sns.lineplot(data=data, x="日期", y="订单数", ax=ax2, color="#E4572E", marker="o", label="订单数") ax1.set_ylabel("销售额(元)") ax2.set_ylabel("订单数(单)") # 合并两个图例 lines1, labels1 = ax1.get_legend_handles_labels() lines2, labels2 = ax2.get_legend_handles_labels() ax1.legend(lines1 + lines2, labels1 + labels2, loc="upper left")代码里的twinx()创建了第二个y轴坐标系,柱状图和折线图各用各的轴。最后合并图例的手法是这类图的通用做法,如果你经常画双轴图,建议把这逻辑封装成函数。
7. 组合布局与画布管理的进阶整理
当图表数量多起来之后,布局管理变得非常关键。我分享两个进阶方案,一个是共享图例的整页多图展示,一个是利用plt.GridSpec精细控制画布划分。
7.1 一页多图、共享图例的实现方式
制作数据周报时,我经常需要把销售趋势、订单分布、促销效果等好几个图放到一张大画布上。常见误区是每张图都有自己独立的图例,挤在一起很乱。更专业的做法是只保留一个总图例:
fig, axes = plt.subplots(2, 2, figsize=(12, 8)) axes = axes.flatten() # 在每个子图里画图,但不显示图例 sns.lineplot(ax=axes[0], data=data, x="日期", y="销售额", hue="促销") sns.histplot(ax=axes[1], data=data, x="订单数", kde=True) sns.boxplot(ax=axes[2], data=data, x="天气", y="销售额", hue="促销") sns.scatterplot(ax=axes[3], data=data, x="订单数", y="销售额", hue="促销") # 统一设置图例 handles, labels = axes[0].get_legend_handles_labels() fig.legend(handles, labels, loc="upper right", bbox_to_anchor=(0.98, 0.98)) # 清除子图自身图例 for ax in axes: ax.legend_.remove() if ax.legend_ else None这里需要注意axes.flatten()将二维数组转为一维数组,循环处理起来方便很多;fig.legend是在画布级别创建图例,通过loc和bbox_to_anchor控制位置;ax.legend_是matplotlib保存图例对象的内部属性,用它可以判断图例是否存在并将其移除。
7.2 不等宽布局的实际场景
假设要画一张“左图是箱线图、右图是散点图,且右图宽度是左图两倍”的复合图:
from matplotlib.gridspec import GridSpec fig = plt.figure(figsize=(12, 4)) gs = GridSpec(1, 3, width_ratios=[1, 2, 2]) ax1 = fig.add_subplot(gs[0]) ax2 = fig.add_subplot(gs[1]) ax3 = fig.add_subplot(gs[2]) sns.boxplot(ax=ax1, data=data, x="天气", y="销售额") sns.scatterplot(ax=ax2, data=data, x="订单数", y="销售额", hue="天气", alpha=0.7) sns.scatterplot(ax=ax3, data=data, x="日期", y="销售额", hue="促销", alpha=0.7)width_ratios=[1,2,2]决定了三列宽度比为1:2:2。所谓“复杂布局”,本质上就是控制这些比例数字。
7.3 坐标轴共享与对齐细节
多图中如果多个子图带有相同的横轴标签,虽然共享坐标轴能减少冗余,但不同子图的标题、刻度标签宽度不同,可能导致子图区域没有严格对齐。解决办法是手动统一y轴标签宽度,或者在开头和结尾只保留一个纵轴标签:
fig, axes = plt.subplots(1, 3, figsize=(12, 4), sharey=True) sns.boxplot(ax=axes[0], data=data, x="天气", y="销售额") sns.boxplot(ax=axes[1], data=data, x="天气", y="订单数") sns.boxplot(ax=axes[2], data=data, x="天气", y="促销") fig.supylabel("数值") for ax in axes: ax.set_ylabel("")sharey=True让三张图共享同一个y轴范围,fig.supylabel在画布级别创建共同的y轴标签,子图内的set_ylabel("")则清空各自的重复标签,整张图干净不少。
8. 两个库配合使用时常踩的坑与排查思路
下面把实际项目里最常遇到、且搜索引擎很难一下子找到答案的几个坑列出来,每一条都附上排查思路。
8.1 中文乱码的终极排查手法
设置完字体后依然乱码,通常有两个原因。第一,字体名拼写错误——中文字体在系统中的显示名称并不总是中文拼音,比如mac上的“PingFang SC”如果写成“PingFang”就找不到。排查办法很直接:
from matplotlib import font_manager for f in font_manager.fontManager.ttflist: if "PingFang" in f.name or "YaHei" in f.name or "SimHei" in f.name: print(f.name, f.fname)把系统中已注册的所有字体打印出来,再去rcParams里填上准确的字体名字。第二,axes.unicode_minus没有设置为False,这个参数只影响负号的显示,但很多人会忘记,导致负数坐标标签变成乱码方块。
8.2 seaborn图表里的图例与hue参数的冲突
使用hue分组后,如果图表本身需要再添加一个图例,很容易出现图例重复混乱的情况。比如boxplot使用hue分组,又额外传了label参数,最后有两个图例叠在一起。我的建议是:所有需要hue分组的情况,统一用ax.legend(title="分组名")来控制图例的标题和位置,而不是依赖seaborn默认的图例生成逻辑。
8.3 颜色映射与palette参数的类型错误
某次项目里遇到一个很隐蔽的报错:传入了一个numpy数组作为palette,seaborn报“Could not interpret value”的错误。原因是palette参数期望的是列表、字典或matplotlib色带名称,而不是numpy数组。排查方法就是先检查最基础的参数类型,不要一上来就去查别的层级。凡是报错信息中出现interpret这个词的,八成是参数类型出了问题。
8.4 子图过多导致渲染缓慢
如果你用pairplot画了十几列数据的全变量关系图,再加hue分组,计算量会指数级上升,渲染慢是正常的。优化思路有三个:一是用corner=True只显示一半子图;二是只选择真正需要考察的数值列,控制在6列以内;三是减少散点采样数,例如对大数据框进行抽样:
sample_df = data.sample(n=500, random_state=42) sns.pairplot(sample_df, corner=True)8.5 坐标轴上限被自动截断的问题
有时候我们故意要画出某个指标的极大值,但seaborn会自动把y轴拉得特别宽,导致数据的主体分布被压成一条线。这时候不要犹豫,直接用matplotlib的ax.set_ylim()限制坐标轴范围:
ax = sns.scatterplot(data=data, x="订单数", y="销售额", hue="促销") ax.set_ylim(0, 18000)设置范围后,请注意此时如果存在超出上限的数据点,图表只是把它隐藏了,并不是删除,汇报时需要提前说明这一点,避免读者误判数据缺失。
9. 我常用的环境配置与自定义样式模板
最后分享一个自己维护的样式配置模块。它延续了前面提到的思路,把字体、网格、配色、输出大小全部集中到一个函数里,每次画图前调用一次即可。
def setup_style(): plt.rcParams.update({ "figure.dpi": 110, "savefig.dpi": 300, "savefig.bbox": "tight", "axes.spines.top": False, "axes.spines.right": False, "axes.grid": True, "grid.color": "#E0E0E0", "grid.linestyle": "--", "grid.alpha": 0.6, "axes.titlesize": 13, "axes.titleweight": "bold", "axes.labelsize": 11, "xtick.labelsize": 10, "ytick.labelsize": 10, "legend.frameon": False, "font.size": 10, })axes.grid设为True后,所有子图默认都会带网格线,省去每次画完图再补grid(True)的麻烦。如果你不喜欢网格线,可以修改grid.alpha让它变淡,或者干脆设为False。
还有一点关于seaborn全局样式的经验。sns.set_theme()会一次性覆盖matplotlib的多个全局参数,包括背景、网格、字体大小等。我一般会把sns.set_theme(style="whitegrid")放在代码开头,之后再用plt.rcParams.update微调局部参数。注意调用顺序:后执行的配置会覆盖先执行的配置,所以如果你自定义的rcParams.update放在set_theme()后面,才会最终生效。
10. 从画图到交付:几个真实场景中的完整示例
既然理论和踩坑都讲了不少,最后用三个最常见的真实需求,把代码串成一个比较完整的流程。
10.1 场景一:日销售额趋势与促销事件标注
需求是观察30天内销售额变化,同时标出哪天有促销活动。实现思路是折线图加垂线标注:
fig, ax = plt.subplots(figsize=(10, 4)) sns.lineplot(data=data, x="日期", y="销售额", marker="o", ax=ax, color="#4C72B0") promo_days = data[data["促销"] == 1]["日期"] for day in promo_days: ax.axvline(x=day, color="#E4572E", linestyle="--", alpha=0.5, linewidth=1) # 用颜色区别促销日与普通日 colors = ["#E4572E" if p else "#9ECAE1" for p in data["促销"]] ax.scatter(data["日期"], data["销售额"], c=colors, s=40, zorder=3) ax.set_ylabel("销售额(元)") ax.set_title("30天销售额趋势:橙点为促销日", pad=12) plt.xticks(rotation=30)散点重新着色后,促销日一眼可见,垂直线是辅助参考。注意zorder=3让散点绘制在网格线和折线之上。
10.2 场景二:不同天气、促销组合的销售额对比
项目里常见的是多因素对比。用分面柱状图做二维比较:
fig, ax = plt.subplots(figsize=(10, 5)) sns.barplot(data=data, x="天气", y="销售额", hue="促销", palette=["#9ECAE1", "#E4572E"]) # 把精确数值标在柱子上方 for container in ax.containers: ax.bar_label(container, fmt="%.0f", label_type="edge", fontsize=9, padding=2) ax.set_ylabel("销售额(元)") ax.set_title("不同天气与促销状态下的平均销售额对比", pad=12)ax.bar_label是matplotlib 3.4以后提供的柱子数值标注方法,不必再手动循环写text计算坐标位置。注意label_type="edge"可以控制数值标注在柱子顶端。
10.3 场景三:一份可直接粘贴到PPT里的汇总图
做汇报时我倾向于一页多图,把所有关键结论放在一张大图上,方便读者对比:
fig = plt.figure(figsize=(14, 8)) gs = GridSpec(3, 3, figure=fig, height_ratios=[1, 1, 1.2], hspace=0.4, wspace=0.3, width_ratios=[1, 1, 1.2]) # 左上:销售额趋势 ax1 = fig.add_subplot(gs[0, :]) sns.lineplot(data=data, x="日期", y="销售额", ax=ax1, color="#4C72B0") ax1.set_title("销售趋势") # 左下:订单数分布 ax2 = fig.add_subplot(gs[1, 0]) sns.histplot(data["订单数"], kde=True, bins=12, color="#55A868", ax=ax2) ax2.set_title("订单数分布") # 中下:天气箱线图 ax3 = fig.add_subplot(gs[1, 1]) sns.boxplot(data=data, x="天气", y="销售额", ax=ax3, palette="Set2") ax3.set_title("天气对销售影响") # 右下:一周销售热力图 pivot_data = pd.crosstab(data["日期"].dt.day_name(), data["促销"]) pivot_data = pivot_data.reindex(["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"]) sns.heatmap(pivot_data, annot=True, fmt="d", cmap="Blues", cbar=False, ax=ax4) fig.suptitle("某便利店30天销售汇总", fontsize=16, y=1.02)打印一张图胜过多页PPT,汇报前直接在fig.savefig时调大dpi即可。有一点小经验:热力图里如果当天没有促销日,显示为0,动画播放时有同学误以为数据缺失,所以热力图的cbar参数我一般设成False或明确标注色带含义。
这几个场景串联起来你会发现,真正有用的可视化技能从来不是记住某个库的全部API,而是知道在什么需求下调用哪个函数,以及在函数处理不好的情况下,如何回到matplotlib层兜底。把这两个库当作一套组合拳来用,绝大多数日常绘图需求都能从容应对。