GroupBy pandasDataFrameと最も一般的な値を選択します


99

3つの文字列列を持つデータフレームがあります。最初の2つのすべての組み合わせに対して、3番目の列の1つの値だけが有効であることを私は知っています。データをクリーンアップするには、最初の2列でデータフレームごとにグループ化し、組み合わせごとに3番目の列の最も一般的な値を選択する必要があります。

私のコード:

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])

コードの最後の行が機能せず、「キーエラー '短い名前'」と表示され、都市のみでグループ化しようとすると、AssertionErrorが発生します。どうすれば修正できますか?

回答:


145

を使用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が誤った回答を表示する可能性があることがわかりました。この方法は、より信頼性が高く見えます。
Viacheslav Nefedov 2013年

1
これはすべきではありません.value_counts(ascending=False)か?
プライベート

1
@Private:ascending=Falseはすでにデフォルト値であるため、順序を明示的に設定する必要はありません。
Schmuddi 2018

2
ジャコットが言ったように、pd.Series.mode今はより適切でより速いです。
柴戸大輔2018年

このソリューションを複数の異なる集計関数で使用するにはどうすればよいですか。たとえば、「短い名前」のような複数の列と、合計関数で集計する数値列がある場合などです。
constiii

99

パンダ> = 0.16

pd.Series.mode 利用可能です!

、、を使用してgroupby、各グループに関数をGroupBy.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は、常にSeriesを返すため、特にgroupby出力を再構築するときに、aggとの互換性が非常に高くなることapplyです。また、より高速です。

# 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、最初の結果を呼び出して抽出するラムダが必要になります。

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

検討する(しない)代替案

statistics.modePythonからも使用できますが...

source.groupby(['Country','City'])['Short name'].apply(statistics.mode)

Country  City            
Russia   Sankt-Petersburg    Spb
USA      New-York             NY
Name: Short name, dtype: object

...複数のモードを処理する必要がある場合はうまく機能しません。aStatisticsErrorが発生します。これはドキュメントに記載されています:

データが空の場合、または最も一般的な値が1つだけではない場合、StatisticsErrorが発生します。

しかし、あなたは自分で見ることができます...

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

@JoshFriedlanderdf.groupby(cols).agg(pd.Series.mode)は私のために働いているようです。それがうまくいかない場合、私の2番目の推測はですdf.groupby(cols).agg(lambda x: pd.Series.mode(x).values[0])
cs 9519年

ありがとう(いつものように!)あなたの2番目のオプションは私にとって物事を改善します、しかし私は得ていますIndexError: index 0 is out of bounds for axis 0 with size 0(おそらくシリーズがNaNだけを持っているグループがあるので)。追加dropna=Falseするとこれは解決しますが、発生するようです'<' not supported between instances of 'float' and 'str'(私のシリーズは文字列です)。(必要に応じて、これを新しい質問にしてください。)
JoshFriedlander19年

2
@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.を開くことをお勧めします
cs95

1
カウントを含めたい場合はnp.nandf.groupy(cols).agg(lambda x: x.mode(dropna=False).iloc[0])タイを気にせず、1つのモードだけが必要であると仮定して、モードを介してそれを行うことができることを追加する必要があります。
アイリーン

17

の場合agg、lambba関数は属性Seriesを持たないを取得し'Short name'ます。

stats.mode 2つの配列のタプルを返すため、このタプルの最初の配列の最初の要素を取得する必要があります。

これらの2つの簡単な変更で:

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

戻り値

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

1
@ ViacheslavNefedov-はい。ただし、純粋なパンダを使用する@HYRYのソリューションを使用してください。の必要はありませんscipy.stats
eumiro 2013年

14

ここでのゲームには少し遅れましたが、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

お役に立てれば!


それは私がやって来る最速の方法です..ありがとう!
ftoTheZ 2017年

1
このアプローチを使用する方法はありますが、aggパラメーター内に直接ありますか?agg({'f1':mode,'f2':np.sum})
パブロ

1
@PabloAは残念ながらそうではありません。インターフェースがまったく同じではないからです。これを個別の操作として実行し、結果を結合することをお勧めします。もちろん、パフォーマンスが問題にならない場合は、HYRYのソリューションを使用してコードをより簡潔に保つことができます。
abw333 2017年

@ abw333 HYRYのソリューションを使用しましたが、パフォーマンスの問題が発生しました... pandas devteamがaggメソッドでより多くの関数をサポートすることを願っています。
パブロ

間違いなく大きなDataFramesに行く方法。私は8300万の行と250万のユニークなグループを持っていました。これには1列あたり28秒かかりましたが、aggは1列あたり11分以上かかりました。
ALollz 2018

5

ここでの2つの上位の回答は次のことを示唆しています。

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、最初のグループは、2つのリストを返すので、(2つのモードがあるので)。(ここに記載されているように、最初のグループがシングルモードを返した場合、これは機能します!)

この場合の2つの可能な解決策は次のとおりです。

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

そして、ここのコメントcs95によって私に与えられた解決策:

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

基本的に、このメソッドは一度に1つの列で機能し、dfを出力します。したがってconcat、集中的なの代わりに、最初values.flatten()の列をdfとして扱い、次に出力配列()をdfの列として繰り返し追加します。


3

正式には、正解は@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

これは明らかに間違っています(4ではなく1である必要があるA値を参照)。これは、一意の値では処理できないためです。

したがって、他の解決策は正しいです:

>>> 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

1

それを解決するための別のアプローチが必要な場合は、に依存しないvalue_countsscipy.statsCounterコレクションを使用できます

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.modeまたはよりも高速pd.Series.value_counts().iloc[0]ですが、カウントするNaN値がある場合、これは失敗します。各NaNの発生は、他のNaNとは異なると見なされるため、各NaNはcountを持つようにカウントされます1stackoverflow.com/questions/61102111/…を
アイリーン

1

NaN値を含めたくない場合Counterは、pd.Series.modeまたはpd.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は個別にカウントされるため、これは失敗します。


0

ここでの問題はパフォーマンスです。行が多い場合は問題になります。

それがあなたの場合であるならば、これで試みてください:

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()

0

大規模なデータセットの場合は少し不器用ですが高速なアプローチでは、対象の列のカウントを取得し、カウントを最高から最低に並べ替えてから、サブセットで重複排除して最大のケースのみを保持します。コード例は次のとおりです。

>>> 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
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.