Eu tenho um quadro de dados com três colunas de string. Eu sei que o único valor na 3ª coluna é válido para todas as combinações dos dois primeiros. Para limpar os dados, tenho que agrupar por quadro de dados pelas duas primeiras colunas e selecionar o valor mais comum da terceira coluna para cada combinação.
Meu código:
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])
A última linha de código não funciona, ele diz "Erro de chave 'Nome abreviado'" e se eu tentar agrupar apenas por cidade, obtenho um AssertionError. O que posso fazer para corrigir isso?
.value_counts(ascending=False)
?ascending=False
já é o valor padrão, portanto, não há necessidade de definir o pedido explicitamente.pd.Series.mode
é mais adequado e rápido agora.IndexError: index 0 is out of bounds for axis 0 with size 0
, como resolver?Pandas> = 0,16
pd.Series.mode
está disponível!Use
groupby
,GroupBy.agg
e aplique apd.Series.mode
função a cada grupo: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
Se for necessário como um DataFrame, use
source.groupby(['Country','City'])['Short name'].agg(pd.Series.mode).to_frame() Short name Country City Russia Sankt-Petersburg Spb USA New-York NY
O útil
Series.mode
é que sempre retorna uma série, o que o torna muito compatível comagg
eapply
, especialmente ao reconstruir a saída de groupby. Também é mais rápido.# 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)
Lidando com vários modos
Series.mode
também faz um bom trabalho quando existem vários modos: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
Ou, se quiser uma linha separada para cada modo, você pode usar
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
Se você não se importa com qual modo é retornado, desde que seja um deles, você precisará de um lambda que chame
mode
e extraia o primeiro resultado.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
Alternativas a (não) considerar
Você também pode usar
statistics.mode
do python, mas ...source.groupby(['Country','City'])['Short name'].apply(statistics.mode) Country City Russia Sankt-Petersburg Spb USA New-York NY Name: Short name, dtype: object
... não funciona bem ao lidar com vários modos; a
StatisticsError
é gerado. Isso é mencionado nos documentos:Mas você pode ver por si mesmo ...
statistics.mode([1, 2]) # --------------------------------------------------------------------------- # StatisticsError Traceback (most recent call last) # ... # StatisticsError: no unique mode; found 2 equally common values
fonte
df.groupby(cols).agg(pd.Series.mode)
parece funcionar para mim. Se isso não funcionar, meu segundo palpite seriadf.groupby(cols).agg(lambda x: pd.Series.mode(x).values[0])
.IndexError: index 0 is out of bounds for axis 0 with size 0
(provavelmente porque há grupos em que uma série tem apenas NaNs). Adicionardropna=False
resolve isso , mas parece aumentar'<' not supported between instances of 'float' and 'str'
(minha série é de cordas). (Fico feliz em transformar isso em uma nova pergunta, se preferir.)def foo(x): m = pd.Series.mode(x); return m.values[0] if not m.empty else np.nan
e usedf.groupby(cols).agg(foo)
. Se isso não funcionar, brinquefoo
um pouco com a implementação de . Se você ainda estiver tendo problemas para começar, recomendo abrir um novo Q.np.nan
, pode fazê-lo viadf.groupy(cols).agg(lambda x: x.mode(dropna=False).iloc[0])
para o modo, supondo que você não se preocupe com empates e queira apenas um modo.Pois
agg
, a função lambba obtém umSeries
, que não tem um'Short name'
atributo.stats.mode
retorna uma tupla de dois arrays, então você deve pegar o primeiro elemento do primeiro array nesta tupla.Com essas duas mudanças simples:
source.groupby(['Country','City']).agg(lambda x: stats.mode(x)[0][0])
retorna
fonte
scipy.stats
.Um pouco tarde para o jogo aqui, mas eu estava tendo alguns problemas de desempenho com a solução de HYRY, então tive que pensar em outra.
Ele funciona encontrando a frequência de cada valor-chave e, em seguida, para cada chave, mantendo apenas o valor que aparece com ela com mais frequência.
Há também uma solução adicional que oferece suporte a vários modos.
Em um teste de escala que representa os dados com os quais estou trabalhando, isso reduziu o tempo de execução de 37,4s para 0,5s!
Aqui está o código para a solução, alguns exemplos de uso e o teste de escala:
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
Executar este código imprimirá algo como:
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
Espero que isto ajude!
fonte
agg({'f1':mode,'f2':np.sum})
agg
método.As duas principais respostas aqui sugerem:
df.groupby(cols).agg(lambda x:x.value_counts().index[0])
ou, de preferência
No entanto, ambos falham em casos extremos simples, conforme demonstrado aqui:
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] })
O primeiro:
df.groupby(['client_id', 'date']).agg(lambda x:x.value_counts().index[0])
rendimentos
IndexError
(por causa da série vazia retornada pelo grupoC
). O segundo:df.groupby(['client_id', 'date']).agg(pd.Series.mode)
retorna
ValueError: Function does not reduce
, já que o primeiro grupo retorna uma lista de dois (já que existem dois modos). (Conforme documentado aqui , se o primeiro grupo retornasse um único modo, isso funcionaria!)Duas soluções possíveis para este caso são:
import scipy x.groupby(['client_id', 'date']).agg(lambda x: scipy.stats.mode(x)[0])
E a solução que me foi dada pelo cs95 nos comentários aqui :
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)
No entanto, todos eles são lentos e não adequados para grandes conjuntos de dados. Uma solução que acabei usando, que a) pode lidar com esses casos eb) é muito, muito mais rápida, é uma versão levemente modificada da resposta de abw33 (que deveria ser maior):
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
Essencialmente, o método funciona em uma coluna por vez e produz um df, portanto, em vez de
concat
, que é intensivo, você trata o primeiro como um df e, em seguida, adiciona iterativamente o array de saída (values.flatten()
) como uma coluna no df.fonte
Formalmente, a resposta correta é a Solução @eumiro. O problema da solução do @HYRY é que quando você tem uma sequência de números como [1,2,3,4] a solução está errada, ou seja, você não tem o modo . Exemplo:
>>> 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] } )
Se você calcular como @HYRY, obterá:
>>> 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
O que está claramente errado (veja o valor A que deve ser 1 e não 4 ) porque não pode lidar com valores únicos.
Assim, a outra solução é correta:
>>> 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
fonte
Se você quiser outra abordagem para resolver isso que não dependa
value_counts
ouscipy.stats
você pode usar aCounter
coleçãofrom collections import Counter get_most_common = lambda values: max(Counter(values).items(), key = lambda x: x[1])[0]
Que pode ser aplicado ao exemplo acima como este
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)
fonte
pd.Series.mode
oupd.Series.value_counts().iloc[0]
- mas se você tiver valores NaN que deseja contar, isso falhará. Cada ocorrência de NaN será vista como diferente dos outros NaNs, então cada NaN é contado para ter contagem1
. Consulte stackoverflow.com/questions/61102111/…Se você não quiser incluir valores NaN , usar
Counter
é muito mais rápido do quepd.Series.mode
oupd.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)
Deveria trabalhar. Isso falhará quando você tiver valores NaN, pois cada NaN será contado separadamente.
fonte
O problema aqui é o desempenho, se você tiver muitas linhas será um problema.
Se for o seu caso, tente com isto:
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()
fonte
Uma abordagem um pouco mais desajeitada, porém mais rápida, para conjuntos de dados maiores envolve obter as contagens para uma coluna de interesse, classificar as contagens da maior para a menor e, em seguida, eliminar a duplicação em um subconjunto para reter apenas os casos maiores. O exemplo de código é o seguinte:
>>> 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
fonte