Tuesday, November 22, 2011

Classifying Results in HQMF

As I said in yesterday's post I'd be thinking more about how to automate classifications in a measure so that results could be classified by different grouping, e.g., by age group and gender.  It didn't take me that long to figure it out, and I have to admit I thought that problem would be much more challenging.

What I wound up doing was adding a new kind of entry to the HQMF format to identify a classifier group.  Each precondition in that entry would describe a single, exclusive group in which the patient belonged.  I've already proposed that this be added to the HQMF format.


<observation classCode="OBS" moodCode="EVN" isCriterionInd="true">
  <id root="c75181d0-73eb-11de-8a39-0800200c9a66"/>
  <code code="ASSERTION" codeSystem="2.16.840.1.113883.5.4"/>
  <value xsi:type="CD" code="CLASSIFIER" codeSystem="2.16.840.1.113883.5.1063"
    codeSystemName="HL7 Observation Value"
    displayName="Measure Group Classifier"/>
  <sourceOf typeCode="PRCN">
    <conjunctionCode code="XOR"/>
    <observation classCode="OBS" moodCode="EVN" 
      isCriterionInd="true">
      <id root="0" extension="ageBetween17and21"/>
    </observation>
  </sourceOf>
  <sourceOf typeCode="PRCN">
    <conjunctionCode code="XOR"/>
    <observation classCode="OBS" moodCode="EVN" 
      isCriterionInd="true">
      <id root="0" extension="ageBetween22and29"/>
    </observation>
  </sourceOf>
      ...

This looks just like the definition of numerator or denominator criteria, which means that classifiers can be as complex as selectors for members of a population, numerator, denominator, et cetera. You can have as many CLASSIFIER entries as you need to group your results by.  In the case I worked with, I just added age and gender to the original HQMF I built for the HbA1C measure (NQF Measure 59).

The hQuery implementation was pretty easy to modify.  I just used the same code I already had to create a variable that would include each patient in the classified group.  That looks something like this:
  var ageBetween17and21= patient.Demographics().match(
    new CD("424144002","2.16.840.1.113883.6.96), null, 
    new IVL(new PQ("17","a"),new PQ("21","a")));
  var ageBetween22and29= patient.Demographics().match(
    new CD("424144002","2.16.840.1.113883.6.96), null, 
    new IVL(new PQ("22","a"),new PQ("29","a")));
  var ageBetween60and64= patient.Demographics().match(
    new CD("424144002","2.16.840.1.113883.6.96), null, 
    new IVL(new PQ("60","a"),new PQ("64","a")));
  var genderMale= patient.Demographics().match(
    new CD("263495000","2.16.840.1.113883.6.96), null, 
    new CD("M","2.16.840.1.113883.5.1));
  var genderFemale= patient.Demographics().match(
    new CD("263495000","2.16.840.1.113883.6.96), null, 
    new CD("F","2.16.840.1.113883.5.1));

The next step was enable the classification of the results.  The new emit statements are shown in bold below:
  if (population(patient)) {
    if (ageBetween17and21.contains(patient)) {
        emit("ageBetween17and21",1);
    } 
    else if (ageBetween22and29.contains(patient)) {
        emit("ageBetween22and29",1);
    } 
      ...
    else if (ageBetween60and64.contains(patient)) {
        emit("ageBetween60and64",1);
    } 
    if (genderMale.contains(patient)) {
        emit("genderMale",1);
    } 
    else if (genderFemale.contains(patient)) {
        emit("genderFemale",1);
    } 
    emit("population", 1);
    if (denominator(patient)) {
      if (numerator(patient)) {
        emit("denominator", 1);
        emit("numerator", 1);
      } else if (exclusion(patient)) {
        emit("exclusion", 1);
      } else {
        emit("denominator", 1);
      }
    }
  }

Essentially what happens is that for each patient in the population, the hQuery script will output a value indicating the age and gender classification.  The reduce function can then handle consolidating the correlated results for population, numerator, denominator and exclusions.

In the XQuery implementation, I create classifier functions just like I do for other numerator and denominator criteria.  Here is an example of one of those for gender:

 declare function local:genderMale($ids, $ccds) {
                for $d in local:Demographics($ids, $ccd)
                where local:matches($e, '',<observation xmlns="urn:hl7-org:v3" classCode="OBS" moodCode="EVN" isCriterionInd="true">
      <id root="0" extension="genderMale"/>
      <code code="263495000" codeSystem="2.16.840.1.113883.6.96" displayName="Gender"/>
      <value xsi:type="CD" codeSystem="2.16.840.1.113883.5.1" code="M"/>
      <sourceOf typeCode="INST">
         <observation classCode="OBS" moodCode="DEF">
            <id root="0" extension="Demographics"/>
         </observation>
      </sourceOf>
   </observation>)
                return $e//cda:recordTarget/cda:id
    }

Then, when I go to output the results, instead of just generating the complete list of identifiers for population, numerator, et cetera, I apply the classifiers to segment the lists, using the XQuery intersect operator.  Here is an example of what gets generated:

declare function local:denominator($ccds, $ids) {
  let $result0 = ((local:HasDiabetes($ccds, $ids)) and (local:EDorInpatientEncounter($ccds, $ids) or local:AmbulatoryEncounter($ccds, $ids)) or local:DiabetesMedAdministered($ccds, $ids) or local:DiabetesMedIntended($ccds, $ids) or local:DiabetesMedSupplied($ccds, $ids) or local:DiabetesMedOrdered($ccds, $ids));
  let $resultageBetween17and21 = local:ageBetween17and21($ccds,$ids);
  let $resultageBetween22and29 = local:ageBetween22and29($ccds,$ids);
  let $resultageBetween30and39 = local:ageBetween30and39($ccds,$ids);
  let $resultageBetween40and49 = local:ageBetween40and49($ccds,$ids);
  let $resultageBetween50and59 = local:ageBetween50and59($ccds,$ids);
  let $resultageBetween60and64 = local:ageBetween60and64($ccds,$ids);
  let $resultgenderMale = local:genderMale($ccds,$ids);
  let $resultgenderFemale = local:genderFemale($ccds,$ids);
  return 
<denominator>
  <group name="ageBetween17and21">
    <group name="genderMale">
     { return $result0 intersect $resultageBetween17and21 intersect $resultgenderMale; }
    </group>
    <group name="genderFemale">
     { return $result0  intersect $resultageBetween17and21 intersect $resultgenderFemale; }
    </group>
  </group>
     ...
  <group name="ageBetween60and64">
    <group name="genderMale">
     { return $result0  intersect $resultageBetween60and64 intersect $resultgenderMale; }
    </group>
    <group name="genderFemale">
     { return $result0  intersect $resultageBetween60and64 intersect $resultgenderFemale; }
    </group>
  </group>
</denominator>  
}

Finally, the SQL.  For that, I created a view for each classifier that had two columns.  The first column was the patient identifier, and the second was the result of the classification.  Originally I was going to fill the second column using a SQL CASE statement, but I realized that each classification could be complex, and I wasn't sure that CASE would be a general enough solution.  So, I modified the view to be the UNION of SELECTS which matched each classifier in a classification, where the second column of the SELECT was just a fixed value.  The SQL for the Gender VIEW looks like this:

CREATE VIEW Gender AS (
/* genderMale*/
  SELECT DISTINCT PATIENTID , 'genderMale' CLASS FROM Demographics
    WHERE (
        CODE = 'M' AND CODESYSTEM = '2.16.840.1.113883.5.1'    )
 UNION /* genderFemale*/
  SELECT DISTINCT PATIENTID , 'genderFemale' CLASS FROM Demographics
    WHERE (
        CODE = 'F' AND CODESYSTEM = '2.16.840.1.113883.5.1'    )
);
 
You can see in this example how CASE would simplify the above view, but then there's no easy way to combine multiple criteria with a single classifier when the criteria needs to access two different tables.  Here's an example where the classification critieria puts men in group 1, and women without gestational diabetes in group 2:

CREATE VIEW Groups AS (
/* group1*/
  SELECT DISTINCT PATIENTID , 'group1' CLASS FROM Demographics
    WHERE (
        CODE = 'M' AND CODESYSTEM = '2.16.840.1.113883.5.1'    )
 UNION 
/* group2 */
 (/* genderFemale */
  SELECT DISTINCT PATIENTID FROM Demographics
    WHERE (
        CODE = 'F' AND CODESYSTEM = '2.16.840.1.113883.5.1'    )
  INTERSECT 
  /* !HasGestationalDiabetes*/
  SELECT DISTINCT PATIENTID FROM Problem
    WHERE NOT(
        CODE IN (SELECT CODE FROM VALUESETS WHERE ID = '2.16.840.1.113883.3.464.1.67')  
        AND EFFECTIVETIME &gt; @StartDate AND EFFECTIVETIME &lt; @EndDate    )
 )
);

Having created these views, I alter the generation query so that it joins with each classification table, and outputs the classification column (which I rename).  Then I group and order the results based on the classifications, which gets the counts grouped and ordered as well.  Here's the output SQL:

SELECT 
    Age.CLASS AS Age, Gender.CLASS AS Gender, 
    COUNT(P.PATIENTID) AS POPULATIONIDS ,
    COUNT(D.PATIENTID) AS DENOMINATORIDS,
    COUNT(N.PATIENTID) AS NUMERATORIDS,
    COUNT(E.PATIENTID) AS EXCEPTIONIDS
FROM POPULATION P
  JOIN Age  ON P.PATIENTID = Age.PATIENTID
  JOIN Gender  ON P.PATIENTID = Gender.PATIENTID
LEFT OUTER JOIN DENOMINATOR D
ON P.PATIENTID = D.PATIENTID 
LEFT OUTER JOIN NUMERATOR N
ON D.PATIENTID = N.PATIENTID 
LEFT OUTER JOIN EXCEPTION E
ON D.PATIENTID = E.PATIENTID AND E.PATIENTID NOT IN (SELECT PATIENTID FROM NUMERATOR) 
GROUP BY Age, Gender
ORDER BY Age, Gender

So, there you have it.  For now, this completes the different features that I'm going to try to implement using HQMF to support Query Health.  I think I've successfully shown that HQMF is powerful enough to represent a variety of different queries that are of interest.  My next project is to see what can be done to make HQMF a more friendly specification for us to work with, and I'll provide that as input into the HL7 update of the DSTU.

   Keith



0 comments:

Post a Comment