Divisão de dicionário / lista dentro de uma coluna do Pandas em colunas separadas

146

Tenho dados salvos em um banco de dados postgreSQL. Estou consultando esses dados usando Python2.7 e transformando-os em um DataFrame do Pandas. No entanto, a última coluna desse quadro de dados possui um dicionário (ou lista?) De valores. O DataFrame fica assim:

[1] df
Station ID     Pollutants
8809           {"a": "46", "b": "3", "c": "12"}
8810           {"a": "36", "b": "5", "c": "8"}
8811           {"b": "2", "c": "7"}
8812           {"c": "11"}
8813           {"a": "82", "c": "15"}

Eu preciso dividir esta coluna em colunas separadas para que o DataFrame fique assim:

[2] df2
Station ID     a      b       c
8809           46     3       12
8810           36     5       8
8811           NaN    2       7
8812           NaN    NaN     11
8813           82     NaN     15

O principal problema que estou enfrentando é que as listas não são do mesmo tamanho. Mas todas as listas contêm apenas os mesmos três valores: a, bec. E eles sempre aparecem na mesma ordem (um primeiro, b segundo, c terceiro).

O código a seguir USADO para trabalhar e retornar exatamente o que eu queria (df2).

[3] df 
[4] objs = [df, pandas.DataFrame(df['Pollutant Levels'].tolist()).iloc[:, :3]]
[5] df2 = pandas.concat(objs, axis=1).drop('Pollutant Levels', axis=1)
[6] print(df2)

Eu estava executando esse código apenas na semana passada e estava funcionando bem. Mas agora meu código está quebrado e recebo esse erro da linha [4]:

IndexError: out-of-bounds on slice (end) 

Não fiz alterações no código, mas agora estou recebendo o erro. Sinto que isso se deve ao fato de meu método não ser robusto ou adequado.

Qualquer sugestão ou orientação sobre como dividir esta coluna de listas em colunas separadas seria super apreciada!

EDIT: Eu acho que os métodos .tolist () e .apply não estão funcionando no meu código porque é uma string unicode, ou seja:

#My data format 
u{'a': '1', 'b': '2', 'c': '3'}

#and not
{u'a': '1', u'b': '2', u'c': '3'}

Os dados estão sendo importados do banco de dados postgreSQL neste formato. Alguma ajuda ou idéias para esse problema? existe uma maneira de converter o unicode?

llaffin
fonte
Respondi com uma solução um pouco diferente, mas seu código também deve funcionar muito bem. Usando meu exemplo fictício abaixo, isso funciona usando pandas 0.18.1 se eu deixar de fora a ilocparte
Joris
Parte disso iloc[:, :3]pressupõe que haverá 3 itens e talvez as fatias de dados mais recentes tenham apenas 1 ou 2 (por exemplo, por exemplo, não há bcomo index 8813).
Dwanderson 06/07/19

Respostas:

166

Para converter a string em um ditado real, você pode fazer df['Pollutant Levels'].map(eval). Posteriormente, a solução abaixo pode ser usada para converter o ditado em colunas diferentes.


Usando um pequeno exemplo, você pode usar .apply(pd.Series):

In [2]: df = pd.DataFrame({'a':[1,2,3], 'b':[{'c':1}, {'d':3}, {'c':5, 'd':6}]})

In [3]: df
Out[3]:
   a                   b
0  1           {u'c': 1}
1  2           {u'd': 3}
2  3  {u'c': 5, u'd': 6}

In [4]: df['b'].apply(pd.Series)
Out[4]:
     c    d
0  1.0  NaN
1  NaN  3.0
2  5.0  6.0

Para combiná-lo com o restante do quadro de dados, você pode concatoutras colunas com o resultado acima:

In [7]: pd.concat([df.drop(['b'], axis=1), df['b'].apply(pd.Series)], axis=1)
Out[7]:
   a    c    d
0  1  1.0  NaN
1  2  NaN  3.0
2  3  5.0  6.0

Usando seu código, isso também funciona se eu deixar de fora a ilocparte:

In [15]: pd.concat([df.drop('b', axis=1), pd.DataFrame(df['b'].tolist())], axis=1)
Out[15]:
   a    c    d
0  1  1.0  NaN
1  2  NaN  3.0
2  3  5.0  6.0
joris
fonte
2
Estou usando pd.DataFrame(df[col].tolist())há muito tempo, nunca pensei nisso apply(pd.Series). Muito agradável.
ayhan 6/07/2016
1
Agora percebo o problema. O .apply (pd.Series) não está funcionando no meu conjunto de dados porque a linha inteira é uma string unicode. É: u '{' a ':' 1 ',' b ':' 2 ',' c ':' 3 '} e não {u'a': '1', u'b ':' 2 ', u'c ':' 3 '} como mostram suas soluções. Portanto, o código não pode dividi-lo em três colunas reconhecíveis.
21136 llaffin
2
@ayhan Na verdade, testei, e a DataFrame(df['col'].tolist())abordagem é bem mais rápida que a abordagem de aplicação!
Joris
3
@llaffin Se for uma string, você pode convertê-lo em um dict real com df[col].map(eval)antes de convertê-lo para uma trama de dados
Joris
2
Funciona perfeito, mas é (muito) mais lento do que a nova solução (2019) contribuíram por Lech Birek stackoverflow.com/a/55355928/2721710
drasc
85

Eu sei que a pergunta é bastante antiga, mas cheguei aqui procurando respostas. Atualmente, existe uma maneira melhor (e mais rápida) de fazer isso usando json_normalize:

import pandas as pd

df2 = pd.json_normalize(df['Pollutant Levels'])

Isso evita funções caras de aplicação ...

Lech Birek
fonte
4
Uau! Venho fazendo funções tediosas e confusas de aplicação o dia inteiro no Pandas em objetos JSON, e então me deparei com essa resposta e pensei "De jeito nenhum, não poderia ter sido tão fácil!" Então eu tentei e foi. Muito obrigado!
Emac
O único problema aqui é que ele não parece copiar sobre outras colunas sem o json, ou seja, se você estiver tentando normalizar uma linha de valores do json, precisará copiá-lo e combinar os dois, ainda muito melhor do que minha iterativa método. Cudos!
Mr.Drew 21/01
para esta solução, como seria possível selecionar dinamicamente a lista de quais colunas precisam ser normalizadas? Os dados transacionais que estou trazendo dos .jsonarquivos são provenientes de fontes diferentes e nem sempre são as mesmas colunas aninhadas. Eu tenho tentado encontrar uma maneira de criar uma lista de colunas que contêm ditados, mas não consigo resolver isso
Callum Smyth
5
from pandas.io.json import json_normalize
Ramin Melikov 25/04
Existe uma maneira de aplicar um prefixo às colunas finais? Eu notei que existem argumentos como meta_prefixe record_prefix. Embora eu não possa fazer isso funcionar com meu dataframe (o dataframe final está correto no meu caso, mas gostaria de aplicar os prefixos).
J. Snow
21

Tente o seguinte: Os dados retornados do SQL devem ser convertidos em um Dict. ou poderia ser "Pollutant Levels" agoraPollutants'

   StationID                   Pollutants
0       8809  {"a":"46","b":"3","c":"12"}
1       8810   {"a":"36","b":"5","c":"8"}
2       8811            {"b":"2","c":"7"}
3       8812                   {"c":"11"}
4       8813          {"a":"82","c":"15"}


df2["Pollutants"] = df2["Pollutants"].apply(lambda x : dict(eval(x)) )
df3 = df2["Pollutants"].apply(pd.Series )

    a    b   c
0   46    3  12
1   36    5   8
2  NaN    2   7
3  NaN  NaN  11
4   82  NaN  15


result = pd.concat([df, df3], axis=1).drop('Pollutants', axis=1)
result

   StationID    a    b   c
0       8809   46    3  12
1       8810   36    5   8
2       8811  NaN    2   7
3       8812  NaN  NaN  11
4       8813   82  NaN  15
Merlin
fonte
13

A resposta de Merlin é melhor e super fácil, mas não precisamos de uma função lambda. A avaliação do dicionário pode ser ignorada com segurança por uma das duas maneiras a seguir, conforme ilustrado abaixo:

Caminho 1: dois passos

# step 1: convert the `Pollutants` column to Pandas dataframe series
df_pol_ps = data_df['Pollutants'].apply(pd.Series)

df_pol_ps:
    a   b   c
0   46  3   12
1   36  5   8
2   NaN 2   7
3   NaN NaN 11
4   82  NaN 15

# step 2: concat columns `a, b, c` and drop/remove the `Pollutants` 
df_final = pd.concat([df, df_pol_ps], axis = 1).drop('Pollutants', axis = 1)

df_final:
    StationID   a   b   c
0   8809    46  3   12
1   8810    36  5   8
2   8811    NaN 2   7
3   8812    NaN NaN 11
4   8813    82  NaN 15

Caminho 2: As duas etapas acima podem ser combinadas de uma só vez:

df_final = pd.concat([df, df['Pollutants'].apply(pd.Series)], axis = 1).drop('Pollutants', axis = 1)

df_final:
    StationID   a   b   c
0   8809    46  3   12
1   8810    36  5   8
2   8811    NaN 2   7
3   8812    NaN NaN 11
4   8813    82  NaN 15
Hafizur Rahman
fonte
13

Eu recomendo fortemente que o método extraia a coluna 'Poluentes':

df_pollutants = pd.DataFrame(df['Pollutants'].values.tolist(), index=df.index)

é muito mais rápido que

df_pollutants = df['Pollutants'].apply(pd.Series)

quando o tamanho de df é gigante.

user9815968
fonte
Seria ótimo se você pudesse explicar como / por que isso funciona e é muito melhor! para mim é sempre mais rápido, e ~ 200 vezes mais rápido uma vez que você obter mais de ~ 1000 linhas
Sam Mason
O @SamMason, quando você faz applytodo o quadro de dados, é gerenciado por pandas, mas quando se trata valuesdele, ele brinca apenas com o numpy ndarraysque é intrinsecamente mais rápido, devido ao fato de ter cimplementações puras .
Sagar Kar
8

Você pode usar joincom pop+ tolist. O desempenho é comparável ao concatcom drop+ tolist, mas alguns podem encontrar este limpador de sintaxe:

res = df.join(pd.DataFrame(df.pop('b').tolist()))

Benchmarking com outros métodos:

df = pd.DataFrame({'a':[1,2,3], 'b':[{'c':1}, {'d':3}, {'c':5, 'd':6}]})

def joris1(df):
    return pd.concat([df.drop('b', axis=1), df['b'].apply(pd.Series)], axis=1)

def joris2(df):
    return pd.concat([df.drop('b', axis=1), pd.DataFrame(df['b'].tolist())], axis=1)

def jpp(df):
    return df.join(pd.DataFrame(df.pop('b').tolist()))

df = pd.concat([df]*1000, ignore_index=True)

%timeit joris1(df.copy())  # 1.33 s per loop
%timeit joris2(df.copy())  # 7.42 ms per loop
%timeit jpp(df.copy())     # 7.68 ms per loop
jpp
fonte
3

Uma solução de linha é a seguinte:

>>> df = pd.concat([df['Station ID'], df['Pollutants'].apply(pd.Series)], axis=1)
>>> print(df)
   Station ID    a    b   c
0        8809   46    3  12
1        8810   36    5   8
2        8811  NaN    2   7
3        8812  NaN  NaN  11
4        8813   82  NaN  15
Jaroslav Bezděk
fonte
1

my_df = pd.DataFrame.from_dict(my_dict, orient='index', columns=['my_col'])

.. teria analisado o dict corretamente (colocando cada chave de dict em uma coluna df separada e valores de chave em linhas df), para que os dict não fossem compactados em uma única coluna em primeiro lugar.

mirekphd
fonte
0

Concatenei essas etapas em um método, você deve passar apenas o dataframe e a coluna que contém o comando para expandir:

def expand_dataframe(dw: pd.DataFrame, column_to_expand: str) -> pd.DataFrame:
    """
    dw: DataFrame with some column which contain a dict to expand
        in columns
    column_to_expand: String with column name of dw
    """
    import pandas as pd

    def convert_to_dict(sequence: str) -> Dict:
        import json
        s = sequence
        json_acceptable_string = s.replace("'", "\"")
        d = json.loads(json_acceptable_string)
        return d    

    expanded_dataframe = pd.concat([dw.drop([column_to_expand], axis=1),
                                    dw[column_to_expand]
                                    .apply(convert_to_dict)
                                    .apply(pd.Series)],
                                    axis=1)
    return expanded_dataframe
Emanuel Fontelles
fonte
-1
df = pd.concat([df['a'], df.b.apply(pd.Series)], axis=1)
Siraj S.
fonte