2026年2月19日 星期四

FHIR Procedure Resource 臨床程序資源結構說明

FHIR Procedure Resource 臨床程序資源結構說明

Procedure 資源是 FHIR 中用來描述「臨床程序」的核心資源,其用途、欄位設計與和其他資源(特別是 Patient)之間的連結機制,是理解 EHR 臨床事件數據交換的關鍵。


什麼是 FHIR Procedure?

在 FHIR R4 中,Procedure 定義為一項已執行或正在執行的臨床行為,是「對 Patient(或 Group)」所進行的活動。它不只限於手術,也包括所有對病人身體或心理產生變化的活動。這些活動包括了手術、診斷檢查、活體組織檢查(biopsy)、輔導、物理治療等。這個資源提供的是該程序的摘要資訊,而不是過程的即時進度詳細資料。

這個定義讓 Procedure 資源在醫療紀錄中扮演「事件描述」的角色,可以串聯不同系統中的程序紀錄,並可被其他內容(Observation、DiagnosticReport 等)引用。


Procedure 資源的價值

FHIR Procedure 資源的價值在於它做為橋接臨床流程與患者記錄之間的標準化描述:

  • 統一紀錄程序發生的時間、狀態與分類,利於跨系統查詢與交換。
  • 明確的欄位可與多種其他資源建立關聯,促進資訊整合(例如:Observation、DiagnosticReport 等)。
  • 標準化的參考連結(Reference) 使得程序與病人、照護事件、醫療提供者之間的關係可追蹤與串接。

Procedure 資源的欄位與用途

FHIR Specification 官方文件以清單方式定義 Procedure 資源的所有欄位。下列內容按照規格逐項解釋每個欄位的意義與用法:

identifier 用來存放 Procedure 的標識符(Identifier),可以對應外部系統中的程序編號或紀錄碼。

instantiatesCanonical & instantiatesUri 可連結程序所依據的流程定義或規範,例如:PlanDefinition、ActivityDefinition 或外部規範 URI,表示該程序實際符合某個醫療工作流程。

basedOn Reference 指向與程序相關的請求或計劃,例如 CarePlan、ServiceRequest。它反映出這個程序是由哪個請求觸發或依據。

partOf Reference 可以指向其他更大的事件(例如另一個 Procedure、Observation 或 MedicationAdministration),表示此程序是某個更大事件的一部分。

status 指定 Procedure 當前的狀態,例如「completed」、「in-progress」、「entered-in-error」等,描述程序是否執行完成或有效。

statusReason 若狀態非正常完成,可以指出原因,如「cancelled due to patient refusal」等。

category 程序類別, 可用 SNOMED CT 之類的系統做分類,例如外科手術、理療等。

code 程序的指定內容,用特定分類系統編碼描述程序本身的類型,例如手術名稱、檢查名稱等。

subject 最具關聯性的欄位之一,用來參考該程序是在哪個 Patient 或 Group 上被執行。這是 Procedure 與病患之間最基本的聯結(見下一節詳述)。

encounter Reference 指向 Encounter(就醫事件),表示程序是在那次門急診/住院事件中執行。

performed[x] 程序執行的時間,可以是 dateTime、Period、Age、Range、或文字等多種型別,視實際需求而定。

recorder Reference 誰記錄了此程序資料,可能是 Patient、自身 RelatedPerson、Practitioner 或 PractitionerRole。

asserter Reference 誰宣稱程序已經發生,表示程序紀錄的宣告者。

performer(Backbone 元素) 這是一個包含 function(參與角色,比如 surgeon、anesthetist)和 actor(Reference) 的結構,用於指引具體的執行者。actor 可能 reference Practitioner、PractitionerRole、Organization、Patient(例如自主治療)、RelatedPerson 或 Device。

location Reference 指向 Location(地點),標示該程序在哪裡執行。

reasonCode 與 reasonReference reasonCode 給出程序執行的原因(以 CodeableConcept 表示)。reasonReference 是可 Reference 至 Condition、Observation、Procedure、DiagnosticReport 或 DocumentReference 的欄位,以便指向具體的理由來源或診斷支持。

bodySite 表示程序涉及的身體部位,可用標準解剖碼表描述。

outcome 表示程序的結果或結局,例如是否達到預期效果。

report Reference 指向與程序相關的結果報告,例如 DiagnosticReport、DocumentReference 或 Composition。

complication 與 complicationDetail Complication 可用 CodeableConcept 表示是否有併發症;complicationDetail 則 reference 到 Condition 表示具體的併發症診斷。

followUp CodeableConcept 顯示後續追蹤與指示,例如安排返診、術後復健等。

note Annotation 形式的自由文本附註,可加入額外說明。

focalDevice / usedReference / usedCode 用於紀錄程序中涉及的器材或物質(如 Device、Medication、Substance),包括是否改變了該設備(focalDevice)或使用了哪些資源(usedReference、usedCode),如手術植入物、消毒用品等。


Procedure 與 Patient 之間的 Reference 連結

在 FHIR 設計下,Reference 是在一個資源內部使用的一種結構,用來指向其他資源的 identity(ID 或者 URL)。Procedure 資源內最重要的 Reference 之一就是 subject,它表示該程序是在誰身上執行。

在 Procedure 中,subject 的參考目標是 Patient 或 Group。典型情況下 subject 會 reference 一個 Patient 資源的實例。

這種設計使得可以透過 Procedure.subject 指向病人來源,從而在查詢病人相關程序時可以展開反向查詢。例如對一位病人,可以搜尋所有其 subject 為該 Patient 的 Procedure 資源。

除了 subject,Procedure 還有其他可能 reference Patient 的欄位,例如 recorder、asserter 與 performer.actor 甚至某些特定 implementation 也可能讓 performer.actor reference Patient(表示病人在自身身上進行程序)。


Patient 也會 reference 其他資源(與 Procedure 有關)

Patient 資源以自身為中心定義患者的基本資訊,但與 Procedure 相關的情況通常不是 Patient 主動 reference Procedure。FHIR 中的概念是「事件大多由事件資源指向患者」,非由患者主動指向事件。

因此:

  • Procedure 引用 Patient 為 subject。
  • Patient 資源中並無欄位直接儲存所有與其相關的 Procedure 列表。
  • 真正的關聯是在資料交換時透過 query 搜尋例如 Procedure?subject=Patient/{id} 這種查詢實現。
  • Patient 可 reference 其自己的 GeneralPractitioner、RelatedPerson 等,但這些不直接鏈到 Procedure。

Profiles 與 Extensions:如何客製 Procedure

FHIR 的強項之一是可透過 Profiles 和 Extensions 自訂標準行為。

Profiles(規範限制)

Procedure-profiles.html 頁面列出不同 Implementation Guide 所對 Procedure 的具體限制與約束,例如 US Core Procedure Profile、IPS Procedure Profile 等。這些 Profile 會指定某些欄位必須存在、允許值必須使用特定 ValueSet,或對某些 Reference 連結作出額外限制。例如 US Core Procedure Profile 要求一定要有 subject、status 以及 code 且符合 USCDI 資料需求。

Profiles 的目的:

  • 提供特定地域或業務需求下更嚴格或更一致的 Procedure 定義。
  • 對欄位使用特定 ValueSet 或強制特定格式,使資料更可交換與可解析。

Extensions(擴充欄位)

FHIR 允許資源在不破壞標準基礎結構下透過 Extensions 擴充欄位。Extensions 允許你新增不在官方規範中的資料點,例如額外的執行詳細資訊、支付相關資訊等。Extensions 可分為一般 Extensions 與 modifierExtensions(後者會影響欄位語義)。Implementation Guide 以及 Profiles 也常會定義一些常用的 Extensions。


小結:Procedure 的臨床與系統意義

FHIR R4 Procedure 資源是臨床程序記錄的標準化描述結構。它不僅包含時間與分類資訊,更透過豐富的 Reference 將程序與 Patient、Encounter、Practitioner、Observation、DiagnosticReport 等資源串聯,使得臨床事件串接與資料交換變得可靠且可查詢。透過 Profiles 與 Extensions 的可擴展性,可以滿足不同地區或業務的特殊需求。 




匯入 FHIR 範例資料集

匯入 FHIR 範例資料集

Synthea™ Patient Generator (GitHub) 是一個 開源的合成病人(synthetic patient)資料生成器(Synthetic Patient Population Simulator),用來在不涉及真實個人隱私資料的情況下,模擬完整的病人健康歷程與電子健康紀錄(EHR)

合成患者(synthetic patient)是由電腦模型生成的虛構個體,看起來像真實患者但不對應任何真實個人。這種資料不含任何實際個人資訊,因此沒有隱私風險。Synthea 的使命是生成:逼真但不是真實的健康資料(realistic synthetic data)、* 涵蓋病人從出生到死亡的完整醫療歷程、* 各種與健康照護相關的資料,包括症狀、診斷、處方、檢驗、就診事件等, 生成的資料可用於研究、測試、教育和開發,而不受 HIPAA 或其他隱私法律限制。

🛠 主要特色與功能 📌 全生命周期模擬 從出生到死亡,逐步模擬個體醫療事件及健康狀態。 📌 可配置的人口統計與統計模型 預設使用馬薩諸塞州人口統計資料,也可以配置其他區域或人口設定。 📌 模組化規則系統 採用「疾病模組(disease modules)」和通用規則框架(Generic Module Framework)來模擬疾病發生與演進。這些規則參考公開資料與統計資料(如 CDC、NIH)。 📌 輸出格式多樣性 支援多種業界標準與格式輸出,包括:

  • HL7 FHIR(R4、STU3、DSTU2)
  • C-CDA、CSV、Bulk FHIR(ndjson)

下載與使用 Synthea 生成 FHIR 資料集

  • 打開 Sample FHIR Bulk Export Datasets 的 GitHub 頁面
  • 找到 [Small] 連結,點選這個連結
  • 如此,將會下載 10 個模擬病人資料集,這些資料集以 ndjson 格式存儲,並且包含了 FHIR 資源的完整結構,可以用來測試和開發 FHIR API 的功能。
  • 打開並且解壓縮這個 [sample-bulk-fhir-datasets-10-patients.zip] 檔案
  • 這個檔案內將會有底下的FHIR資源檔案:

    • Organization : 模擬的醫療機構資料。
    • Location : 模擬的醫療機構地點資料。
    • Practitioner : 模擬的醫療人員資料。
    • PractitionerRole : 模擬的醫療人員角色資料。
    • Patient : 模擬的病人資料。
    • Encounter : 模擬的就診事件資料。
    • Condition : 模擬的疾病或健康狀態資料。
    • AllergyIntolerance : 模擬的過敏或不耐受資料。
    • Immunization : 模擬的疫苗接種資料。
    • Device : 模擬的醫療設備資料。
    • Observation : 模擬的觀察資料。
    • Procedure : 模擬的醫療程序資料。
    • DiagnosticReport : 模擬的診斷報告資料。
    • DocumentReference : 模擬的文件參考資料。
    • MedicationRequest : 模擬的用藥請求資料。
  • 在這個目錄下,建立一個 PowerShell 檔案 [import.ps1]
  • 在這個檔案內輸入底下的程式碼:
param(
  [string]$Base   = "https://hapi.fhir.org/baseR4",
  [string]$Folder = "."
)

$ErrorActionPreference = "Stop"

# 判斷物件是否「有內容」(不是空物件/空陣列/空字串/null)
function Test-HasContent {
  param($Value)

  if ($null -eq $Value) { return $false }

  # string
  if ($Value -is [string]) {
    return -not [string]::IsNullOrWhiteSpace($Value)
  }

  # array
  if ($Value -is [System.Collections.IEnumerable] -and -not ($Value -is [PSCustomObject])) {
    # 注意:string 也是 IEnumerable,上面已處理
    try {
      $arr = @($Value)
      return ($arr.Count -gt 0)
    } catch {
      return $true
    }
  }

  # object (PSCustomObject)
  if ($Value -is [PSCustomObject] -or $Value -is [hashtable]) {
    $props = $Value.PSObject.Properties
    if ($props.Count -eq 0) { return $false }

    foreach ($p in $props) {
      if (Test-HasContent $p.Value) { return $true }
    }
    return $false
  }

  # other primitive types
  return $true
}

# 清洗 FHIR JSON:
# - 移除 extension(synthea 常見 unknown extension)
# - 移除 meta.profile;若 meta 變空 => 移除整個 meta(避免 dom-6 / meta empty)
function Clean-FhirJsonLine {
  param([string]$JsonLine)

  $obj = $JsonLine | ConvertFrom-Json

  # 1) 移除 extension
  if ($obj.PSObject.Properties.Name -contains "extension") {
    $obj.PSObject.Properties.Remove("extension")
  }

  # 2) meta: 移除 profile;meta 若空就整個移除
  if ($obj.PSObject.Properties.Name -contains "meta") {
    $meta = $obj.meta

    if ($meta -and ($meta.PSObject.Properties.Name -contains "profile")) {
      $meta.PSObject.Properties.Remove("profile")
    }

    # 有些資料可能還留 profile: [],確保刪乾淨
    if ($meta -and ($meta.PSObject.Properties.Name -contains "profile")) {
      $meta.PSObject.Properties.Remove("profile")
    }

    # 若 meta 沒有任何有效內容,移除 meta
    if (-not (Test-HasContent $meta)) {
      $obj.PSObject.Properties.Remove("meta")
    }
  }

  # 3) 可選:移除空的 text(某些 server 對空 narrative 也會抱怨)
  if ($obj.PSObject.Properties.Name -contains "text") {
    if (-not (Test-HasContent $obj.text)) {
      $obj.PSObject.Properties.Remove("text")
    }
  }

  return ($obj | ConvertTo-Json -Depth 100 -Compress)
}

# PUT 單筆 Resource(保留 client-assigned id)
function Invoke-FhirPut {
  param(
    [string]$Url,
    [string]$JsonLine
  )

  $tmp = Join-Path $env:TEMP ("fhir_import_" + [Guid]::NewGuid().ToString("N") + ".json")

  # ✅ 用 UTF-8 無 BOM 寫入(避免 HAPI-0450 / HAPI-1861)
  $utf8NoBom = New-Object System.Text.UTF8Encoding($false)
  [System.IO.File]::WriteAllText($tmp, $JsonLine, $utf8NoBom)

  try {
    $output = & curl.exe -sS `
      -X PUT $Url `
      -H "Content-Type: application/fhir+json; charset=utf-8" `
      --data-binary "@$tmp" `
      -w "`n%{http_code}"

    $lines = $output -split "`n"
    $http  = ($lines[-1]).Trim()
    $body  = ($lines[0..($lines.Length-2)] -join "`n").Trim()

    return @{
      Http = $http
      Body = $body
    }
  }
  finally {
    Remove-Item -Force -ErrorAction SilentlyContinue $tmp
  }
}

# 匯入單一 ndjson 檔
function Import-NdjsonFile {
  param([string]$Path)

  $fileName     = [System.IO.Path]::GetFileName($Path)
  $resourceType = $fileName.Split(".")[0]

  Write-Host "==> Importing $fileName -> $resourceType"

  $i = 0
  Get-Content -Path $Path -ReadCount 1 | ForEach-Object {
    $line = $_
    if ([string]::IsNullOrWhiteSpace($line)) { return }
    $i++

    try {
      $clean = Clean-FhirJsonLine -JsonLine $line
      $obj   = $clean | ConvertFrom-Json
    } catch {
      throw "Invalid JSON at $fileName line $i"
    }

    $id = $obj.id
    if ([string]::IsNullOrWhiteSpace($id)) {
      throw "Missing id at $fileName line $i"
    }

    $url  = "$Base/$resourceType/$id"
    $resp = Invoke-FhirPut -Url $url -JsonLine $clean

    if ($resp.Http -ne "200" -and $resp.Http -ne "201") {
      Write-Host ""
      Write-Host "FAILED: $fileName line $i => HTTP $($resp.Http)"
      if (-not [string]::IsNullOrWhiteSpace($resp.Body)) {
        Write-Host "Response body (first 1600 chars):"
        Write-Host ($resp.Body.Substring(0, [Math]::Min(1600, $resp.Body.Length)))
      } else {
        Write-Host "Response body: <empty>"
      }
      throw "Import failed."
    }

    if (($i % 200) -eq 0) {
      Write-Host "  ...$i resources imported"
    }
  }

  Write-Host "    OK ($i resources)"
}

# 匯入順序(先被參考者)
$order = @(
  "Organization*.ndjson",
  "Location*.ndjson",
  "Practitioner*.ndjson",
  "PractitionerRole*.ndjson",
  "Patient*.ndjson",
  "Encounter*.ndjson",
  "Condition*.ndjson",
  "AllergyIntolerance*.ndjson",
  "Immunization*.ndjson",
  "Device*.ndjson",
  "Observation*.ndjson",
  "Procedure*.ndjson",
  "DiagnosticReport*.ndjson",
  "DocumentReference*.ndjson",
  "MedicationRequest*.ndjson"
)

foreach ($pattern in $order) {
  $files = Get-ChildItem -Path $Folder -Filter $pattern -File | Sort-Object Name
  if ($files.Count -eq 0) {
    Write-Host "Skip (not found): $pattern"
    continue
  }

  foreach ($f in $files) {
    Import-NdjsonFile -Path $f.FullName
  }
}

Write-Host ""
Write-Host "=============================="
Write-Host "FHIR NDJSON IMPORT COMPLETED"
Write-Host "=============================="
  • 使用底下命令來執行這個 PowerShell 腳本
powershell -ExecutionPolicy Bypass -File .\import.ps1 -Base "https://server.fire.ly" -Folder "."

*將會把這些 FHIR 資源逐一匯入到指定的 FHIR Server(預設是 https://hapi.fhir.org/baseR4)。腳本會自動處理 JSON 清洗(移除不必要的 extension 和 meta.profile)並且使用 HTTP PUT 方法來上傳資源,保留原有的 client-assigned id。匯入過程中會顯示進度和任何可能的錯誤訊息。