columnas pandas GroupBy con valores NaN (faltantes)


147

Tengo un DataFrame con muchos valores faltantes en columnas que deseo agrupar por:

import pandas as pd
import numpy as np
df = pd.DataFrame({'a': ['1', '2', '3'], 'b': ['4', np.NaN, '6']})

In [4]: df.groupby('b').groups
Out[4]: {'4': [0], '6': [2]}

ver que Pandas ha eliminado las filas con valores objetivo de NaN. (¡Quiero incluir estas filas!)

Como necesito muchas operaciones de este tipo (muchas columnas tienen valores perdidos) y uso funciones más complicadas que solo medianas (generalmente bosques aleatorios), quiero evitar escribir fragmentos de código demasiado complicados.

¿Alguna sugerencia? ¿Debo escribir una función para esto o hay una solución simple?


1
@PhillipCloud He editado esta pregunta para incluir solo la pregunta, que en realidad es bastante buena, relacionada con la mejora de pandas abiertos de Jeff's.
Andy Hayden

1
No poder incluir (y propagar) NaNs en grupos es bastante agravante. Citar a R no es convincente, ya que este comportamiento no es consistente con muchas otras cosas. De todos modos, el truco ficticio también es bastante malo. Sin embargo, el tamaño (incluye NaNs) y el recuento (ignora los NaNs) de un grupo serán diferentes si hay NaNs. dfgrouped = df.groupby (['b']). a.agg (['sum', 'size', 'count']) dfgrouped ['sum'] [dfgrouped ['size']! = dfgrouped ['count ']] = Ninguno
Brian Preslopsky

¿Puedes resumir lo que estás tratando específicamente de lograr? es decir, vemos una salida, pero ¿cuál es la salida "deseada"?
ca

2
Con pandas 1.1 que pronto será capaz de especificar dropna=Falseen groupby()conseguir el resultado deseado. Más información
cs95

Respuestas:


130

Esto se menciona en la sección Datos faltantes de los documentos :

Los grupos de NA en GroupBy se excluyen automáticamente. Este comportamiento es consistente con R, por ejemplo.

Una solución alternativa es usar un marcador de posición antes de hacer el grupo (p. Ej. -1):

In [11]: df.fillna(-1)
Out[11]: 
   a   b
0  1   4
1  2  -1
2  3   6

In [12]: df.fillna(-1).groupby('b').sum()
Out[12]: 
    a
b    
-1  2
4   1
6   3

Dicho esto, esto parece un truco bastante horrible ... tal vez debería haber una opción para incluir NaN en groupby (vea este tema de github , que utiliza el mismo truco de marcador de posición).


44
Esta es una solución lógica pero divertida en la que he pensado anteriormente, Pandas crea campos NaN a partir de los vacíos, y tenemos que cambiarlos de nuevo. Esta es la razón por la que estoy pensando en buscar otras soluciones, como ejecutar un servidor SQL y consultar las tablas desde allí (parece un poco complicado), o buscar otra biblioteca a pesar de Pandas, o usar la mía (que quiero deshacerse de). Thx
Gyula Sámuel Karli

@ GyulaSámuelKarli Para mí, esto parece un pequeño error (ver el informe de errores anterior), y mi solución es una solución alternativa. Me resulta extraño que descartes toda la biblioteca.
Andy Hayden

1
No quiero escribir Pandas, solo busque la herramienta que mejor se adapte a mis solicitudes.
Gyula Sámuel Karli

1
Eche un vistazo a mi respuesta a continuación, creo que he encontrado una solución bastante buena (más limpia y probablemente más rápida). stackoverflow.com/a/43375020/408853
ca

44
No, esto no es consistente con R. df%>% group_by también dará resúmenes de NA con una advertencia que puede evitarse pasando la columna de agrupación a través de fct_explicit_na y luego se crea un nivel (Falta).
Atención devastadora

40

Tema antiguo, si alguien todavía tropieza con esto, otra solución es convertir a través de .astype (str) a cadena antes de agrupar. Eso conservará los NaN's.

in:
df = pd.DataFrame({'a': ['1', '2', '3'], 'b': ['4', np.NaN, '6']})
df['b'] = df['b'].astype(str)
df.groupby(['b']).sum()
out:
    a
b   
4   1
6   3
nan 2

@ K3 --- rnc: Vea el comentario a su enlace: el autor de la publicación en su enlace hizo algo mal.
Thomas

@Thomas, sí, exactamente como en el ejemplo anterior. Edite si puede hacer que el ejemplo sea seguro (y trivial).
K3 --- rnc

El sumde aes la concatenación de cadenas aquí, no una suma numérica. Esto solo "funciona" porque 'b' consistía en entradas distintas. Necesitas 'a' para ser numérico y 'b' para ser cadena
BallpointBen

28

pandas> = 1.1

Desde pandas 1.1 tiene un mejor control sobre este comportamiento, los valores de NA ahora están permitidos en el mero usando dropna=False:

pd.__version__
# '1.1.0.dev0+2004.g8d10bfb6f'

# Example from the docs
df

   a    b  c
0  1  2.0  3
1  1  NaN  4
2  2  1.0  3
3  1  2.0  2

# without NA (the default)
df.groupby('b').sum()

     a  c
b        
1.0  2  3
2.0  2  5
# with NA
df.groupby('b', dropna=False).sum()

     a  c
b        
1.0  2  3
2.0  2  5
NaN  1  4

44
Esperemos que esta respuesta haga una marcha gradual hacia la cima. Es el enfoque correcto.
kdbanman

No creo que 1.1 haya sido lanzado todavía. Comprobado en conda y pip y las versiones todavía hay 1.0.4
sammywemmy

1
@sammywemmy Sí, por ahora esto solo puede ejecutarse dentro de un entorno de desarrollo . Me gusta tener una ventaja inicial cuando se trata de introducir nuevas funciones en publicaciones SO antiguas. ;-)
cs95

9

No puedo agregar un comentario a M. Kiewisch ya que no tengo suficientes puntos de reputación (solo tengo 41 pero necesito más de 50 para comentar).

De todos modos, solo quiero señalar que la solución de M. Kiewisch no funciona como es y puede necesitar más ajustes. Considere por ejemplo

>>> df = pd.DataFrame({'a': [1, 2, 3, 5], 'b': [4, np.NaN, 6, 4]})
>>> df
   a    b
0  1  4.0
1  2  NaN
2  3  6.0
3  5  4.0
>>> df.groupby(['b']).sum()
     a
b
4.0  6
6.0  3
>>> df.astype(str).groupby(['b']).sum()
      a
b
4.0  15
6.0   3
nan   2

lo que muestra que para el grupo b = 4.0, el valor correspondiente es 15 en lugar de 6. Aquí solo se concatena 1 y 5 como cadenas en lugar de agregarlo como números.


12
Eso es porque convertiste todo el DF en str, en lugar de solo la bcolumna
Korem

Tenga en cuenta que esto se ha solucionado en la respuesta mencionada ahora.
Shaido - Restablece a Mónica el

1
La nueva solución es mejor, pero aún no es segura, en mi opinión. Considere un caso donde una de las entradas en la columna 'b' es la misma que np.NaN en cadena. Entonces esas cosas se aporrean juntas. df = pd.DataFrame ({'a': [1, 2, 3, 5, 6], 'b': ['foo', np.NaN, 'bar', 'foo', 'nan']}) ; df ['b'] = df ['b']. astype (str); df.groupby (['b']). sum ()
Kamaraju Kusumanchi

6

Un pequeño punto a la solución de Andy Hayden: no funciona (¿ya?) Porque np.nan == np.nancede False, por lo que la replacefunción en realidad no hace nada.

Lo que funcionó para mí fue esto:

df['b'] = df['b'].apply(lambda x: x if not np.isnan(x) else -1)

(Al menos ese es el comportamiento de Pandas 0.19.2. Lamento agregarlo como una respuesta diferente, no tengo suficiente reputación para comentar).


12
También existe df['b'].fillna(-1).
K3 --- rnc

6

Todas las respuestas proporcionadas hasta ahora dan como resultado un comportamiento potencialmente peligroso, ya que es muy posible que seleccione un valor ficticio que realmente sea parte del conjunto de datos. Esto es cada vez más probable a medida que crea grupos con muchos atributos. En pocas palabras, el enfoque no siempre se generaliza bien.

Una solución menos hacky es usar pd.drop_duplicates () para crear un índice único de combinaciones de valores, cada una con su propia ID, y luego agruparlas en esa identificación. Es más detallado pero hace el trabajo:

def safe_groupby(df, group_cols, agg_dict):
    # set name of group col to unique value
    group_id = 'group_id'
    while group_id in df.columns:
        group_id += 'x'
    # get final order of columns
    agg_col_order = (group_cols + list(agg_dict.keys()))
    # create unique index of grouped values
    group_idx = df[group_cols].drop_duplicates()
    group_idx[group_id] = np.arange(group_idx.shape[0])
    # merge unique index on dataframe
    df = df.merge(group_idx, on=group_cols)
    # group dataframe on group id and aggregate values
    df_agg = df.groupby(group_id, as_index=True)\
               .agg(agg_dict)
    # merge grouped value index to results of aggregation
    df_agg = group_idx.set_index(group_id).join(df_agg)
    # rename index
    df_agg.index.name = None
    # return reordered columns
    return df_agg[agg_col_order]

Tenga en cuenta que ahora simplemente puede hacer lo siguiente:

data_block = [np.tile([None, 'A'], 3),
              np.repeat(['B', 'C'], 3),
              [1] * (2 * 3)]

col_names = ['col_a', 'col_b', 'value']

test_df = pd.DataFrame(data_block, index=col_names).T

grouped_df = safe_groupby(test_df, ['col_a', 'col_b'],
                          OrderedDict([('value', 'sum')]))

Esto devolverá el resultado exitoso sin tener que preocuparse por sobrescribir datos reales que se confunden con un valor ficticio.


Esta es la mejor solución para el caso general, pero en los casos en que sé de una cadena / número no válido que puedo usar en su lugar, probablemente iré con la respuesta de Andy Hayden a continuación ... Espero que los pandas solucionen este comportamiento pronto.
Sarah Messer

4

Ya respondí esto, pero por alguna razón la respuesta se convirtió en un comentario. Sin embargo, esta es la solución más eficiente:

No poder incluir (y propagar) NaNs en grupos es bastante agravante. Citar a R no es convincente, ya que este comportamiento no es consistente con muchas otras cosas. De todos modos, el truco ficticio también es bastante malo. Sin embargo, el tamaño (incluye NaNs) y el recuento (ignora los NaNs) de un grupo serán diferentes si hay NaNs.

dfgrouped = df.groupby(['b']).a.agg(['sum','size','count'])

dfgrouped['sum'][dfgrouped['size']!=dfgrouped['count']] = None

Cuando estos difieren, puede volver a establecer el valor en Ninguno para el resultado de la función de agregación para ese grupo.


1
Esto fue muy útil para mí, pero responde una pregunta ligeramente diferente a la original. IIUC, su solución propaga los NaN en la sumatoria, pero los elementos de NaN en la columna "b" aún se eliminan como filas.
Andrew

0

Pandas 1.1 instalado en Anaconda

No puedo comentar la respuesta de cs95 pero él me ayudó a resolver el problema.

Traté de instalar Pandas 1.1 pero falló usando su código, así que busqué en Google y pude instalar.

Primero ejecuto anaconda prompt como administrador y pego el siguiente código:

pip install pandas==1.1.0rc0

Después de eso incluye el uso dropna = False

Enlace: https://libraries.io/pypi/pandas


Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.