Создайте pandas DataFrame из элементов во вложенном словаре

Предположим, у меня есть вложенный словарь user_dict со структурой:

  • Уровень 1: идентификатор пользователя (длинное целое число)
  • Уровень 2: категория (строка)
  • Уровень 3: различные атрибуты (числа с плавающей запятой, целые числа и т. д.)

Например, запись этого словаря будет:

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

каждый элемент в user_dict имеет одинаковую структуру, а user_dict содержит большое количество элементов, которые я хочу передать в кадр данных pandas, создавая серию из атрибутов. В этом случае для этой цели будет полезен иерархический индекс.

В частности, мой вопрос заключается в том, существует ли способ помочь конструктору DataFrame понять, что серия должна быть построена из значений «уровня 3» в словаре?

Если я попробую что-то вроде:

df = pandas.DataFrame(users_summary)

Элементы на «уровне 1» (UserId) принимаются как столбцы, что противоположно тому, чего я хочу достичь (иметь UserId в качестве индекса).

Я знаю, что мог бы построить серию после повторения записей словаря, но если есть более прямой способ, это было бы очень полезно. Аналогичный вопрос будет заключаться в том, можно ли создать pandas DataFrame из объектов json, перечисленных в файле.


person vladimir montealegre    schedule 26.11.2012    source источник
comment
См. этот ответ для более простых альтернатив.   -  person cs95    schedule 22.01.2019


Ответы (7)


Pandas MultiIndex состоит из списка кортежей. Таким образом, наиболее естественным подходом было бы изменить форму вашего входного словаря так, чтобы его ключи были кортежами, соответствующими требуемым значениям нескольких индексов. Затем вы можете просто построить свой фрейм данных, используя pd.DataFrame.from_dict, используя опцию 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

Альтернативным подходом было бы создание вашего фрейма данных путем объединения фреймов данных компонентов:

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
person Wouter Overmeire    schedule 27.11.2012
comment
Есть ли разумный способ обобщить это для работы со списками произвольной глубины? например списки произвольной глубины, где некоторые ветки могут быть короче других, а None или nan используются, когда более короткие ветки не достигают конца? - person naught101; 11.11.2013
comment
Вы смотрели на поддержку pandas json (инструменты io) и нормализацию? pandas.pydata.org/pandas-docs/dev/io.html# нормализация - person Wouter Overmeire; 12.11.2013
comment
для меня первый метод создал фрейм данных с одним индексом с кортежами. второй метод работал так, как хотелось/ожидалось! - person arturomp; 30.04.2018
comment
Любые советы о том, как назвать эти новые столбцы? Например, если я хочу, чтобы эти числа 12 и 15 были в столбце «id». - person cheremushkin; 02.03.2019
comment
@cheremushkin 12 и 15 теперь находятся в строке «id», если вы транспонируете (pandas.pydata.org/pandas-docs/stable/reference/api/) они находятся в столбце "id". Вы также можете распаковать (pandas.pydata.org /pandas-docs/stable/reference/api/) Все зависит от того, что вам действительно нужно. - person Wouter Overmeire; 03.03.2019
comment
@WouterOvermeire, но если я хочу отсортировать по имени этого столбца, я получаю ['id'] не в индексе - person cheremushkin; 06.03.2019
comment
@cheremushkin Не могли бы вы открыть новый вопрос о SO? - person Wouter Overmeire; 06.03.2019
comment
в python 3 у dict больше нет метода iteritems, во втором подходе эта строка for user_id, d in user_dict.iteritems(): должна быть изменена for user_id, d in user_dict.items(): - person Madcat; 29.08.2020
comment
Вау... Второй метод настолько интуитивно понятен и прост, что мне удалось сделать ТОЧНО то, что я хотел, даже если я не знал, как использовать панд, так как это мой первый раз с ним. Отличное объяснение @WouterOvermeire... Спасибо, что нашли время - person Luke Savefrogs; 03.02.2021

pd.concat принимает словарь. Имея это в виду, можно улучшить принятый в настоящее время ответ с точки зрения простоты и производительности, используя понимание словаря для создания словаря, отображающего ключи на подкадры.

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

Or,

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
person cs95    schedule 22.01.2019
comment
Великолепно! Намного лучше :) - person pg2455; 28.03.2019
comment
Как бы вы поступили, если бы у вас была еще одна внутренняя категория? Например, 12:{cat1:{cat11:{att1:val1,att2:val2}}}. Другими словами: как можно обобщить решение для нерелевантного числа категорий? - person Lucas Aimaretto; 17.09.2019
comment
@LucasAimaretto Обычно произвольно вложенные структуры можно сгладить с помощью json_normalize. У меня есть другой ответ, который показывает, как это работает. - person cs95; 18.09.2019
comment
Не работает, например, если v является одним целым числом. Знаете ли вы альтернативу в таком случае? - person s.k; 09.04.2020
comment
Почему бы не рассматривать как вложенный json? pd.json_normalize должен сделать всю работу за вас - person Tito Sanz; 19.01.2021
comment
@TitoSanz Ну, ты пробовал? Как это работает? - person cs95; 30.01.2021
comment
Я опубликовал ответ, который должен работать для произвольной глубины - person tRosenflanz; 25.03.2021
comment
@ cs95 Если у меня есть 3 вложенных словаря, подобных этому dict = {L1: {L2: {L3: {L4: [array([, то как мне писать? Я пишу pd.json_normalize(dict), но получаю ошибку KeyError: '0.1' (поскольку мой L1 равен 0,1) - person 0Knowledge; 25.05.2021

Поэтому я использовал цикл for для итерации по словарю, но я обнаружил, что одна вещь работает намного быстрее, это преобразование в панель, а затем в фрейм данных. Скажем, у вас есть словарь 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}}}

Команда

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

где pd.Panel(d)[item] дает кадр данных

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

Затем вы можете нажать команду to_frame(), чтобы превратить ее в фрейм данных. Я также использую reset_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

Наконец, если вам не нравится, как выглядит фрейм, вы можете использовать функцию транспонирования панели, чтобы изменить внешний вид перед вызовом to_frame(), см. документацию здесь http://pandas.pydata.org/pandas-docs/dev/generated/pandas.Panel.transpose.html

Просто как пример

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

Надеюсь это поможет.

person Mishiko    schedule 07.11.2014
comment
Panel устарела в более поздних версиях pandas (v0.23 на момент написания статьи). - person cs95; 22.01.2019

Если кто-то хочет получить фрейм данных в «длинном формате» (конечные значения имеют один и тот же тип) без мультииндекса, вы можете сделать это:

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

(Я знаю, что исходный вопрос, вероятно, хочет, чтобы (I.) имел уровни 1 и 2 как мультииндекс и уровень 3 как столбцы, и (II.) спрашивает о других способах, кроме итерации значений в dict. Но я надеюсь, что этот ответ все еще актуален и полезно (I.): для таких людей, как я, которые пытались найти способ привести вложенный дикт в эту форму, и Google возвращает только этот вопрос и (II.): потому что другие ответы также включают некоторую итерацию, и я нахожу это подход гибкий и легко читаемый, но не уверен в производительности.)

person Melkor.cz    schedule 30.03.2020

Для других способов представления данных вам не нужно много делать. Например, если вы просто хотите, чтобы внешний ключ был индексом, внутренний ключ был столбцом, а значения были значениями ячейки, это поможет:

df = pd.DataFrame.from_dict(user_dict, orient='index')


person Amogh Joshi    schedule 09.03.2021
comment
Я получаю сообщение об ошибке: Anaconda3\lib\site-packages\pandas\core\internals\construction.py:309: VisibleDeprecationWarning: Creating an ndarray from ragged nested sequences (which is a list-or-tuple of lists-or-tuples-or ndarrays with different lengths or shapes) is deprecated. If you meant to do this, you must specify 'dtype=object' when creating the ndarray values = np.array([convert(v) for v in values]) - person PM0087; 05.04.2021
comment
Спасибо за простое решение!!! @амог - person karthik; 24.06.2021

Это решение должно работать для произвольной глубины, объединяя ключи словаря в цепочку кортежей.

def flatten_dict(nested_dict):
    res = {}
    if isinstance(nested_dict, dict):
        for k in nested_dict:
            flattened_dict = flatten_dict(nested_dict[k])
            for key, val in flattened_dict.items():
                key = list(key)
                key.insert(0, k)
                res[tuple(key)] = val
    else:
        res[()] = nested_dict
    return res


def nested_dict_to_df(values_dict):
    flat_dict = flatten_dict(values_dict)
    df = pd.DataFrame.from_dict(flat_dict, orient="index")
    df.index = pd.MultiIndex.from_tuples(df.index)
    df = df.unstack(level=-1)
    df.columns = df.columns.map("{0[1]}".format)
    return df
person tRosenflanz    schedule 24.03.2021

Основываясь на проверенном ответе, для меня это сработало лучше всего:

ab = pd.concat({k: pd.DataFrame(v).T for k, v in data.items()}, axis=0)
ab.T
person El_1988    schedule 05.10.2020
comment
Было бы лучше, если бы вы могли объяснить в деталях. - person Saurabh Bade; 06.04.2021