【问题标题】:Flatten lists of dicts on specified keys展平指定键上的 dicts 列表
【发布时间】:2023-03-26 03:10:01
【问题描述】:

目标:从列包含 JSON(数组)的 SQL 表中读取数据,从 JSON 中提取某些键/值到新列中,然后写入新表。原始数据格式的乐趣之一是有些数据记录是 JSON 数组,有些不是数组(只是 JSON)。因此我们可以从开始:

testcase = [(1, [{'a': 1, 'b': 2, 'c': 3}, {'a': 11, 'b': 12, 'c': 13}]), 
            (2, {'a': 30, 'b': 40}), 
            (3, {'a': 100, 'b': 200, 'd': 300})]
for x in testcase:
    print(x)
(1, [{'a': 1, 'b': 2, 'c': 3}, {'a': 11, 'b': 12, 'c': 13}])
(2, {'a': 30, 'b': 40})
(3, {'a': 100, 'b': 200, 'd': 300})

注意每个元组的第一个元素是记录 ID。第一条记录是长度为 2 的数组,第二条和第三条记录不是数组。 所需的输出是(作为 DataFrame):

    a   b   data
1   1   2   '{"c": 3}'
1   11  12  '{"c": 13}'
2   30  40  '{}'
3   100     200     '{"d": 300}'

在这里,您可以看到我已将键“a”和“b”从字典中提取到新列中,而将剩余的键/值留在原处。 id=2 的空字典是可取的行为。

首先,我将 id 和数据提取到单独的列表中。我借此机会将 dict 制成一个 dicts 列表(长度为 1),因此类型现在是一致的:

id = [x[0] for x in testcase]
data_col = [x[1] if type(x[1]) == list else [x[1]] for x in testcase]
for x in data_col:
    print(x)
[{'a': 1, 'b': 2, 'c': 3}, {'a': 11, 'b': 12, 'c': 13}]
[{'a': 30, 'b': 40}]
[{'a': 100, 'b': 200, 'd': 300}]

将 id 和 data_col 提取为单独的列表感觉有点笨拙,尽管至少我们有一个不错的属性,即我们不会复制数据:

id[0] is testcase[0][0]
True
data_col[0] is testcase[0][1]
True

而且,正如我所说,我必须处理一些记录包含 dicts 数组和一些只是 dicts 的问题,所以这使它们都保持一致。

主要的细节发生在这里,我在双列表推导中执行字典推导以迭代每个字典:

popped = [(id, {key: element.pop(key, None) for key in ['a', 'b']}) \
for id, row in zip(id, data_col) for element in row]
for x in popped:
    print(x)
(1, {'a': 1, 'b': 2})
(1, {'a': 11, 'b': 12})
(2, {'a': 30, 'b': 40})
(3, {'a': 100, 'b': 200})

我需要能够将每个新行与其原始 id 相关联,以上实现了这一点,正确地再现了适当的 id 值(1、1、2、3)。通过一些整理工作,我可以将所有目标行排成一行:

import pandas as pd
from psycopg2.extras import Json
id2 = [x[0] for x in popped]
cols = [x[1] for x in popped]
data = [Json(item) for sublist in data_col for item in sublist]
popped_df = pd.DataFrame(cols, index=id2)
popped_df['data'] = data

这给了我想要的 DataFrame,如上所示。但是......我所有的清单都是必要的吗?我不能做一个简单的 json_normalize 因为我不想提取所有的键,它会随着数组和非数组的组合而崩溃。

它还需要尽可能高性能,因为它将处理数百万行。出于这个原因,我实际上使用以下方法将 DataFrame 转换为列表: 列表(popped_df.itertuples()) 然后传递给 psycopg2.extras 的 execute_values() 所以我可能还没有费心构建 DataFrame 而只是构建输出列表,但在这篇文章中,我真的想问是否有一种更清洁、更快的方法可以将这些特定键从 dicts 提取到新的列和行中,对于是否record 是否为数组,并跟踪相关的记录 id。

我避开了端到端的 pandas 方法,使用 pd.read_sql() 读取数据,因为我正在读取 DataFrame.to_sql() 相对较慢。

【问题讨论】:

  • 我不太确定这是否相关,但 PostgreSQL 可以很好地使用 json:postgresql.org/docs/10/functions-json.html
  • 是的,Postgres 肯定有很好的 JSON 函数。但正如@jpp 所说,数据很混乱。我希望能够轻松地对当前在 JSON 中的某些字段进行索引。有时 JSON 是一个数组。因此,我希望尽可能高效地对数百万行进行批量数据工程。

标签: python arrays json dictionary


【解决方案1】:

您的数据很混乱,因为testcase 的第二个元素可以是list dict。在这种情况下,您可以通过for 循环构造一个列表,然后馈送到pd.DataFrame 构造函数:

testcase = [(1, [{'a': 1, 'b': 2, 'c': 3}, {'a': 11, 'b': 12, 'c': 13}]), 
            (2, {'a': 30, 'b': 40}), 
            (3, {'a': 100, 'b': 200, 'd': 300})]

L = []
for idx, data in testcase:
    for d in ([data] if isinstance(data, dict) else data):
        # string conversion not strictly necessary below
        others = str({k: v for k, v in d.items() if k not in ('a', 'b')})
        L.append((idx, d['a'], d['b'], others))

df = pd.DataFrame(L, columns=['index', 'a', 'b', 'data']).set_index('index')

print(df)

         a    b        data
index                      
1        1    2    {'c': 3}
1       11   12   {'c': 13}
2       30   40          {}
3      100  200  {'d': 300}

【讨论】:

  • 你使用 isinstance(data, dict)。这比 type(data) == dict 更惯用吗?
  • @doctor,是的。因为它占子类。例如,type(collections.defaultdict) == dict 返回False,而collections.defaultdictdict 的子类。
【解决方案2】:

你可以这样做:

import pandas as pd

testcase = [(1, [{'a': 1, 'b': 2, 'c': 3}, {'a': 11, 'b': 12, 'c': 13}]),
            (2, {'a': 30, 'b': 40}),
            (3, {'a': 100, 'b': 200, 'd': 300})]


def split_dict(d, keys=['a', 'b']):
    """Split the dictionary by keys"""
    preserved = {key: value for key, value in d.items() if key in keys}
    complement = {key: value for key, value in d.items() if key not in keys}
    return preserved, complement


def get_row(val):
    preserved, complement = split_dict(val)
    preserved['data'] = complement
    return preserved


rows = []
index = []
for i, values in testcase:
    if isinstance(values, list):
        for value in values:
            rows.append(get_row(value))
            index.append(i)
    else:
        rows.append(get_row(values))
        index.append(i)


df = pd.DataFrame.from_records(rows, index=index)
print(df)

输出

     a    b        data
1    1    2    {'c': 3}
1   11   12   {'c': 13}
2   30   40          {}
3  100  200  {'d': 300}

【讨论】:

    猜你喜欢
    • 2018-05-16
    • 2018-07-12
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-11-25
    • 1970-01-01
    • 2021-10-21
    • 1970-01-01
    相关资源
    最近更新 更多