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出發,透過程式語言,將複雜的邏輯轉換為可執行程式。 ![cql-2](https://hackmd.io/_uploads/B1OcIRfPa.png) 以Firely這家公司為例,在DevDays 2023就發表了CQL Engine,將ELM轉換成為C#原始碼,透過編譯成DLL後,其他C#程式就可以直接使用原始CQL所定義的運算邏輯。 ![cql-1](https://hackmd.io/_uploads/BkBMWYGD6.png) 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.ini`和`input/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介紹](https://hackmd.io/@hongyu0324/cql-to-elm-service) [CQL程式Walkthrough](https://hackmd.io/@hongyu0324/cqlwalk) [Cooking with CQL 69 - Colorectal Cancer Concepts](https://hackmd.io/@hongyu0324/CookingCQL069)