Konstruuj pandy DataFrame z elementów w zagnieżdżonym słowniku

90

Załóżmy, że mam zagnieżdżony słownik „user_dict” o strukturze:

  • Poziom 1: UserId (długa liczba całkowita)
  • Poziom 2: Kategoria (ciąg znaków)
  • Poziom 3: różne atrybuty (liczby zmiennoprzecinkowe, liczby wewnętrzne itp.)

Na przykład wpis w tym słowniku wyglądałby tak:

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

każdy element w user_dictma taką samą strukturę i user_dictzawiera dużą liczbę elementów, które chcę przekazać do pandy DataFrame, konstruując serię z atrybutów. W tym przypadku przydatny byłby indeks hierarchiczny.

W szczególności, moje pytanie brzmi: czy istnieje sposób, aby pomóc konstruktorowi DataFrame zrozumieć, że seria powinna być zbudowana z wartości „poziomu 3” w słowniku?

Jeśli spróbuję czegoś takiego:

df = pandas.DataFrame(users_summary)

Elementy na „poziomie 1” (UserId's) są traktowane jako kolumny, co jest przeciwieństwem tego, co chcę osiągnąć (mieć UserId jako indeks).

Wiem, że mógłbym skonstruować serię po iteracji po wpisach w słowniku, ale jeśli istnieje bardziej bezpośredni sposób, byłoby to bardzo przydatne. Podobnym pytaniem byłoby pytanie, czy możliwe jest skonstruowanie pandy DataFrame z obiektów json wymienionych w pliku.

vladimir montealegre
źródło
Zobacz tę odpowiedź, aby uzyskać prostsze alternatywy.
cs95

Odpowiedzi:

138

MultiIndex pandy składa się z listy krotek. Zatem najbardziej naturalnym podejściem byłoby przekształcenie dyktu wejściowego, tak aby jego klucze były krotkami odpowiadającymi wymaganym wartościom z wieloma indeksami. Następnie możesz po prostu skonstruować swoją ramkę danych za pomocą pd.DataFrame.from_dict, używając opcji 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

Alternatywnym podejściem byłoby zbudowanie ramki danych poprzez konkatenację składowych ramek danych:

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
źródło
11
Czy istnieje rozsądny sposób na uogólnienie tego, aby pracować z arbitralnie pogłębionymi listami? np. listy do dowolnej głębokości, gdzie niektóre gałęzie mogą być krótsze od innych, a brak lub nan jest używane, gdy krótsze gałęzie nie docierają do końca?
naught101
5
Czy spojrzałeś na obsługę pandas json (narzędzia io) i normalizację? pandas.pydata.org/pandas-docs/dev/io.html#normalization
Wouter Overmeire
1
dla mnie pierwsza metoda stworzyła ramkę danych z pojedynczym indeksem z krotkami. druga metoda zadziałała zgodnie z oczekiwaniami / oczekiwaniami!
arturomp
Jakieś wskazówki, jak nazwać te nowe kolumny? Na przykład, jeśli chcę, aby te liczby 12 i 15 znalazły się w kolumnie „id”.
Cheremushkin
1
@cheremushkin 12 i 15 znajdują się teraz w wierszu „id”, jeśli umieścisz je ( pandas.pydata.org/pandas-docs/stable/reference/api/… ), znajdują się w kolumnie „id”. Możesz także rozpakować ( pandas.pydata.org/pandas-docs/stable/reference/api/… ) Wszystko zależy od tego, czego naprawdę potrzebujesz.
Wouter Overmeire
31

pd.concatakceptuje słownik. Mając to na uwadze, możliwe jest ulepszenie obecnie przyjętej odpowiedzi pod względem prostoty i wydajności poprzez użycie rozumienia słownikowego do zbudowania słownika mapującego klucze do podramek.

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

Lub,

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
źródło
4
Znakomity! Dużo lepiej :)
pg2455
3
Jak byś to zrobił, gdybyś miał jeszcze inną wewnętrzną kategorię? Takich jak 12:{cat1:{cat11:{att1:val1,att2:val2}}}. Innymi słowy: jak ktoś mógłby uogólnić rozwiązanie na nieistotną liczbę kategorii?
Lucas Aimaretto
1
@LucasAimaretto Zwykle dowolnie zagnieżdżone struktury mogą być spłaszczane json_normalize. Mam inną odpowiedź, która pokazuje, jak to działa.
cs95
1
Nie działa, jeśli vna przykład jest pojedynczą liczbą całkowitą. Czy znasz alternatywę w takim przypadku?
sk
11

Więc użyłem pętli for również do iteracji w słowniku, ale jedna rzecz, która działa znacznie szybciej, to konwersja do panelu, a następnie do ramki danych. Powiedzmy, że masz słownik 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}}}

Komenda

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

gdzie pd.Panel (d) [item] daje ramkę danych

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

Następnie możesz nacisnąć polecenie to_frame (), aby przekształcić je w ramkę danych. Używam reset_index również do przekształcenia osi głównej i pomocniczej w kolumny, zamiast mieć je jako indeksy.

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

Wreszcie, jeśli nie podoba ci się wygląd ramki, możesz użyć funkcji transpozycji panelu, aby zmienić wygląd przed wywołaniem to_frame (), zobacz dokumentację tutaj http://pandas.pydata.org/pandas-docs/dev/generated /pandas.Panel.transpose.html

Jako przykład

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

Mam nadzieję że to pomoże.

Mishiko
źródło
8
Panel jest przestarzały w nowszych wersjach pand (wersja 0.23 w chwili pisania tego tekstu).
cs95
6

W przypadku, gdy ktoś chce pobrać ramkę danych w „długim formacie” (wartości liści mają ten sam typ) bez multiindeksu, możesz to zrobić:

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

(Wiem, że pierwotne pytanie prawdopodobnie chce (I.) mieć Poziomy 1 i 2 jako multiindeks, a Poziom 3 jako kolumny, a (II.) Pyta o inne sposoby niż iteracja wartości w dyktandzie. Mam jednak nadzieję, że ta odpowiedź jest nadal aktualna i użyteczne (I.): dla ludzi takich jak ja, którzy próbowali znaleźć sposób, aby uzyskać zagnieżdżony dykt w tym kształcie, a Google zwraca tylko to pytanie i (II.): ponieważ inne odpowiedzi również obejmują pewną iterację i znajduję to podejście elastyczne i łatwe do odczytania; nie jestem jednak pewien wydajności).

Melkor.cz
źródło
0

Opierając się na zweryfikowanej odpowiedzi, dla mnie zadziałało najlepiej:

ab = pd.concat({k: pd.DataFrame(v).T for k, v in data.items()}, axis=0)
ab.T
El_1988
źródło