Seaborn统计可视化:分布图与关系图
Matplotlib 能把图画的每个元素精确摆到指定位置,代价是画一张「按物种分三组、每组一个颜色的箱线图」要写十几行。seaborn 把这些统计图形的常见需求封装成了函数,参数直接接受 DataFrame 的列名。
它的定位不是替代 Matplotlib,而是建立在 Matplotlib 之上的一层:画出来的图仍然是 Matplotlib 对象,可以继续用 ax.set_title() 之类的接口修改。
数据得先是长表
Section titled “数据得先是长表”seaborn 的函数签名几乎都是 data=DataFrame 加上若干列名:
import seaborn as snsfrom sklearn.datasets import load_iris
iris = load_iris(as_frame=True)df = iris.frame.copy()df["species"] = iris.target_names[df["target"]] # 把 0/1/2 换成物种名
sns.scatterplot(data=df, x="petal length (cm)", y="petal width (cm)", hue="species")df 的形状:(150, 6)列:sepal length (cm)、sepal width (cm)、petal length (cm)、 petal width (cm)、target、species后文所有例子都基于这个 df。
对比 Matplotlib 的写法 ax.scatter(df["petal length (cm)"], df["petal width (cm)"]),区别不只是少打几个字:传列名意味着 seaborn 知道每个变量叫什么,于是轴标签、图例标题可以自动生成,hue 分组也不用自己写循环。
代价是数据必须整理成长表(long / tidy data):一行一个观测单位,一列一个变量。如果你的测量值分散在 day1、day2、day3 这样的多列里,seaborn 画不出「按天分组」的图——先用 pd.melt() 把它们摞成两列(一列变量名、一列数值),这一类整形操作见 /python/pandas/cleaning/。R 那边对应的是 tidyr 的 pivot_longer(),思路完全一致。
另外提醒一句:seaborn 文档里的例子大量使用 sns.load_dataset("tips"),这个函数会联网下载示例数据集,实验室的内网机器上会直接报连接错误。本文所有例子都用 sklearn 内置的 iris。
sns.set_theme(style="whitegrid", palette="deep", context="notebook")style影响背景和网格:whitegrid适合看数值大小,ticks更接近期刊风格,darkgrid是旧默认值。palette是配色方案,分类变量用deep、muted、colorblind,连续变量用viridis、rocket。context按输出场景缩放字号和线宽:paper、notebook、talk、poster。
set_theme() 的本质是改 Matplotlib 的 rcParams。这意味着它必须写在所有绘图代码之前,否则已经创建的图不受影响;也意味着后面再手动改 plt.rcParams 会覆盖它。想只改一项就用 sns.set_style("whitegrid")。
分布:histplot 与 kdeplot
Section titled “分布:histplot 与 kdeplot”ax = sns.histplot(data=df, x="petal length (cm)", bins=20)bins 的取值直接决定读者看到什么。取 5 太粗,分布形状被抹平;取 100 太细,看上去全是噪声。经验做法是先用 Freedman-Diaconis 规则试一下,再手工调到能看出结构为止。
ax = sns.histplot( data=df, x="petal length (cm)", hue="species", bins=20, multiple="stack", kde=True,)hue 让三个物种各画一组,multiple 决定重叠方式:layer 半透明叠加(默认)、stack 堆叠、dodge 并排、fill 归一化成比例。kde=True 在直方图上叠一条核密度估计(kernel density estimation,KDE)曲线。
KDE 可以单独画,sns.kdeplot(data=df, x="petal length (cm)", hue="species", fill=True)。它用一堆小波包(通常是高斯核)叠加来估计概率密度,bw_adjust 控制带宽:大于 1 更平滑,小于 1 更贴合数据。KDE 只在样本量足够时才可信——每组只有十几个观测时,曲线上的每一个小峰都可能是两个点造成的,画出来就是在展示不存在的精度。这种情况直接画直方图或者蜂群图(sns.swarmplot)。
print(df.groupby("species")["petal length (cm)"].mean().round(3))speciessetosa 1.462versicolor 4.260virginica 5.552Name: petal length (cm), dtype: float64三个物种的花瓣长度均值差了将近 3 倍,直方图上会是三个几乎不重叠的峰,这种分布不需要 KDE 也很好读。
关系图:scatterplot、regplot、lmplot
Section titled “关系图:scatterplot、regplot、lmplot”这三个函数画的东西很像,用途完全不同:
| 函数 | 画什么 | 返回对象 | 能否分面 |
|---|---|---|---|
scatterplot |
只有散点 | Axes | 否 |
regplot |
散点 + 回归线 + 置信带 | Axes | 否 |
lmplot |
散点 + 回归线,按列分面 | FacetGrid | 是 |
scatterplot 是纯粹的散点图,hue、size、style 三个参数可以同时按三个变量分组。regplot 在散点基础上拟合一条回归线,默认是线性最小二乘,配一条 95% 置信带。这条带子是用 bootstrap 重采样算出来的(n_boot 默认 1000 次),所以它反映的是拟合的不确定性,不是数据的离散程度;点少的时候带子会很宽,别把它读成「预测区间」。
需要按某个分类变量分面时用 lmplot,它接受 col=、row= 参数;regplot 没有这个能力,得用 plt.subplots() 自己拼。拟合形状可以通过 order=2 改成二次曲线,或者 lowess=True 用局部加权回归(适合关系明显非线性的探索阶段)。
完整例子:
import matplotlib.pyplot as pltimport numpy as npimport seaborn as snsfrom sklearn.datasets import load_iris
sns.set_theme(style="whitegrid")
iris = load_iris(as_frame=True)df = iris.frame.copy()df["species"] = iris.target_names[df["target"]]
fig, ax = plt.subplots(figsize=(6, 4.5))sns.scatterplot( data=df, x="petal length (cm)", y="petal width (cm)", hue="species", s=35, alpha=0.85, ax=ax,)sns.regplot( data=df, x="petal length (cm)", y="petal width (cm)", scatter=False, ci=95, color="grey", ax=ax,)ax.set_title("Petal width vs petal length")
slope, intercept = np.polyfit(df["petal length (cm)"], df["petal width (cm)"], 1)print(f"三个物种合并拟合:y = {slope:.3f}x {intercept:+.3f}")三个物种合并拟合:y = 0.416x -0.363这条线的斜率 0.416 是三个物种混在一起的结果:setosa 在左下角单独成团,把整体斜率拉低了。分开拟合的话,versicolor 和 virginica 各自的斜率都比它大——看到分组结构明显的数据,先分组再拟合,这是比调参数更重要的一步。
分类变量:箱线图与小提琴图
Section titled “分类变量:箱线图与小提琴图”ax = sns.boxplot(data=df, x="species", y="petal width (cm)", hue="species", legend=False)ax = sns.violinplot(data=df, x="species", y="petal width (cm)", inner="quart")箱线图(boxplot)的五要素要背下来才能正确解读:箱子上下边是上下四分位数(Q1、Q3),箱内横线是中位数,须(whisker)延伸到 1.5 倍四分位距(IQR = Q3 − Q1)以内的最远数据点,超出的点单独画成离群点。它默认不显示均值和样本量,一组 5 个点和一组 500 个点的箱线图长得一样宽。
小提琴图(violinplot)在箱线图外面套了一层 KDE,能看出双峰这类箱线图藏起来的结构。inner="quart" 在内部画四分位线,也可以用 inner="box" 或 inner=None。它的短板和 KDE 一样:样本量小的时候,小提琴的「腹部」形状基本是核函数画出来的,和数据没关系。每组样本少于 20 个时,宁可画 sns.stripplot(抖动散点)或 sns.swarmplot(不重叠散点),让读者直接看到有几个点。
新版 seaborn 对 hue 和 x 用同一个变量会给出冗余警告,加上 legend=False 就安静了——同一个信息没必要在轴标签和图例里各出现一次。
与 Matplotlib 混用
Section titled “与 Matplotlib 混用”规则只有一条:看函数返回什么。
axes-level 函数(histplot、scatterplot、regplot、boxplot、kdeplot 等)返回一个 Axes,后面可以照常接 Matplotlib 的调用:
ax = sns.boxplot(data=df, x="species", y="petal width (cm)", hue="species", legend=False)ax.axhline(1.0, color="red", linestyle=":", linewidth=1)ax.set_ylabel("Petal width (cm)")ax.set_title("Petal width by species")反过来,想把 seaborn 画在已经存在(或者已经用 Matplotlib 布置好)的坐标系上,传 ax=ax 参数即可,前面的完整例子里就是这么做的。
figure-level 函数(displot、lmplot、catplot、relplot、jointplot、pairplot)返回的是 FacetGrid 或 JointGrid,它们自己管理整个画布,不接受 ax= 参数。要调整就得用它们自己的接口:g.set_axis_labels("x", "y")、g.set_titles("{col_name}")、g.figure.suptitle("...")。混用两者的接口是最常见的报错来源,遇到 AttributeError: 'FacetGrid' object has no attribute 'set_title' 时,回头看看是不是用了 figure-level 函数。
ggplot2 里这些图形是 geom_point()、geom_smooth()、geom_boxplot() 一个个图层叠加的结果,seaborn 把它们各自封装成了一个函数,参数更多、写法更短。R 语言里每种几何对象的用法见 /r/visualization/geometries/,对照着看能理解「图层」和「函数」两种组织方式的取舍。
一张图上叠多个坐标系、多张图排成版式,是画图里另一类问题。/python/visualization/subplots/ 讲子图布局、共享坐标轴和双坐标轴。