CQL - 101

Clinical Quality Language(CQL)概述

官方說明:
Clinical Quality Language (CQL) is a high-level, domain-specific language focused on clinical quality and targeted at measure and decision support artifact authors.

In addition, this specification describes a machine-readable canonical representation called Expression Logical Model (ELM) targeted at implementations and designed to enable sharing of clinical knowledge.

https://build.fhir.org/ig/HL7/cql/

簡單的說,CQL為領域專家撰寫的語言,人可以看得懂;ELM則是機器看得懂的語言,專注在系統開發與整合使用。下圖說明了彼此的關係,透過工具,可以將CQL與ELM互相轉換,應用於分析與需求確認。實際系統開發則是從ELM出發,透過程式語言,將複雜的邏輯轉換為可執行程式。

Image Not Showing Possible Reasons
  • The image was uploaded to a note which you don't have access to
  • The note which the image was originally uploaded to has been deleted
Learn More →

以Firely這家公司為例,在DevDays 2023就發表了CQL Engine,將ELM轉換成為C#原始碼,透過編譯成DLL後,其他C#程式就可以直接使用原始CQL所定義的運算邏輯。

Image Not Showing Possible Reasons
  • The image was uploaded to a note which you don't have access to
  • The note which the image was originally uploaded to has been deleted
Learn More →

Evan Machusak & Ann Smith - CQL on Firely: Digital measures for .NET | DevDays 2023 Amsterdam https://www.youtube.com/watch?v=_fEZOxjtyY0&list=PLKuZNI94tzWY1J988TdEGhJ69r4DnTiAN&index=33&t=2241s

CQL的開發流程可區分為以下步驟:

  • 撰寫CQL(使用Visual Studio Code + CQFramework Clinical Quality Landuage Extension)
  • 使用工具(cql-translation-service - https://github.com/cqframework/cql-translation-service),將CQL轉換成ELM
  • 以C#(Firely)為例,使用CQL Engine(Hl7.Cql package),將ELM轉換成C#,編譯產生DLL,應用程式可直接使用。

這樣的流程代表著將來Cinical Quality相關需求就可以由使用者直接撰寫CQL,使用工具產生相關Library,搭配FHIR Server即可快速產生可運作的程式。另外,使用LLM來產生CQL也成為非常有潛力的應用。

CQL開發環境

開發CQL所需要的環境為Visual Studio Code + CQFramework Clinical Quality Landuage Extension。安裝流程非常簡單(就是一般VS Code安裝plugin的基本步驟,選擇cqframework.cql Extension,按下inslall),相關資料可參考 https://github.com/cqframework/vscode-cql/wiki/User-Guide

安裝完成後,可從git hub執行git clone https://github.com/cqframework/cqf-exercises.git匯入CQF Exercises。這個專案提供了由簡入繁的CQL程式開發練習。

  • Exercises01: Preliminaries
  • Exercises02: Types and Values
  • Exercises03: Operators
  • Exercises04: Intervals and Lists
  • Exercises05: Terminology
  • Exercises06: Data Access
  • Exercises07: Queries
  • Exercises08: Advanced Queries
  • Exercises09: Patterns
  • Exercises10: Lung Cancer Screening Decision Support
  • Exercises11: Breast Cancer Screening Measure

建立整個開發環境的過程中,有以下幾點需要注意:

  1. 如果直接clone master,會缺少兩個檔案:ig.iniinput/cqf-exercise.xml,可手動加入或是下載另一個版本(issue3-missing-ini-file)
  2. 每一個ExerciseXX.cql都有一個對應的ExerciseXXKey.cql。當初建立這個Repository的作者的用意應該是原本檔案要讀者練習,而正確答案在Key檔案顯示。
  3. 執行CQL的方式:按右鍵選擇Execule CQL(附帶一提,cqframework.cql也提供了產生ELM的方式,執行方式相同,選項改為VView ELM)。若CQL語法正確,執行結果則會記錄在另一個檔案中。
  4. 每一個CQL Project都需要遵循以下檔案結構:
input/cql
input/tests
input/tests/<cql-library-name>
input/tests/<cql-library-name>/<patient-id>
input/tests/<cql-library-name>/<patient-id>/<resource-type-name>/<resource files> // flexible structure
input/tests/results
input/vocabulary/codesystem
input/vocabulary/valueset

簡單的說,每一個cql檔案必須位於input/cql,每個檔案就是一個library,名稱必須與檔案名稱相同。所需要的測試資料必須放在input/test/<cql-libray>下(若使用到Patient時,相關資料必須置於input/tests/<cql-library-name>/<patient-id>),執行CQL後的輸出檔案會放在input/tests/results/<cql-library-name>.txt。CQL所需要的Terminology則是放在input/vocabulary

CQL語法範例

基本語法在Exercise1 ~ Exercise6都有說明,就不再贅述,實際應用案例以Exercise 10為例:

  • 應用情境:
    The USPSTF recommends annual screening for lung cancer with low-dose computed tomography (LDCT) in adults aged 55 to 80 years who have a 30 pack-year smoking history and currently smoke or have quit within the past 15 years. Screening
    should be discontinued once a person has not smoked for 15 years or develops a health problem that substantially limits life expectancy or the ability or willingness to have curative lung surgery(USPSTF 建議對55至80歲、有30年吸菸史、目前吸菸或在過去 15 年內戒菸的成年人每年進行一次低劑量電腦斷層掃描(LDCT)肺癌篩檢。一旦一個人15年不吸煙或出現嚴重限制預期壽命或進行治療性肺部手術的能力或意願的健康問題,就應停止篩檢 ~ from google translator)

  • 測試資料與執行結果

input/tests/Exercise10共計有三筆測試資料,分別是Heavy-Smoker、Never-Smoker與Former-Smoker。執行結果如下:

Patient=Patient(id=Former-Smoker)
Patient age in years based on date of birth=81
Smoking status observation=[Observation(id=observation-Former-Smoker-1), Observation(id=observation-Former-Smoker-2)]
Lung cancer diagnosis=[]
Chest CT procedure=[Procedure(id=procedure-Former-Smoker-1)]
55 through 80=false
Most recent smoking status observation=Observation(id=observation-Former-Smoker-1)
Current smoker observation=null
Former smoker observation=Observation(id=observation-Former-Smoker-1)
Is current smoker=false
Is former smoker who quit within past 15 years=true
Pack-years=64 '{Pack-years}'
Has 30 pack-year smoking history=true
Has lung cancer=false
Had chest CT in past year=false
Inclusion Criteria=false
Exclusion Criteria=false

Patient=Patient(id=Heavy-Smoker)
Patient age in years based on date of birth=66
Smoking status observation=[Observation(id=observation-Heavy-Smoker-1)]
Lung cancer diagnosis=[Condition(id=example-condition-2)]
Chest CT procedure=[Procedure(id=procedure-Heavy-Smoker-1)]
55 through 80=true
Most recent smoking status observation=Observation(id=observation-Heavy-Smoker-1)
Current smoker observation=Observation(id=observation-Heavy-Smoker-1)
Former smoker observation=null
Is current smoker=true
Is former smoker who quit within past 15 years=false
Pack-years=128 '{Pack-years}'
Has 30 pack-year smoking history=true
Has lung cancer=true
Had chest CT in past year=false
Inclusion Criteria=false
Exclusion Criteria=true

Patient=Patient(id=Never-Smoker)
Patient age in years based on date of birth=84
Smoking status observation=[Observation(id=observation-Never-Smoker-1)]
Lung cancer diagnosis=[]
Chest CT procedure=[]
55 through 80=false
Most recent smoking status observation=Observation(id=observation-Never-Smoker-1)
Current smoker observation=null
Former smoker observation=null
Is current smoker=false
Is former smoker who quit within past 15 years=false
Pack-years=null '{Pack-years}'
Has 30 pack-year smoking history=null
Has lung cancer=false
Had chest CT in past year=false
Inclusion Criteria=false
Exclusion Criteria=true

程式碼說明如下:

  • 宣告與引用的外部Library()
library Exercises10Key
  
using FHIR version '4.0.1'

include FHIRHelpers version '4.0.1' called FHIRHelpers
include FHIRCommon version '4.0.1' called FC

codesystem "SNOMED": 'http://snomed.info/sct'
codesystem "LOINC": 'http://loinc.org'
  • 參數設定:
valueset "Lung Cancer":  'http://cts.nlm.nih.gov/fhir/ValueSet/2.16.840.1.113762.1.4.1116.89'
valueset "Smoking Status": 'http://hl7.org/fhir/us/core/ValueSet/us-core-observation-smokingstatus'
valueset "Current Smoker": 'http://example.org/fhir/ValueSet/currentsmoker'
valueset "Chest CT": 'http://example.org/fhir/ValueSet/chest-ct-procedure'
valueset "Condition Clinical Status Active": 'http://example.org/fhir/ValueSet/conditionclinicalstatusactive'

code "Tobacco Smoking Status": '72166-2' from "LOINC"
code "Former Smoker": '8517006' from "SNOMED"
code "PACKS A DAY": '8663-7' from "LOINC" display 'packs per day'

context Patient
  • Data Element定義
define "Patient age in years based on date of birth":
  AgeInYears()

define "Smoking status observation":
  [Observation: "Tobacco Smoking Status"] O
    where O.status in { 'final', 'amended' }

define "Lung cancer diagnosis":
  [Condition: "Lung Cancer"] C
    where C.clinicalStatus in "Condition Clinical Status Active"

define "Chest CT procedure":
  [Procedure: "Chest CT"] P
    where P.status = 'completed'
  • Intermediate Data Elements
define "55 through 80":
  AgeInYears() >= 55 and AgeInYears() <= 80

define "Most recent smoking status observation":
  Last("Smoking status observation" O
    sort by (FHIRHelpers.ToDateTime(issued))
  )

define "Current smoker observation":
  "Most recent smoking status observation" O
    where (O.value as CodeableConcept) in "Current Smoker"

define "Former smoker observation":
  "Most recent smoking status observation" O
    where (O.value as CodeableConcept) ~ "Former Smoker"

define "Is current smoker":
  "Current smoker observation" is not null

define "Is former smoker who quit within past 15 years":
  ("Former smoker observation" O
    where O.effective ends 15 years or less before Today()
  ) is not null

define "Pack-years":
  "Most recent smoking status observation" O
    let PacksPerDay: singleton from (O.component C where C.code ~ "PACKS A DAY").value,
    DurationInDays: duration in days of O.effective
    return System.Quantity { value: Round((PacksPerDay * (DurationInDays / 365.25)).value), unit: '{Pack-years}' }

define "Has 30 pack-year smoking history":
  "Pack-years" >= 30 '{Pack-years}'

define "Has lung cancer":
  exists ("Lung cancer diagnosis")

define "Had chest CT in past year":
  exists ("Chest CT procedure" P
    where FC.ToInterval(P.performed) ends 1 year or less before Today()
  )
  • Inclusion Criteria
define "Inclusion Criteria":
  "55 through 80"
    and ("Is current smoker" or "Is former smoker who quit within past 15 years")
    and "Has 30 pack-year smoking history"
    and not "Has lung cancer"
    and not "Had chest CT in past year"
  • Exclusion Criteria
define "Exclusion Criteria":
  (
    not ("Is current smoker" or "Is former smoker who quit within past 15 years")
      or "Has lung cancer"
  )

延伸閱讀

cql-translation-service介紹
CQL程式Walkthrough
Cooking with CQL 69 - Colorectal Cancer Concepts