Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

4 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

CQM population-membership examples

Runnable FHIR artifacts for four measure scoring types — proportion, ratio, continuous-variable, and cohort — built on deliberately non-clinical, resource-existence CQL so the focus stays on population membership mechanics rather than clinical logic.

All four measures evaluate one shared set of nine patients (shared/Bundle-patients.json). Each expected/ folder holds the summary MeasureReport a conformant engine should produce for that measure over that bundle.

Layout

shared/
  Bundle-patients.json   the nine patients — the single input for every measure
proportion/              ratio/              continuous-variable/       cohort/
  IGProportion.cql         IGRatio.cql         IGContinuousVariable.cql   IGCohort.cql
  Library-*.json           Library-*.json      Library-*.json             Library-*.json
  Measure-*.json           Measure-*.json      Measure-*.json             Measure-*.json
  expected/                expected/           expected/                  expected/
    MeasureReport-*-summary.json       (one summary report per measure)
    individual/                        (one report per patient: -ada … -ivy)
      MeasureReport-*-<patient>.json
INDEX.md  overview.svg  matrices.md  README.md   (top-level map and docs)
gen.py                   (regenerates the bundle, measures, expected reports, matrices)

Why one shared patient set

Earlier drafts gave each measure its own patient bundle, which reused ids like Patient/ada across folders — loading two of them into the same FHIR server made the second overwrite the first, and evaluating one measure over a server holding all of them would count patients meant for other measures. A single shared roster removes both problems: one Patient per id, and one intended population that every measure scores its own way.

The nine patients

Profiles over the resources the measures retrieve (E=Encounter, Pr=Procedure, D=deceased, O=Observation count, R=ResearchSubject, C=Condition, Im=Immunization). Each patient is chosen to fire a distinct membership rule in one or more measures — the interesting cases are where a criterion evaluates true but the patient is not counted.

id E Pr D O R C Im notable roles
ada 1 1 0 2 0 0 1 clean member everywhere
ben 1 1 0 0 0 0 0 denominator/measure-pop member with no numerator; CV value 0
cyd 0 1 0 9 0 0 1 no Encounter → gated out of proportion/CV; numerator with no denominator in ratio
dev 1 1 1 4 0 0 1 deceased → excluded (prop/CV); still counts in the ratio numerator
eve 1 1 0 1 1 0 1 numerator-excluded via ResearchSubject (prop/ratio)
fin 1 1 0 0 0 1 0 denominator exception (prop); denominator-only (ratio); CV value 0
gus 1 1 0 3 0 1 1 met numerator, so the Condition-driven exception is suppressed (prop)
hana 0 0 1 0 0 0 0 deceased but never in a denominator → exclusion criterion gated
ivy 1 0 0 7 0 0 0 Encounter but no Procedure → initial-population-only (prop/CV)

Expected results over the shared bundle

  • Proportion — IPP 7, DENOM 6, DENEX 1, NUMER 3, NUMEX 1, DENEXCEP 1; score = (3 − 1) / (6 − 1 − 1) = 0.5
  • Ratio — IPP 1 = 7, DENOM 6, DENEX 1, IPP 2 = 5, NUMER 5, NUMEX 1; ratio = (5 − 1) / (6 − 1) = 0.8 (numerator and denominator counted independently)
  • Continuous variable — IPP 7, MPOP 6, MPOPEX 1; observations [0, 0, 1, 2, 3]; median = 1 (sum 6, mean 1.2, count 5)
  • Cohort — initial-population 7; the score is that count

See matrices.md for the per-patient breakdown behind each of these.

Individual reports

Each measure folder also has expected/individual/ with one type: individual MeasureReport per patient (MeasureReport-<measure>-<patient>.json). Each is that patient's row from matrices.md expressed as FHIR: subject points to the Patient, and every group.population carries a count of 0 or 1 for that patient's membership. Summed across the nine individual reports, the population counts equal the summary report — that is the check these are built to support.

Conventions used, since individual measureScore handling varies by engine:

  • ProportionmeasureScore is included only for patients in the scored denominator: 1 if they meet the numerator, 0 otherwise. Patients who are out of scope, excluded, or excepted carry population counts but no measureScore.
  • Continuous variable — scored patients carry the measure observation as a contained Observation (referenced from evaluatedResource) and as group.measureScore; non-members have measure-observation count 0 and no score.
  • Ratio, cohort — population memberships only, no individual measureScore (a ratio has no clean per-subject score; a cohort individual is just membership).

The two ratio initial populations are disambiguated by the population element id (ipp-1 for the denominator branch, ipp-2 for the numerator branch) in both the summary and individual reports.

Test-case marking

Every MeasureReport in this package — summary and individual — is marked as a test case with two CQF Measures IG extensions, so anyone opening one sees that it's an expected-outcome fixture and what it exercises:

  • cqfm-isTestCase (a boolean modifier extension, so it lives in modifierExtension) set to true.
  • cqfm-testCaseDescription (markdown, in extension) describing that specific case — e.g. a proportion individual report for a deceased patient explains that the Numerator is gated out by the exclusion even though its criterion evaluates true.

Both are from http://hl7.org/fhir/us/cqfmeasures/StructureDefinition/, matching the cqfm- extensions used on the Measure resources. If you are targeting the universal hl7.fhir.uv.cqm IG instead, the equivalents are cqm-isTestCase and cqm-testCaseDescription — change the two URLs in gen.py.

Running against an engine

The Library resources carry the CQL as text/cql content. Most execution engines (fqm-execution, cqf-ruler, the reference CQL engine) additionally need compiled ELM in Library.content — the one step these files don't pre-bake, because ELM must come from the translator.

  1. Compile each CQL to ELM with the CQL-to-ELM translator (the cql-translation-service, the translator-cli jar, or fqm-execution's bundled translator), then add an application/elm+json entry alongside the existing text/cql entry in each Library.

  2. Assemble a measure bundle per scoring type (Measure + Library, plus FHIRHelpers).

  3. Calculate against the shared patient bundle. With fqm-execution:

    fqm-execution calculate \
      --measure-bundle measure-bundle.json \
      --patient-bundle shared/Bundle-patients.json \
      --report-type summary
    

    Or POST shared/Bundle-patients.json to a FHIR server and call Measure/$evaluate-measure.

  4. Diff the returned summary MeasureReport.group.population[].count and measureScore against the file in that folder's expected/.

Re-run python3 gen.py to regenerate the bundle, all four measures, the expected reports, and matrices.md from the single ROSTER definition, so they can't drift.

Caveats

  • Evaluation scope. Each measure counts every patient in the shared bundle — that is the intent here (one population, four scorings). To score a subset on a shared server, evaluate with a subject / Group parameter.
  • Numerator Exclusion uses ResearchSubject. A patient's participation in a study is a ResearchSubject, which links to the patient via individual, so exists([ResearchSubject]) resolves correctly in Patient context. It affects only eve. The bundle includes one shared ResearchStudy (cqm-example-study) for each ResearchSubject.study to reference, since that element is required in R4.
  • Aggregate-method code casing. The continuous-variable measure uses valueCode: "Median"; confirm the casing your engine expects against its bound aggregate-method value set.
  • Gating precedence (exclusion before numerator, numerator before exception) is the reference-engine behavior, not spelled out in the IG prose — worth stating explicitly wherever you reuse these.
  • Cohort measureScore. The expected cohort report sets measureScore to the initial-population count (7), since the cohort score is the member count. Some engines report only the initial-population count and leave measureScore unset — treat the count as authoritative.

About

Site demonstrating measure calculation for the various scoring types

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages