Construir pandas DataFrame a partir de itens no dicionário aninhado

90

Suponha que eu tenha um dicionário aninhado 'user_dict' com a estrutura:

  • Nível 1: UserId (número inteiro longo)
  • Nível 2: Categoria (String)
  • Nível 3: Atributos variados (flutuadores, ints, etc.)

Por exemplo, uma entrada deste dicionário seria:

user_dict[12] = {
    "Category 1": {"att_1": 1, 
                   "att_2": "whatever"},
    "Category 2": {"att_1": 23, 
                   "att_2": "another"}}

cada item em user_dicttem a mesma estrutura e user_dictcontém um grande número de itens que desejo alimentar a um DataFrame pandas, construindo a série a partir dos atributos. Nesse caso, um índice hierárquico seria útil para o propósito.

Especificamente, minha pergunta é se existe uma maneira de ajudar o construtor DataFrame a entender que a série deve ser construída a partir dos valores do "nível 3" no dicionário.

Se eu tentar algo como:

df = pandas.DataFrame(users_summary)

Os itens no "nível 1" (os UserId's) são considerados colunas, o que é o oposto do que desejo alcançar (ter UserId's como índice).

Sei que poderia construir a série após iterar as entradas do dicionário, mas se houver uma maneira mais direta, isso seria muito útil. Uma pergunta semelhante seria se é possível construir um DataFrame do pandas a partir de objetos json listados em um arquivo.

vladimir montealegre
fonte
Veja esta resposta para alternativas mais simples.
cs95 de

Respostas:

141

Um pandas MultiIndex consiste em uma lista de tuplas. Portanto, a abordagem mais natural seria remodelar seu dicionário de entrada para que suas chaves sejam tuplas correspondentes aos valores de vários índices que você precisa. Então, você pode simplesmente construir seu dataframe usando pd.DataFrame.from_dict, usando a opção orient='index':

user_dict = {12: {'Category 1': {'att_1': 1, 'att_2': 'whatever'},
                  'Category 2': {'att_1': 23, 'att_2': 'another'}},
             15: {'Category 1': {'att_1': 10, 'att_2': 'foo'},
                  'Category 2': {'att_1': 30, 'att_2': 'bar'}}}

pd.DataFrame.from_dict({(i,j): user_dict[i][j] 
                           for i in user_dict.keys() 
                           for j in user_dict[i].keys()},
                       orient='index')


               att_1     att_2
12 Category 1      1  whatever
   Category 2     23   another
15 Category 1     10       foo
   Category 2     30       bar

Uma abordagem alternativa seria construir seu dataframe concatenando os dataframes do componente:

user_ids = []
frames = []

for user_id, d in user_dict.iteritems():
    user_ids.append(user_id)
    frames.append(pd.DataFrame.from_dict(d, orient='index'))

pd.concat(frames, keys=user_ids)

               att_1     att_2
12 Category 1      1  whatever
   Category 2     23   another
15 Category 1     10       foo
   Category 2     30       bar
Wouter Overmeire
fonte
11
Existe uma maneira razoável de generalizar isso para trabalhar com listas irregulares de profundidade arbitrária? por exemplo, listas com uma profundidade arbitrária, onde alguns ramos podem ser mais curtos do que outros, e nenhum ou nan é usado quando ramos mais curtos não chegam ao fim?
naught101
5
Você já viu o suporte pandas json (ferramentas IO) e a normalização? pandas.pydata.org/pandas-docs/dev/io.html#normalization
Wouter Overmeire
1
para mim, o primeiro método criou um dataframe com um único índice com tuplas. o segundo método funcionou como desejado / esperado!
arturomp
Alguma dica sobre como nomear essas novas colunas? Por exemplo, se eu quiser que esses números 12 e 15 estejam na coluna 'id'.
cheremushkin
1
@cheremushkin 12 e 15 agora estão na linha 'id', se você transpor ( pandas.pydata.org/pandas-docs/stable/reference/api/… ) eles estão na coluna 'id'. Você também pode desempilhar ( pandas.pydata.org/pandas-docs/stable/reference/api/… ) Tudo depende do que você realmente precisa.
Wouter Overmeire
33

pd.concataceita um dicionário. Com isso em mente, é possível melhorar a resposta atualmente aceita em termos de simplicidade e desempenho, usando uma compreensão de dicionário para construir um dicionário de chaves de mapeamento para subquadros.

pd.concat({k: pd.DataFrame(v).T for k, v in user_dict.items()}, axis=0)

Ou,

pd.concat({
        k: pd.DataFrame.from_dict(v, 'index') for k, v in user_dict.items()
    }, 
    axis=0)

              att_1     att_2
12 Category 1     1  whatever
   Category 2    23   another
15 Category 1    10       foo
   Category 2    30       bar
cs95
fonte
4
Brilhante! Muito melhor :)
pg2455
3
Como você faria se ainda tivesse outra categoria interna? Como 12:{cat1:{cat11:{att1:val1,att2:val2}}}. Em outras palavras: como alguém poderia generalizar a solução para um número irrelevante de categorias?
Lucas Aimaretto
1
@LucasAimaretto Normalmente, estruturas aninhadas arbitrariamente podem ser niveladas com json_normalize. Tenho outra resposta que mostra como funciona.
cs95 de
1
Não funciona se vfor um único inteiro, por exemplo. Você conhece uma alternativa nesse caso?
sk
11

Eu costumava usar um loop for para iterar no dicionário também, mas uma coisa que descobri que funciona muito mais rápido é converter para um painel e depois para um dataframe. Digamos que você tenha um dicionário d

import pandas as pd
d
{'RAY Index': {datetime.date(2014, 11, 3): {'PX_LAST': 1199.46,
'PX_OPEN': 1200.14},
datetime.date(2014, 11, 4): {'PX_LAST': 1195.323, 'PX_OPEN': 1197.69},
datetime.date(2014, 11, 5): {'PX_LAST': 1200.936, 'PX_OPEN': 1195.32},
datetime.date(2014, 11, 6): {'PX_LAST': 1206.061, 'PX_OPEN': 1200.62}},
'SPX Index': {datetime.date(2014, 11, 3): {'PX_LAST': 2017.81,
'PX_OPEN': 2018.21},
datetime.date(2014, 11, 4): {'PX_LAST': 2012.1, 'PX_OPEN': 2015.81},
datetime.date(2014, 11, 5): {'PX_LAST': 2023.57, 'PX_OPEN': 2015.29},
datetime.date(2014, 11, 6): {'PX_LAST': 2031.21, 'PX_OPEN': 2023.33}}}

O comando

pd.Panel(d)
<class 'pandas.core.panel.Panel'>
Dimensions: 2 (items) x 2 (major_axis) x 4 (minor_axis)
Items axis: RAY Index to SPX Index
Major_axis axis: PX_LAST to PX_OPEN
Minor_axis axis: 2014-11-03 to 2014-11-06

onde pd.Panel (d) [item] produz um dataframe

pd.Panel(d)['SPX Index']
2014-11-03  2014-11-04  2014-11-05 2014-11-06
PX_LAST 2017.81 2012.10 2023.57 2031.21
PX_OPEN 2018.21 2015.81 2015.29 2023.33

Você pode então clicar no comando to_frame () para transformá-lo em um dataframe. Eu uso reset_index também para transformar os eixos principais e secundários em colunas, em vez de tê-los como índices.

pd.Panel(d).to_frame().reset_index()
major   minor      RAY Index    SPX Index
PX_LAST 2014-11-03  1199.460    2017.81
PX_LAST 2014-11-04  1195.323    2012.10
PX_LAST 2014-11-05  1200.936    2023.57
PX_LAST 2014-11-06  1206.061    2031.21
PX_OPEN 2014-11-03  1200.140    2018.21
PX_OPEN 2014-11-04  1197.690    2015.81
PX_OPEN 2014-11-05  1195.320    2015.29
PX_OPEN 2014-11-06  1200.620    2023.33

Finalmente, se você não gosta da aparência do quadro, pode usar a função transpor do painel para alterar a aparência antes de chamar to_frame () consulte a documentação aqui http://pandas.pydata.org/pandas-docs/dev/generated /pandas.Panel.transpose.html

Só como exemplo

pd.Panel(d).transpose(2,0,1).to_frame().reset_index()
major        minor  2014-11-03  2014-11-04  2014-11-05  2014-11-06
RAY Index   PX_LAST 1199.46    1195.323     1200.936    1206.061
RAY Index   PX_OPEN 1200.14    1197.690     1195.320    1200.620
SPX Index   PX_LAST 2017.81    2012.100     2023.570    2031.210
SPX Index   PX_OPEN 2018.21    2015.810     2015.290    2023.330

Espero que isto ajude.

Mishiko
fonte
8
O painel está obsoleto nas versões mais recentes do pandas (v0.23 no momento da escrita).
cs95 de
6

Caso alguém deseje obter o quadro de dados em um "formato longo" (os valores de folha são do mesmo tipo) sem multiindex, você pode fazer isso:

pd.DataFrame.from_records(
    [
        (level1, level2, level3, leaf)
        for level1, level2_dict in user_dict.items()
        for level2, level3_dict in level2_dict.items()
        for level3, leaf in level3_dict.items()
    ],
    columns=['UserId', 'Category', 'Attribute', 'value']
)

    UserId    Category Attribute     value
0       12  Category 1     att_1         1
1       12  Category 1     att_2  whatever
2       12  Category 2     att_1        23
3       12  Category 2     att_2   another
4       15  Category 1     att_1        10
5       15  Category 1     att_2       foo
6       15  Category 2     att_1        30
7       15  Category 2     att_2       bar

(Eu sei que a pergunta original provavelmente quer (I.) ter os níveis 1 e 2 como multiíndices e o nível 3 como colunas e (II.) Perguntar sobre outras maneiras além da iteração sobre os valores no dicionário. Mas espero que esta resposta ainda seja relevante e útil (I.): para pessoas como eu, que tentaram encontrar uma maneira de colocar o dicionário aninhado nesta forma e o Google só retorna esta pergunta e (II.): porque outras respostas também envolvem alguma iteração e eu acho isso abordagem flexível e fácil de ler; não tenho certeza sobre o desempenho, no entanto.)

Melkor.cz
fonte
0

Com base na resposta verificada, para mim funcionou melhor:

ab = pd.concat({k: pd.DataFrame(v).T for k, v in data.items()}, axis=0)
ab.T
El_1988
fonte