Grupuj według pand DataFrame i wybierz najpopularniejszą wartość

108

Mam ramkę danych z trzema kolumnami ciągów. Wiem, że jedyna wartość w trzeciej kolumnie jest ważna dla każdej kombinacji dwóch pierwszych. Aby wyczyścić dane, muszę pogrupować ramkę danych według pierwszych dwóch kolumn i wybrać najbardziej powszechną wartość trzeciej kolumny dla każdej kombinacji.

Mój kod:

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

Ostatnia linia kodu nie działa, wyświetla komunikat „Błąd klucza„ Krótka nazwa ”” i jeśli spróbuję zgrupować tylko według miasta, otrzymam AssertionError. Co mogę to naprawić?

Wiaczesław Nefedow
źródło

Odpowiedzi:

152

Możesz użyć, value_counts()aby uzyskać serię liczenia i uzyskać pierwszy wiersz:

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

Jeśli zastanawiasz się nad wykonaniem innych funkcji agg w .agg (), spróbuj tego.

# 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') \
                                      )
HYRY
źródło
Zauważyłem, że stats.mode może pokazywać nieprawidłowe odpowiedzi w przypadku zmiennych typu string. W ten sposób wygląda bardziej niezawodnie.
Viacheslav Nefedov
2
Nie powinno tak być .value_counts(ascending=False)?
Szeregowy
1
@Private: ascending=Falsejest już wartością domyślną, więc nie ma potrzeby jawnego ustawiania kolejności.
Schmuddi,
3
Jak powiedział Jacquot, pd.Series.modejest teraz bardziej odpowiedni i szybszy.
Daisuke SHIBATO
2
Spotykam się z błędem o nazwie IndexError: index 0 is out of bounds for axis 0 with size 0, jak go rozwiązać?
rosefun
112

Pandy> = 0,16

pd.Series.mode jest dostępny!

Użyj groupby, GroupBy.aggi zastosuj pd.Series.modefunkcję do każdej grupy:

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

Jeśli jest to potrzebne jako DataFrame, użyj

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

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

Użyteczną rzeczą Series.modejest to, że zawsze zwraca Seria, dzięki czemu jest bardzo kompatybilna z aggi apply, szczególnie podczas rekonstrukcji wyjścia grupowego. Jest też szybszy.

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

Radzenie sobie z wieloma trybami

Series.modewykonuje również dobrą robotę, gdy istnieje wiele trybów:

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

Jeśli chcesz mieć osobny wiersz dla każdego trybu, możesz użyć 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

Jeśli nie obchodzi cię , który tryb jest zwracany, o ile jest to jeden z nich, będziesz potrzebować lambdy, która wywołuje modei wyodrębnia pierwszy wynik.

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

Alternatywy dla (nie) rozważania

Możesz także użyć statistics.modez Pythona, ale ...

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

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

... nie działa dobrze, gdy mamy do czynienia z wieloma trybami; a StatisticsErrorjest podniesiony. Jest to wspomniane w dokumentach:

Jeśli dane są puste lub nie ma dokładnie jednej najczęściej stosowanej wartości, generowany jest błąd StatisticsError.

Ale sam możesz się przekonać ...

statistics.mode([1, 2])
# ---------------------------------------------------------------------------
# StatisticsError                           Traceback (most recent call last)
# ...
# StatisticsError: no unique mode; found 2 equally common values
cs95
źródło
@JoshFriedlander df.groupby(cols).agg(pd.Series.mode)wydaje się działać dla mnie. Jeśli to nie zadziała, moje drugie przypuszczenie będzie df.groupby(cols).agg(lambda x: pd.Series.mode(x).values[0]).
cs95
Dzięki (jak zawsze!) Twoja druga opcja poprawia sytuację, ale dostaję IndexError: index 0 is out of bounds for axis 0 with size 0(prawdopodobnie dlatego, że są grupy, w których serial ma tylko NaN). Dodanie dropna=Falserozwiązuje ten problem , ale wydaje się, że podnosi '<' not supported between instances of 'float' and 'str'(moja seria to stringi). (Jeśli wolisz, z przyjemnością zrobię z tego nowe pytanie.)
Josh Friedlander
2
@JoshFriedlander Zdefiniuj, def foo(x): m = pd.Series.mode(x); return m.values[0] if not m.empty else np.nana następnie użyj df.groupby(cols).agg(foo). Jeśli to nie zadziała, pobaw się fooprzez chwilę implementacją . Jeśli nadal masz problemy z uruchomieniem, polecam otwarcie nowego
pytania
2
Powinienem dodać, że jeśli chcesz uwzględnić liczenie np.nan, możesz to zrobić przez df.groupy(cols).agg(lambda x: x.mode(dropna=False).iloc[0])tryb, zakładając, że nie obchodzą cię remisy i chcesz tylko jeden tryb.
Irene
17

Dla aggfunkcji lambba pobiera a Series, który nie ma 'Short name'atrybutu.

stats.mode zwraca krotkę z dwóch tablic, więc musisz wziąć pierwszy element pierwszej tablicy w tej krotce.

Dzięki tym dwóm prostym zmianom:

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

zwroty

                         Short name
Country City                       
Russia  Sankt-Petersburg        Spb
USA     New-York                 NY
eumiro
źródło
1
@ViacheslavNefedov - tak, ale weź rozwiązanie @ HYRY, które wykorzystuje czyste pandy. Nie ma potrzeby scipy.stats.
eumiro
15

Trochę spóźniłem się do gry tutaj, ale miałem problemy z wydajnością w rozwiązaniu HYRY, więc musiałem wymyślić inny.

Działa poprzez znalezienie częstotliwości każdej pary klucz-wartość, a następnie dla każdego klucza zachowuje tylko wartość, która pojawia się wraz z nią najczęściej.

Jest też dodatkowe rozwiązanie obsługujące wiele trybów.

W teście skali, który reprezentuje dane, z którymi pracuję, skrócił się czas działania z 37,4 do 0,5 sekundy!

Oto kod rozwiązania, przykładowe użycie i test skali:

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

Uruchomienie tego kodu wypisze coś takiego:

   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

Mam nadzieję że to pomoże!

abw333
źródło
To najszybszy sposób, w jaki przyjdę ... Dzięki!
FtoTheZ
1
Czy jest sposób na użycie tego podejścia, ale bezpośrednio w parametrach agg? Np. agg({'f1':mode,'f2':np.sum})
Pablo
1
@PabloA niestety nie, ponieważ interfejs nie jest taki sam. Zalecam zrobienie tego jako osobnej operacji, a następnie dołączenie wyników. I oczywiście, jeśli wydajność nie jest problemem, możesz użyć rozwiązania HYRY, aby zachować zwięzły kod.
abw333
@ abw333 Użyłem rozwiązania HYRY, ale napotykam problemy z wydajnością ... Mam nadzieję, że zespół programistów pandy będzie obsługiwał więcej funkcji w tej aggmetodzie.
Pablo
Zdecydowanie droga do dużych ramek DataFrame. Miałem 83 mln wierszy i 2,5 mln unikalnych grup. Zajmowało to 28 sekund na kolumnę, podczas gdy agr zajmował ponad 11 minut na kolumnę.
ALollz,
6

Dwie najważniejsze odpowiedzi sugerują:

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

lub najlepiej

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

Jednak oba z nich zawodzą w prostych przypadkach skrajnych, jak pokazano tutaj:

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

Pierwszy:

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

plony IndexError(z powodu pustej serii zwróconej przez grupę C). Drugi:

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

zwraca ValueError: Function does not reduce, ponieważ pierwsza grupa zwraca listę dwóch (ponieważ istnieją dwa tryby). (Jak opisano tutaj , gdyby pierwsza grupa zwróciła pojedynczy tryb, zadziałałoby!)

Dwa możliwe rozwiązania w tym przypadku to:

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

I rozwiązanie podane mi przez cs95 w komentarzach tutaj :

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)

Jednak wszystkie z nich są powolne i nie nadają się do dużych zbiorów danych. Rozwiązanie, z którego korzystałem, a które a) radzi sobie z tymi przypadkami i b) jest dużo, dużo szybsze, jest lekko zmodyfikowaną wersją odpowiedzi abw33 (która powinna być wyższa):

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

Zasadniczo metoda działa na jednej kolumnie na raz i generuje df, więc zamiast concat, co jest intensywne, traktujesz pierwszą jako df, a następnie iteracyjnie dodajesz tablicę wyjściową ( values.flatten()) jako kolumnę w df.

Josh Friedlander
źródło
3

Formalnie prawidłowa odpowiedź to @eumiro Solution. Problem z rozwiązaniem @HYRY polega na tym, że jeśli masz ciąg liczb, takich jak [1,2,3,4], rozwiązanie jest nieprawidłowe, tj. Nie masz trybu . Przykład:

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

Jeśli obliczysz jak @HYRY, otrzymasz:

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

Co jest ewidentnie błędne (zobacz wartość A, która powinna wynosić 1, a nie 4 ), ponieważ nie obsługuje ona unikalnych wartości.

Zatem drugie rozwiązanie jest poprawne:

>>> 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
nunodsousa
źródło
1

Jeśli chcesz innego podejścia do rozwiązania tego problemu, które nie zależy od kolekcji value_countslub scipy.statsmożesz użyć tej Counterkolekcji

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

Które można zastosować do powyższego przykładu w ten sposób

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)
kmader
źródło
Jest to szybsze niż pd.Series.modelub pd.Series.value_counts().iloc[0]- ale jeśli masz wartości NaN, które chcesz policzyć, to się nie powiedzie. Każde wystąpienie NaN będzie postrzegane jako różne od innych NaN, więc każdy NaN jest liczony jako licznik 1. Zobacz stackoverflow.com/questions/61102111/…
irene
1

Jeśli nie chcesz uwzględniać wartości NaN , użycie Counterjest znacznie szybsze niż pd.Series.modelub 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)

powinno działać. To się nie powiedzie, jeśli masz wartości NaN, ponieważ każdy NaN będzie liczony osobno.

Irene
źródło
0

Problem tutaj jest wydajność, jeśli masz dużo wierszy, to będzie problem.

Jeśli tak jest w Twoim przypadku, spróbuj z tym:

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()
Diego Perez Sastre
źródło
0

Nieco bardziej niezgrabne, ale szybsze podejście do większych zbiorów danych polega na uzyskaniu liczb dla kolumny będącej przedmiotem zainteresowania, sortowaniu zliczeń od najwyższych do najniższych, a następnie usuwaniu duplikatów na podzbiorze, aby zachować tylko największe obserwacje. Przykładowy kod jest następujący:

>>> 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
Dimitri
źródło