GroupBy pandas DataFrame y seleccione el valor más común

99

Tengo un marco de datos con tres columnas de cadena. Sé que el único valor en la tercera columna es válido para cada combinación de los dos primeros. Para limpiar los datos, tengo que agrupar por marco de datos en las dos primeras columnas y seleccionar el valor más común de la tercera columna para cada combinación.

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

La última línea de código no funciona, dice "Error de clave 'Nombre corto'" y si trato de agrupar solo por Ciudad, obtengo un AssertionError. ¿Qué puedo hacer para solucionarlo?

Viacheslav Nefedov
fuente

Respuestas:

145

Puede usar value_counts()para obtener una serie de recuento y obtener la primera fila:

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

En caso de que se esté preguntando cómo realizar otras funciones agg en .agg (), intente esto.

# 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
fuente
He descubierto que stats.mode puede mostrar respuestas incorrectas en caso de variables de cadena. De esta manera parece más confiable.
Viacheslav Nefedov
1
¿No debería ser esto .value_counts(ascending=False)?
Privado
1
@Private: ascending=Falseya es el valor predeterminado, por lo que no es necesario establecer el orden explícitamente.
Schmuddi
2
Como dijo Jacquot, ahora pd.Series.modees más apropiado y rápido.
Daisuke SHIBATO
¿Cómo puedo usar esta solución con múltiples funciones de agregación diferentes, por ejemplo, si tengo múltiples columnas como "Nombre corto" y columnas numéricas adicionales que quiero agregar con una función de suma?
constiii
99

Pandas> = 0,16

pd.Series.mode ¡está disponible!

Utilice groupby, GroupBy.aggy aplique la pd.Series.modefunción 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

Si esto es necesario como un 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

Lo útil Series.modees que siempre devuelve una Serie, lo que la hace muy compatible con aggy apply, sobre todo, a la hora de reconstruir la salida groupby. También es más 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)

Tratar con múltiples modos

Series.modetambién hace un buen trabajo cuando hay varios 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

O, si desea una fila separada para cada modo, puede 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

Si no le importa qué modo se devuelve siempre que sea uno de ellos, necesitará una lambda que llame modey extraiga el primer 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 (no) considerar

También puedes usar statistics.modedesde python, pero ...

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

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

... no funciona bien cuando se tiene que lidiar con múltiples modos; una StatisticsErrorse eleva. Esto se menciona en los documentos:

Si los datos están vacíos, o si no hay exactamente un valor más común, se genera StatisticsError.

Pero puedes verlo por ti mismo ...

statistics.mode([1, 2])
# ---------------------------------------------------------------------------
# StatisticsError                           Traceback (most recent call last)
# ...
# StatisticsError: no unique mode; found 2 equally common values
cs95
fuente
@JoshFriedlander df.groupby(cols).agg(pd.Series.mode)parece funcionar para mí. Si eso no funciona, mi segunda suposición sería df.groupby(cols).agg(lambda x: pd.Series.mode(x).values[0]).
cs95
Gracias (¡como siempre!) Tu segunda opción mejora las cosas para mí, pero IndexError: index 0 is out of bounds for axis 0 with size 0obtengo un (probablemente porque hay grupos en los que una serie solo tiene NaN). Agregar dropna=Falseresuelve esto , pero parece aumentar '<' not supported between instances of 'float' and 'str'(mi serie son cadenas). (Feliz de convertir esto en una nueva pregunta si lo prefiere.)
Josh Friedlander
2
@JoshFriedlander Definir def foo(x): m = pd.Series.mode(x); return m.values[0] if not m.empty else np.nany luego usar df.groupby(cols).agg(foo). Si eso no funciona, juega fooun poco con la implementación de . Si sigues teniendo problemas para arrancar, te recomiendo abrir una nueva Q.
cs95
1
Debo agregar que si quieres incluir el conteo np.nan , puede hacerlo a través df.groupy(cols).agg(lambda x: x.mode(dropna=False).iloc[0])del modo, asumiendo que no le importan los vínculos y solo quiere un modo.
irene
17

Porque agg, la función lambba obtiene un Series, que no tiene un 'Short name'atributo.

stats.mode devuelve una tupla de dos matrices, por lo que debe tomar el primer elemento de la primera matriz en esta tupla.

Con estos dos sencillos cambios:

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

devoluciones

                         Short name
Country City                       
Russia  Sankt-Petersburg        Spb
USA     New-York                 NY
eumiro
fuente
1
@ViacheslavNefedov: sí, pero tome la solución de @ HYRY, que usa pandas puros. No es necesario scipy.stats.
eumiro
14

Un poco tarde para el juego aquí, pero estaba teniendo algunos problemas de rendimiento con la solución de HYRY, así que tuve que idear otra.

Funciona al encontrar la frecuencia de cada clave-valor y luego, para cada clave, solo conserva el valor que aparece con ella con mayor frecuencia.

También hay una solución adicional que admite múltiples modos.

En una prueba de escala que es representativa de los datos con los que estoy trabajando, ¡este tiempo de ejecución reducido de 37,4 sa 0,5 s!

Aquí está el código de la solución, algunos ejemplos de uso y la prueba 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

Ejecutar 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 esto ayude!

abw333
fuente
Esa es la forma más rápida en que vengo ... ¡Gracias!
FtoTheZ
1
¿Hay alguna forma de usar este enfoque pero directamente dentro de los parámetros agg? agg({'f1':mode,'f2':np.sum})
Pablo
1
@PabloA lamentablemente no, porque la interfaz no es exactamente la misma. Recomiendo hacer esto como una operación separada y luego unir sus resultados. Y, por supuesto, si el rendimiento no es un problema, puede usar la solución de HYRY para mantener su código más conciso.
abw333
@ abw333 Usé la solución de HYRY, pero me encontré con problemas de rendimiento ... Espero que el equipo de desarrollo de pandas admita más funciones en el aggmétodo.
Pablo
Definitivamente el camino a seguir para DataFrames grandes. Tenía 83 millones de filas y 2,5 millones de grupos únicos. Esto tomó 28 segundos por columna, mientras que la agg tomó más de 11 minutos por columna.
ALollz
5

Las dos respuestas principales aquí sugieren:

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

o, preferiblemente

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

Sin embargo, ambos fallan en casos extremos simples, como se demuestra aquí:

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

El primero:

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

rendimientos IndexError(debido a la Serie vacía devuelta por grupo C). El segundo:

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

devuelve ValueError: Function does not reduce, ya que el primer grupo devuelve una lista de dos (ya que hay dos modos). (Como se documenta aquí , si el primer grupo devolviera un modo único, ¡esto funcionaría!)

Dos posibles soluciones para este caso son:

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

Y la solución que me dio cs95 en los comentarios aquí :

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)

Sin embargo, todos estos son lentos y no adecuados para grandes conjuntos de datos. Una solución que terminé usando que a) puede lidiar con estos casos yb) es mucho, mucho más rápida, es una versión ligeramente modificada de la respuesta de abw33 (que debería ser más alta):

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

Esencialmente, el método funciona en una columna a la vez y genera un df, por lo que en lugar de concat, que es intensivo, trata el primero como un df y luego agrega iterativamente la matriz de salida ( values.flatten()) como una columna en el df.

Josh Friedlander
fuente
3

Formalmente, la respuesta correcta es la Solución @eumiro. El problema de la solución @HYRY es que cuando tienes una secuencia de números como [1,2,3,4] la solución es incorrecta, es decir, no tienes el modo . Ejemplo:

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

Si calcula como @HYRY obtiene:

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

Lo cual es claramente incorrecto (consulte el valor A que debería ser 1 y no 4 ) porque no se puede manejar con valores únicos.

Por tanto, la otra solución es correcta:

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

Si desea otro enfoque para resolverlo que no depende value_countso scipy.statspuede usar la Countercolección

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

Que se puede aplicar al ejemplo anterior 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
fuente
Esto es más rápido que pd.Series.modeo pd.Series.value_counts().iloc[0], pero si tiene valores NaN que desea contar, esto fallará. Cada aparición de NaN se verá como diferente de las otras NaN, por lo que se cuenta que cada NaN tiene recuento 1. Ver stackoverflow.com/questions/61102111/…
irene
1

Si no desea incluir valores de NaN , usar Counteres mucho más rápido que pd.Series.modeo 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)

Deberia trabajar. Esto fallará cuando tenga valores de NaN, ya que cada NaN se contará por separado.

Irene
fuente
0

El problema aquí es el rendimiento, si tienes muchas filas será un problema.

Si es tu caso, prueba con esto:

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 Pérez Sastre
fuente
0

Un enfoque un poco más torpe pero más rápido para conjuntos de datos más grandes implica obtener los recuentos de una columna de interés, ordenar los recuentos de mayor a menor y luego eliminar la duplicación en un subconjunto para retener solo los casos más grandes. El ejemplo de código es el siguiente:

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