JSON 并不足够:为分析而将 FHIR 扁平化的工程痛点
Source: Dev.to
(请提供需要翻译的正文内容,我才能为您进行简体中文翻译。)
问题:它是图,而不是表
问题不在于 FHIR 是 JSON。问题在于 FHIR 资源深度嵌套、递归且多态。
拿一个简单的 Patient 资源来说。在 SQL 表中,你会期望有一个 Name 列。而在 FHIR 中?Name 是一个对象数组。
- 你有一个 Legal(法定)名称。
- 你有一个 Maiden(婚前)名称。
- 你有一个 Nickname(昵称)。
每个对象都包含一个 given 数组(名、其中间名)和一个 family 字符串。
{
"resourceType": "Patient",
"id": "example",
"name": [
{
"use": "official",
"family": "Chalmers",
"given": ["Peter", "James"]
},
{
"use": "nickname",
"given": ["Jim"]
}
],
"identifier": [
{
"system": "http://hospital.org/mrns",
"value": "12345"
}
]
}
如果你直接把它扔进 BigQuery 并尝试查询 name.family,会得到 null 或错误,因为它位于数组内部。
“爆炸”问题
在 SQL 中分析这些数组时,通常需要使用 UNNEST 或 LATERAL VIEW EXPLODE。
如果一个患者有 2 个姓名、3 个标识符和 5 个扩展,并且你在不小心的情况下尝试将其展平为单个宽行,就会触发 笛卡尔积。
2 * 3 * 5 = 30 行对应单个患者
现在想象一下,对包含数百个项目和诊断代码的 ExplanationOfBenefit 资源执行相同操作。你的 100 万记录数据集瞬间会变成 1 亿行重复的垃圾数据。
Strategy 1: “够用”提取
如果你的分析师只关心 Official(官方)名称和 MRN,不要尝试对扁平化过程进行通用化。直接硬编码路径即可。
在 Python/Pandas 中,这通常表现为编写特定的 lambda 函数。虽然这种方式比较脆弱,但对 MVP(最小可行产品)来说是可行的。
import pandas as pd
# Assume 'data' is your list of dicts
def get_official_family_name(patient_row):
names = patient_row.get('name', [])
for n in names:
if n.get('use') == 'official':
return n.get('family')
return None
# Apply it
df['official_last_name'] = df.apply(get_official_family_name, axis=1)
这种做法适用于小脚本,但在处理 TB 级别数据的 ETL 流程中并不具备良好的可扩展性。
Strategy 2: Array‑aware Columnar Formats (The Real Fix)
现代的做法——也是通常推荐的做法——是不要立刻把 FHIR 强行塞进 CSV‑式的平面表格。相反,先把它转到支持嵌套结构的列式格式,例如 Parquet。
使用 Apache Spark 或 Databricks 等工具可以保留模式(schema)。最终,你确实需要为 Tableau/PowerBI 用户把数据展平。关键是创建 Satellite Tables(卫星表)。
不要只建一个巨大的 Patient 表,而是创建:
Patient_Core(id、birthdate、gender)Patient_Names(id、use、family、given)Patient_Identifiers(id、system、value)
这样就对数据进行了规范化,实际上是把 JSON 逆向工程回关系型的第三范式。
# Pseudo-code for a Spark transformation
from pyspark.sql.functions import col, explode
# Read raw FHIR JSON
df = spark.read.json("s3://bucket/fhir/Patient/")
# Create the Names table
df_names = df.select(
col("id").alias("patient_id"),
explode(col("name")).alias("name_struct")
).select(
"patient_id",
"name_struct.use",
"name_struct.family",
"name_struct.given" # Note: given is still an array here!
)
# Write out as Parquet
df_names.write.parquet("s3://bucket/analytics/patient_names/")
Context is King
最大的头疼点不仅仅是结构本身;而是 上下文的丢失。
当你将 Observation.component 扁平化时,可能会得到数值 80 和 120。哪一个是收缩压,哪一个是舒张压?在 JSON 中,它们是兄弟对象。如果你盲目地把它们扁平化为两行,你必须确保把对应的 code 字段与相应的 value 字段一起携带。
始终将包含数值 及其标签 的 对象 一起扁平化。绝不要把它们独立扁平化,否则会失去数字与其含义之间的关联。
结论
FHIR 将会长期存在,其模式在临床准确性方面非常出色。只需在数据工程上转变思维方式:摆脱 “单一大表” 心态,将数组和结构体视为管道中的一等公民。
如果你在处理复杂数据架构时遇到困难,或想了解他人如何应对这些 ETL 挑战,请查看我的其他文章中的更多技术指南:我的其他文章。
祝编码愉快,愿你的 JSON 永远有效!