Ich habe einen Datenrahmen mit drei String-Spalten. Ich weiß, dass der einzige Wert in der 3. Spalte für jede Kombination der ersten beiden Werte gilt. Um die Daten zu bereinigen, muss ich nach Datenrahmen nach ersten zwei Spalten gruppieren und den am häufigsten verwendeten Wert der dritten Spalte für jede Kombination 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 sagt "Schlüsselfehler 'Kurzname'" und wenn ich versuche, nur nach Stadt zu gruppieren, habe ich einen AssertionError. Was kann ich beheben?
Sie können value_counts()
verwenden, um eine Zählreihe zu erhalten 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])
Für agg
erhält die Lambba-Funktion eine Series
, die kein 'Short name'
-Attribut besitzt.
stats.mode
gibt einen Tupel aus zwei Arrays zurück, daher müssen Sie das erste Element des ersten Arrays in diesem Tuple nehmen.
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
Ein wenig zu spät zum Spiel hier, aber ich hatte einige Probleme mit der HYRY-Lösung, daher musste ich mir ein anderes vorstellen.
Sie ermittelt die Häufigkeit jedes Schlüsselwerts und behält dann für jeden Schlüssel nur den Wert bei, der am häufigsten angezeigt wird.
Es gibt auch eine zusätzliche Lösung, die mehrere Modi unterstützt.
Bei einem Skalentest, der für die Daten, mit denen ich arbeite, repräsentativ ist, wurde die Laufzeit von 37,4 Sekunden auf 0,5 Sekunden reduziert.
Hier ist der Code für die Lösung, einige Anwendungsbeispiele 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!
Formell ist die richtige Antwort die @eumiro-Lösung. Das Problem der @HYRY-Lösung besteht darin, dass die Lösung falsch ist, wenn Sie eine Zahlenfolge wie [1,2,3,4] haben. 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 berechnen, erhalten Sie:
df.groupby(['socio']).agg(lambda x: x.value_counts().index[0])
und du erhältst:
Das ist eindeutig falsch (siehe den A -Wert, der 1 und nicht 4 sein sollte, da er mit eindeutigen Werten nicht umgehen kann.
Somit ist die andere Lösung richtig:
import scipy.stats
df3.groupby(['client']).agg(lambda x: scipy.stats.mode(x)[0][0])
bekommen:
pd.Series.mode
ist verfügbar.Verwenden Sie groupby
, GroupBy.agg
, und wenden Sie die Funktion pd.Series.mode
auf jede Gruppe an:
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 erforderlich ist, 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 an Series.mode
ist, dass immer eine Serie zurückgegeben wird, sodass sie sehr gut mit agg
und apply
kompatibel ist, insbesondere beim Rekonstruieren der Group-Ausgabe. 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)
Series.mode
macht auch gute Arbeit, wenn es multiple 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 GroupBy.apply
verwenden:
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 Ihnen egal egal ist, welcher Modus zurückgegeben wird, solange er einer von ihnen ist, benötigen Sie ein Lambda, das mode
aufruft und das erste Ergebnis 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
Sie können auch statistics.mode
aus 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 Sie mit mehreren Modi umgehen müssen; eine StatisticsError
wird ausgelöst. Dies wird in den Dokumenten erwähnt:
Wenn Daten leer sind oder wenn nicht genau ein häufigster Wert vorhanden ist, StatisticsError wird ausgelöst.
Aber du kannst es selbst sehen ...
statistics.mode([1, 2])
# ---------------------------------------------------------------------------
# StatisticsError Traceback (most recent call last)
# ...
# StatisticsError: no unique mode; found 2 equally common values
Das Problem hier ist die Leistung. Wenn Sie viele Zeilen haben, wird dies ein Problem sein.
Wenn es dein Fall ist, versuche es bitte mit:
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()
Wenn Sie einen anderen Lösungsweg suchen, der nicht von value_counts
oder scipy.stats
abhängig ist, können Sie die Counter
-Sammlung verwenden
from collections import Counter
get_most_common = lambda values: max(Counter(values).items(), key = lambda x: x[1])[0]
Was auf das obige Beispiel so angewendet werden kann
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)
Ein etwas unbeholfeneres, aber schnelleres Verfahren für größere Datensätze besteht darin, die Zählungen für eine interessierende Spalte zu ermitteln, die Zählungen von Höchst zu niedrigsten zu sortieren und anschließend in einer Teilmenge zu duplizieren, um nur die größten Fälle beizubehalten.
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()
grouped_df = grouped_df.sort_values('count',ascending=False)
grouped_df = grouped_df.drop_duplicates(subset=['Country','City']).drop('count', axis=1)
grouped_df
Die zwei Top-Antworten hier schlagen vor:
df.groupby(cols).agg(lambda x:x.value_counts().index[0])
oder vorzugsweise
df.groupby(cols).agg(pd.Series.mode)
Beide schlagen jedoch in einfachen Edge-Fällen fehl, 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])
ergibt IndexError
(wegen der von der Gruppe C
zurückgegebenen leeren Serie). Der Zweite:
df.groupby(['client_id', 'date']).agg(pd.Series.mode)
gibt ValueError: Function does not reduce
zurück, da die erste Gruppe eine Liste von zwei zurückgibt (da es zwei Modi gibt). (Wie dokumentiert hier , wenn die erste Gruppe einen einzelnen Modus zurückgibt, würde dies funktionieren!)
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 gegeben hat hier :
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]
Im Wesentlichen arbeitet die Methode mit jeweils einer Spalte und gibt ein df aus. Anstelle von concat
, das intensiv ist, wird das erste als df behandelt und anschließend das Ausgabearray iterativ hinzugefügt (values.flatten()
) als spalte im df.