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?
.value_counts(ascending=False)
?ascending=False
ya es el valor predeterminado, por lo que no es necesario establecer el orden explícitamente.pd.Series.mode
es más apropiado y rápido.Pandas> = 0,16
pd.Series.mode
¡está disponible!Utilice
groupby
,GroupBy.agg
y aplique lapd.Series.mode
funció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.mode
es que siempre devuelve una Serie, lo que la hace muy compatible conagg
yapply
, 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.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á una lambda que llame
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
Alternativas a (no) considerar
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; una
StatisticsError
se eleva. Esto se menciona en los documentos: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
fuente
df.groupby(cols).agg(pd.Series.mode)
parece funcionar para mí. Si eso no funciona, mi segunda suposición seríadf.groupby(cols).agg(lambda x: pd.Series.mode(x).values[0])
.IndexError: index 0 is out of bounds for axis 0 with size 0
obtengo un (probablemente porque hay grupos en los que una serie solo tiene NaN). Agregardropna=False
resuelve 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.)def foo(x): m = pd.Series.mode(x); return m.values[0] if not m.empty else np.nan
y luego usardf.groupby(cols).agg(foo)
. Si eso no funciona, juegafoo
un poco con la implementación de . Si sigues teniendo problemas para arrancar, te recomiendo abrir una nueva Q.np.nan
, puede hacerlo a travésdf.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.Porque
agg
, la función lambba obtiene unSeries
, 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
fuente
scipy.stats
.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!
fuente
agg({'f1':mode,'f2':np.sum})
agg
método.Las dos respuestas principales aquí sugieren:
df.groupby(cols).agg(lambda x:x.value_counts().index[0])
o, preferiblemente
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 grupoC
). 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.fuente
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
fuente
Si desea otro enfoque para resolverlo que no depende
value_counts
oscipy.stats
puede usar laCounter
colecciónfrom 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)
fuente
pd.Series.mode
opd.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 recuento1
. Ver stackoverflow.com/questions/61102111/…Si no desea incluir valores de NaN , usar
Counter
es mucho más rápido quepd.Series.mode
opd.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.
fuente
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()
fuente
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
fuente