GroupBy pandas DataFrame und wählen Sie den häufigsten Wert

94

Ich habe einen Datenrahmen mit drei Zeichenfolgenspalten. Ich weiß, dass der einzige Wert in der 3. Spalte für jede Kombination der ersten beiden gültig ist. Um die Daten zu bereinigen, muss ich nach Datenrahmen nach den ersten beiden Spalten gruppieren und für jede Kombination den häufigsten Wert der dritten Spalte auswählen.

Mein Code:

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

Die letzte Codezeile funktioniert nicht. Sie lautet "Schlüsselfehler 'Kurzname'". Wenn ich versuche, nur nach Stadt zu gruppieren, wird ein AssertionError angezeigt. Was kann ich tun, um das Problem zu beheben?

Viacheslav Nefedov
quelle

Antworten:

141

Sie können verwenden value_counts(), um eine Zählreihe abzurufen und die erste Zeile abzurufen:

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

Wenn Sie sich fragen, ob Sie andere Agg-Funktionen in .agg () ausführen möchten, versuchen Sie dies.

# 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
quelle
Ich habe festgestellt, dass stats.mode bei Zeichenfolgenvariablen falsche Antworten anzeigen kann. Dieser Weg sieht zuverlässiger aus.
Viacheslav Nefedov
1
Sollte das nicht sein .value_counts(ascending=False)?
Privat
1
@Private: ascending=Falseist bereits der Standardwert, sodass die Reihenfolge nicht explizit festgelegt werden muss.
Schmuddi
2
Wie Jacquot sagte, pd.Series.modeist jetzt angemessener und schneller.
Daisuke SHIBATO
Wie kann ich diese Lösung mit mehreren verschiedenen Aggregationsfunktionen verwenden, z. B. wenn ich mehrere Spalten wie "Kurzname" und zusätzlich numerische Spalten habe, die ich mit einer Summenfunktion aggregieren möchte?
constiii
94

Pandas> = 0,16

pd.Series.mode ist verfügbar!

Verwendung groupby, GroupBy.aggund die Anwendung pd.Series.modeFunktion jeder Gruppe:

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

Wenn dies als DataFrame benötigt wird, verwenden Sie

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

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

Das Nützliche daran Series.modeist, dass es immer eine Serie zurückgibt, was es sehr kompatibel mit aggund applyinsbesondere bei der Rekonstruktion der Groupby-Ausgabe macht. Es ist auch schneller.

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

Umgang mit mehreren Modi

Series.modemacht auch einen guten Job, wenn es mehrere Modi gibt:

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

Wenn Sie für jeden Modus eine eigene Zeile wünschen, können Sie Folgendes verwenden 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

Wenn es Ihnen egal ist, welcher Modus zurückgegeben wird, solange es sich um einen von beiden handelt, benötigen Sie ein Lambda, das modedas erste Ergebnis aufruft und extrahiert.

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

Alternativen zu (nicht) zu berücksichtigen

Sie können auch statistics.modevon Python verwenden, aber ...

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

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

... es funktioniert nicht gut, wenn mehrere Modi verwendet werden müssen; a StatisticsErrorwird angehoben. Dies wird in den Dokumenten erwähnt:

Wenn die Daten leer sind oder wenn es nicht genau einen der häufigsten Werte gibt, wird StatisticsError ausgelöst.

Aber Sie können selbst sehen ...

statistics.mode([1, 2])
# ---------------------------------------------------------------------------
# StatisticsError                           Traceback (most recent call last)
# ...
# StatisticsError: no unique mode; found 2 equally common values
cs95
quelle
@ JoshFriedlander df.groupby(cols).agg(pd.Series.mode)scheint für mich zu arbeiten. Wenn das nicht funktioniert, wäre meine zweite Vermutung df.groupby(cols).agg(lambda x: pd.Series.mode(x).values[0]).
CS95
Danke (wie immer!) Ihre zweite Option verbessert die Dinge für mich, aber ich bekomme eine IndexError: index 0 is out of bounds for axis 0 with size 0(wahrscheinlich, weil es Gruppen gibt, in denen eine Serie nur NaNs hat). Das Hinzufügen dropna=Falselöst dieses Problem , scheint sich jedoch zu erhöhen '<' not supported between instances of 'float' and 'str'(meine Serie besteht aus Zeichenfolgen). (Gerne machen wir daraus eine neue Frage, wenn Sie es vorziehen.)
Josh Friedlander
2
@JoshFriedlander Definieren def foo(x): m = pd.Series.mode(x); return m.values[0] if not m.empty else np.nanund dann verwenden df.groupby(cols).agg(foo). Wenn das nicht funktioniert, spielen Sie fooein bisschen mit der Implementierung von . Wenn Sie immer noch Probleme mit dem Start haben, empfehle ich, ein neues Q.
cs95
1
Ich sollte hinzufügen, dass man, wenn man das Zählen einbeziehen möchte, np.nandies df.groupy(cols).agg(lambda x: x.mode(dropna=False).iloc[0])für den Modus tun kann , vorausgesetzt, man kümmert sich nicht um Krawatten und möchte nur einen Modus.
Irene
17

Denn aggdie Lambba-Funktion erhält ein Series, das kein 'Short name'Attribut hat.

stats.mode Gibt ein Tupel aus zwei Arrays zurück, sodass Sie das erste Element des ersten Arrays in diesem Tupel übernehmen müssen.

Mit diesen zwei einfachen Änderungen:

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

kehrt zurück

                         Short name
Country City                       
Russia  Sankt-Petersburg        Spb
USA     New-York                 NY
Eumiro
quelle
1
@ViacheslavNefedov - ja, aber nehmen Sie die Lösung von @ HYRY, die reine Pandas verwendet. Keine Notwendigkeit für scipy.stats.
Eumiro
14

Ein bisschen zu spät zum Spiel hier, aber ich hatte einige Leistungsprobleme mit der HYRY-Lösung, also musste ich mir eine andere einfallen lassen.

Es funktioniert, indem die Häufigkeit jedes Schlüsselwerts ermittelt wird und dann für jeden Schlüssel nur der Wert beibehalten wird, der am häufigsten mit ihm angezeigt wird.

Es gibt auch eine zusätzliche Lösung, die mehrere Modi unterstützt.

Bei einem Skalentest, der repräsentativ für die Daten ist, mit denen ich arbeite, wurde die Laufzeit von 37,4 auf 0,5 Sekunden reduziert!

Hier ist der Code für die Lösung, einige Beispiele für die Verwendung und der Skalentest:

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

Wenn Sie diesen Code ausführen, wird Folgendes gedruckt:

   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

Hoffe das hilft!

abw333
quelle
Das ist der schnellste Weg, den ich komme. Danke!
FtoTheZ
1
Gibt es eine Möglichkeit, diesen Ansatz zu verwenden, jedoch direkt innerhalb der Agg-Parameter?, Z. agg({'f1':mode,'f2':np.sum})
Pablo
1
@PabloA leider nicht, da die Schnittstelle nicht ganz gleich ist. Ich empfehle, dies als separate Operation auszuführen und dann Ihre Ergebnisse zusammenzuführen. Wenn die Leistung keine Rolle spielt, können Sie natürlich die HYRY-Lösung verwenden, um Ihren Code präziser zu gestalten.
abw333
@ abw333 Ich habe die Lösung von HYRY verwendet, aber es treten Leistungsprobleme auf ... Ich hoffe, dass das Pandas-Entwicklerteam mehr Funktionen in der aggMethode unterstützt.
Pablo
Auf jeden Fall der richtige Weg für große DataFrames. Ich hatte 83 Millionen Zeilen und 2,5 Millionen eindeutige Gruppen. Dies dauerte 28 Sekunden pro Säule, während das Agg über 11 Minuten pro Säule dauerte.
ALollz
4

Die beiden besten Antworten hier legen nahe:

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

oder vorzugsweise

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

Beide scheitern jedoch in einfachen Randfällen, wie hier gezeigt:

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

Der Erste:

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

Ausbeuten IndexError(wegen der leeren Reihe, die von der Gruppe zurückgegeben wird C). Der Zweite:

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

gibt zurück ValueError: Function does not reduce, da die erste Gruppe eine Liste von zwei zurückgibt (da es zwei Modi gibt). (Wie hier dokumentiert , würde dies funktionieren, wenn die erste Gruppe einen einzelnen Modus zurückgeben würde!)

Zwei mögliche Lösungen für diesen Fall sind:

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

Und die Lösung, die mir cs95 in den Kommentaren hier gegeben hat :

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)

All dies ist jedoch langsam und nicht für große Datenmengen geeignet. Eine Lösung, mit der ich am Ende a) diese Fälle behandeln kann und b) viel, viel schneller ist, ist eine leicht modifizierte Version der Antwort von abw33 (die höher sein sollte):

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

Im Wesentlichen arbeitet die Methode jeweils mit einer Spalte und gibt einen df aus. Statt concatintensiv zu behandeln, behandeln Sie den ersten als df und fügen dann iterativ das Ausgabearray ( values.flatten()) als Spalte im df hinzu.

Josh Friedlander
quelle
3

Formal ist die richtige Antwort die @eumiro-Lösung. Das Problem der @ HYRY-Lösung ist, dass wenn Sie eine Folge von Zahlen wie [1,2,3,4] haben, die Lösung falsch ist, dh Sie haben nicht den Modus . Beispiel:

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

Wenn Sie wie @HYRY rechnen, erhalten Sie:

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

Was eindeutig falsch ist (siehe den A- Wert, der 1 und nicht 4 sein sollte ), weil er nicht mit eindeutigen Werten umgehen kann.

Somit ist die andere Lösung richtig:

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

Wenn Sie einen anderen Lösungsansatz wünschen, der nicht davon abhängt, value_countsoder scipy.statsSie die CounterSammlung verwenden können

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

Welches kann auf das obige Beispiel wie folgt angewendet werden

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
quelle
Dies ist schneller als pd.Series.modeoder pd.Series.value_counts().iloc[0]- aber wenn Sie NaN-Werte haben, die Sie zählen möchten, schlägt dies fehl. Jedes NaN-Vorkommen wird als von den anderen NaNs verschieden angesehen, so dass jedes NaN als gezählt gezählt wird 1. Siehe stackoverflow.com/questions/61102111/…
irene
0

Das Problem hierbei ist die Leistung. Wenn Sie viele Zeilen haben, ist dies ein Problem.

Wenn es Ihr Fall ist, versuchen Sie bitte Folgendes:

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

Ein etwas ungeschickterer, aber schnellerer Ansatz für größere Datensätze besteht darin, die Anzahl für eine interessierende Spalte abzurufen, die Anzahl der höchsten bis niedrigsten zu sortieren und dann eine Teilmenge zu duplizieren, um nur die größten Fälle beizubehalten. Das Codebeispiel lautet wie folgt:

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

Wenn Sie keine NaN-Werte einschließen möchten , ist die Verwendung Counterviel schneller als pd.Series.modeoder 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)

sollte arbeiten. Dies schlägt fehl, wenn Sie NaN-Werte haben, da jedes NaN separat gezählt wird.

irene
quelle