pandas GroupBy colunas com valores NaN (ausentes)

147

Eu tenho um DataFrame com muitos valores ausentes nas colunas que desejo agrupar:

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

veja que o Pandas eliminou as linhas com os valores alvo de NaN. (Eu quero incluir essas linhas!)

Como preciso de muitas dessas operações (muitas colunas têm valores ausentes) e utilizo funções mais complicadas do que apenas medianas (geralmente florestas aleatórias), quero evitar escrever partes de código muito complicadas.

Alguma sugestão? Devo escrever uma função para isso ou existe uma solução simples?

Gyula Sámuel Karli
fonte
1
@PhillipCloud Eu editei esta pergunta para incluir apenas a pergunta, que é realmente muito boa, relacionada ao aprimoramento de pandas abertos do Jeff.
Andy Hayden
1
Não é possível incluir (e propagar) NaNs em grupos é bastante agravante. Citar R não é convincente, pois esse comportamento não é consistente com muitas outras coisas. Enfim, o truque falso também é muito ruim. No entanto, o tamanho (inclui NaNs) e a contagem (ignora NaNs) de um grupo serão diferentes se houver NaNs. dfgrouped = df.groupby (['b']). a.agg (['soma', 'tamanho', 'contagem']) dfgrouped ['soma'] [dfgrouped ['tamanho']! = dfgrouped ['count ']] = Nenhum
Brian Preslopsky
Você pode resumir o que você está especificamente tentando alcançar? ou seja, vemos uma saída, mas qual é a saída "desejada"?
ca
2
Com pandas 1.1 em breve você vai ser capaz de especificar dropna=Falseem groupby()obter o resultado desejado. Mais informações
cs95 20/05

Respostas:

130

Isso é mencionado na seção Dados ausentes dos documentos :

Grupos de NA no GroupBy são automaticamente excluídos. Esse comportamento é consistente com R, por exemplo.

Uma solução alternativa é usar um espaço reservado antes de fazer o groupby (por exemplo, -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

Dito isso, isso parece um hack horrível ... talvez deva haver uma opção para incluir o NaN no groupby (veja este problema do github - que usa o mesmo hack do espaço reservado).

Andy Hayden
fonte
4
Essa é uma solução lógica, mas meio engraçada, que eu pensei anteriormente: o Pandas cria campos NaN a partir dos vazios, e precisamos alterá-los novamente. Essa é a razão pela qual estou pensando em procurar outras soluções, como executar um servidor SQL e consultar as tabelas de lá (parece um pouco complicado), ou procurar outra biblioteca, apesar do Pandas, ou usar a minha (que eu quero se livrar). Thx
Gyula Sámuel Karli
@ GyulaSámuelKarli Para mim, isso parece um pequeno erro (veja o relatório de erros acima) e minha solução é uma solução alternativa. Acho estranho você escrever toda a biblioteca.
Andy Hayden
1
Não quero escrever os pandas, basta procurar a ferramenta que melhor se adapta aos meus pedidos.
Gyula Sámuel Karli
1
Dê uma olhada na minha resposta abaixo, acredito que encontrei uma solução muito boa (mais limpa e provavelmente mais rápida). stackoverflow.com/a/43375020/408853
ca
4
Não, isso não é consistente com R. df%>% group_by também fornecerá aos resumos de NA um aviso que pode ser evitado passando a coluna de agrupamento por fct_explicit_na e, em seguida, um nível (ausente) é criado.
Ravaging Care
40

Tópico antigo, se alguém ainda tropeçar nisso - outra solução alternativa é converter via .astype (str) em string antes de agrupar. Isso conservará os NaNs.

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
M. Kiewisch
fonte
@ K3 --- rnc: Veja o comentário no seu link - o autor da postagem no seu link fez algo errado.
Thomas
@ Thomas, sim, exatamente como no exemplo acima. Edite se você pode tornar o exemplo seguro (e o mais trivial).
K3 --- rnc
O sumde aé uma concatenação de string aqui, não uma soma numérica. Isso apenas "funciona" porque 'b' consistia em entradas distintas. Você precisa de 'a' a ser numérico e 'b' para ser corda
BallpointBen
28

pandas> = 1.1

No pandas 1.1, você terá melhor controle sobre esse comportamento, agora os valores de NA são permitidos na garoupa 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

Você pode instalar a versão de pré-lançamento da v1.1 usando o seguinte comando:

pip install https://github.com/pandas-dev/pandas/releases/download/v1.1.0rc0/pandas-1.1.0rc0.tar.gz
cs95
fonte
4
Esperemos que esta resposta faça uma marcha gradual até o topo. É a abordagem correta.
kdbanman 01/06
Eu não acho que 1.1 foi lançado ainda. Verificado em conda e pip e as versões ainda existem 1.0.4
sammywemmy
1
@sammywemmy Sim, por enquanto isso só pode ser executado dentro de um ambiente de desenvolvimento . Eu gosto de ter uma vantagem quando se trata de introduzir novos recursos em postagens antigas de SO. ;-)
cs95 11/06
9

Não consigo adicionar um comentário a M. Kiewisch, pois não tenho pontos de reputação suficientes (só tenho 41, mas preciso de mais de 50 para comentar).

Enfim, só quero salientar que a solução M. Kiewisch não funciona como está e pode precisar de mais ajustes. Considere por exemplo

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

que mostra que, para o grupo b = 4,0, o valor correspondente é 15 em vez de 6. Aqui está apenas concatenando 1 e 5 como seqüências de caracteres, em vez de adicioná-lo como números.

Kamaraju Kusumanchi
fonte
12
Isso é porque você converteu todo o DF para str, em vez de apenas a bcoluna
Korem
Observe que isso foi corrigido na resposta mencionada agora.
Shaido - Restabelece Monica
1
A nova solução é melhor, mas ainda não é segura, na minha opinião. Considere um caso em que uma das entradas na coluna 'b' é igual à string np.NaN. Então essas coisas são batidas 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

Um pequeno ponto na solução de Andy Hayden - ela não funciona (mais?) Porque np.nan == np.nanproduz False, então areplace função não faz nada.

O que funcionou para mim foi o seguinte:

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

(Pelo menos esse é o comportamento do Pandas 0.19.2. Desculpe adicioná-lo como uma resposta diferente, não tenho reputação suficiente para comentar.)

Tuetschek
fonte
12
Há também df['b'].fillna(-1).
K3 --- rnc
6

Todas as respostas fornecidas até agora resultam em comportamento potencialmente perigoso, pois é bem possível que você selecione um valor simulado que faça parte do conjunto de dados. É cada vez mais provável que você crie grupos com muitos atributos. Simplificando, a abordagem nem sempre generaliza bem.

Uma solução menos invasiva é usar pd.drop_duplicates () para criar um índice exclusivo de combinações de valores, cada um com seu próprio ID e, em seguida, agrupar esse ID. É mais detalhado, mas faz o trabalho:

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]

Observe que agora você pode simplesmente fazer o seguinte:

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

Isso retornará o resultado bem-sucedido sem ter que se preocupar com a substituição de dados reais que são confundidos com um valor fictício.

Grant Langseth
fonte
Esta é a melhor solução para o caso geral, mas nos casos em que conheço uma string / número inválido que posso usar, provavelmente vou seguir a resposta de Andy Hayden abaixo ... Espero que os pandas corrigam esse comportamento em breve.
Sarah Messer
4

Eu já respondi isso, mas por algum motivo a resposta foi convertida em um comentário. No entanto, esta é a solução mais eficiente:

Não ser capaz de incluir (e propagar) NaNs em grupos é bastante agravante. Citar R não é convincente, pois esse comportamento não é consistente com muitas outras coisas. Enfim, o truque falso também é muito ruim. No entanto, o tamanho (inclui NaNs) e a contagem (ignora NaNs) de um grupo serão diferentes se houver NaNs.

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

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

Quando estes diferem, você pode definir o valor novamente como Nenhum para o resultado da função de agregação para esse grupo.

Brian Preslopsky
fonte
1
Isso foi super útil para mim, mas responde a uma pergunta um pouco diferente da original. IIUC, sua solução propaga NaNs na soma, mas os itens NaN na coluna "b" ainda são descartados como linhas.
Andrew
0

Pandas 1.1 instalados no Anaconda

Não consigo comentar a resposta do cs95, mas ele me ajudou a resolver o problema.

Tentei instalar o Pandas 1.1, mas ele falhou ao usar o código dele, por isso pesquisei no Google e consegui instalar.

Primeiro, execute o prompt do anaconda como administrador e cole o seguinte código:

pip install pandas==1.1.0rc0

Depois disso, inclua o uso dropna = False

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

EzrealReal
fonte
0

df = df.fillna("") isso funcionou para mim

Vineet Kumar
fonte