在seaborn中对DataFrame输入的箱型图进行分组

27 投票
5 回答
60426 浏览
提问于 2025-04-18 17:08

我想在一个 pandas dataframe 中绘制多个列,并且希望这些列是按照另一列分组的,使用 groupby 这个功能在 seaborn.boxplot 中实现。这里有一个不错的回答,针对 matplotlib 中类似的问题 matplotlib: 分组箱线图,但因为 seaborn.boxplot 自带 groupby 选项,我觉得在 seaborn 中做这个应该会简单很多。

接下来是一个可复现的例子,但结果不如人意:

import seaborn as sns
import pandas as pd
df = pd.DataFrame([[2, 4, 5, 6, 1], [4, 5, 6, 7, 2], [5, 4, 5, 5, 1],
                   [10, 4, 7, 8, 2], [9, 3, 4, 6, 2], [3, 3, 4, 4, 1]],
                  columns=['a1', 'a2', 'a3', 'a4', 'b'])

# display(df)
   a1  a2  a3  a4  b
0   2   4   5   6  1
1   4   5   6   7  2
2   5   4   5   5  1
3  10   4   7   8  2
4   9   3   4   6  2
5   3   3   4   4  1

#Plotting by seaborn
sns.boxplot(df[['a1','a2', 'a3', 'a4']], groupby=df.b)

我得到的结果完全忽略了 groupby 选项:

失败的分组

而如果我只用一列来做,这个方法就能成功,得益于另一个 SO 问题 Seaborn 分组 pandas 系列

sns.boxplot(df.a1, groupby=df.b)

没有失败的seaborn

所以我希望能把所有的列都放在一个图里(所有列的尺度都差不多)。

编辑:

上面的 SO 问题已经被编辑,现在包含了一个“不是很干净”的解决方案,但如果有人有更好的主意,那就太好了。

5 个回答

1

虽然这段话对这个讨论没什么大帮助,但我在这方面纠结了很久(实际上这些聚类是没法用的),所以我想分享一下我的实现,作为另一个例子。这个例子里有一个叠加的散点图(因为我的数据集实在太麻烦了),展示了如何用索引来进行数据融化,还有一些美观的调整。希望这对某些人有用。

输出图

这是一个不使用列标题的版本(我看到有个不同的讨论想知道如何用索引来做这件事):

combined_array: ndarray = np.concatenate([dbscan_output.data, dbscan_output.labels.reshape(-1, 1)], axis=1)
cluster_data_df: DataFrame = DataFrame(combined_array)

if you want to use labelled columns:
column_names: List[str] = list(outcome_variable_names)
column_names.append('cluster')
cluster_data_df.set_axis(column_names, axis='columns', inplace=True)

graph_data: DataFrame = pd.melt(
    frame=cluster_data_df,
    id_vars=['cluster'],
    # value_vars is an optional param - by default it uses columns except the id vars, but I've included it as an example
    # value_vars=['outcome_var_1', 'outcome_var_2', 'outcome_var_3', 'outcome_var_4', 'outcome_var_5', 'outcome_var_6'] 
    var_name='psychometric_test',
    value_name='standard deviations from the mean'
)

生成的数据框(行数 = 样本数 x 变量数(在我的例子中是1626 x 6 = 9756)):

索引 聚类 心理测验 与均值的标准差
0 0.0 结果变量1 -1.276182
1 0.0 结果变量1 -1.118813
2 0.0 结果变量1 -1.276182
9754 0.0 结果变量6 0.892548
9755 0.0 结果变量6 1.420480

如果你想用索引进行数据融化:

graph_data: DataFrame = pd.melt(
    frame=cluster_data_df,
    id_vars=cluster_data_df.columns[-1],
    # value_vars=cluster_data_df.columns[:-1],
    var_name='psychometric_test',
    value_name='standard deviations from the mean'
)

这是绘图的代码: (使用了列标题 - 只需注意y轴=值名称,x轴=变量名称,色调=hue = id_vars):

# plot graph grouped by cluster
sns.set_theme(style="ticks")
fig = plt.figure(figsize=(10, 10))
fig.set(font_scale=1.2)
fig.set_style("white")

# create boxplot
fig.ax = sns.boxplot(y='standard deviations from the mean', x='psychometric_test', hue='cluster', showfliers=False,
                     data=graph_data)

# set box alpha:
for patch in fig.ax.artists:
    r, g, b, a = patch.get_facecolor()
    patch.set_facecolor((r, g, b, .2))

# create scatterplot
fig.ax = sns.stripplot(y='standard deviations from the mean', x='psychometric_test', hue='cluster', data=graph_data,
                       dodge=True, alpha=.25, zorder=1)

# customise legend:
cluster_n: int = dbscan_output.n_clusters
## create list with legend text
i = 0
cluster_info: Dict[int, int] = dbscan_output.cluster_sizes  # custom method
legend_labels: List[str] = []
while i < cluster_n:
    label: str = f"cluster {i+1}, n = {cluster_info[i]}"
    legend_labels.append(label)
    i += 1
if -1 in cluster_info.keys():
    cluster_n += 1
    label: str = f"Unclustered, n = {cluster_info[-1]}"
    legend_labels.insert(0, label)

## fetch existing handles and legends (each tuple will have 2*cluster number -> 1 for each boxplot cluster, 1 for each scatterplot cluster, so I will remove the first half)
handles, labels = fig.ax.get_legend_handles_labels()
index: int = int(cluster_n*(-1))
labels = legend_labels
plt.legend(handles[index:], labels[0:])
plt.xticks(rotation=45)
plt.show()

asds

顺便提一下:我花了很多时间在调试融化函数上。我主要遇到的错误是 "*只有整数标量数组可以转换为标量索引,使用1D numpy索引数组*" 。我的输出需要将结果变量值表和聚类(DBSCAN)进行拼接,而我在拼接方法中给聚类数组加了额外的方括号。结果是我有一列的每个值都是一个看不见的 List[int],而不是普通的 int。这可能比较小众,但也许能帮到某些人。

  1. 列表项
5

其实这个方法并没有比你链接的答案更好,但我觉得在seaborn中实现这个功能的方法是使用FacetGrid这个功能,因为groupby参数只适用于传给箱线图函数的Series。

这里有一段代码 - 使用pd.melt是必要的,因为(据我所知)facet映射只能接受单独的列作为参数,所以数据需要转换成“长格式”。

g = sns.FacetGrid(pd.melt(df, id_vars='b'), col='b')
g.map(sns.boxplot, 'value', 'variable')

faceted seaborn boxplot

8

Seaborn的groupby函数只能处理Series类型的数据,而不能处理DataFrame,所以它才没法正常工作。

作为解决方法,你可以这样做:

fig, ax = plt.subplots(1,2, sharey=True)
for i, grp in enumerate(df.filter(regex="a").groupby(by=df.b)):
    sns.boxplot(grp[1], ax=ax[i])

这样做会得到:sns

需要注意的是,df.filter(regex="a")的效果和df[['a1','a2', 'a3', 'a4']]是一样的。

   a1  a2  a3  a4
0   2   4   5   6
1   4   5   6   7
2   5   4   5   5
3  10   4   7   8
4   9   3   4   6
5   3   3   4   4

希望这对你有帮助。

28

正如其他回答所提到的,boxplot 函数只能绘制一层的箱线图,而 groupby 参数只有在输入是一个序列(Series)并且你有第二个变量想用来将观察值分组到每个箱子里时才会生效。

不过,你可以使用 factorplot 函数来实现你想要的效果,只需设置 kind="box"。但在此之前,你需要把样本数据框(dataframe)转换成一种叫做长格式(long-form)或“整洁格式”(tidy format)的样子,这样每一列都是一个变量,每一行都是一个观察值:

df_long = pd.melt(df, "b", var_name="a", value_name="c")

然后绘图就非常简单了:

sns.factorplot("a", hue="b", y="c", data=df_long, kind="box")

enter image description here

11

你可以直接使用 sns.boxplot 这个函数,或者用 sns.catplot 并设置 kind='box',这也是一个函数,只不过是针对整个图形的。想了解更多,可以查看 图形级别和轴级别的函数

sns.catplotcolrow 这两个变量,可以用来创建不同变量的子图。

默认的 palette(调色板)是根据传给 hue 的变量类型来决定的,可能是连续的(数字型)或者分类的。

正如 @mwaskom 所解释的,你需要把样本数据框用 melt 函数转换成“长格式”,这样每一列就是一个变量,每一行就是一个观察值。

python 3.12.0pandas 2.1.2matplotlib 3.8.1seaborn 0.13.0 中测试过

df_long = pd.melt(df, "b", var_name="a", value_name="c")

# display(df_long.head())
   b   a   c
0  1  a1   2
1  2  a1   4
2  1  a1   5
3  2  a1  10
4  2  a1   9

sns.boxplot

fig, ax = plt.subplots(figsize=(5, 5))
sns.boxplot(x="a", hue="b", y="c", data=df_long, ax=ax)
ax.spines[['top', 'right']].set_visible(False)
sns.move_legend(ax, bbox_to_anchor=(1, 0.5), loc='center left', frameon=False)

sns.catplot

用更少的代码行创建和 sns.boxplot 一样的图。

g = sns.catplot(kind='box', data=df_long, x='a', y='c', hue='b', height=5, aspect=1)

结果图

enter image description here

撰写回答