You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在Pandas中json_normalize含空列表的列且不丢失记录

解决pd.json_normalize丢失空sections行的问题

你的问题很典型:当record_path指向的字段是空列表时,pd.json_normalize会直接丢弃该行,导致对应ID完全消失。针对百万级数据,我们有两种高效的解决方案,既能保证每个唯一ID至少保留一行,又能兼顾性能:

方案一:左连接原始数据(优化你的思路)

这种方法逻辑直观,先按现有流程展开非空sections,再通过左连接把原始数据中丢失的空sections行补回来,最后处理answers的展开。

步骤分解:

  1. 提取原始数据中的核心元数据(_id、created_at)作为基础表,确保所有ID都存在
  2. 展开非空sections的行,得到部分数据的展开表
  3. 基础表左连接sections展开表,补全空sections的行
  4. 对连接后的表展开answers字段,空sections的行对应的answer字段会自动填充为NaN

代码实现:

import pandas as pd

sample = [{'_id': '5f48bee4c54cf6b5e8048274', 'created_at': '2020-08-28T08:23:00Z', 'sections': [{'comment': '', 'type_fail': None, 'answers': [{'comment': 'stuff', 'feedback': [], 'value': 10.0, 'answer_type': 'default', 'question_id': '5e59599c68369c24069630fd', 'answer_id': '5e595a7c3fbb70448b6ff935'}, {'comment': 'stuff', 'feedback': [], 'value': 10.0, 'answer_type': 'default', 'question_id': '5e598939cedcaf5b865ef99a', 'answer_id': '5e598939cedcaf5b865ef998'}], 'score': 20.0, 'passed': True, '_id': '5e59599c68369c24069630fe', 'custom_fields': []}, {'comment': '', 'type_fail': None, 'answers': [{'comment': '', 'feedback': [], 'value': None, 'answer_type': 'not_applicable', 'question_id': '5e59894f68369c2398eb68a8', 'answer_id': '5eaad4e5b513aed9a3c996a5'}, {'comment': '', 'feedback': [], 'value': None, 'answer_type': 'not_applicable', 'question_id': '5e598967cedcaf5b865efe3e', 'answer_id': '5eaad4ece3f1e0794372f8b2'}, {'comment': "stuff", 'feedback': [], 'value': 0.0, 'answer_type': 'default', 'question_id': '5e598976cedcaf5b865effd1', 'answer_id': '5e598976cedcaf5b865effd3'}], 'score': 0.0, 'passed': True, '_id': '5e59894f68369c2398eb68a9', 'custom_fields': []}]}, {'_id': '5f48f708fe22ca4d15fb3b55', 'created_at': '2020-08-28T12:22:32Z', 'sections': []}]

# 1. 提取基础元数据表,确保所有ID都存在
df_base = pd.DataFrame(sample)[['_id', 'created_at']]

# 2. 展开非空sections的行
df_sections = pd.json_normalize(
    sample,
    meta=['_id', 'created_at'],
    record_path='sections',
    record_prefix='section_'
)

# 3. 左连接补全行
df_combined = df_base.merge(df_sections, on=['_id', 'created_at'], how='left')

# 4. 展开answers字段,忽略不存在record_path的行
df_final = pd.json_normalize(
    df_combined.to_dict(orient='records'),
    meta=['_id', 'created_at', 'section__id', 'section_score', 'section_passed', 'section_type_fail', 'section_comment'],
    record_path='section_answers',
    record_prefix='',
    errors='ignore'
)

# 验证所有ID都被保留
print(df_final['_id'].unique())

这种方法逻辑清晰,不需要修改原始数据,merge操作在pandas中效率很高,尤其当_id是唯一键时。

方案二:预处理空sections,从根源避免丢失行

另一种思路是提前把空的sections列表替换成包含一个空字典的列表[{}],这样pd.json_normalize展开时会为该行生成一行所有section字段为NaN的记录,后续展开answers时也能保留该行。

代码实现:

import pandas as pd

sample = [{'_id': '5f48bee4c54cf6b5e8048274', 'created_at': '2020-08-28T08:23:00Z', 'sections': [{'comment': '', 'type_fail': None, 'answers': [{'comment': 'stuff', 'feedback': [], 'value': 10.0, 'answer_type': 'default', 'question_id': '5e59599c68369c24069630fd', 'answer_id': '5e595a7c3fbb70448b6ff935'}, {'comment': 'stuff', 'feedback': [], 'value': 10.0, 'answer_type': 'default', 'question_id': '5e598939cedcaf5b865ef99a', 'answer_id': '5e598939cedcaf5b865ef998'}], 'score': 20.0, 'passed': True, '_id': '5e59599c68369c24069630fe', 'custom_fields': []}, {'comment': '', 'type_fail': None, 'answers': [{'comment': '', 'feedback': [], 'value': None, 'answer_type': 'not_applicable', 'question_id': '5e59894f68369c2398eb68a8', 'answer_id': '5eaad4e5b513aed9a3c996a5'}, {'comment': '', 'feedback': [], 'value': None, 'answer_type': 'not_applicable', 'question_id': '5e598967cedcaf5b865efe3e', 'answer_id': '5eaad4ece3f1e0794372f8b2'}, {'comment': "stuff", 'feedback': [], 'value': 0.0, 'answer_type': 'default', 'question_id': '5e598976cedcaf5b865effd1', 'answer_id': '5e598976cedcaf5b865effd3'}], 'score': 0.0, 'passed': True, '_id': '5e59894f68369c2398eb68a9', 'custom_fields': []}]}, {'_id': '5f48f708fe22ca4d15fb3b55', 'created_at': '2020-08-28T12:22:32Z', 'sections': []}]

# 预处理:把空sections替换成[{}]
processed_sample = [
    {**item, 'sections': item['sections'] if len(item['sections']) > 0 else [{}]}
    for item in sample
]

# 第一次展开sections:所有ID都被保留
df2 = pd.json_normalize(
    processed_sample,
    meta=['_id', 'created_at'],
    record_path='sections',
    record_prefix='section_'
)

# 第二次展开answers:空sections的行answer字段为NaN
df3 = pd.json_normalize(
    df2.to_dict(orient="records"),
    meta=["_id", "created_at", "section__id", "section_score", "section_passed", "section_type_fail", "section_comment"],
    record_path="section_answers",
    record_prefix="",
    errors='ignore'
)

# 验证所有ID都被保留
print(df3['_id'].unique())

这种方法避免了额外的merge操作,列表推导式处理百万级数据效率很高,不会成为性能瓶颈。

性能优化建议

针对百万级记录:

  • 优先选择方案二,减少内存占用和计算时间
  • 如果用方案一,建议把_id设为索引后再执行join操作,能大幅提升速度
  • 避免用apply处理大DataFrame,方案二中的列表推导式比df['sections'].apply(...)效率更高

内容的提问来源于stack exchange,提问作者ldacey

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.09 06:42:27