从嵌套字典中的条目构建pandas DataFrame

155
假设我有一个嵌套字典'user_dict',其结构如下:
  • 第一层: 用户Id (长整数)
  • 第二层: 类别 (字符串)
  • 第三层: 各种属性(浮点数、整数等)
例如,该字典的一个条目如下:
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的项目,从属性构建系列。在这种情况下,分层索引对此目的非常有用。
具体而言,我的问题是是否存在一种方法可以帮助DataFrame构造函数理解应该从字典中的“第3级”值构建系列?
如果我尝试类似于以下内容:
df = pandas.DataFrame(users_summary)

“级别1”中的项目(即UserId)被视为列,这与我想要实现的相反(将UserId作为索引)。

我知道我可以在迭代字典条目后构建系列,但如果有更直接的方法,那将非常有用。类似的问题是是否可以从文件中列出的json对象构造pandas数据框架。


请参考此答案以获取更简单的替代方案。 - cs95
7个回答

208

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

19
有没有一个合理的方法来泛化这个函数,使其能够处理任意深度的不规则嵌套列表?例如:可以处理任意深度的列表,其中一些分支可能比其他分支短,并且在较短的分支未达到末尾时使用None或nan。 - naught101
7
你是否查看过pandas的JSON支持(io工具)和归一化功能?http://pandas.pydata.org/pandas-docs/dev/io.html#normalization - Wouter Overmeire
1
对我来说,第一种方法创建了一个带有元组的单索引数据框。第二种方法按预期工作! - arturomp
1
@cheremushkin,12和15现在在行“id”中,如果你转置(https://pandas.pydata.org/pandas-docs/stable/reference/api/pandas.DataFrame.transpose.html),它们会在列“id”中。你也可以使用unstack(https://pandas.pydata.org/pandas-docs/stable/reference/api/pandas.DataFrame.unstack.html)。这完全取决于你真正需要什么。 - Wouter Overmeire
4
在Python 3中,字典不再有iteritems方法。在第二种方法中,这行代码 for user_id, d in user_dict.iteritems(): 需要改为 for user_id, d in user_dict.items(): - Madcat
显示剩余4条评论

65

pd.concat函数支持字典类型的参数。因此,可以通过使用字典推导式来构建一个将键映射到子数据帧的字典,从而在简化和性能方面改进当前已接受的答案。

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

或者,

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

7
太棒了!好多了 :) - pg2455
5
如果您还有进一步的内部类别,比如12:{cat1:{cat11:{att1:val1,att2:val2}}},您会如何处理?换句话说,如何将解决方案推广到无关类别的任意数量? - Lucas Aimaretto
3
通常使用 json_normalize 可以将任意嵌套的结构扁平化。我有另一个答案展示了它的工作原理。链接在此:another answer - cs95
1
如果 v 是一个单独的整数,那么这个方法不起作用。你知道在这种情况下有什么替代方法吗? - swiss_knight
如果我有三个嵌套的字典,例如dict = {L1: {L2: {L3: {L4: [array([},那么我应该如何编写代码呢? 我正在编写 pd.json_normalize(dict) 但是会出现错误 KeyError: '0.1' (因为我的 L1 是 0.1)。 - 0Knowledge
显示剩余3条评论

31

这个解决方案应该适用于任意深度,通过将字典键平铺为元组链。

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

4
这是最通用的解决方案,适用于n级深度,并且处理短分支。+1 - Wacao
这个解决方案简直神奇!感谢@Wacao分享。 - Ernesto

19

如果有人想以“长格式”(叶值具有相同类型)而不是多级索引的方式获取数据帧,可以执行以下操作:

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.)询问除了迭代字典中的值之外的其他方法。但我希望这个答案仍然相关和有用(I.):对像我这样的人来说,他们已经尝试找到将嵌套的字典转换成这种形式的方法,而谷歌只返回这个问题和(II.):因为其他答案也涉及一些迭代,我发现这种方法灵活且易于阅读;不确定性能如何。)


这对于可视化非常有用,我相信如果有人有时间,他们可以将其制作成一个函数,该函数仅接受字典和列列表作为参数,并从列中推断嵌套深度。 - grofte

12

我曾经也使用for循环来迭代字典,但是我发现更快的方法是将其转换为panel再转换为dataframe。假设你有一个名为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

最后,如果您不喜欢框架的外观,您可以使用panel的transpose函数在调用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
希望这有所帮助。

12
在最新版本的pandas中(截至撰写本文时为v0.23),Panel已被弃用。 - cs95

6

对于其他表示数据的方式,你不需要做太多事情。例如,如果你只是想让“外部”键成为索引,而“内部”键成为列,而值则成为单元格值,那么这样就可以了:

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



我收到了错误信息: Anaconda3\lib\site-packages\pandas\core\internals\construction.py:309: VisibleDeprecationWarning: 从不规则嵌套序列(即长度或形状不同的列表、元组或ndarray的列表或元组)创建ndarray已被弃用。如果您打算这样做,必须在创建ndarray时指定'dtype=object'。 values = np.array([convert(v) for v in values]) - PM0087
不适用于嵌套字典,其中值保持在字典形式中。如果您想要正确的输出形状(转置)作为起点仍然很好。 - questionto42
它适用于嵌套最多两层的 dict()。我还没有测试更多层级。 - odunayo12

5

在经过验证的答案基础上,对我来说这个方法效果最佳:

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

4
最好能详细说明。 - Saurabh Bade
1
这是正确的答案... 但似乎无法编辑,因为“建议的编辑队列已满”。 - Robbie Capps

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接