多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

Seaborn统计绘图实战:从数据整理到可视化全流程指南

Seaborn统计绘图实战:从数据整理到可视化全流程指南 Seaborn 这个库我第一次用是把它当成“更好看的 Matplotlib”。后来真正折腾过几张统计图才发现它的价值远不止换了个皮肤而是把“画图之前先算清楚”这件事替你做了。用 Seaborn 绘制统计图形最大的感受就是“更美、更简单”但这份简单不是靠牺牲灵活性换来的而是它把统计绘图的默认逻辑理顺了。这篇文章我想把这几年的实际用法梳理一遍从数据格式到选图思路再到踩过的坑给正打算入坑或者已经在用但总觉得别扭的朋友一份能直接照抄的参考。我默认读者已经会一点 Python 和 pandas不会的也能先跑起来代码我都放在下面可以直接复制。文章里的数据全部用模拟销售数据不涉及任何真实业务口径你只要关注画图思路就行。1. 统计绘图不是“画线”Seaborn 的力气花在了统计计算上1.1 从 Matplotlib 的“手工作坊”说起先聊一个很多人的共同经历。最早我用 Matplotlib 画分组折线图流程是这样的先按月份 groupby手动算每个月的均值再手动算 95% 置信区间然后用plt.plot画均值线用plt.fill_between画误差带。画完一张图代码已经二十多行。想再加一个分组维度也就是把城市分成甲乙丙三类我又得把分组循环、颜色映射、图例生成全部重写一遍。这还不是最痛苦的。最痛苦的是画到第二十张图的时候图例位置、坐标轴刻度格式、网格透明度、色板冲突这些小问题会一起冒出来像房间里同时飞进来二十只蚊子。最后图是画出来了但代码已经膨胀得没法维护。我后来反思问题的根源不是 Matplotlib 本身不好而是我一直在用“指令式绘图”的方式做“声明式绘图”该做的事。Seaborn 换了一个思路你告诉它“数据是什么、X 轴是什么、Y 轴是什么、按什么分组”它自己去完成聚合、置信区间计算、颜色分配、图例生成这一整套流程。这个转变看起来只是 API 形式变了实际上是把画图的心智模型从“怎么画”切换到了“画什么”。1.2 Seaborn 默认替你做好的三件事很多人第一次看到 Seaborn 的图觉得好看以为是配色问题。其实配色只是最表层的东西真正让图“显得专业”的是下面三件事第一统计变换自动完成。以sns.lineplot为例你只要传入原始明细数据它默认会把 X 轴上相同位置的 Y 值求均值并画出置信区间。这个行为对应到 Matplotlib就是上面那一整段手写代码。第二变量类型自动识别。数值列、类别列、时间列传入后Seaborn 会根据类型决定坐标轴的刻度形式、颜色映射方式和图例位置。你不需要告诉它“这个字段是离散的”它能从数据里读出来。第三视觉默认值统一。背景网格、坐标轴边框、字体大小、调色板对比度这些默认值都经过设计单张图好看拼成大图也不会乱。对比效果可以参考下面这个简化版表格。绘图任务Matplotlib 手写Seaborn 一行实现分组折线图 置信区间groupby mean std plot fill_betweensns.lineplot(x, y, hue)分组箱线图自己循环分组并逐个调整坐标sns.boxplot(x, y, hue)相关性热力图画色块矩阵手工加标签sns.heatmap(df.corr(), annotTrue)1.3 一个容易搞反的前提给它“原始明细”别给它“算好的结果”这里有个常见误解既然 Seaborn 会自动算均值那我是不是得先把数据算好再喂给它恰恰相反。你如果把已经 groupby 过后只剩均值的结果传进去lineplot仍然会再求一次均值画出来的图可能跟你预想的不一样而且置信区间也消失了。我现在的习惯是只要数据量在可接受范围内一律把原始明细传给 Seaborn由它内部完成统计。这样有几个好处切换分组维度不需要重算数据改hue就能看不同分组的对比bootstrap置信区间也会自动出现。只有数据量特别大比如千万行以上我才会提前聚合那时用estimator参数手动指定聚合方式。2. 给 Seaborn 的“数据脾气”长表是王道宽表要改造2.1 整洁数据的三条准则Seaborn 对数据格式有一个明确的要求业内叫“整洁数据”三条规则非常简单每列是一个变量每行是一条观测每个格子是一个观测值。听起来是废话但实际拿到的数据很少长这样。比如业务那边的报表经常是一个宽表行是某渠道列是 1 月、2 月、3 月这种月份交叉点是销售额。这种表给人看很方便但给绘图工具用就很别扭因为月份在表里不是“一列”而是散落成了很多列。你要画按月份变化的趋势图就必须先把月份从列名里拆出来。2.2 用 melt 把宽表变长表pandas 里的melt就是专门干这个的。我拿一个模拟数据举例import pandas as pd df_wide pd.DataFrame({ 区域: [甲区, 乙区, 丙区], 1月: [102, 96, 85], 2月: [110, 98, 87], 3月: [108, 102, 90], }) df_long df_wide.melt( id_vars[区域], var_name月份, value_name销售额, ) print(df_long.head())id_vars是保留不变的列var_name是给原来那些列名起的新列名value_name是数值放进去的列名。经过这一步原来的“月份”从变量名变成了变量值Seaborn 才能正确地把月份当横轴、把销售额当纵轴、把区域当分组。2.3 changelog 到 transform宽表改长表的三条建议实际操作中有三条建议我反复用到。一是melt之后的列类型经常不对。列名 “1月”“2月” 是字符串如果只有三个月还好一旦有 12 个月顺序就会变成 10 月、11 月、12 月排在 1 月前面。要先把月份列转成真正的日期或有序类别用pd.Categorical指定顺序或者用pd.to_datetime转时间。二是不要为了画图把原始数据改掉。我一般会把长表存成一个新变量保留df_wide作他用。因为长表只是绘图的中间格式清洗完继续写回别的分析脚本时宽表往往更方便。三是优先保留明细数据。只要明细在任何维度的聚合随时可以重算。一旦你把明细聚合掉了后面想换一个分组维度就得回头重新导数据。所以我画图前只做必要的类型修正和缺失值处理不主动做聚合。3. 四类高频场景我的默认函数这样选3.1 看分布histplot 还是 kdeplot拿到一个数值列第一反应是看分布。早年的distplot已经退役现在推荐的是histplot和kdeplot我日常以histplot为主因为直方图更直观而且能把密度曲线叠加上去。import seaborn as sns import matplotlib.pyplot as plt sns.set_theme(stylewhitegrid) sns.histplot( datadf_long, x销售额, hue区域, bins30, kdeTrue, alpha0.6, ) plt.show()kdeTrue会在直方图上叠加核密度曲线能弥补直方图对分布形状的钝感。bins控制分组数量30 是我用比较多的默认值但如果数据量小bins 太多会出现大量空桶影响判断。当我想看单条分布的平滑形态或者要把多条分布叠在一起对比时会更倾向单独用kdeplot因为核密度曲线不受 bin 边界影响叠加在一起更干净。3.2 看关系relplot 和 jointplot 的分工两列数值之间的关系我分两个颗粒度来看。如果只是想快速确认有没有相关性用jointplot最方便它会同时画出散点图、两条边缘分布和相关系数。sns.jointplot( datadf, x单价, y销售额, kindreg, height6, ) plt.show()kindreg会加一条回归拟合线和置信带。height控制整体尺寸。当点数很多散点相互遮盖时我会把kind换成hex六边形分箱图能看清密度中心。如果还要加入分组维度比如想看不同区域的趋势差异jointplot就有点力不从心了。这时我会用relplot它本质上是散点图和折线图的通用接口靠kindscatter或kindline切换而且天然支持hue、col、row分面。3.3 看分类对比catplot 的 box 和 violin 组合分类变量和数值变量放一起最常见的是比较不同类别的分布和中心趋势。catplot提供了一个统一入口靠kind参数切换六种图。我用得最多的是“箱线图散点”组合。sns.catplot( datadf, x品类, y销售额, hue区域, kindbox, order[数码配件, 家居日用, 食品饮料], height5, aspect1.2, ) plt.show()普通箱线图会隐藏数据点的位置尤其是小样本。catplot支持kindviolin加上innerquart展示更多分布细节但数据点太多时小提琴图会显得拥挤。我的选择规律是样本量少于 50 时用violin样本量超过 50用box必要时叠加strip或swarm。3.4 看整体相关heatmap 的前提和装饰相关性矩阵是探索性分析的收尾动作。先对数据做相关系数计算再交给heatmap绘制。corr df[[销量, 单价, 销售额, 毛利]].corr() sns.heatmap( corr, annotTrue, fmt.2f, cmapRdBu_r, center0, vmin-1, vmax1, linewidths0.5, linecolorwhite, ) plt.show()annotTrue显示数值fmt.2f控制小数位数center0让正负相关以白色为界向两端发散vmin-1, vmax1固定颜色范围。这种做法比默认色阶容易读因为你能一眼分清正负相关而不是只看出大小。4. 颜值不是玄学主题、调色板和坐标轴细节的三层开关4.1 set_theme 一次搞定全局风格很多人美化图是从“加个背景色”开始的但零散地改风格很容易让整套图不统一。Seaborn 把风格分成两层主题风格和绘图环境。sns.set_theme( stylewhitegrid, palettemuted, font[SimHei, Arial], rc{figure.figsize: (8, 5)} )style可选darkgrid、whitegrid、dark、white、ticks。我做业务报告常用whitegrid做偏视觉展示的图会用white因为网格会抢注意力。palette决定默认色板rc可以传任意 Matplotlib 配置比如默认画布大小。还有一个context参数控制字体、线条、坐标轴尺寸的缩放比例可选paper、notebook、talk、poster。我提交给 PPT 的图会用talk放进论文或附件用paper。这个参数只需要一行却能避免“PPT 上字太小看不清”这种尴尬。4.2 调色板语义色永远比彩虹色好用调色板的选择有个简单原则类别少用定性色板数值连续用顺序色板正负对比用发散色板。类别图如果只有 4 到 6 个分组我建议手动指定一组饱和度适中的颜色而不要依赖 “tab10” 那种高饱和撞色。一个示例custom_palette [#4C72B0, #DD8452, #55A868, #C44E52] sns.set_palette(custom_palette)因为彩虹色会让图表像儿童填色本颜色之间的区分度虽然高但视觉重量没有主次。语义色的意思是同一个类别在整份报告的所有图里永远用同一个颜色。我在画多张图之前会先定义一个调色板保证从折线图到箱线图甲区永远是蓝色乙区永远是橙色。4.3 最后的 10% 细节标题、标签和导出Seaborn 的默认图直接看没问题但真要放进报告通常还需要补三个信息标题、坐标轴标签、数值格式。ax sns.boxplot(datadf, x品类, y销售额, hue区域) ax.set_title(各品类销售额分布对比) ax.set_xlabel(商品品类) ax.set_ylabel(销售额元) ax.legend(title区域, locupper right)保存图片时我习惯用bbox_inchestight否则图例和标题容易被裁掉plt.savefig(output.png, dpi300, bbox_inchestight)dpi300用于打印或正式文档dpi150用于屏幕展示已经足够清晰。5. 代码跑通却发现图“不对”五个高频坑和排查方法5.1 分类顺序乱七八糟这是我被问得最多的一个问题。明明数据里是“甲区、乙区、丙区”画出来却变成了“乙区、丙区、甲区”。原因是 pandas 的类别顺序或者字符串排序跟你预期不一致。解决办法是在catplot等函数里显式传order和hue_ordersns.catplot( datadf, x品类, y销售额, hue区域, kindbox, order[数码配件, 家居日用, 食品饮料], hue_order[甲区, 乙区, 丙区], )如果有多张图都要用同一个顺序更优雅的做法是在 pandas 里先把列转成有序类别df[区域] pd.Categorical(df[区域], categories[甲区, 乙区, 丙区], orderedTrue)这样后续所有图都会自动沿用这个顺序。5.2 图例重复、图例位置挡住数据用hue分组之后Seaborn 一般会自动生成图例。但当你手动再调一次坐标轴或者对 FacetGrid 做二次操作时图例可能重复出现。我的排查顺序是先确认有没有在plt.show()之前多次调用ax.legend()再检查是否用了多个 axes。如果使用catplot返回的是FacetGrid不是单个 Axes这时图例要用g.legend调整而不是plt.legend。g sns.catplot(datadf, x品类, y销售额, hue区域, kindbox) g.add_legend(title区域, bbox_to_anchor(1.05, 0.5))5.3 坐标轴数值突然变成科学计数法当数值范围偏大时Matplotlib 会自动把刻度改成1e6这种形式。看起来专业但业务同事经常看不懂。解决方法是先用plt.gca()拿到坐标轴再用ticker模块重新设置格式化器import matplotlib.ticker as ticker ax plt.gca() ax.xaxis.set_major_formatter(ticker.FuncFormatter(lambda x, _: f{x:,.0f}))这样刻度就会显示成带千分位的普通数字。5.4 用错对象返回 Axes 还是 FacetGridcatplot、relplot、pairplot这类函数返回的是FacetGridlineplot、boxplot这类较底层的函数返回的是Axes。两者能调用的方法不一样。比如lineplot生成后想改标题直接ax.set_title()没问题。但catplot生成后g.set_title()会报错正确的做法是g.fig.suptitle()或g.set_titles()。我现在的自查方式是打印一下返回对象的类型g sns.catplot(...) print(type(g))看到类型再决定下一步操作能省下很多试错时间。5.5 默认聚合和你想的“堆叠”不是一回事barplot的默认行为是画均值而不是总和很多人第一次看会以为数据算错了。如果你确实想画总和必须显式改estimatorsns.barplot( datadf, x品类, y销售额, hue区域, estimatorsum, errorbarNone, )errorbarNone会去掉误差线。需求是“看比例”还是“看均值”“看总和”要先想清楚再选图类型否则图出来之后才发现口径错了返工成本很高。6. 一个完整示例从销售明细到三张可直接汇报的图6.1 数据结构与目标假设我们拿到一张模拟订单表字段包括日期、区域、品类、销量、单价、销售额。目标是画三张图分别回答三个业务问题月度销售趋势、各品类在各区域的销售额分布、销售额与单价的关系。先做基础的数据读取和预处理import pandas as pd import seaborn as sns import matplotlib.pyplot as plt df pd.read_csv(sales_data.csv, parse_dates[日期]) df[月份] df[日期].dt.to_period(M).astype(str)这一步把日期字段规整成月份字符串后面画折线图就不用担心时间刻度的问题。6.2 三张图的实现第一张月度趋势折线图。传原始明细数据让 Seaborn 自动计算每月的均值sns.set_theme(stylewhitegrid, palettemuted) plt.figure(figsize(10, 5)) sns.lineplot(datadf, x月份, y销售额, hue区域, markero) plt.title(各区域月度销售额趋势) plt.xticks(rotation45) plt.tight_layout() plt.savefig(monthly_trend.png, dpi300, bbox_inchestight) plt.show()第二张分类对比箱线图。这里不关心时间变化只看品类和区域两个维度对销售额的影响sns.catplot( datadf, x品类, y销售额, hue区域, kindbox, height5, aspect1.3, palettecustom_palette, ) plt.savefig(category_box.png, dpi300, bbox_inchestight) plt.show()第三张销售额与单价的关系按区域分色sns.relplot( datadf, x单价, y销售额, hue区域, style区域, kindscatter, alpha0.7, height5, aspect1.2, ) plt.savefig(price_sales.png, dpi300, bbox_inchestight) plt.show()6.3 出图后的检查清单图导出来不等于工作做完。我会按三件事过一遍标题是否写清楚、坐标轴单位是否明确、图例是否完整。标题和坐标轴标签我一般直接在代码里补上因为出图后再用工具改字字体和位置很难对齐。如果图里有多类信息我还会额外加一句注释在图上比如用plt.annotate标出最值得关注的峰值或异常点。这种小批注在汇报时很管用能引导观众先看你想强调的地方。提示一套图里的中文字体也要统一。我在脚本开头会同时设置font.sans-serif和axes.unicode_minus否则中文容易变成方框负号也可能显示异常。plt.rcParams[font.sans-serif] [SimHei, Arial] plt.rcParams[axes.unicode_minus] False最后提一个我在实际项目中养成的习惯凡是能用 Seaborn 默认统计逻辑解决的绝不写手动聚合代码。这样图的性质更稳定、代码更短、视觉也更统一。画图这件事做到后来拼的不是 API 记多少而是数据准备和审美的稳定输出。现在每回拿到一份新数据我都会先问自己“这是宽表还是长表”“我要回答的是分布、关系还是比较”想清楚这两件事再打开编辑器效率比一开始就敲代码高得多。
返回列表