将两个具有一对多关系的CSV表转换为具有嵌入子文档列表的JSON



我有两个CSV文件,它们之间有一对多关系。

main.csv:

"main_id","name"
"1","foobar"

attributes.csv:

"id","main_id","name","value","updated_at"
"100","1","color","red","2020-10-10"
"101","1","shape","square","2020-10-10"
"102","1","size","small","2020-10-10"

我想将其转换为以下结构的JSON:

[
{
"main_id": "1",
"name": "foobar",
"attributes": [
{
"id": "100",
"name": "color",
"value": "red",
"updated_at": "2020-10-10"
},
{
"id": "101",
"name": "shape",
"value": "square",
"updated_at": "2020-10-10"
},
{
"id": "103",
"name": "size",
"value": "small",
"updated_at": "2020-10-10"
}
]
}
]

我试着使用Python和Pandas,比如:

import pandas
def transform_group(group):
group.reset_index(inplace=True)
group.drop('main_id', axis='columns', inplace=True)
return group.to_dict(orient='records')
main = pandas.read_csv('main.csv')
attributes = pandas.read_csv('attributes.csv', index_col=0)
attributes = attributes.groupby('main_id').apply(transform_group)
attributes.name = "attributes"
main = main.merge(
right=attributes,
on='main_id',
how='left',
validate='m:1',
copy=False,
)
main.to_json('out.json', orient='records', indent=2)

它有效。但问题是,它似乎没有扩大规模。当在我的整个数据集上运行时,我可以毫无问题地加载单个CSV文件,但当试图在调用to_json之前修改数据结构时,内存使用量会激增。

那么,有没有更有效的方法来实现这种转变呢?也许我缺少一些熊猫的特征?或者还有其他图书馆可以使用吗?此外,apply在这里的使用似乎相当缓慢。

这是一个棘手的问题,我们都感受到了你的痛苦。

我有三种方法来解决这个问题。首先,如果你允许熊猫进行突围,分组会慢一些。

import pandas as pd
import numpy as np
from collections import defaultdict
df = pd.DataFrame({'id': np.random.randint(0, 100, 5000), 
'name': np.random.randint(0, 100, 5000)})

现在,如果你按进行标准分组

groups = []
for k, rows in df.groupby('id'):
groups.append(rows)

你会发现

groups = defaultdict(lambda: [])
for id, name in df.values:
groups[id].append((id, name))

大约快3倍。

第二种方法是我将使用更改它来使用Dask和Dask并行化。关于dask的讨论是什么是dask,它与熊猫有什么不同。

第三是算法。加载主文件,然后按ID加载,然后只加载该ID的数据,对内存和磁盘中的内容进行多个处理,然后在可用时保存部分结果。

因此,在我的情况下,我可以在内存中加载原始表,但嵌入会使大小爆炸,因此不再适合内存。因此,我最终仍然使用Pandas加载CSV文件,但随后我逐行迭代生成,并将每一行保存到一个单独的JSON中。这意味着我在内存中没有用于一个大型JSON的大型数据结构。

另一个重要的实现是,将相关列作为索引很重要,并且必须对其进行排序,以便快速查询(因为通常在相关列中存在重复条目(。

我制作了以下两个助手功能:

def get_related_dict(related_table, label):
assert related_table.index.is_unique
if pandas.isna(label):
return None
row = related_table.loc[label]
assert isinstance(row, pandas.Series), label
result = row.to_dict()
result[related_table.index.name] = label
return result

def get_related_list(related_table, label):
# Important to be more performant when selecting non-unique labels.
assert related_table.index.is_monotonic_increasing
try:
# We use this syntax for always get a DataFrame and not a Series when there is only one row matching.
return related_table.loc[[label], :].to_dict(orient='records')
except KeyError:
return []

然后我做:

main = pandas.read_csv('main.csv', index_col=0)
attributes = pandas.read_csv('attributes.csv', index_col=1)
# We sort index to be more performant when selecting non-unique labels. We use stable sort.
attributes.sort_index(inplace=True, kind='mergesort')
columns = [main.index.name] + list(main.columns)
for row in main.itertuples(index=True, name=None):
assert len(columns) == len(row)
data = dict(zip(columns, row))
data['attributes'] = get_related_list(attributes, data['main_id'])
json.dump(data, sys.stdout, indent=2)
sys.stdout.write("n")

最新更新