Construire des pandas DataFrame à partir d'éléments dans un dictionnaire imbriqué

90

Supposons que j'ai un dictionnaire imbriqué 'user_dict' avec la structure:

  • Niveau 1: UserId (Long Integer)
  • Niveau 2: Catégorie (chaîne)
  • Niveau 3: Attributs assortis (flottants, entiers, etc.)

Par exemple, une entrée de ce dictionnaire serait:

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

chaque élément de user_dicta la même structure et user_dictcontient un grand nombre d'éléments que je veux nourrir à un pandas DataFrame, en construisant la série à partir des attributs. Dans ce cas, un index hiérarchique serait utile à cette fin.

Plus précisément, ma question est de savoir s'il existe un moyen d'aider le constructeur DataFrame à comprendre que la série doit être construite à partir des valeurs du «niveau 3» dans le dictionnaire?

Si j'essaye quelque chose comme:

df = pandas.DataFrame(users_summary)

Les éléments du "niveau 1" (les UserId) sont considérés comme des colonnes, ce qui est l'opposé de ce que je veux réaliser (avoir UserId comme index).

Je sais que je pourrais construire la série après avoir parcouru les entrées du dictionnaire, mais s'il existe un moyen plus direct, ce serait très utile. Une question similaire serait de demander s'il est possible de construire un pandas DataFrame à partir d'objets json répertoriés dans un fichier.

Vladimir Montealegre
la source
Voir cette réponse pour des alternatives plus simples.
cs95

Réponses:

138

Un pandas MultiIndex se compose d'une liste de tuples. L'approche la plus naturelle serait donc de remodeler votre dict d'entrée pour que ses clés soient des tuples correspondant aux valeurs multi-index dont vous avez besoin. Ensuite, vous pouvez simplement construire votre dataframe en utilisant pd.DataFrame.from_dict, en utilisant l'option 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

Une approche alternative consisterait à créer votre dataframe en concaténant les dataframes des composants:

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
la source
11
Existe-t-il un moyen raisonnable de généraliser cela pour travailler avec des listes irrégulières de profondeur arbitraire? par exemple des listes à une profondeur arbitraire, où certaines branches peuvent être plus courtes que d'autres, et un None ou nan est utilisé lorsque les branches plus courtes n'atteignent pas la fin?
rien101
5
Avez-vous examiné le support et la normalisation de pandas json (outils io)? pandas.pydata.org/pandas-docs/dev/io.html#normalization
Wouter Overmeire
1
pour moi, la première méthode a créé un dataframe avec un seul index avec des tuples. la deuxième méthode a fonctionné comme souhaité / attendu!
arturomp
Des conseils sur la façon de nommer ces nouvelles colonnes? Par exemple, si je veux que ces nombres 12 et 15 soient dans la colonne «id».
cheremushkin
1
@cheremushkin 12 et 15 sont maintenant dans la ligne 'id', si vous transposez ( pandas.pydata.org/pandas-docs/stable/reference/api/… ) ils sont dans la colonne 'id'. Vous pouvez également désempiler ( pandas.pydata.org/pandas-docs/stable/reference/api/… ) Tout dépend de ce dont vous avez vraiment besoin.
Wouter Overmeire
31

pd.concataccepte un dictionnaire. Dans cet esprit, il est possible d'améliorer la réponse actuellement acceptée en termes de simplicité et de performance en utilisant une compréhension de dictionnaire pour construire un dictionnaire mappant des clés à des sous-trames.

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
la source
4
Brillant! Bien mieux :)
pg2455
3
Comment feriez-vous si vous aviez encore une autre catégorie intérieure? Tels que 12:{cat1:{cat11:{att1:val1,att2:val2}}}. En d'autres termes: comment quelqu'un généraliserait-il la solution à un nombre non pertinent de catégories?
Lucas Aimaretto
1
@LucasAimaretto Habituellement, les structures imbriquées arbitrairement peuvent être aplaties avec json_normalize. J'ai une autre réponse qui montre comment cela fonctionne.
cs95 le
1
Ne fonctionne pas s'il vs'agit d'un seul entier par exemple. Connaissez-vous une alternative dans ce cas?
sk
11

J'avais donc l'habitude d'utiliser une boucle for pour parcourir le dictionnaire également, mais une chose que j'ai trouvée qui fonctionne beaucoup plus rapidement est de convertir en panneau puis en dataframe. Disons que vous avez un dictionnaire 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}}}

La commande

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

où pd.Panel (d) [item] donne une trame de données

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

Vous pouvez ensuite appuyer sur la commande to_frame () pour le transformer en dataframe. J'utilise aussi reset_index pour transformer les axes majeur et mineur en colonnes plutôt que de les avoir comme index.

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

Enfin, si vous n'aimez pas l'apparence du cadre, vous pouvez utiliser la fonction de transposition du panneau pour changer l'apparence avant d'appeler to_frame () voir la documentation ici http://pandas.pydata.org/pandas-docs/dev/generated /pandas.Panel.transpose.html

Juste à titre d'exemple

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

J'espère que cela t'aides.

Mishiko
la source
8
Panel est obsolète dans les versions plus récentes de pandas (v0.23 au moment de la rédaction).
cs95
6

Si quelqu'un souhaite obtenir la trame de données dans un "format long" (les valeurs de feuille ont le même type) sans multi-index, vous pouvez le faire:

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

(Je sais que la question d'origine veut probablement que (I.) ait les niveaux 1 et 2 comme multi-index et le niveau 3 comme colonnes et (II.) Demande d'autres moyens que l'itération sur les valeurs dans le dict. Mais j'espère que cette réponse est toujours pertinente et utile (I.): aux personnes comme moi qui ont essayé de trouver un moyen d'obtenir le dict imbriqué dans cette forme et Google ne renvoie que cette question et (II.): parce que d'autres réponses impliquent également une itération et je trouve ceci approche flexible et facile à lire; pas sûr de la performance, cependant.)

Melkor.cz
la source
0

En s'appuyant sur une réponse vérifiée, pour moi, cela a fonctionné le mieux:

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