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

Resuelto Viacheslav Nefedov asked hace 11 años • 14 respuestas

Tengo un marco de datos con tres columnas de cadena. Sé que el único valor de 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 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']})

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

La última línea de código no funciona, dice KeyError: 'Short name'y si intento agrupar solo por ciudad, obtengo un AssertionError. ¿Qué puedo hacer para solucionarlo?

Viacheslav Nefedov avatar Mar 05 '13 18:03 Viacheslav Nefedov
Aceptado

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 lo hace muy compatible con aggy apply, especialmente al reconstruir la salida de 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 múltiples 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 cualquiera 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; StatisticsErrorse eleva a . 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 avatar Jan 22 '2019 09:01 cs95

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

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

En caso de que se pregunte cómo realizar otras funciones agregadas en el archivo .agg(), pruebe 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 avatar Mar 05 '2013 11:03 HYRY

Un poco tarde para empezar el juego, pero me encontré con algunos problemas de rendimiento con la solución de HYRY, así que tuve que idear otra.

Funciona encontrando la frecuencia de cada clave-valor y luego, para cada clave, manteniendo solo 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, ¡esto redujo el tiempo de ejecución de 37,4 segundos a 0,5 segundos!

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

Al ejecutar este código se 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 avatar Jul 06 '2016 03:07 abw333

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

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

Con estos dos simples 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 avatar Mar 05 '2013 11:03 eumiro