当输入为 DataFrame 时,在 seaborn 中对箱线图进行分组

27

我打算在一个 pandas dataframe 中绘制多列数据,所有数据都按另一列进行分组,使用 seaborn.boxplot 内部的 groupby。这里有一个很好的答案,解决了类似于 matplotlib matplotlib: Group boxplots 的问题,但考虑到 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选项的东西:

Failed groupby

然而,如果我只对一列进行操作,那么可以通过另一个 SO 问题 Seaborn groupby pandas Series 实现,感谢该问题的帮助:

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

seaborn that does not fail

所以我想在一个图中获取所有列(所有列都以类似的比例显示)。

编辑:

上述SO问题已被编辑,现在包括了一个“不干净”的解决方案,但如果有人对此问题有更好的想法,那将是很好的。

5个回答

28

如其他回答所述,boxplot函数仅限于绘制单个“层”箱线图,并且groupby参数仅在输入为Series并且您有第二变量要用于将观察结果分成每个箱子时才起作用。

然而,您可以使用factorplot函数的kind="box"实现您希望的效果。但是,您首先需要将示例数据框转换为所谓的长格式或“整洁”格式,其中每列是一个变量,每行是一个观测值:

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

那么绘图就非常简单:

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

在此输入图片描述


13
自从0.6版本以来,sns.boxplot已经可以绘制嵌套箱线图。这篇帖子偶尔会收到点赞。 - mwaskom
1
这个 melt 真是疯狂和超级出乎意料的。 - seralouk

11
你可以直接使用sns.boxplot,一个基于坐标轴的函数,或者使用sns.catplot并设置kind='box',一个基于图形的函数。详细信息请参见图形级别 vs. 坐标轴级别函数sns.catplot具有colrow变量,用于根据不同的变量创建子图/分面。
默认的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


8
Seaborn的groupby函数需要Series而不是DataFrames,这就是为什么它无法工作的原因。
作为一种解决方法,您可以这样做:
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

希望这有所帮助。

谢谢,我接受下面的答案,因为它在一个图中给出了所有的情节。 - Arman

5
这并不比您链接的答案更好,但我认为在seaborn中实现这一点的方法是使用FacetGrid功能,因为groupby参数仅针对传递给boxplot函数的Series定义。
以下是一些代码 - pd.melt是必需的,因为(据我所知)facet映射只能将单独的列作为参数,因此需要将数据转换为“长”格式。
g = sns.FacetGrid(pd.melt(df, id_vars='b'), col='b')
g.map(sns.boxplot, 'value', 'variable')

faceted seaborn boxplot


如果你想要这种类型的图表,其实并不需要直接使用 FacetGrid,你可以在这里使用 factorplot 并带上 col=b。(这并没有错,只是比必要的工作更多而已)。 - mwaskom

1
这并没有为这个对话增加太多内容,但是我在处理这个问题上挣扎了比应该更长的时间(实际集群无法使用),所以我想将我的实现作为另一个示例添加进来。它有一个叠加的散点图(因为我的数据集非常麻烦),使用索引显示融合,并进行了一些美学调整。希望对某人有用。 output_graph 这是不使用列标题的版本(我看到一个不同的线程想知道如何使用索引来做到这一点):
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
如果要在melt中使用索引:
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

注意:我的大部分时间都花在调试melt函数上。我主要遇到了错误"*只有整数标量数组可以通过1D numpy索引数组转换为标量索引*"。我的输出需要将结果变量值表和聚类(DBSCAN)连接起来,我在concat方法中给聚类数组加了额外的方括号。因此,我有一列每个值都是一个不可见的List[int],而不是一个普通的int。这很狭窄,但也许能帮助某些人。

  1. 列表项

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接