JSON 并不足够:为分析而将 FHIR 扁平化的工程痛点

发布: (2026年1月3日 GMT+8 00:00)
5 min read
原文: Dev.to

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 中分析这些数组时,通常需要使用 UNNESTLATERAL 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 扁平化时,可能会得到数值 80120。哪一个是收缩压,哪一个是舒张压?在 JSON 中,它们是兄弟对象。如果你盲目地把它们扁平化为两行,你必须确保把对应的 code 字段与相应的 value 字段一起携带。

始终将包含数值 及其标签对象 一起扁平化。绝不要把它们独立扁平化,否则会失去数字与其含义之间的关联。

结论

FHIR 将会长期存在,其模式在临床准确性方面非常出色。只需在数据工程上转变思维方式:摆脱 “单一大表” 心态,将数组和结构体视为管道中的一等公民。

如果你在处理复杂数据架构时遇到困难,或想了解他人如何应对这些 ETL 挑战,请查看我的其他文章中的更多技术指南:我的其他文章

祝编码愉快,愿你的 JSON 永远有效!

Back to Blog

相关文章

阅读更多 »

AWS Community Day 厄瓜多尔 2025

活动概述:2025年10月,AWS Community Day Ecuador 在基多举办。那是充满活力的一天,有贴纸,还有一个“serp...”的版本。

介绍 :)

关于我 你好,欢迎阅读我的第一篇帖子,也是我的自我介绍。我的名字是 M4iR0N,我认为自己是一名 Cyber Security 和 Privacy Advocate。在家里,我…