pandas按列对NaN(缺失)值进行分组

284
我有一个DataFrame,其中的列中有许多缺失值,我希望按照这些列进行分组。
import pandas as pd
import numpy as np
df = pd.DataFrame({'a': ['1', '2', '3'], 'b': ['4', np.NaN, '6']})

In [4]: df.groupby('b').groups
Out[4]: {'4': [0], '6': [2]}

看到Pandas已经删除了具有NaN目标值的行。我想要包括这些行!有什么建议吗?

1
@PhillipCloud 我已经编辑了这个问题,只包括问题本身,实际上非常好,与Jeff的开放pandas增强相关。 - Andy Hayden
2
无法在组中包含(和传播)NaN非常令人恼火。引用R并不令人信服,因为这种行为与许多其他事物不一致。无论如何,虚拟的hack也很糟糕。但是,如果存在NaN,则组的大小(包括NaN)和计数(忽略NaN)将有所不同。dfgrouped = df.groupby(['b']).a.agg(['sum','size','count']) dfgrouped['sum'][dfgrouped['size']!=dfgrouped['count']] = None - user7969724
你能概括一下你具体想要实现什么吗?也就是说,我们看到了一个输出,但是“期望”的输出是什么? - c-a
14
在pandas 1.1中,您很快就可以在groupby()中指定dropna=False以获得所需的结果。更多信息 - cs95
1
请注意,截至本文撰写时,存在一个错误,使得在使用MultiIndex分组时dropna=False无法正常工作。在他们的Github上有一些提到这个问题的未解决问题,并且目前还没有太多修复的动力,不幸的是。 - totalhack
有关 @totalhack 提到的 MultiIndex 错误的详细信息:截至 pandas 1.5,仍未修复。最接近的问题是此问题,它被关闭为其他更模糊的问题的重复项,这些问题正在进行中但仍然未解决:https://github.com/pandas-dev/pandas/issues/36470 - Neil Traft
7个回答

343

pandas >= 1.1

从pandas 1.1版本开始,您可以更好地控制此行为,使用dropna=False,在分组器中现在允许NA值

pd.__version__
# '1.1.0.dev0+2004.g8d10bfb6f'

# Example from the docs
df

   a    b  c
0  1  2.0  3
1  1  NaN  4
2  2  1.0  3
3  1  2.0  2

# without NA (the default)
df.groupby('b').sum()

     a  c
b        
1.0  2  3
2.0  2  5
# with NA
df.groupby('b', <b>dropna=False</b>).sum()

     a  c
b        
1.0  2  3
2.0  2  5
NaN  1  4

32
希望这个答案能够逐步上升到顶部。这是正确的方法。 - kdbanman
1
它对我不起作用。kroscek_jupyter_metabase = fromdb_1474_detail.groupby(groupby, dropna = False)[col_to_count].count() 返回 TypeError: groupby() got an unexpected keyword argument 'dropna' - Cignitor
@Cignitor 请运行 print(pd.version) 并告诉我它显示的内容。 - cs95
6
很遗憾,这种方法无法在多级索引分组中使用。到目前为止,我看到的最简单的解决方法是在分组之前替换NaN值,虽然不太美观。 - totalhack
我希望None能够在同一组中! - Ievgen
显示剩余2条评论

177
这是在文档的缺失数据部分提到的

GroupBy中的NA组会自动被排除。这种行为与R一致。

一个解决方法是在进行GroupBy之前使用占位符(例如-1):

In [11]: df.fillna(-1)
Out[11]: 
   a   b
0  1   4
1  2  -1
2  3   6

In [12]: df.fillna(-1).groupby('b').sum()
Out[12]: 
    a
b    
-1  2
4   1
6   3

话虽如此,这种感觉非常糟糕的hack...也许应该有一个选项来包括NaN在groupby中(请参见this github issue - 它使用相同的占位符hack)。

然而,正如另一个答案中所描述的那样,“从pandas 1.1开始,您可以更好地控制此行为,使用dropna=False允许在分组器中使用NA值。”


4
这是一个我之前想到的逻辑上可行但有些滑稽的解决方案,Pandas会将空字段转换为NaN,我们需要将它们改回来。 这就是我考虑寻找其他解决方案的原因,比如运行一个SQL服务器并从中查询表格(看起来有点太复杂),或者尝试使用其他库而不是Pandas,或者使用自己的库(我想摆脱它)。谢谢。 - Gyula Sámuel Karli
@GyulaSámuelKarli 对我来说,这似乎是一个小错误(请参见上面的错误报告),我的解决方案是一个解决方法。我觉得你把整个库都否定了很奇怪。 - Andy Hayden
1
我不想写Pandas,只是寻找最符合我的需求的工具。 - Gyula Sámuel Karli
1
请看下面我的回答,我相信我已经找到了一个相当不错的(更干净、可能更快)解决方案。 http://stackoverflow.com/a/43375020/408853 - c-a
4
不,这与R不一致。df %>% group_by 也会给出带有警告的NA摘要,可以通过通过fct_explicit_na传递分组列来避免,然后创建一个(缺失)级别。 - Ravaging Care
显示剩余9条评论

52

古老的话题,如果有人仍然遇到这个问题——另一个解决方法是在分组之前通过.astype(str)将其转换为字符串。这将保留 NaN 值。

df = pd.DataFrame({'a': ['1', '2', '3'], 'b': ['4', np.NaN, '6']})
df['b'] = df['b'].astype(str)
df.groupby(['b']).sum()
    a
b   
4   1
6   3
nan 2

@K3---rnc:请看您链接中的评论 - 您链接中的帖子作者做错了什么。 - Thomas
@Thomas,就像上面的例子一样。如果您能使示例更安全(并且更简单),请进行编辑。 - K3---rnc
3
这里的a的“和”指的是字符串连接,而不是数值相加。这种做法只能在'b'的条目各不相同的情况下“奏效”。你需要将'a'转换为数字,并将'b'转换为字符串。 - BallpointBen
1
请注意,列a是一个对象,在groupby之后得到的平均值可能不是您想要的东西!!!!! - Jason Goal

13

由于我没有足够的声望积分(只有41分,但需要超过50分才能发表评论),因此无法向M. Kiewisch添加评论。

无论如何,我只想指出M. Kiewisch的解决方案并不起作用,可能需要更多的调整。例如考虑

>>> df = pd.DataFrame({'a': [1, 2, 3, 5], 'b': [4, np.NaN, 6, 4]})
>>> df
   a    b
0  1  4.0
1  2  NaN
2  3  6.0
3  5  4.0
>>> df.groupby(['b']).sum()
     a
b
4.0  6
6.0  3
>>> df.astype(str).groupby(['b']).sum()
      a
b
4.0  15
6.0   3
nan   2

这表明当 b=4.0 时,相应的值为15而不是6。在这里,只是将1和5作为字符串连接起来,而不是将它们作为数字相加。


14
因为您将整个数据框转换为字符串,而不仅仅是b列。 - Korem
请注意,此问题现在已在提到的答案中得到修复。 - Shaido
1
在我看来,新的解决方案更好,但仍然不够安全。考虑一种情况,即'b'列中的一个条目与字符串化的np.NaN相同。然后这些东西被组合在一起。df = pd.DataFrame({'a': [1, 2, 3, 5, 6], 'b': ['foo', np.NaN, 'bar', 'foo', 'nan']});df['b'] = df['b'].astype(str);df.groupby(['b']).sum() - Kamaraju Kusumanchi

8
到目前为止提供的所有答案都存在潜在的危险行为,因为您很可能选择一个实际上是数据集一部分的虚拟值。随着您创建具有许多属性的组,这种情况变得越来越普遍。简而言之,这种方法并不总是很好地推广。
一种不太糊弄人的解决方法是使用pd.drop_duplicates()创建一个唯一的值组合索引,每个组合都有自己的ID,然后根据该ID进行分组。它更冗长,但确实可以完成工作:
def safe_groupby(df, group_cols, agg_dict):
    # set name of group col to unique value
    group_id = 'group_id'
    while group_id in df.columns:
        group_id += 'x'
    # get final order of columns
    agg_col_order = (group_cols + list(agg_dict.keys()))
    # create unique index of grouped values
    group_idx = df[group_cols].drop_duplicates()
    group_idx[group_id] = np.arange(group_idx.shape[0])
    # merge unique index on dataframe
    df = df.merge(group_idx, on=group_cols)
    # group dataframe on group id and aggregate values
    df_agg = df.groupby(group_id, as_index=True)\
               .agg(agg_dict)
    # merge grouped value index to results of aggregation
    df_agg = group_idx.set_index(group_id).join(df_agg)
    # rename index
    df_agg.index.name = None
    # return reordered columns
    return df_agg[agg_col_order]

请注意,现在您可以简单地执行以下操作:
data_block = [np.tile([None, 'A'], 3),
              np.repeat(['B', 'C'], 3),
              [1] * (2 * 3)]

col_names = ['col_a', 'col_b', 'value']

test_df = pd.DataFrame(data_block, index=col_names).T

grouped_df = safe_groupby(test_df, ['col_a', 'col_b'],
                          OrderedDict([('value', 'sum')]))

这将返回成功的结果,而不必担心覆盖被误认为是虚拟值的真实数据。

这是一般情况下最好的解决方案,但在我知道有无效字符串/数字可以替代的情况下,我可能会选择下面Andy Hayden的答案...希望pandas能尽快修复这个问题。 - Sarah Messer

6

对于 Andy Hayden 的解决方案,有一个小问题 - 它不再起作用了吗?因为 np.nan == np.nan 的结果是 False,所以 replace 函数实际上并没有起到任何作用。

对我而言,下面的方法可以解决这个问题:

df['b'] = df['b'].apply(lambda x: x if not np.isnan(x) else -1)

(至少在Pandas 0.19.2版本中是这样的行为。抱歉将其作为不同的答案添加,我没有足够的声望来进行评论。)

14
还有df['b'].fillna(-1) - K3---rnc

5

我已经回答过这个问题了,但由于某些原因,答案被转换为评论。尽管如此,这是最有效的解决方案:

不能在组中包含(和传播)NaNs非常令人恼火。引用R并不令人信服,因为这种行为与许多其他事物不一致。无论如何,虚拟操作也很糟糕。然而,如果存在NaN,则组的大小(包括NaN)和计数(忽略NaN)将不同。

dfgrouped = df.groupby(['b']).a.agg(['sum','size','count'])

dfgrouped['sum'][dfgrouped['size']!=dfgrouped['count']] = None

如果这些值不同,您可以将聚合函数的结果值设为 None。


1
这对我非常有帮助,但它回答的问题与原始问题略有不同。如果我理解正确,您的解决方案在求和中传播NaN,但“b”列中的NaN项仍会被删除为行。 - Andrew

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