GroupBy pandas DataFrame ve en yaygın değeri seçin


100

Üç dize sütunu olan bir veri çerçevem ​​var. 3. sütundaki tek değerin ilk ikisinin her kombinasyonu için geçerli olduğunu biliyorum. Verileri temizlemek için veri çerçevesine göre ilk iki sütuna göre gruplandırmalı ve her kombinasyon için üçüncü sütunun en yaygın değerini seçmeliyim.

Kodum:

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

Son kod satırı çalışmıyor, "Anahtar hatası 'Kısa ad" diyor ve yalnızca Şehre göre gruplamayı denersem, o zaman bir Onay Hatası aldım. Düzeltmek için ne yapabilirim?

Yanıtlar:


145

Sen kullanabilirsiniz value_counts()bir sayım serisini elde etmek ve ilk satırı almak:

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 () içindeki diğer agg işlevlerini gerçekleştirmeyi merak ediyorsanız, bunu deneyin.

# 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'un dize değişkenleri durumunda yanlış cevaplar gösterebileceğini buldum. Bu yol daha güvenilir görünüyor.
Viacheslav Nefedov

1
Bu olmamalı .value_counts(ascending=False)mı?
Özel

1
@Private: ascending=Falsezaten varsayılan değerdir, bu nedenle sırayı açıkça ayarlamanıza gerek yoktur.
Schmuddi

2
Jacquot'un dediği gibi pd.Series.modeartık daha uygun ve daha hızlı.
Daisuke SHIBATO

Bu çözümü birden çok farklı toplama işleviyle nasıl kullanabilirim, örneğin "Kısa ad" gibi birden çok sütunum ve ayrıca bir toplama işleviyle toplamak istediğim sayısal sütunlar varsa?
constiii

102

Pandalar> = 0.16

pd.Series.mode kullanılabilir!

Kullanım groupby, GroupBy.aggve uygulamak pd.Series.modeher gruba işlevi:

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 olarak buna ihtiyaç duyulursa, şunu kullanın:

source.groupby(['Country','City'])['Short name'].agg(pd.Series.mode).to_frame()

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

Yararlı olan şey Series.mode, her zaman bir Seri döndürmesidir, bu onu çok uyumlu hale getirir aggve applyözellikle de grup çıkışını yeniden yapılandırırken. Aynı zamanda daha hızlıdır.

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

Çoklu Modlarla Başa Çıkmak

Series.modebirden fazla mod olduğunda da iyi bir iş çıkarır:

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

Ya da her mod için ayrı bir satır istiyorsanız, şunları kullanabilirsiniz 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

Herhangi biri olduğu sürece hangi modun döndürüleceğini umursamıyorsanız , o zaman modeilk sonucu çağıran ve çıkaran bir lambda'ya ihtiyacınız olacaktır .

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

Dikkate alınması (değil) alternatifleri

statistics.modePython'dan da kullanabilirsiniz , ancak ...

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

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

... birden fazla modla uğraşmak zorunda kaldığınızda iyi çalışmıyor; a StatisticsErroryükseltilir. Bu, dokümanlarda belirtilmiştir:

Veriler boşsa veya tam olarak en yaygın bir değer yoksa, StatisticsError hatası yükselir.

Ama kendiniz görebilirsiniz ...

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)benim için çalışıyor gibi görünüyor. Bu işe yaramazsa, ikinci tahminim olur df.groupby(cols).agg(lambda x: pd.Series.mode(x).values[0]).
cs95

Teşekkürler (her zamanki gibi!) İkinci seçeneğiniz benim için işleri iyileştiriyor, ancak bir alıyorum IndexError: index 0 is out of bounds for axis 0 with size 0(muhtemelen bir dizide sadece NaN'lerin olduğu gruplar var). Ekleme dropna=Falseçözer bu , ama zam görünüyor '<' not supported between instances of 'float' and 'str'(benim dizi dizeleri olduğunu). (Tercih ederseniz bunu yeni bir soru haline getirmekten mutluluk duyarız.)
Josh Friedlander

2
@JoshFriedlander Tanımlayın def foo(x): m = pd.Series.mode(x); return m.values[0] if not m.empty else np.nanve sonra kullanın df.groupby(cols).agg(foo). Bu işe yaramazsa, uygulama ile foobiraz uğraşın. Hâlâ sorun yaşıyorsanız, yeni bir Q
açmanızı

1
Saymayı dahil etmek istiyorsanız np.nan, df.groupy(cols).agg(lambda x: x.mode(dropna=False).iloc[0])bağları umursamadığınızı ve sadece bir mod istediğinizi varsayarak, bunu mod için yapabilirsiniz.
irene

17

Çünkü agglambba işlevi Series, 'Short name'özniteliği olmayan a alır .

stats.mode iki diziden oluşan bir demet döndürür, bu nedenle bu dizideki ilk dizinin ilk öğesini almanız gerekir.

Bu iki basit değişiklikle:

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

İadeler

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

1
@ViacheslavNefedov - evet, ancak @ HYRY'nin saf pandalar kullanan çözümünü alın. Gerek yok scipy.stats.
eumiro

14

Buradaki oyuna biraz geç kaldım, ancak HYRY'nin çözümüyle ilgili bazı performans sorunları yaşıyordum, bu yüzden başka bir tane bulmam gerekti.

Her bir anahtar / değer çiftinin sıklığını bularak ve ardından her anahtar için yalnızca onunla en sık görünen değeri koruyarak çalışır.

Birden çok modu destekleyen ek bir çözüm de var.

Üzerinde çalıştığım verileri temsil eden bir ölçek testinde, bu çalışma süresi 37,4 saniyeden 0,5 saniyeye düşürüldü!

İşte çözümün kodu, bazı örnek kullanım ve ölçek testi:

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

Bu kodu çalıştırmak aşağıdaki gibi bir şey yazdıracaktır:

   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

Bu yardımcı olur umarım!


Bu en hızlı geldiğim yol .. Teşekkürler!
FtoTheZ

1
Bu yaklaşımı kullanmanın bir yolu var ama doğrudan agg parametreleri içinde mi? agg({'f1':mode,'f2':np.sum})
Pablo

1
@PabloA ne yazık ki hayır, çünkü arayüz tamamen aynı değil. Bunu ayrı bir işlem olarak yapmanızı ve ardından sonuçlarınıza katılmanızı öneririm. Ve tabii ki, performans bir sorun değilse, kodunuzu daha kısa tutmak için HYRY'nin çözümünü kullanabilirsiniz.
abw333

@ abw333 HYRY'nin çözümünü kullandım, ancak performans sorunlarıyla karşılaştım ... Umarım pandalar geliştirici ekip aggyöntemde daha fazla işlevi destekler .
Pablo

Kesinlikle büyük DataFrame'lere gitmenin yolu. 83 milyon satır ve 2,5 milyon benzersiz grubum vardı. Bu, sütun başına 28 saniye sürerken, toplam, sütun başına 11 dakikadan fazla sürdü.
ALollz

5

Buradaki en iyi iki cevap şunları öneriyor:

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

veya tercihen

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

Ancak, burada gösterildiği gibi, bunların her ikisi de basit uç durumlarda başarısız olur:

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

İlk:

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

getiri IndexError(grup tarafından döndürülen boş Seriler nedeniyle C). İkinci:

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

döndürür ValueError: Function does not reduce, çünkü ilk grup iki liste döndürür (iki mod olduğundan). (As belgelenmiş burada ilk grup bu işe yarar tek bir mod döndü eğer!)

Bu durum için iki olası çözüm:

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

Ve çözüm yorumlarında cs95 tarafından bana verildi burada :

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)

Ancak bunların tümü yavaştır ve büyük veri kümeleri için uygun değildir. Sonunda, a) bu durumlarla başa çıkabilen ve b) çok, çok daha hızlı olan, abw33'ün cevabının (daha yüksek olması gereken) hafifçe değiştirilmiş bir versiyonu olan bir çözüm:

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

Esasen, yöntem bir seferde bir sütun üzerinde çalışır ve bir df verir, bu nedenle concat, yoğun olan yerine , ilkini bir df olarak kabul edersiniz ve ardından çıktı dizisini ( values.flatten()) yinelemeli olarak df'ye bir sütun olarak eklersiniz .


3

Resmi olarak, doğru cevap @eumiro Çözümüdür. @HYRY çözümünün problemi, [1,2,3,4] gibi bir sayı dizisine sahip olduğunuzda çözümün yanlış olmasıdır, yani kipe sahip değilsiniz . Misal:

>>> 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 gibi hesaplarsanız şunları elde edersiniz:

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

Bu açıkça yanlıştır ( 4 değil 1 olması gereken A değerine bakın ) çünkü benzersiz değerlerle başa çıkamaz.

Dolayısıyla, diğer çözüm doğrudur:

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

Çözmek için bağlı olmayan başka bir yaklaşım istiyorsanız value_countsveya koleksiyonu scipy.statskullanabilirsiniz.Counter

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

Yukarıdaki örneğe bunun gibi uygulanabilir

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)

Bu pd.Series.modeveya pd.Series.value_counts().iloc[0]- ' den daha hızlıdır, ancak saymak istediğiniz NaN değerleriniz varsa, bu başarısız olacaktır. Her NaN oluşumu, diğer NaN'lerden farklı olarak görülecektir, bu nedenle her NaN sayılır 1. Stackoverflow.com/questions/61102111/… sayfasına
irene

1

NaN değerlerini dahil etmek istemiyorsanız , kullanmak veya şundanCounter çok daha hızlıdır :pd.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)

çalışmalı. Her NaN ayrı olarak sayılacağından, NaN değerleriniz olduğunda bu başarısız olur.


0

Sorun burada satır uzunluğunda bir çok fazlaysa bu bir sorun olacak, performanstır.

Sizin durumunuzsa, lütfen şunu deneyin:

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

Daha büyük veri kümeleri için biraz daha beceriksiz ancak daha hızlı bir yaklaşım, ilgilenilen bir sütun için sayımları almayı, sayıları en yüksekten en düşüğe doğru sıralamayı ve ardından yalnızca en büyük vakaları korumak için bir alt kümede tekilleştirmeyi içerir. Kod örneği aşağıdaki gibidir:

>>> 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
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.