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ć?
.value_counts(ascending=False)
?ascending=False
jest już wartością domyślną, więc nie ma potrzeby jawnego ustawiania kolejności.pd.Series.mode
jest teraz bardziej odpowiedni i szybszy.IndexError: index 0 is out of bounds for axis 0 with size 0
, jak go rozwiązać?Pandy> = 0,16
pd.Series.mode
jest dostępny!Użyj
groupby
,GroupBy.agg
i zastosujpd.Series.mode
funkcję 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.mode
jest to, że zawsze zwraca Seria, dzięki czemu jest bardzo kompatybilna zagg
iapply
, 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.mode
wykonuje 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
mode
i 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.mode
z 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
StatisticsError
jest podniesiony. Jest to wspomniane w dokumentach: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
źródło
df.groupby(cols).agg(pd.Series.mode)
wydaje się działać dla mnie. Jeśli to nie zadziała, moje drugie przypuszczenie będziedf.groupby(cols).agg(lambda x: pd.Series.mode(x).values[0])
.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). Dodaniedropna=False
rozwią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.)def foo(x): m = pd.Series.mode(x); return m.values[0] if not m.empty else np.nan
a następnie użyjdf.groupby(cols).agg(foo)
. Jeśli to nie zadziała, pobaw sięfoo
przez chwilę implementacją . Jeśli nadal masz problemy z uruchomieniem, polecam otwarcie nowegonp.nan
, możesz to zrobić przezdf.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.Dla
agg
funkcji lambba pobiera aSeries
, 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
źródło
scipy.stats
.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!
źródło
agg({'f1':mode,'f2':np.sum})
agg
metodzie.Dwie najważniejsze odpowiedzi sugerują:
df.groupby(cols).agg(lambda x:x.value_counts().index[0])
lub najlepiej
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.źródło
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
źródło
Jeśli chcesz innego podejścia do rozwiązania tego problemu, które nie zależy od kolekcji
value_counts
lubscipy.stats
możesz użyć tejCounter
kolekcjifrom 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)
źródło
pd.Series.mode
lubpd.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 licznik1
. Zobacz stackoverflow.com/questions/61102111/…Jeśli nie chcesz uwzględniać wartości NaN , użycie
Counter
jest znacznie szybsze niżpd.Series.mode
lubpd.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.
źródło
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()
źródło
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
źródło