拖了挺久的第七篇终于来了。这期聊的是“核心库实操”说白了就是拿 Python 里最常用那套可视化工具把日常工作里见过的统计图一个一个手把手画出来。不少朋友一提到“画图”就开始翻文档画完一个散点图又忘了柱状图的参数其实核心库的方法就那么多关键是脑子里要有一个“哪种数据该用哪种图”的映射表。这篇我会按单变量、双变量、分类对比、主题美化这样的顺序把直方图、密度图、箱线图、小提琴图、散点图、折线图、热力图、柱状图、饼图、堆叠图全都过一遍凡是能贴代码的地方我都贴完整可跑的代码适合刚学完 pandas 想进阶可视化的新手也适合那些已经画过不少图但总觉得出图不够专业的老手。先说明一下我这边默认你已经装了 Python 3.8 以上环境并且安装好了 matplotlib、seaborn、pandas、numpy 这四件套。1. 选库不纠结matplotlib 是底座seaborn 是加速器1.1 为什么绕不开这两个很多人一上来就纠结要不要学 plotly、pyecharts甚至是 R 的 ggplot2。但我的观点一直很明确只要是做统计分析、写报告、出论文插图先把 matplotlib 和 seaborn 吃透比什么都强。matplotlib 是所有 Python 绘图库的地基它的 pyplot 接口提供了从画布、坐标系到每个图形元素的完全控制权。你用它画一个最简单的散点图本质上是在一张空白的纸上一点点把点画上去每个细节都可以自己说了算。而 seaborn 是在 matplotlib 之上封装出来的高级接口它把“分组”“聚合”“统计变换”这些最常见的需求做成了参数比如 hue、col、row、kind传一个字符串就能搞定。我常用的比喻是matplotlib 是手动挡汽车seaborn 是自动挡但发动机都是同一个。你用 seaborn 画图生成的对象依然是 matplotlib 的 Axes最后用 plt 去调整标题、坐标轴、保存两者无缝衔接。1.2 我的选型原则什么时候该用哪个这不是一道二选一的选择题而是“谁打头谁收尾”的问题。我从实际项目里总结了一条简单规则如果只是快速探索数据先上 seaborn因为它的默认统计处理更完善比如箱线图中的四分位数、直方图的密度归一化、散点图的回归拟合都是一行搞定。如果画出来的图要放在论文、汇报材料里或者要非常精确地控制刻度、文字、边框那就用 seaborn 出主体再用 matplotlib 的 API 做精细调整。至于 pandas 内置的 plot 方法我更多是用在快速看一眼前面几行数据的趋势它本质上是 matplotlib 的薄封装适合临时摸情况不适合做正式输出。下面对比一下我实际使用中的差异场景推荐工具理由快速看字段分布sns.histplot / sns.kdeplot自动算好频率与密度不用自己修 bin分组对比sns.barplot / sns.boxplot自带误差线和分位数统计相关系数图sns.heatmap内置注释和颜色映射比手写 imshow 方便精确控制刻度plt.xticks / plt.yticksseaborn 也依赖这两个底层方法多子图排版plt.subplots先建画布再传 ax 给 seaborn动态交互图plotly只有需要网页交互时才用平时不用我对新人的建议是不要一上来就学一堆库先认准“seaborn 出图、matplotlib 修饰”这个组合跑通 20 张图之后你自然会知道哪里该用谁。2. 绘图前的三件套数据、字体、画布2.1 先用自带数据集别在数据清洗上卡住很多人学习画图时容易掉进一个陷阱先找一个“真实但复杂”的 Excel 文件结果前两个小时都在处理缺失值和数据类型到最后还没看到图长什么样。我的建议是学绘图阶段统一用 seaborn 自带的数据集比如经典的 tips餐厅小费、iris鸢尾花、penguins企鹅。它们都是干净的表格数据有数值列、分类列正好用来演示所有统计图。第一次跑通代码之后再换自己的脏数据你会发现思路清晰很多。import numpy as np import pandas as pd import matplotlib.pyplot as plt import seaborn as sns sns.set_theme(stylewhitegrid) tips sns.load_dataset(tips) penguins sns.load_dataset(penguins)这里要注意seaborn 的 load_dataset 需要联网下载数据文件第一次运行可能稍慢。如果没网也可以自己生成一份模拟数据后面我会给一个可复制的构造方法。tips 数据集里 total_bill 表示账单总金额tip 表示小费sex 是性别day 是星期几time 是午餐还是晚餐size 是就餐人数。penguins 里有岛屿、喙长、喙深、鳍长、体重等字段。这份数据足够我们画后面所有图。2.2 彻底解决中文乱码与负号显示如果你点开画出的图发现中文全变成了方框或者坐标轴负号变成了奇怪的符号那说明你的 matplotlib 没有配置好中文字体。这是新手最常见的坑比绘图逻辑本身还让人头疼。解决方法是显式指定一个系统里存在的中文字体同时关闭 Unicode 负号。plt.rcParams[font.sans-serif] [SimHei, Microsoft YaHei, PingFang SC] plt.rcParams[axes.unicode_minus] FalseSimHei 是黑体Windows 通常自带Microsoft YaHei 是微软雅黑也很稳定Mac 上可能要用 PingFang SC。如果这几个都没有可以用以下代码查看系统里有哪些中文字体from matplotlib import font_manager font_manager.fontManager.addfont(你下载的字体文件路径.ttf)手动下载一个开源字体如“思源黑体”然后把文件路径传上去再重新设置 rcParams 就能生效。我自己的习惯是统一用 SimHei并在博客代码里加上这一行这样发给别人也能直接用。2.3 画布与子图的正确姿势多数统计图不是孤立存在的你要把多个图拼在一起对比。要养成好习惯任何一次绘图都从建立一个画布和子图对象开始而不是直接调用 plt.plot。初级写法是fig, ax plt.subplots(figsize(8, 5)) sns.histplot(tips[total_bill], axax) ax.set_title(Total Bill 分布) plt.show()这里的关键在于先创建一个 fig 对象和一个 ax 坐标系然后把 seaborn 的图显式画到 ax 上。这样当你需要做 2 行 2 列的子图时只需要把 subplots 改成fig, axes plt.subplots(2, 2, figsize(12, 8)) sns.histplot(tips[total_bill], axaxes[0, 0]) sns.boxplot(xday, ytip, datatips, axaxes[0, 1]) sns.scatterplot(xtotal_bill, ytip, datatips, axaxes[1, 0]) sns.barplot(xday, ytotal_bill, datatips, axaxes[1, 1]) plt.tight_layout() plt.show()使用显式 ax 的好处是每个图例、图例标签、坐标轴的修改都有明确的对象可以引用调试起来思路非常清楚。很多人喜欢用 plt.subplot(2, 2, 1) 这种顺序写法在简单场景还好一旦图多了就容易搞混。我现在一律用 axes[row, col] 索引直观且不容易错。3. 单变量可视化看懂一个字段的分布3.1 直方图与核密度估计先看形状再看概率直方图是最基础的分布图它把数据分成若干区间然后统计每个区间里有多少样本。区间数bins的选择会极大影响你看到的信息。bins 太小图形锯齿感强bins 太大又会把真实分布抹平。我的常用方法是先自动选一个默认值看整体形状再有意识地调大调小试试。看下面代码fig, axes plt.subplots(1, 2, figsize(12, 4)) sns.histplot(tips[total_bill], bins20, kdeTrue, axaxes[0]) axes[0].set_title(直方图 密度曲线bins20) sns.kdeplot(tips[total_bill], axaxes[1], shadeTrue) axes[1].set_title(单独核密度估计图) plt.tight_layout() plt.show()看到效果会发现直方图里的柱状高度代表落在该区间的样本数量而核密度估计是一条平滑曲线它的面积累计为 1y 轴不再是计数而是概率密度。实际阅读报表时我通常在直方图上叠加 kde这样既能看具体频次又能感受整体分布的峰度和偏态。如果数据分成多个组还可以用 hue 参数分组呈现sns.histplot(tips, xtotal_bill, huetime, bins20, kdeTrue)这会在同一坐标系里画出午餐和晚餐的分布颜色区分方便观察两组数据的中心位置是否不同。这里有个细节hue 分组后kde 曲线是分别对每组做的不会叠加成一条总和曲线。3.2 箱线图与提琴图用四分位数讲故事箱线图简直是统计图里的瑞士军刀。它用五个数字概括一组数据最小值下须、下四分位数Q1、中位数Q2、上四分位数Q3、最大值上须另外单独画出异常点。你不需要任何描述性统计表就能通过箱体位置看出一组数据的集中趋势和离散程度。代码很朴素fig, axes plt.subplots(1, 2, figsize(12, 4)) sns.boxplot(xday, ytotal_bill, datatips, axaxes[0]) axes[0].set_title(箱线图按星期分组的小费金额分布) sns.violinplot(xday, ytip, datatips, axaxes[1]) axes[1].set_title(小提琴图可以看到峰与谷) plt.tight_layout() plt.show()箱线图的箱体上下界代表 Q1 和 Q3中间那条线是中位数。须的长度一般延伸到 1.5 倍四分位距超过这个范围的点就被标记为离群点。注意这里不一定是“异常值”只能说是有别于大多数数据的特殊样本。小提琴图是箱线图与核密度图的结合体每个组别先算一个旋转的密度曲线再把它对称填充中间还保留了箱线图的关键信息。它比箱线图多展示了“是否有双峰”“数据是否集中在均值附近”等形状信息。比如某些组的分布明显有两个峰用箱线图看不出来小提琴图一下就暴露了。4. 两变量关系关联、趋势与相关4.1 散点图不要只是把点画上去两个数值变量之间的关系第一选择永远是散点图。但散点图有一个致命弱点如果数据点太多比如过万条所有点叠在一起会糊成一团黑色根本看不出密度分布。这时候需要给散点图加一点透明度或者样本密度。对于常规数据回归拟合线能帮你快速判断是正相关还是负相关以及这个关系是不是线性的。代码如下fig, axes plt.subplots(1, 2, figsize(12, 4)) sns.scatterplot(xtotal_bill, ytip, datatips, alpha0.7, axaxes[0]) axes[0].set_title(普通散点图 sns.regplot(xtotal_bill, ytip, datatips, axaxes[1]) axes[1].set_title(带回归线的散点图) plt.tight_layout() plt.show()regplot 默认会用线性回归拟合数据旁边还会画出一个半透明的置信区间带表示拟合的参数不确定性。实际中我经常用 regplot 做相关性分析的第一步看到一条明显上升的直线再去计算 Pearson 相关系数。如果数据里有很多离群点回归线会被拉偏建议先用散点图看看有没有明显的离群值再决定是保留还是剔除。对于分类变量与数值变量的叠加散点图也可以利用 hue 参数区分组别sns.scatterplot(xbill_length_mm, ybill_depth_mm, datapenguins, huespecies)这样不同企鹅物种会用不同颜色呈现一张图就能看出三个物种在二维平面上的分离情况。4.2 折线图画趋势前必须有一个“顺序轴”折线图在统计图中承担的是“随时间或某个有序变量变化的趋势”展示。很多人直接用 sns.lineplot 把数据丢进去结果发现折线乱成蜘蛛网。原因就是 x 轴数据类型不是真正有序的。处理原则是要么 x 轴是日期时间要么是明确的数值区间要么是经过排序的等级。用 seaborn 自带的一个小例子来演示时间趋势# 构造一个带日期和数值的模拟数据 date_list pd.date_range(2024-01-01, periods60, freqD) value np.cumsum(np.random.randn(60)) 100 df pd.DataFrame({date: date_list, value: value}) sns.lineplot(xdate, yvalue, datadf) plt.title(折线图60 天累计值趋势) plt.xticks(rotation45) plt.tight_layout() plt.show()这里需要注意lineplot 默认会对相同 x 值的多个 y 做聚合即算出均值和置信区间。如果我们只是想画出原始序列而没有重复点常常需要加参数 estimatorNone 来关闭聚合sns.lineplot(xdate, yvalue, datadf, estimatorNone)如果不加这个参数当 x 是离散日期而每天的 y 可能有多个观测时你会获得一条带阴影置信带的聚合曲线而不是原始数据折线。这个坑相当常见我在处理实验重复测量时踩过很多次。判断一个折线图要不要加 estimator核心问题是x 上是否有重复值没有重复就关闭聚合。4.3 热力图相关系数矩阵的快速扫描要说一张图包含最多信息量的相关系数热力图绝对排前三。它把多个数值变量的两两 Pearson 相关系数映射成颜色一眼就能看出哪些变量强相关、哪些基本独立。这种图适合做特征筛选也适合排查多重共线性。corr penguins[[bill_length_mm, bill_depth_mm, flipper_length_mm, body_mass_g]].corr() plt.figure(figsize(7, 5)) sns.heatmap(corr, annotTrue, cmapcoolwarm, fmt.2f, linewidths0.5) plt.title(企鹅数据集数值变量相关系数矩阵) plt.show()annotTrue 会在方块里显示具体的相关系数fmt.2f 控制小数为两位。cmap 我用 coolwarm蓝色代表负相关红色代表正相关无色接近零相关。还有两个容易被忽略的参数vmin 和 vmax。如果不显式设置seaborn 默认对应数据的最小值和最大值这会导致两张相关系数图颜色深浅不可比。所以当你需要横向比较若干张热力图时要统一设置 vmin-1, vmax1否则颜色会误导视觉判断。这是我替换过无数张报告插图后才记住的要点。5. 分类数据对比柱状图、饼图与分组图5.1 柱状图的多种形态垂直、水平、堆叠、分组柱状图的核心逻辑是“用柱子的高度表达数量大小”它特别适合比较几个类别的取值差异。画一个最普通的柱状图有两种方式pandas 直接画或者 seaborn 统计画。很多人的误区是直接用 matplotlibr 的 plt.bar 手工统计再传入两组数组。这样写没错但当数据以长表形式存在时手工统计要写不少聚合代码。我更喜欢让 seaborn 帮我在内部完成统计fig, axes plt.subplots(1, 2, figsize(12, 4)) # 普通柱状图误差线表示 95% 置信区间 sns.barplot(xday, ytotal_bill, datatips, axaxes[0]) axes[0].set_title(柱状图带误差线) # 水平柱状图适合类别名很长的情况 sns.barplot(yday, xtotal_bill, datatips, axaxes[1]) axes[1].set_title(水平柱状图) plt.tight_layout() plt.show()注意默认情况下sns.barplot 计算的是每组数据的均值黑色短线代表置信区间。如果你的工作重心不是统计推断而是展示原始总量可以直接用 pandas 的 value_counts 或 groupby 之后传入 plt.bar。另一种常见样式的堆叠柱状图适合表达“总体由若干部分构成”且有分组的情况。比如按天展示晚餐和午餐的人均账单占比可以这样画# 先生成按 day 和 time 聚合的人数 count_by_day_time tips.groupby([day, time]).size().unstack(fill_value0) count_by_day_time.plot(kindbar, stackedTrue) plt.title(堆叠柱状图各星期就餐类型人数) plt.xticks(rotation0) plt.tight_layout() plt.show()这里使用 pandas 的 plot 是因为它可以直接处理宽表数据stackedTrue 会把两段柱体叠在一起。如果你想要的是百分比堆叠只需要把 count_by_day_time 按行归一化就行percent count_by_day_time.div(count_by_day_time.sum(axis1), axis0)5.2 饼图与圆环图能少用就少用但要懂怎么画饼图被一些数据可视化专家批评“人类很难准确比较面积和角度”这话没错。但如果你的分类少3 到 5 类且只是想传达“占比”而不是“精确数值”饼图依然有它的位置。画饼图我用的是 matplotlibseaborn 没有封装这个类型。基本代码是time_counts tips[time].value_counts() plt.figure(figsize(6, 6)) plt.pie(time_counts.values, labelstime_counts.index, autopct%.1f%%, startangle90) plt.title(晚间与日间用餐占比) plt.axis(equal) plt.show()这里 startangle90 让第一个扇形从正上方逆时针排起axis(equal) 保证饼图是正圆而不是椭圆。如果我想要一个圆环图只需要在 plt.pie 里加一个 wedgeprops 参数plt.pie(time_counts.values, labelstime_counts.index, autopct%.1f%%, wedgepropsdict(width0.5))width0.5 会让半径为原来的一半形成甜甜圈形态。圆环的好处是中心区域可以再放补充说明文字或总数值视觉上更轻盈。我建议不要在饼图里放超过 6 个类别否则小扇形挤在一起很难阅读。如果类别多宁可改成水平柱状图。5.3 分组数据hue 参数是分类对比的灵魂上面提到的柱状图已经涉及分组但 seaborn 里更灵活的是用 hue 同时控制颜色结合不同图形类型做多维比较。比如我想看不同性别和不同聚餐时间下账单金额的差异用分组柱状图最直观plt.figure(figsize(8, 5)) sns.barplot(xday, ytotal_bill, huesmoker, datatips, cisd) plt.title(按星期和是否吸烟分组的账单均值) plt.show()参数 cisd 表示误差线用标准差而非默认的置信区间这样你看到的是组内波动大小。如果还想看数据内部的具体分布可以先画散点再在上面覆盖箱线图这就是所谓的“散点箱线”叠加plt.figure(figsize(8, 5)) sns.stripplot(xday, ytotal_bill, datatips, huesmoker, dodgeTrue, jitterTrue, alpha0.6, palettedeep) sns.boxplot(xday, ytotal_bill, datatips, huesmoker, width0.3, boxpropsdict(alpha0.3)) plt.title(分布与箱线对比) plt.show()这个方法在论文里很常见因为原始数据点可见统计摘要也清晰。不过在同一个图上叠加两个 hue 各自独立的分组时要注意 dodge 参数控制分组偏移不然同一 x 位置不同颜色的点会挤成一团。dodgeTrue 时每个类别会错开才不至于重叠。6. 让统计图“能上台面”主题、颜色与排版细节6.1 主题设置三行代码改变整个画风默认的 matplotlib 白底黑框已在很多场合显得平淡。seaborn 的优势是自带多种主题风格叫 set_theme 或 set_style。我平时会根据使用场景切换做汇报 PPT 或演示大屏用 whitegrid因为横向网格线有助于对齐数值写博客或印刷资料用 white 或 ticks减少背景噪音做海报或数据大屏用 darkgrid 或者直接调暗背景。代码很简单sns.set_theme(stylewhitegrid, rc{figure.dpi: 120, savefig.dpi: 300})这里还顺便把屏幕分辨率和保存分辨率设置了。默认 dpi 只有 80 或 100保存到 PPT 里会发虚300 dpi 是针对投稿级别的。另外字体大小、刻度大小这些都可以通过 rc 参数统一设置sns.set_theme(stylewhitegrid, rc{ font.size: 12, axes.labelsize: 14, axes.titlesize: 16, xtick.labelsize: 11, ytick.labelsize: 11, })如果你的项目对尺寸要求特别严格把这些配置放到一个单独的 py 文件里每次开头导入时统一加载以后所有图风格一致改起来也容易。6.2 颜色、标签、图例的实操调整颜色是统计图的“滤镜”选不好容易让观众误读。seaborn 自带多个颜色面板最常用的是 deep、muted、bright、dark、colorblind。如果你要打印出来用 muted如果是屏幕演示用 deep如果有人是色盲记得用 colorblind。指定方式有两种一种是在绘图函数里直接写 palettesns.barplot(xday, ytotal_bill, huesmoker, datatips, palettemuted)另一种是拿到当前颜色循环列表palette sns.color_palette(deep, n_colors3) print(palette)如果对默认颜色不满意可以自定义颜色列表custom_palette [#3498db, #e74c3c, #2ecc71] sns.barplot(xday, ytotal_bill, huesmoker, datatips, palettecustom_palette)标签方面我习惯在画完图后统一用 ax.set() 设置标题、x 轴名、y 轴名避免在绘图函数里塞一堆参数导致代码混乱ax sns.boxplot(xday, ytotal_bill, datatips) ax.set(title每日账单分布, xlabel星期, ylabel账单金额美元)图例是另一个容易翻车的地方。当使用 hue 时seaborn 会自动生成图例但当你对图例位置有要求时要显式调用 ax.legend() 修改。比如放到图外ax.legend(title是否吸烟, locupper left, bbox_to_anchor(1.02, 1))bbox_to_anchor 是位置偏移1.02 表示图例框在 x 方向上超出坐标轴 2% 的位置。之后再用 plt.tight_layout() 调整画布就不会和图主体挤在一起了。6.3 多子图组合输出时的统一风格当你需要把多张图拼到一张大图里时最大的问题是各个子图的坐标轴范围、刻度密度、标签长度参差不齐。我现在固定套路是先用 plt.subplots 建立统一的画布然后对需要共享尺度的子图手动设置 xlim 和 ylim让它们保持一致视觉上更好对比。例如对比直方图和箱线图时我可以令两张图使用同样的 x 轴范围fig, axes plt.subplots(1, 2, figsize(12, 4)) sns.histplot(tips[total_bill], kdeTrue, axaxes[0]) sns.boxplot(xtips[total_bill], axaxes[1]) for ax in axes: ax.set_xlim(0, 60) plt.tight_layout() plt.show()这样一眼就能看到离群点的位置在分布中的对应关系。还要注意子图之间的标题字号与标签字号不要差太多统一用 set 方法逐个设置或者干脆在建立画布后直接用 fig.supxlabel、fig.supylabel 设置整张大图的公共坐标轴标签避免每个子图都重复写一些冗长标签。另外保存图片时不要只靠鼠标右键截图而是用 plt.savefigfig.savefig(summary_plot.png, dpi300, bbox_inchestight, facecolorwhite)bbox_inchestight 会自动裁剪掉多余的留白facecolorwhite 防止透明背景在部分文档里变黑。如果你要发到网上PNG 合适如果要印刷建议用 PDF 或 SVG 矢量格式。7. 收尾我踩过的坑和现在的固定习惯7.1 最容易让图“失真”的几类错误画了这么多年图我发现最容易让人误解的并不是图表类型选错而是坐标轴和统计设置出了偏差。第一个坑是截断坐标轴有些人为了让两组数据的差异看起来更夸张会把 y 轴起点设为 10而不是 0。这在柱状图中尤其危险因为柱子高度比例会被扭曲。务必牢记柱状图默认从 0 开始折线图则可以适当放宽以展示波动。第二个坑是错误地使用 boxplot 的离群点剔除逻辑以为须之外的点是“错误数据”实际它们只是超出四分位距 1.5 倍的点是否剔除需要结合实际业务判断。第三个坑是在数据量很小的情况下画密度图比如只有 10 个点核密度估计会做出非常奇怪的曲线这时候老老实实画散点。最后一个坑是保存图片时忘记设置 dpi结果报告一放大全是锯齿。7.2 我的固定绘图模板我现在大多数项目的绘图代码都长成一个模板保证每次不重写基础配置import matplotlib.pyplot as plt import seaborn as sns import pandas as pd import numpy as np plt.rcParams[font.sans-serif] [SimHei, Microsoft YaHei] plt.rcParams[axes.unicode_minus] False sns.set_theme(stylewhitegrid, rc{figure.dpi: 120, savefig.dpi: 300}) def setup_ax(ax, titleNone, xlabelNone, ylabelNone): if title: ax.set_title(title) if xlabel: ax.set_xlabel(xlabel) if ylabel: ax.set_ylabel(ylabel) return ax tips sns.load_dataset(tips)有了这个模板接下来无论画什么图都属于填空。我还习惯把每一次生成的图文件名按“日期-数据集-图表类型”的方式命名比如 2025-06-tips-boxplot.png时间久了找图非常方便。7.3 一句真心话学绘图最忌讳“背代码”。真正有效的方法是把每个图当作一个解决问题的工具拿到数据后先问自己三个问题我想看分布还是看关系分类变量有几个这些图是给自己探索还是给别人汇报想清楚这三件事你会发现核心库那十几个函数已经足够覆盖九成以上需求。这篇我尽量把每个图的适用范围和关键参数讲了至于更花哨的高阶图形等以后有空再单独拆解。如果有朋友在练习过程中遇到哪种图画不出理想效果建议先从数据格式、字体和画布这三个环节检查多半是这三个地方在作怪。