Construya pandas DataFrame a partir de elementos en un diccionario anidado

90

Supongamos que tengo un diccionario anidado 'user_dict' con estructura:

  • Nivel 1: UserId (entero largo)
  • Nivel 2: Categoría (cadena)
  • Nivel 3: Atributos surtidos (flotadores, entradas, etc.)

Por ejemplo, una entrada de este diccionario sería:

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

cada elemento user_dicttiene la misma estructura y user_dictcontiene una gran cantidad de elementos que quiero alimentar a un DataFrame de pandas, construyendo la serie a partir de los atributos. En este caso, un índice jerárquico sería útil para este propósito.

Específicamente, mi pregunta es si existe una forma de ayudar al constructor de DataFrame a comprender que la serie debe construirse a partir de los valores del "nivel 3" en el diccionario.

Si intento algo como:

df = pandas.DataFrame(users_summary)

Los elementos en el "nivel 1" (los UserId) se toman como columnas, que es lo opuesto a lo que quiero lograr (tener UserId como índice).

Sé que podría construir la serie después de iterar sobre las entradas del diccionario, pero si hubiera una forma más directa, sería muy útil. Una pregunta similar sería preguntar si es posible construir un DataFrame de pandas a partir de objetos json enumerados en un archivo.

vladimir montealegre
fuente
Consulte esta respuesta para conocer alternativas más simples.
cs95

Respuestas:

138

Un MultiIndex de pandas consiste en una lista de tuplas. Por lo tanto, el enfoque más natural sería remodelar su dictado de entrada para que sus claves sean tuplas correspondientes a los valores de índices múltiples que necesita. Luego puede simplemente construir su marco de datos usando pd.DataFrame.from_dict, usando la opción 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

Un enfoque alternativo sería construir su marco de datos concatenando los marcos de datos de los componentes:

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
fuente
11
¿Existe una forma razonable de generalizar esto para trabajar con listas irregulares de profundidad arbitraria? por ejemplo, listas a una profundidad arbitraria, donde algunas ramas pueden ser más cortas que otras, y se usa None o nan cuando las ramas más cortas no llegan al final.
naught101
5
¿Ha observado la normalización y la compatibilidad con pandas json (herramientas io)? pandas.pydata.org/pandas-docs/dev/io.html#normalization
Wouter Overmeire
1
para mí, el primer método creó un marco de datos con un único índice con tuplas. ¡el segundo método funcionó como se esperaba / se esperaba!
arturomp
¿Algún consejo sobre cómo nombrar estas nuevas columnas? Por ejemplo, si quiero que estos números 12 y 15 estén en la columna 'id'.
cheremushkin
1
@cheremushkin 12 y 15 ahora están en la fila 'id', si transpone ( pandas.pydata.org/pandas-docs/stable/reference/api/… ) están en la columna 'id'. También puede desapilar ( pandas.pydata.org/pandas-docs/stable/reference/api/… ) Todo depende de lo que realmente necesite.
Wouter Overmeire
31

pd.concatacepta un diccionario. Con esto en mente, es posible mejorar la respuesta actualmente aceptada en términos de simplicidad y rendimiento mediante el uso de una comprensión de diccionario para construir un diccionario que mapee las claves de los sub-marcos.

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

O,

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
fuente
4
¡Brillante! Mucho mejor :)
pg2455
3
¿Cómo lo harías si todavía tuvieras una categoría interna adicional? Como 12:{cat1:{cat11:{att1:val1,att2:val2}}}. En otras palabras: ¿cómo podría alguien generalizar la solución a un número irrelevante de categorías?
Lucas Aimaretto
1
@LucasAimaretto Normalmente, las estructuras anidadas arbitrariamente se pueden aplanar con json_normalize. Tengo otra respuesta que muestra cómo funciona.
cs95
1
No funciona si ves un único entero, por ejemplo. ¿Conoce alguna alternativa en tal caso?
sk
11

Así que solía usar un bucle for para iterar a través del diccionario también, pero una cosa que encontré que funciona mucho más rápido es convertir a un panel y luego a un marco de datos. Digamos que tienes un diccionario 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}}}

El 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

donde pd.Panel (d) [item] produce un marco de datos

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

Luego puede presionar el comando to_frame () para convertirlo en un marco de datos. También uso reset_index para convertir los ejes mayor y menor en columnas en lugar de tenerlos 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, si no le gusta la apariencia del marco, puede usar la función de transposición del panel para cambiar la apariencia antes de llamar a to_frame () ver la documentación aquí http://pandas.pydata.org/pandas-docs/dev/generated /pandas.Panel.transpose.html

Solo como un ejemplo

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 esto ayude.

Mishiko
fuente
8
El panel está obsoleto en las versiones más recientes de pandas (v0.23 en el momento de escribir este artículo).
cs95
6

En caso de que alguien quiera obtener el marco de datos en un "formato largo" (los valores de hoja tienen el mismo tipo) sin multiindex, puede hacer esto:

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

(Sé que la pregunta original probablemente quiera que (I.) tenga los Niveles 1 y 2 como multiíndice y el Nivel 3 como columnas y (II.) Pregunta sobre otras formas además de la iteración sobre los valores en el dict. Pero espero que esta respuesta siga siendo relevante y útil (I.): para personas como yo que han intentado encontrar una manera de obtener el dict anidado en esta forma y Google solo devuelve esta pregunta y (II.): porque otras respuestas también implican alguna iteración y encuentro esto enfoque flexible y fácil de leer; sin embargo, no estoy seguro del rendimiento).

Melkor.cz
fuente
0

Sobre la base de una respuesta verificada, para mí esto funcionó mejor:

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