Pandas按分组聚合并选择特定列中最小值的行

65

我有一个DataFrame,包含A、B和C三列。对于每个A的值,我想选择B列中最小值所在的行。

也就是说,从这个数据框中:

df = pd.DataFrame({'A': [1, 1, 1, 2, 2, 2],
                   'B': [4, 5, 2, 7, 4, 6],
                   'C': [3, 4, 10, 2, 4, 6]})      
    A   B   C
0   1   4   3
1   1   5   4
2   1   2   10
3   2   7   2
4   2   4   4
5   2   6   6  

我想得到:

    A   B   C
0   1   2   10
1   2   4   4

目前我正在按列A分组,然后创建一个值,该值指示我将保留哪些行:

a = data.groupby('A').min()
a['A'] = a.index
to_keep = [str(x[0]) + str(x[1]) for x in a[['A', 'B']].values]
data['id'] = data['A'].astype(str) + data['B'].astype('str')
data[data['id'].isin(to_keep)]

我相信有一种更加直接的方法来做这件事。 我在这里看到了许多使用MultiIndex的答案,但我希望避免使用它。

谢谢您的帮助。

7个回答

84

我觉得你在过度思考。只需使用groupbyidxmin

df.loc[df.groupby('A').B.idxmin()]

   A  B   C
2  1  2  10
4  2  4   4

df.loc[df.groupby('A').B.idxmin()].reset_index(drop=True)

   A  B   C
0  1  2  10
1  2  4   4

我正在尝试这个解决方案,但是在使用pandas 1.0.0时,我遇到了错误: 不再支持将缺失标签的列表传递给.loc或[]。@cs95你有什么建议如何修复它吗? - Eve Edomenko
4
这将导致每个A值对应一行结果,如果在每个A值中有多个具有最小值的行,则会发生什么情况。例如,所有科学成绩最低的学生。 - A-dude

14

我曾遇到一个类似的情况,但列标题更加复杂(例如,“B val”),这种情况下需要执行以下操作:

df.loc[df.groupby('A')['B val'].idxmin()]

6

5

被接受的答案(建议使用idxmin)不能与管道模式一起使用。一个适合管道模式的替代方案是先对值进行排序,然后使用groupbyDataFrame.head

data.sort_values('B').groupby('A').apply(DataFrame.head, n=1)

这是可能的,因为默认情况下 groupby 保留每个组内行的顺序,这是稳定和记录的行为(请参见pandas.DataFrame.groupby)。
此方法还具有其他好处:
  • it can be easily expanded to select n rows with smallest values in specific column
  • it can break ties by providing another column (as a list) to .sort_values(), e.g.:
    data.sort_values(['final_score', 'midterm_score']).groupby('year').apply(DataFrame.head, n=1)
    
与其他答案一样,为了精确匹配问题中所需的结果,需要使用.reset_index(drop=True),使最终片段变为:

df.sort_values('B').groupby('A').apply(DataFrame.head, n=1).reset_index(drop=True)

2
不错的答案。我想补充说,我是这样做的,似乎也可以达到同样的效果:data.sort_values('B').groupby('A').head(1) - igorkf

3
我找到了一个答案,虽然有点啰嗦,但是效率更高:

这是示例数据集:

data = pd.DataFrame({'A': [1,1,1,2,2,2], 'B':[4,5,2,7,4,6], 'C':[3,4,10,2,4,6]})
data

Out:
   A  B   C
0  1  4   3
1  1  5   4
2  1  2  10
3  2  7   2
4  2  4   4
5  2  6   6 

首先,我们将从groupby操作中获取Series的最小值:

min_value = data.groupby('A').B.min()
min_value

Out:
A
1    2
2    4
Name: B, dtype: int64

然后,我们将这个序列结果合并到原始数据框中。
data = data.merge(min_value, on='A',suffixes=('', '_min'))
data

Out:
   A  B   C  B_min
0  1  4   3      2
1  1  5   4      2
2  1  2  10      2
3  2  7   2      4
4  2  4   4      4
5  2  6   6      4

最后,我们只获取B等于B_min的行,并且删除B_min因为我们不再需要它。
data = data[data.B==data.B_min].drop('B_min', axis=1)
data

Out:
   A  B   C
2  1  2  10
4  2  4   4

我已经在非常大的数据集上进行了测试,这是我能够在合理的时间内使其工作的唯一方法。


非常好的解决方案,易于理解。 - Niccola Tartaglia

1
解决方案如前所述;
df.loc[df.groupby('A')['B'].idxmin()]

如果您遇到错误,那么解决方案如下:

"Passing list-likes to .loc or [] with any missing labels is no longer supported.
The following labels were missing: Float64Index([nan], dtype='float64').
See https://pandas.pydata.org/pandas-docs/stable/user_guide/indexing.html#deprecate-loc-reindex-listlike"

在我的情况下,列B中有'NaN'值。因此,我使用了 'dropna()' 然后它就起作用了。

df.loc[df.groupby('A')['B'].idxmin().dropna()]

1
您还可以使用布尔索引来选择 B 列为最小值的行。
out = df[df['B'] == df.groupby('A')['B'].transform('min')]

print(out)

   A  B   C
2  1  2  10
4  2  4   4

谢谢,这就是我正在寻找的。 - Mikhail Genkin

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