如何在Pandas中json_normalize含空列表的列且不丢失记录
解决pd.json_normalize丢失空sections行的问题
你的问题很典型:当record_path指向的字段是空列表时,pd.json_normalize会直接丢弃该行,导致对应ID完全消失。针对百万级数据,我们有两种高效的解决方案,既能保证每个唯一ID至少保留一行,又能兼顾性能:
方案一:左连接原始数据(优化你的思路)
这种方法逻辑直观,先按现有流程展开非空sections,再通过左连接把原始数据中丢失的空sections行补回来,最后处理answers的展开。
步骤分解:
- 提取原始数据中的核心元数据(
_id、created_at)作为基础表,确保所有ID都存在 - 展开非空sections的行,得到部分数据的展开表
- 基础表左连接sections展开表,补全空sections的行
- 对连接后的表展开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
相关产品推荐
相关产品推荐

