GroupBy pandas DataFrame e selecione o valor mais comum

108

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?

Viacheslav Nefedov
fonte

Respostas:

152

Você pode usar value_counts()para obter uma série de contagem e obter a primeira linha:

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

Caso você esteja se perguntando sobre como executar outras funções agg no .agg (), tente isto.

# 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
fonte
Descobri que stats.mode pode mostrar respostas incorretas no caso de variáveis ​​de string. Dessa forma, parece mais confiável.
Viacheslav Nefedov
2
Não deveria ser assim .value_counts(ascending=False)?
Privado
1
@Private: ascending=Falsejá é o valor padrão, portanto, não há necessidade de definir o pedido explicitamente.
Schmuddi
3
Como disse Jacquot, pd.Series.modeé mais adequado e rápido agora.
Daisuke SHIBATO
2
Encontro um erro chamado IndexError: index 0 is out of bounds for axis 0 with size 0, como resolver?
rosefun
112

Pandas> = 0,16

pd.Series.mode está disponível!

Use groupby, GroupBy.agge aplique a pd.Series.modefunçã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 com agge apply, 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.modetambé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 modee 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.modedo 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:

Se os dados estiverem vazios ou se não houver exatamente um valor mais comum, StatisticsError é gerado.

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
cs95
fonte
@JoshFriedlander df.groupby(cols).agg(pd.Series.mode)parece funcionar para mim. Se isso não funcionar, meu segundo palpite seria df.groupby(cols).agg(lambda x: pd.Series.mode(x).values[0]).
cs95
Obrigado (como sempre!) Sua segunda opção melhora as coisas para mim, mas estou recebendo um 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). Adicionar dropna=Falseresolve 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.)
Josh Friedlander
2
@JoshFriedlander Defina def foo(x): m = pd.Series.mode(x); return m.values[0] if not m.empty else np.nane use df.groupby(cols).agg(foo). Se isso não funcionar, brinque fooum pouco com a implementação de . Se você ainda estiver tendo problemas para começar, recomendo abrir um novo Q.
cs95
2
Devo acrescentar que se você quiser incluir a contagem np.nan, pode fazê-lo via df.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.
irene,
17

Pois agg, a função lambba obtém um Series, 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

                         Short name
Country City                       
Russia  Sankt-Petersburg        Spb
USA     New-York                 NY
eumiro
fonte
1
@ViacheslavNefedov - sim, mas pegue a solução do @HYRY, que usa pandas puros. Não há necessidade scipy.stats.
eumiro
15

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!

abw333
fonte
Essa é a maneira mais rápida que eu venho .. Obrigado!
FtoTheZ
1
Existe uma maneira de usar essa abordagem, mas diretamente dentro dos parâmetros agg ?, por exemplo. agg({'f1':mode,'f2':np.sum})
Pablo
1
@PabloA infelizmente não, porque a interface não é exatamente a mesma. Eu recomendo fazer isso como uma operação separada e, em seguida, juntar seus resultados. E, claro, se o desempenho não for uma preocupação, você pode usar a solução de HYRY para manter seu código mais conciso.
abw333 01 de
@ abw333 Usei a solução do HYRY, mas encontrei problemas de desempenho ... Espero que a equipe de desenvolvimento do pandas suporte mais funções no aggmétodo.
Pablo
Definitivamente, o caminho a percorrer para grandes DataFrames. Eu tinha 83 milhões de linhas e 2,5 milhões de grupos exclusivos. Isso levou 28 segundos por coluna, enquanto o agg levou mais de 11 minutos por coluna.
ALollz de
6

As duas principais respostas aqui sugerem:

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

ou, de preferência

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

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 grupo C). 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.

Josh Friedlander
fonte
3

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
nunodsousa
fonte
1

Se você quiser outra abordagem para resolver isso que não dependa value_countsou scipy.statsvocê pode usar a Countercoleção

from 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)
kmader
fonte
Isso é mais rápido do que pd.Series.modeou pd.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 contagem 1. Consulte stackoverflow.com/questions/61102111/…
irene,
1

Se você não quiser incluir valores NaN , usar Counteré muito mais rápido do que pd.Series.modeou 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)

Deveria trabalhar. Isso falhará quando você tiver valores NaN, pois cada NaN será contado separadamente.

irene
fonte
0

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()
Diego Perez Sastre
fonte
0

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