【问题标题】:GroupBy pandas DataFrame and select most common valueGroupBy pandas DataFrame 并选择最常见的值
【发布时间】:2013-02-19 19:00:53
【问题描述】:

我有一个包含三个字符串列的数据框。我知道第三列中唯一的一个值对于前两个的每个组合都是有效的。要清理数据,我必须按数据框按前两列分组,并为每个组合选择第三列的最常见值。

我的代码:

import pandas as pd
from scipy import stats

source = pd.DataFrame({'Country' : ['USA', 'USA', 'Russia','USA'], 
                  'City' : ['New-York', 'New-York', 'Sankt-Petersburg', 'New-York'],
                  'Short name' : ['NY','New','Spb','NY']})

print source.groupby(['Country','City']).agg(lambda x: stats.mode(x['Short name'])[0])

最后一行代码不起作用,它显示“Key error 'Short name'”,如果我尝试仅按城市分组,则会收到 AssertionError。我能做些什么来解决它?

【问题讨论】:

    标签: python pandas group-by pandas-groupby mode


    【解决方案1】:

    您可以使用value_counts() 获取计数系列,并获取第一行:

    import pandas as pd
    
    source = pd.DataFrame({'Country' : ['USA', 'USA', 'Russia','USA'], 
                      'City' : ['New-York', 'New-York', 'Sankt-Petersburg', 'New-York'],
                      'Short name' : ['NY','New','Spb','NY']})
    
    source.groupby(['Country','City']).agg(lambda x:x.value_counts().index[0])
    

    如果您想知道在 .agg() 中执行其他 agg 函数 试试这个。

    # Let's add a new col,  account
    source['account'] = [1,2,3,3]
    
    source.groupby(['Country','City']).agg(mod  = ('Short name', \
                                            lambda x: x.value_counts().index[0]),
                                            avg = ('account', 'mean') \
                                          )
    

    【讨论】:

    • 我发现 stats.mode 可以在字符串变量的情况下显示不正确的答案。这种方式看起来更可靠。
    • 不应该是.value_counts(ascending=False)吗?
    • @Private: ascending=False 已经是默认值,所以不需要明确设置顺序。
    • 正如 Jacquot 所说,pd.Series.mode 现在更合适也更快了。
    • 我遇到了一个名为IndexError: index 0 is out of bounds for axis 0 with size 0的错误,如何解决?
    【解决方案2】:

    熊猫 >= 0.16

    pd.Series.mode 可用!

    使用groupbyGroupBy.agg,并将pd.Series.mode函数应用于每个组:

    source.groupby(['Country','City'])['Short name'].agg(pd.Series.mode)
    
    Country  City            
    Russia   Sankt-Petersburg    Spb
    USA      New-York             NY
    Name: Short name, dtype: object
    

    如果需要将其作为 DataFrame,请使用

    source.groupby(['Country','City'])['Short name'].agg(pd.Series.mode).to_frame()
    
                             Short name
    Country City                       
    Russia  Sankt-Petersburg        Spb
    USA     New-York                 NY
    

    Series.mode 的有用之处在于它始终返回一个系列,使其与aggapply 非常兼容,尤其是在重构 groupby 输出时。它也更快。

    # Accepted answer.
    %timeit source.groupby(['Country','City']).agg(lambda x:x.value_counts().index[0])
    # Proposed in this post.
    %timeit source.groupby(['Country','City'])['Short name'].agg(pd.Series.mode)
    
    5.56 ms ± 343 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
    2.76 ms ± 387 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
    

    处理多种模式

    Series.mode 在有多种模式时也能很好地工作:

    source2 = source.append(
        pd.Series({'Country': 'USA', 'City': 'New-York', 'Short name': 'New'}),
        ignore_index=True)
    
    # Now `source2` has two modes for the 
    # ("USA", "New-York") group, they are "NY" and "New".
    source2
    
      Country              City Short name
    0     USA          New-York         NY
    1     USA          New-York        New
    2  Russia  Sankt-Petersburg        Spb
    3     USA          New-York         NY
    4     USA          New-York        New
    

    source2.groupby(['Country','City'])['Short name'].agg(pd.Series.mode)
    
    Country  City            
    Russia   Sankt-Petersburg          Spb
    USA      New-York            [NY, New]
    Name: Short name, dtype: object
    

    或者,如果您想为每种模式单独设置一行,您可以使用GroupBy.apply

    source2.groupby(['Country','City'])['Short name'].apply(pd.Series.mode)
    
    Country  City               
    Russia   Sankt-Petersburg  0    Spb
    USA      New-York          0     NY
                               1    New
    Name: Short name, dtype: object
    

    如果您不关心返回哪种模式,只要它是其中之一,那么您将需要一个调用 mode 并提取第一个结果的 lambda。

    source2.groupby(['Country','City'])['Short name'].agg(
        lambda x: pd.Series.mode(x)[0])
    
    Country  City            
    Russia   Sankt-Petersburg    Spb
    USA      New-York             NY
    Name: Short name, dtype: object
    

    替代(不)考虑

    你也可以在 python 中使用statistics.mode,但是...

    source.groupby(['Country','City'])['Short name'].apply(statistics.mode)
    
    Country  City            
    Russia   Sankt-Petersburg    Spb
    USA      New-York             NY
    Name: Short name, dtype: object
    

    ...在处理多种模式时效果不佳;提出了StatisticsError。文档中提到了这一点:

    如果数据为空,或者不存在最常见的值, 引发了 StatisticsError。

    但你可以自己看看……

    statistics.mode([1, 2])
    # ---------------------------------------------------------------------------
    # StatisticsError                           Traceback (most recent call last)
    # ...
    # StatisticsError: no unique mode; found 2 equally common values
    

    【讨论】:

    • @JoshFriedlander df.groupby(cols).agg(pd.Series.mode)似乎对我有用。如果这不起作用,我的第二个猜测是df.groupby(cols).agg(lambda x: pd.Series.mode(x).values[0])
    • 谢谢(一如既往!)您的第二个选项对我来说有所改善,但我得到了IndexError: index 0 is out of bounds for axis 0 with size 0(可能是因为有些组中的系列只有 NaN)。添加dropna=False 解决了this,但似乎提高了'<' not supported between instances of 'float' and 'str'(我的系列是字符串)。 (如果您愿意,很高兴将其变成一个新问题。)
    • @JoshFriedlander 定义def foo(x): m = pd.Series.mode(x); return m.values[0] if not m.empty else np.nan,然后使用df.groupby(cols).agg(foo)。如果这不起作用,请稍微调整一下foo 的实现。如果您仍然遇到问题,我建议您打开一个新 Q。
    • 我应该补充一点,如果你想包括计数np.nan,可以通过df.groupy(cols).agg(lambda x: x.mode(dropna=False).iloc[0]) 来实现模式,假设你不关心领带,只想要一种模式。跨度>
    • 如果您收到ValueError: Must produce aggregated value,请尝试使用apply 而不是agg(然后您可能需要droplevel(1) 删除您获得的额外索引列。
    【解决方案3】:

    对于agg,lamba 函数得到一个Series,它没有'Short name' 属性。

    stats.mode 返回两个数组的元组,所以你必须在这个元组中取第一个数组的第一个元素。

    有了这两个简单的改变:

    source.groupby(['Country','City']).agg(lambda x: stats.mode(x)[0][0])
    

    返回

                             Short name
    Country City                       
    Russia  Sankt-Petersburg        Spb
    USA     New-York                 NY
    

    【讨论】:

    • @ViacheslavNefedov - 是的,但是采用@HYRY 的解决方案,它使用纯熊猫。不需要scipy.stats
    【解决方案4】:

    在这里玩游戏有点晚了,但我遇到了 HYRY 解决方案的一些性能问题,所以我不得不想出另一个解决方案。

    它的工作原理是找出每个键值对的频率,然后,对于每个键,只保留最常出现的值。

    还有一个支持多种模式的附加解决方案。

    在代表我正在使用的数据的规模测试中,这将运行时间从 37.4 秒减少到了 0.5 秒!

    这是解决方案的代码、一些示例用法和规模测试:

    import numpy as np
    import pandas as pd
    import random
    import time
    
    test_input = pd.DataFrame(columns=[ 'key',          'value'],
                              data=  [[ 1,              'A'    ],
                                      [ 1,              'B'    ],
                                      [ 1,              'B'    ],
                                      [ 1,              np.nan ],
                                      [ 2,              np.nan ],
                                      [ 3,              'C'    ],
                                      [ 3,              'C'    ],
                                      [ 3,              'D'    ],
                                      [ 3,              'D'    ]])
    
    def mode(df, key_cols, value_col, count_col):
        '''                                                                                                                                                                                                                                                                                                                                                              
        Pandas does not provide a `mode` aggregation function                                                                                                                                                                                                                                                                                                            
        for its `GroupBy` objects. This function is meant to fill                                                                                                                                                                                                                                                                                                        
        that gap, though the semantics are not exactly the same.                                                                                                                                                                                                                                                                                                         
    
        The input is a DataFrame with the columns `key_cols`                                                                                                                                                                                                                                                                                                             
        that you would like to group on, and the column                                                                                                                                                                                                                                                                                                                  
        `value_col` for which you would like to obtain the mode.                                                                                                                                                                                                                                                                                                         
    
        The output is a DataFrame with a record per group that has at least one mode                                                                                                                                                                                                                                                                                     
        (null values are not counted). The `key_cols` are included as columns, `value_col`                                                                                                                                                                                                                                                                               
        contains a mode (ties are broken arbitrarily and deterministically) for each                                                                                                                                                                                                                                                                                     
        group, and `count_col` indicates how many times each mode appeared in its group.                                                                                                                                                                                                                                                                                 
        '''
        return df.groupby(key_cols + [value_col]).size() \
                 .to_frame(count_col).reset_index() \
                 .sort_values(count_col, ascending=False) \
                 .drop_duplicates(subset=key_cols)
    
    def modes(df, key_cols, value_col, count_col):
        '''                                                                                                                                                                                                                                                                                                                                                              
        Pandas does not provide a `mode` aggregation function                                                                                                                                                                                                                                                                                                            
        for its `GroupBy` objects. This function is meant to fill                                                                                                                                                                                                                                                                                                        
        that gap, though the semantics are not exactly the same.                                                                                                                                                                                                                                                                                                         
    
        The input is a DataFrame with the columns `key_cols`                                                                                                                                                                                                                                                                                                             
        that you would like to group on, and the column                                                                                                                                                                                                                                                                                                                  
        `value_col` for which you would like to obtain the modes.                                                                                                                                                                                                                                                                                                        
    
        The output is a DataFrame with a record per group that has at least                                                                                                                                                                                                                                                                                              
        one mode (null values are not counted). The `key_cols` are included as                                                                                                                                                                                                                                                                                           
        columns, `value_col` contains lists indicating the modes for each group,                                                                                                                                                                                                                                                                                         
        and `count_col` indicates how many times each mode appeared in its group.                                                                                                                                                                                                                                                                                        
        '''
        return df.groupby(key_cols + [value_col]).size() \
                 .to_frame(count_col).reset_index() \
                 .groupby(key_cols + [count_col])[value_col].unique() \
                 .to_frame().reset_index() \
                 .sort_values(count_col, ascending=False) \
                 .drop_duplicates(subset=key_cols)
    
    print test_input
    print mode(test_input, ['key'], 'value', 'count')
    print modes(test_input, ['key'], 'value', 'count')
    
    scale_test_data = [[random.randint(1, 100000),
                        str(random.randint(123456789001, 123456789100))] for i in range(1000000)]
    scale_test_input = pd.DataFrame(columns=['key', 'value'],
                                    data=scale_test_data)
    
    start = time.time()
    mode(scale_test_input, ['key'], 'value', 'count')
    print time.time() - start
    
    start = time.time()
    modes(scale_test_input, ['key'], 'value', 'count')
    print time.time() - start
    
    start = time.time()
    scale_test_input.groupby(['key']).agg(lambda x: x.value_counts().index[0])
    print time.time() - start
    

    运行此代码将打印如下内容:

       key value
    0    1     A
    1    1     B
    2    1     B
    3    1   NaN
    4    2   NaN
    5    3     C
    6    3     C
    7    3     D
    8    3     D
       key value  count
    1    1     B      2
    2    3     C      2
       key  count   value
    1    1      2     [B]
    2    3      2  [C, D]
    0.489614009857
    9.19386196136
    37.4375009537
    

    希望这会有所帮助!

    【讨论】:

    • 这是我最快的方法.. 谢谢!
    • 有没有办法使用这种方法,但直接在 agg 参数内?例如。 agg({'f1':mode,'f2':np.sum})
    • @PabloA 很遗憾不是,因为界面不太一样。我建议将此作为一个单独的操作,然后将您的结果加入其中。当然,如果性能不是问题,您可以使用 HYRY 的解决方案来使您的代码更简洁。
    • @abw333 我使用了HYRY的解决方案,但是遇到了性能问题...希望pandas开发团队支持agg方法中的更多功能。
    • 绝对是大型 DataFrame 的必经之路。我有 8300 万行和 250 万个唯一组。每列耗时 28 秒,而 agg 每列耗时超过 11 分钟。
    【解决方案5】:

    这里的两个最佳答案建议:

    df.groupby(cols).agg(lambda x:x.value_counts().index[0])
    

    或者,最好

    df.groupby(cols).agg(pd.Series.mode)
    

    然而,这两种情况在简单的边缘情况下都失败了,如下所示:

    df = pd.DataFrame({
        'client_id':['A', 'A', 'A', 'A', 'B', 'B', 'B', 'C'],
        'date':['2019-01-01', '2019-01-01', '2019-01-01', '2019-01-01', '2019-01-01', '2019-01-01', '2019-01-01', '2019-01-01'],
        'location':['NY', 'NY', 'LA', 'LA', 'DC', 'DC', 'LA', np.NaN]
    })
    

    第一个:

    df.groupby(['client_id', 'date']).agg(lambda x:x.value_counts().index[0])
    

    产生IndexError(因为C 组返回的空系列)。第二个:

    df.groupby(['client_id', 'date']).agg(pd.Series.mode)
    

    返回ValueError: Function does not reduce,因为第一组返回两个列表(因为有两种模式)。 (如文档中的here 所述,如果第一组返回单一模式,这将起作用!)

    这种情况的两种可能的解决方案是:

    import scipy
    x.groupby(['client_id', 'date']).agg(lambda x: scipy.stats.mode(x)[0])
    

    还有cs95在cmetshere给我的解决方案:

    def foo(x): 
        m = pd.Series.mode(x); 
        return m.values[0] if not m.empty else np.nan
    df.groupby(['client_id', 'date']).agg(foo)
    

    但是,所有这些都很慢,不适合大型数据集。我最终使用的解决方案a)可以处理这些情况,b)要快得多,是 abw33 答案的轻微修改版本(应该更高):

    def get_mode_per_column(dataframe, group_cols, col):
        return (dataframe.fillna(-1)  # NaN placeholder to keep group 
                .groupby(group_cols + [col])
                .size()
                .to_frame('count')
                .reset_index()
                .sort_values('count', ascending=False)
                .drop_duplicates(subset=group_cols)
                .drop(columns=['count'])
                .sort_values(group_cols)
                .replace(-1, np.NaN))  # restore NaNs
    
    group_cols = ['client_id', 'date']    
    non_grp_cols = list(set(df).difference(group_cols))
    output_df = get_mode_per_column(df, group_cols, non_grp_cols[0]).set_index(group_cols)
    for col in non_grp_cols[1:]:
        output_df[col] = get_mode_per_column(df, group_cols, col)[col].values
    

    本质上,该方法一次处理一个列并输出一个 df,因此您将第一个视为 df,而不是密集的 concat,然后迭代地添加输出数组 (values.flatten())作为 df 中的一列。

    【讨论】:

    • 如果一个组中空值的数量多于有值的数量怎么办。我有这样一种情况,我想使用 None 以外的下一个频繁数据作为组的值。有可能吗?
    • nth 是可能的。但您应该将此作为新问题发布
    【解决方案6】:

    形式上,正确答案是@eumiro 解决方案。 @HYRY 解决方案的问题是,当您有一个像 [1,2,3,4] 这样的数字序列时,解决方案是错误的,即。例如,您没有模式。 示例:

    >>> import pandas as pd
    >>> df = pd.DataFrame(
            {
                'client': ['A', 'B', 'A', 'B', 'B', 'C', 'A', 'D', 'D', 'E', 'E', 'E', 'E', 'E', 'A'], 
                'total': [1, 4, 3, 2, 4, 1, 2, 3, 5, 1, 2, 2, 2, 3, 4], 
                'bla': [10, 40, 30, 20, 40, 10, 20, 30, 50, 10, 20, 20, 20, 30, 40]
            }
        )
    

    如果你像@HYRY 一样计算,你会得到:

    >>> print(df.groupby(['client']).agg(lambda x: x.value_counts().index[0]))
            total  bla
    client            
    A           4   30
    B           4   40
    C           1   10
    D           3   30
    E           2   20
    

    这显然是错误的(请参阅应该是 1 而不是 4A 值)因为它无法处理唯一值.

    因此,另一种解决方案是正确的:

    >>> import scipy.stats
    >>> print(df.groupby(['client']).agg(lambda x: scipy.stats.mode(x)[0][0]))
            total  bla
    client            
    A           1   10
    B           4   40
    C           1   10
    D           3   30
    E           2   20
    

    【讨论】:

      【解决方案7】:

      如果您不想包含 NaN 值,使用 Counterpd.Series.modepd.Series.value_counts()[0] 快得多:

      def get_most_common(srs):
          x = list(srs)
          my_counter = Counter(x)
          return my_counter.most_common(1)[0][0]
      
      df.groupby(col).agg(get_most_common)
      

      应该可以。 当您有 NaN 值时,这将失败,因为每个 NaN 将被单独计算。

      【讨论】:

        【解决方案8】:

        如果您想要另一种不依赖于value_countsscipy.stats 的解决方法,您可以使用Counter 集合

        from collections import Counter
        get_most_common = lambda values: max(Counter(values).items(), key = lambda x: x[1])[0]
        

        这可以像这样应用到上面的例子中

        src = pd.DataFrame({'Country' : ['USA', 'USA', 'Russia','USA'], 
                      'City' : ['New-York', 'New-York', 'Sankt-Petersburg', 'New-York'],
                      'Short_name' : ['NY','New','Spb','NY']})
        
        src.groupby(['Country','City']).agg(get_most_common)
        

        【讨论】:

        • 这比pd.Series.modepd.Series.value_counts().iloc[0] 快——但如果你有想要计算的NaN 值,这将失败。每个 NaN 的出现都将被视为与其他 NaN 不同,因此每个 NaN 都被计为计数 1。见stackoverflow.com/questions/61102111/…
        【解决方案9】:

        问题here 是性能问题,如果你有很多行就会有问题。

        如果是你的情况,请试试这个:

        import pandas as pd
        
        source = pd.DataFrame({'Country' : ['USA', 'USA', 'Russia','USA'], 
                      'City' : ['New-York', 'New-York', 'Sankt-Petersburg', 'New-York'],
                      'Short_name' : ['NY','New','Spb','NY']})
        
        source.groupby(['Country','City']).agg(lambda x:x.value_counts().index[0])
        
        source.groupby(['Country','City']).Short_name.value_counts().groupby['Country','City']).first()
        

        【讨论】:

          【解决方案10】:

          对于较大的数据集,一种稍微笨拙但速度更快的方法包括获取感兴趣列的计数,将计数从高到低排序,然后对子集进行重复数据删除以仅保留最大的案例。代码示例如下:

          >>> import pandas as pd
          >>> source = pd.DataFrame(
                  {
                      'Country': ['USA', 'USA', 'Russia', 'USA'], 
                      'City': ['New-York', 'New-York', 'Sankt-Petersburg', 'New-York'],
                      'Short name': ['NY', 'New', 'Spb', 'NY']
                  }
              )
          >>> grouped_df = source\
                  .groupby(['Country','City','Short name'])[['Short name']]\
                  .count()\
                  .rename(columns={'Short name':'count'})\
                  .reset_index()\
                  .sort_values('count', ascending=False)\
                  .drop_duplicates(subset=['Country', 'City'])\
                  .drop('count', axis=1)
          >>> print(grouped_df)
            Country              City Short name
          1     USA          New-York         NY
          0  Russia  Sankt-Petersburg        Spb
          

          【讨论】:

            猜你喜欢
            • 2021-12-22
            • 2016-07-16
            • 1970-01-01
            • 2020-10-18
            • 2018-11-08
            • 1970-01-01
            • 2019-06-25
            相关资源
            最近更新 更多