-
Notifications
You must be signed in to change notification settings - Fork 24
Authoring Measures in CQL
This topic will provide a complete discussion of using Clinical Quality Language (CQL) to author electronic Clinical Quality Measures (eCQMs). The discussion assumes familiarity with clinical quality measurement in general, and the representation of eCQMs using Health Quality Measure Format (HQMF) and Quality Data Model (QDM) in particular.
Clinical Quality Language (CQL) is a high-level query language, meaning that it can be used to write expressions that determine what data is to be returned, not how it is to be returned. How the data is to be returned is part of the implementation of a quality measure, and may be accomplished in any number of ways (e.g. by queries in a database system, or map-reduce processing on a Hadoop cluster), so CQL is intentionally silent on any of those details, allowing the logic expressed by CQL queries to be used in a broad variety of implementation environments to achieve the same result.
One of the central constructs available in CQL is the expression, which is any sequence of CQL that returns a value. Values are the data that CQL operates on, and can be simple values like the integer 5
and the string John Doe
, or they can be complex structures (or rows, also called tuples) like a QDM Encounter, Performed
, which contains attributes such as admissionSource
and relevantPeriod
that are themselves values of different types.
The type of value that an expression returns is based on the contents of the expression. For example, an expression that consists only of a value, such as 5
will simply return that value. Operators, such as +
and *
can be used to create more complex expressions such as 2 + 3
, which will return the result of evaluating the operation, the value 5
in this case. Expressions can also make use of functions, such as CalculateAge()
that allow more complex operations to be defined and reused.
One of the most important types of values available in CQL is the list, which is a sequence of values of any type. Lists can contain simple values, like a list of integers, { 1, 2, 3 }
, or they can contain tuples, like a list of encounters.
CQL includes a full suite of operators and functions to allow expressions to be written that can describe the logic and criteria used to precisely represent clinical quality measures. The following sections will discuss how to use this language to author a measure from the ground up.
One of the first considerations when expressing a clinical quality measure in CQL is determining what the measure is counting. Is the measure patient-based, meaning that the measure is expressed as some relationship between populations of patients, or is the measure counting some other kind of case such as encounters or procedures?
If the measure is patient-based, then the criteria we define will all return true
or false
for each specific patient, indicating whether the patient is part of the population being defined by that criteria. A true
or false
value is type Boolean in CQL, and many of the built-in operations such as comparison (e.g. value > 5
) return values of this type.
For example, to express the demographic criteria for a patient-based measure, we can define an In Demographic
expression:
define "In Demographic":
AgeInYearsAt(start of "Measurement Period") >= 13
This example returns true
or false
for a given patient, depending on whether they satisfy the criteria (13 years of age or older at the start of the measurement period).
Often, we want to identify whether a patient has had a particular type of encounter as part of a patient-based measure. For example:
define "Inpatient Encounters":
["Encounter, Performed": "Inpatient Encounter Codes"] Encounter
where Encounter.relevantPeriod during "Measurement Period"
This expression returns a list of encounters for the patient that match one of the codes in the Inpatient Encounter Codes
value set, and that occurred during the measurement period. Now because the result of this expression is a list of encounters, we use an exists
to turn the value into a Boolean so we combine it with the previous expression:
define "Initial Population":
"In Demographic"
and exists ("Inpatient Encounters")
However, if we are counting encounters in an episode-of-care measure, the population criteria will all need to result in lists of encounters:
define "Measurement Period Encounters":
["Encounter, Performed": "Ambulatory/ED Visit"] Encounter
where Encounter.relevantPeriod during "Measurement Period"
and "In Demographic"
define "Pharyngitis Encounters With Antibiotics":
"Measurement Period Encounters" Encounter
with "Pharyngitis" Pharyngitis
such that Common."Includes Or Starts During"(Pharyngitis, Encounter)
with "Antibiotics" Antibiotics
such that Antibiotics.authorDatetime 3 days or less after start of Encounter.relevantPeriod
Since the measure is counting encounters, we can return the list in the initial population:
define "Initial Population":
"Pharyngitis Encounters With Antibiotics"
Once we've identified the type of information the measure will be counting, we need to describe what information needs to be retrieved from the source in order to build the population criteria. QDM allows us to describe clinical information as Data Elements, which identify a Data Type and a Value Set. The Data Type combines a Category of clinical information, such as Encounter, Medication, or Procedure, with a context, such as Performed, Administered, or Ordered. The Data Type then determines the structure of the information. For example, the Medication, Administered Data Type has the following attributes:
- authorDatetime (DateTime)
- relevantPeriod (Interval)
- dosage (Quantity)
- supply (Quantity)
- frequency (Code)
- reason (Code)
- negationRationale (Code)
In addition to the Data Type, a QDM Data Element specifies a Value Set to indicate precisely what kinds of information should be retrieved. For example, the Inpatient Encounter Codes
Value Set identifies encounters that take place in an in-patient setting. The QDM Data Element is then:
"Encounter, Performed: Inpatient Encounter Codes"
In CQL, this is expressed using a retrieve, which encloses the QDM Data Element information in square brackets ([]
):
["Encounter, Performed": "Inpatient Encounter Codes"]
This retrieve expression results in only the highlighted encounters, because they have codes that match the "Inpatient Encounter Codes" Value Set:
Note that the quotation marks in this expression are important, as they are delineating the names of the constructs involved:
Identifier | Description |
---|---|
"Encounter, Performed" |
The name of the QDM Data Type |
"Inpatient Encounter Codes" |
The name of the Value Set |
Any expression in CQL can be given a name so that it can be reused by referencing it in other expressions. The define statement is used for this purpose:
define "Inpatient Encounters":
["Encounter, Performed": "Inpatient Encounter Codes"]
With this statement in a CQL library, the identifier "Inpatient Encounters"
can now be used in other expressions, and returns the same result as repeating the full expression would.
Another central constructs in CQL is the query, which is a specific type of expression that allows relationships between data to be easily and precisely expressed. Queries in CQL are clause-based meaning that they have different types of clauses that can be used depending on what operations need to be performed on the data. Each clause in a query is introduced with a different keyword, and must appear in a particular location within the query in order to be valid CQL.
The general structure of a CQL query is:
<source> <alias>
<with or without clauses>
<where clause>
<return clause>
<sort clause>
All the clauses are optional, so the simplest query is just a source and an alias:
"Inpatient Encounters" Encounter
Here, the source is just a reference to Inpatient Encounters
, which is an expression that returns a list of encounters. This source is given the alias Encounter
. The alias allows the elements of the source to be referenced anywhere within the query. However, this simple query doesn't have any clauses, so it simply returns the same result as the source.
The where
keyword introduces a where clause, which allows you to filter the results of the source:
"Inpatient Encounters" Encounter
where Encounter.relevantPeriod during "Measurement Period"
This query returns only those encounters from the source whose relevantPeriod
occurred entirely during the measurement period:
For where versus such that, a where clause is only used as a direct clause in a query, and a such that is only used in a with or without clause. That's actually why CQL uses a different keyword, so that it would always be clear when you were providing a where clause, or when you were defining the conditions for a with or without clause.
If you're familiar with SQL, you can think of a with as a subquery with an exists, for example:
Encounters E with Medications M such that E.relevantPeriod includes M.relevantPeriod
is equivalent to
select *
from Encounters E
where exists (select * from Medications M where E.relevantPeriod includes M.relevantPeriod)
(Assuming SQL had the same temporal operators as CQL).
Similarly, a without is like a subquery with a not exists:
Encounters E without Medications M such that E.relevantPeriod includes M.relevantPeriod
is equivalent to
select *
from Encounters E
where not exists (select * from Medications M where E.relevantPeriod includes M.relevantPeriod)
Because with and without apply to the whole query, you can't combine them optionally in the same query. To do that, you can either:
-
Use a union to combine the results:
EncountersWithComfortMeasures union EncountersWithASpecificDischarge
-
Combine the logic within a single query using or:
Encounters E where exists (ComfortMeasures C where C.relevantPeriod during E.relevantPeriod) or E.dischargeStatus in ExpectedDischargeStatuses
["Encounter, Performed": "Encounter Inpatient" ] Encounter where "Encounter.dischargeDisposition" in "Patient Expired" and ( not "Newborn Hearing Screening Left" or not "Newborn Hearing Screening Right" )
["Encounter, Performed": "Encounter Inpatient" ] Encounter where ("Encounter.dischargeDisposition" in "Patient Expired" and not "Newborn Hearing Screening Left") or not "Newborn Hearing Screening Right"
behavior is additive
["Encounter, Performed": "Encounter Inpatient" ] Encounter
without "Newborn Hearing Screening Left" Left such that Left.relevantPeriod during Encounter.relevantPeriod
without "Newborn Hearing Screening Right" Right such that Right.relevantPeriod during Encounter.relevantPeriod
where Encounter.dischargeDisposition in "Patient Expired"
define "Encounters Without Left":
["Encounter, Performed": "Encounter Inpatient" ] Encounter
without "Newborn Hearing Screening Left" Left such that Left.relevantPeriod during Encounter.relevantPeriod
define "Encounters Without Right":
["Encounter, Performed": "Encounter Inpatient" ] Encounter
without "Newborn Hearing Screening Right" Right such that Right.relevantPeriod during Encounter.relevantPeriod
define "Encounters Without Screening":
"Encounters Without Left" union "Encounters Without Right"
To determine the length of time between two dates, CQL provides two different approaches. The first approach, calculating the duration, determines the number of whole periods that occur between the two dates. Conceptually, the calculation is performed by considering the two dates on a timeline, and counting the number of whole periods that fit on that timeline between the two dates. For example:
define Date1: @2012-03-10
define Date2: @2013-03-10
define DurationInYears: years between Date1 and Date2
This expression gives one year, because an entire year has passed between the two dates. Note that time is considered for the purposes of calculating the number of years:
define DateTime1: @2012-03-10T10:20:00
define DateTime2: @2013-03-10T09:20:00
define DurationInYears: years between DateTime1 and DateTime2
This expression gives zero years, because the year has not passed until 10:20:00 on the day in the following year. To calculate the number of years, ignoring the time, extract the date from the date/time value:
define DateTime1: @2012-03-10T10:20:00
define DateTime2: @2013-03-10T09:20:00
define DurationInYears: years between (date from DateTime1) and (date from DateTime2)
The second approach, calculating the difference, determines the number of boundaries crossed between two dates. To illustrate the difference, consider the following example:
define Date1: @2012-12-31
define Date2: @2013-01-01
define DurationInYears: years between Date1 and Date2
define DifferenceInYears: difference in years between Date1 and Date2
The DurationInYears expression returns zero because a full year has not passed between the two dates. However, the second expression returns 1 because a year boundary was crossed between the two dates.
In CQL, a year is defined as the duration of any time interval which starts at a certain time of day at a certain calendar date of the calendar year and ends at:
- The same time of at the same calendar date of the next calendar year, if it exists
- The same time of day at the immediately following calendar date of the ending calendar, if the same calendar date of the ending calendar year does not exist.
Note: When in the next calendar year the same calendar date does not exist, the ISO states that the ending calendar day has to be agreed upon. The above convention is used in CQL as a resolution to this issue.
- Month (date 2) < month (date 1): Duration (years) = year (date 2) - year (date 1) - 1
Example 1:
Date 1: 2012-03-10 22:05:09
Date 2: 2013-02-18 19:10:03
Duration = year (date 2) - year (date 1) - 1 = 2013 - 2012 - 1 = 0 years
- Month (date 2) = month (date 1) and day (date 2) >= day (date 1)
Duration (years) = year (date 2) - year (date 1)
Example 2.a: day (date 1) = day (date 2)
Date 1: 2012-03-10 22:05:09
Date 2: 2012-03-10 22:05:09
Duration = year (date 2) - year (date 1) = 2013 - 2012 = 1 year
Note: Time of day is important in this calculation. If the time of day of Date 2 were less than the time of day for Date 1, the duration of the time interval would be 0 years according to the definition.
Example 2.b: day (date 2) > day (date 1)
Date 1: 2012-03-10 22:05:09
Date 2: 2013-03-20 04:01:30
Duration = year (date 2) - year (date 1) = 2013 - 2012 = 1 year
- Month (date 2) = month (date 1) and day (date 2) < day (date 1)
Duration (years) = year (date 2) - year (date 1) - 1
Example 3.a:
Date 1: 2012-02-29
Date 2: 2014-02-28
Duration = year (date 2) - year (date 1) - 1 = 2014 - 2012 - 1 = 1 year
- Month (date 2) > month (date 1)
Duration (years) = year (date 2) - year (date 1)
Example 4.a:
Date 1: 2012-03-10 11:16:02
Date 2: 2013-08-15 21:34:16
Duration = year (date 2) - year (date 1) = 2013 - 2012 - 1 year
Example 4.b:
Date 1: 2012-02-29 10:18:56
Date 2: 2014-03-01 19:02:34
Duration = year (date 2) - year (date 1) = 2014 - 2012 = 2 years
Note: Because there is no February 29 in 2014, the number of years can only change when the date reaches March 1, the first date in 2014 that surpasses the month and day of date 1 (Feburary 29).
A month in CQL is defines as the duration of any time interval which starts at a certain time of day at a certain calendar day of the calendar month and ends at:
- The same time of day at the same calendar day of the ending calendar month, if it exists
- The same time of day at the immediately following calendar date of the ending calendar month, if the same calendar date of the ending month in the ending year does not exist.
Notes: When in the next calendar year the same calendar date does not exist, the ISO states that the ending calendar day has to be agreed upon. The above convention is used in CQL as a resolution to this issue.
- Day (date 2) >= day (date 1)
Duration (months) = (year (date 2) - year (date 1)) * 12 + (month (date 2) - month (date 1))
Example 1.a:
Date 1: 2012-03-01 14:05:45
Date 2: 2012-03-31 23:01:49
Duration = (year (date 2) - year (date 1)) * 12 + (month (date 2) - (month (date 1))
= (2012 - 2012) * 12 + (3 - 3) = 0 months
Example 1.b:
Date 1: 2012-03-10 22:05:09
Date 2: 2013-06-30 13:00:23
Duration = (year (date 2) - year (date 1)) * 12 + (month (date 2) - (month date 1))
= (2013 - 2012) * 12 + (6 - 3) = 12 + 3 = 15 months
- Day (day 2) < day (date 1)
Duration (months) = (year (date 2) - year (date 1)) * 12 + (month (date 2) - month (date 1)) - 1
Example 2:
Date 1: 2012-03-10 22:05:09
Date 2: 2013-01-09 07:19:33
Duration = (year (date 2) - year (date 1)) * 12 + (month (date 2) - month (date 1)) - 1
= (2013 - 2012) * 12 + (1 - 3) - 1 = 12 - 2 - 1 = 9 months
In CQL, a week is defined as a duration of any time interval which starts at a certain time of day at a certain calendar day at a certain calendar week and ends at the same time of day at the same calendar day of the ending calendar week. In other words, a complete week is always seven days long.
- Duration = [date 2 - date 1 (days)] / 7
Example 1:
Date 1: 2012-03-10 22:05:09
Date 2: 2012-03-20 07:19:33
Duration = [# days (month (date 1)) - day (date 1) + # days (month (date 1) + 1) + #days (month (date 1) + 2) + ... + # days (month (date 2) - 1) + day (date 2)] / 7
= (20 - 10) / 7 = 10 / 7 = 1 week
In CQL, a day is defined as a duration of any time interval which starts at a certain calendar day and ends at the next calendar day (1 second to 23 hours, 59 minutes, and 59 seconds).
The duration in days between two dates will generally be given by subtracting the start calendar date from the end calendar date, respecting the time of day between the two dates.
- Time (date 2) < time (date 1)
Duration = [date 2 - date 1 (days)] - 1
Example 1:
Date 1: 2012-01-31 12:30:00
Date 2: 2012-02-01 09:00:00
Duration = 02-01 - 01-31 - 1 = 0 days
- Time (date 2) >= time (date 1)
Duration = date 2 - date 1 (days)
Example 2:
Date 1: 2012-01-31 12:30:00
Date 2: 2012-02-01 14:00:00
Duration = 02-01 - 01-31 = 1 day
In CQL, an hour is defined as 60 minutes. The duration in hours between two dates is the number of minutes between the two dates, divided by 60. The result is truncated to the unit.
-
Example 1:
Date 1: 2012-03-01 03:10:00
Date 2: 2012-03-01 05:09:00
Duration = 1 hour -
Example 2:
Date 1: 2012-02-29 23:10:00
Date 2: 2012-03-01 00:10:00
Duration = 1 hour -
Example 3:
Date 1: 2012-03-01 03:10
Date 2: 2012-03-01 04:00
Duration = 0 hours
In CQL, a minute is defined as 60 seconds. The duration in minutes between two dates is the number of seconds between the two dates, divided by 60. The result is truncated to the unit.
-
Example 1:
Date 1: 2012-03-01 03:10:00
Date 2: 2012-03-01 05:20:00
Duration = 130 minutes -
Example 2:
Date 1: 2012-02-29 23:10:00
Date 2: 2012-03-01 00:20:00
Duration = 70 minutes
Authoring Patterns - QICore v4.1.1
Authoring Patterns - QICore v5.0.0
Authoring Patterns - QICore v6.0.0
Cooking with CQL Q&A All Categories
Additional Q&A Examples
Developers Introduction to CQL
Specifying Population Criteria