Python 如何用 pandas 展平嵌套 JSON,使每个 outcome.id 对应一行?
结论
将 record_path 设为 "reports",并通过 meta=["_id"] 保留顶层标识。pd.json_normalize 会把每条 report 中嵌套的 outcome 和 status 字典展开成列,不必把 outcome 放入 record_path。
示例中每条 report 都包含一个 outcome,展开 reports 后,两个 outcome.id 各占一行。下面的代码将顶层 _id 命名为 report_id;如果期望 report_id 来自其他字段,需要相应调整。
展平 JSON 并调整列名
假设 data 已经是题目中的 Python 字典。如果输入是 JSON 字符串,请先用 json.loads 解析。
import pandas as pd
# data 为题目中的 JSON 解析得到的字典
df_reports = pd.json_normalize(
data,
record_path="reports",
meta=["_id"],
sep=".",
)
上面的赋值语句多了前导缩进。下面的简写示例也有同样的问题,实际使用时请采用后面的完整代码。
import pandas as pd
# 每个 reports 元素生成一行,并保留顶层 _id
df_reports = pd.json_normalize(data, record_path="reports", meta=["_id"])
完整处理代码如下:
import pandas as pd
# data 为题目中的 JSON 解析得到的字典
reports = pd.json_normalize(
data,
record_path="reports",
meta=["_id"],
sep=".",
)
# 将顶层标识命名为 report_id
reports = reports.rename(columns={"_id": "report_id"})
# 自动生成的 outcome.status.* 列改为期望的 status.*
prefix = "outcome.status."
reports = reports.rename(columns={
column: "status." + column[len(prefix):]
for column in reports.columns
if column.startswith(prefix)
})
# 调整列顺序,其余字段继续保留
first_columns = [
"report_id",
"outcome.id",
"body",
"outcome.comments",
]
remaining_columns = [
column for column in reports.columns
if column not in first_columns
]
df_reports = reports[first_columns + remaining_columns]
得到的列包括:
report_id
outcome.id
body
outcome.comments
status.failed.both
status.failed.human
status.failed.auto
status.failed.total
status.open.both
status.open.human
status.open.auto
status.open.total
status.passed.both
status.passed.human
status.passed.auto
status.passed.total
status.code._input
status.code._output
status.code.canceled
status.total
type
原始 type 字段也会保留。如果不需要,可以执行:
df_reports = df_reports.drop(columns=["type"])
为什么原来的参数不合适?
record_path 用于指定生成多条记录的列表。题目中 reports 是列表,每条 report 内的 outcome 是字典,因此应在 reports 这一层确定行数。
record_path=["reports", "outcome"]
这个路径最终指向字典,不符合这里需要的记录列表结构。json_normalize 可以递归展开 outcome.status 内部的字典,无需通过 record_path 继续深入。
meta 用于携带记录列表之外的上下文字段。_id 位于 reports 外部,所以放入 meta;body、outcome.id、outcome.comments 和 outcome.status 都在 report 内部,会自动进入结果,无需重复声明。
原代码还将 comments 写成了 comment,示例末尾也缺少结束函数调用的右括号 )。
如何确认每个 outcome 的状态都在同一行?
可以选取几个字段查看结果:
print(df_reports[[
"report_id",
"outcome.id",
"status.passed.auto",
"status.total",
]].to_string(index=False))
按题目中的数据,对应值为:
report_id outcome.id status.passed.auto status.total
12345 1 1 1
12345 2 1 1
outcome.id 在原始 JSON 中是字符串,展平后仍保留这个类型。outcome.comments 是列表,也会保留在单元格中;不需要调用 explode,否则可能改变行数。
json_normalize 按 reports 元素生成行,不会按 outcome.id 自动去重或合并。如果实际数据中同一个 ID 出现多次,需要先明确这些记录应如何合并;展平操作不能保证 ID 唯一。
备注:内容仅供参考。
