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

Tengo un dataframe con tres columnas de cadena. Sé que el único valor en la tercera columna es válido para cada combinación de las dos primeras. Para limpiar los datos, tengo que agrupar por cuadro de datos por 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 bash agrupar solo por Ciudad, obtengo un error de aserción. ¿Qué puedo hacer para solucionarlo?

Puedes usar value_counts() para obtener una serie de conteo 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]) 

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

stats.mode devuelve una tupla de dos matrices, por lo que debe tomar el primer elemento de la primera matriz de 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 

2019 respuesta, pd.Series.mode está disponible.

Use groupby , GroupBy.agg y aplique la función pd.Series.mode 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 de Series.mode es que siempre devuelve una serie, por lo que es muy compatible con agg y se apply , especialmente cuando se reconstruye 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) 

Series.mode tambié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á un lambda que llame al mode y 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 

También puedes usar statistics.mode desde 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; Se StatisticsError un StatisticsError . Esto se menciona en los documentos:

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

Pero puedes ver por ti mismo …

 statistics.mode([1, 2]) # --------------------------------------------------------------------------- # StatisticsError Traceback (most recent call last) # ... # StatisticsError: no unique mode; found 2 equally common values 

Llegué un poco tarde al juego, pero me encontré con algunos problemas de rendimiento con la solución de HYRY, así que tuve que encontrar otro.

Funciona encontrando la frecuencia de cada clave-valor, y luego, para cada clave, solo mantiene el valor que aparece con mayor frecuencia.

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

En una prueba de escala que es representativa de los datos con los que trabajo, ¡este tiempo de ejecución reducido de 37.4s a 0.5s!

Aquí está el código para 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!

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 computas como @HYRY obtienes:

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

y obtienes:

introduzca la descripción de la imagen aquí

Lo que es claramente incorrecto (ver el valor A que debe ser 1 y no 4 ) porque no se puede manejar con valores únicos.

Por lo tanto, la otra solución es correcta:

 import scipy.stats df3.groupby(['client']).agg(lambda x: scipy.stats.mode(x)[0][0]) 

consiguiendo:

introduzca la descripción de la imagen aquí

Un enfoque un poco más torpe pero más rápido para conjuntos de datos más grandes implica obtener los recuentos para una columna de interés, clasificar los recuentos de mayor a menor y luego desduplicar en un subconjunto para conservar solo los casos más grandes.

 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() grouped_df = grouped_df.sort_values('count',ascending=False) grouped_df = grouped_df.drop_duplicates(subset=['Country','City']).drop('count', axis=1) grouped_df 

El problema aquí es el rendimiento, si tiene 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() 

Si desea otro enfoque para resolverlo que no dependa de value_counts o scipy.stats , puede usar la colección Counter

 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)