Skip to content

Commit

Permalink
Measure scoring specified at group level (#391)
Browse files Browse the repository at this point in the history
* allow setting measureScorer on group level or Measurelevel

* spotless apply edits

* group scoring logic
  • Loading branch information
Capt-Mac authored Dec 20, 2023
1 parent 67a69d3 commit 6ee71b7
Show file tree
Hide file tree
Showing 106 changed files with 64,520 additions and 164 deletions.
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package org.opencds.cqf.fhir.cr.measure.common;

import java.util.List;
import java.util.Map;
import org.opencds.cqf.cql.engine.runtime.Interval;

public class MeasureDef {
Expand All @@ -9,18 +10,23 @@ public class MeasureDef {
private final String url;
private final String version;
private Interval defaultMeasurementPeriod;
private final MeasureScoring scoring;
private final Map<GroupDef, MeasureScoring> scoring;
private final List<GroupDef> groups;
private final List<SdeDef> sdes;

public MeasureDef(
String id, String url, String version, MeasureScoring scoring, List<GroupDef> groups, List<SdeDef> sdes) {
String id,
String url,
String version,
Map<GroupDef, MeasureScoring> scoring,
List<GroupDef> groups,
List<SdeDef> sdes) {
this.id = id;
this.url = url;
this.version = version;
this.scoring = scoring;
this.groups = groups;
this.sdes = sdes;
this.scoring = scoring;
}

public String id() {
Expand All @@ -35,10 +41,6 @@ public String version() {
return this.version;
}

public MeasureScoring scoring() {
return this.scoring;
}

public Interval getDefaultMeasurementPeriod() {
return defaultMeasurementPeriod;
}
Expand All @@ -54,4 +56,8 @@ public List<SdeDef> sdes() {
public List<GroupDef> groups() {
return this.groups;
}

public Map<GroupDef, MeasureScoring> scoring() {
return this.scoring;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
import java.time.OffsetDateTime;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import org.apache.commons.lang3.tuple.Pair;
Expand All @@ -31,11 +32,16 @@
import org.slf4j.LoggerFactory;

/**
* This class implements the core Measure evaluation logic that's defined in the Quality Measure
* implementation guide and HQMF specifications. There are a number of model-independent concepts
* such as "groups", "populations", and "stratifiers" that can be used across a number of different
* data models including FHIR, QDM, and QICore. To the extent feasible, this class is intended to be
* model-independent so that it can be used in any Java-based implementation of Quality Measure
* This class implements the core Measure evaluation logic that's defined in the
* Quality Measure
* implementation guide and HQMF specifications. There are a number of
* model-independent concepts
* such as "groups", "populations", and "stratifiers" that can be used across a
* number of different
* data models including FHIR, QDM, and QICore. To the extent feasible, this
* class is intended to be
* model-independent so that it can be used in any Java-based implementation of
* Quality Measure
* evaluation.
*
* @see <a href=
Expand Down Expand Up @@ -209,10 +215,7 @@ protected MeasureDef evaluate(MeasureDef measureDef, MeasureReportType type, Lis
type.toCode(),
subjectIds.size());

MeasureScoring scoring = measureDef.scoring();
if (scoring == null) {
throw new RuntimeException("MeasureScoring type is required in order to calculate.");
}
Map<GroupDef, MeasureScoring> scoring = measureDef.scoring();

for (String subjectId : subjectIds) {
if (subjectId == null) {
Expand All @@ -229,7 +232,7 @@ protected MeasureDef evaluate(MeasureDef measureDef, MeasureReportType type, Lis
}

protected void evaluateSubject(
MeasureDef measureDef, MeasureScoring scoring, String subjectType, String subjectId) {
MeasureDef measureDef, Map<GroupDef, MeasureScoring> scoring, String subjectType, String subjectId) {
evaluateSdes(subjectId, measureDef.sdes());
for (GroupDef groupDef : measureDef.groups()) {
evaluateGroup(scoring, groupDef, subjectType, subjectId);
Expand Down Expand Up @@ -399,9 +402,11 @@ protected void evaluateCohort(GroupDef groupDef, String subjectType, String subj
}

protected void evaluateGroup(
MeasureScoring measureScoring, GroupDef groupDef, String subjectType, String subjectId) {
Map<GroupDef, MeasureScoring> measureScoring, GroupDef groupDef, String subjectType, String subjectId) {
evaluateStratifiers(subjectId, groupDef.stratifiers());
switch (measureScoring) {

var scoring = measureScoring.get(groupDef);
switch (scoring) {
case PROPORTION:
case RATIO:
evaluateProportion(groupDef, subjectType, subjectId);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package org.opencds.cqf.fhir.cr.measure.common;

import java.util.Map;

public interface MeasureReportScorer<MeasureReportT> {
public void score(MeasureScoring measureScoring, MeasureReportT measureReport);
public void score(Map<GroupDef, MeasureScoring> measureScoring, MeasureReportT measureReport);
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
package org.opencds.cqf.fhir.cr.measure.dstu3;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.apache.commons.lang3.StringUtils;
import org.hl7.fhir.dstu3.model.CodeableConcept;
import org.hl7.fhir.dstu3.model.Coding;
Expand Down Expand Up @@ -51,7 +53,12 @@ public MeasureDef build(Measure measure) {
}

// Groups
MeasureScoring groupMeasureScoringCode = getMeasureScoring(measure);
if (groupMeasureScoringCode == null) {
throw new IllegalStateException("MeasureScoring must be specified on Measure");
}
List<GroupDef> groups = new ArrayList<>();
Map<GroupDef, MeasureScoring> groupMeasureScoring = new HashMap<>();
for (MeasureGroupComponent group : measure.getGroup()) {
checkId(group);

Expand All @@ -78,21 +85,17 @@ public MeasureDef build(Measure measure) {

stratifiers.add(stratifierDef);
}

groups.add(new GroupDef(
var groupDef = new GroupDef(
group.getId(),
null, // No code on group in dstu3
stratifiers,
populations));
populations);
groups.add(groupDef);
groupMeasureScoring.put(groupDef, groupMeasureScoringCode);
}

return new MeasureDef(
measure.getId(),
measure.getUrl(),
measure.getVersion(),
MeasureScoring.fromCode(measure.getScoring().getCodingFirstRep().getCode()),
groups,
sdes);
measure.getId(), measure.getUrl(), measure.getVersion(), groupMeasureScoring, groups, sdes);
}

private ConceptDef conceptToConceptDef(CodeableConcept codeable) {
Expand Down Expand Up @@ -123,4 +126,8 @@ private void checkId(Resource r) {
throw new NullPointerException("id is required on all Resources of type: " + r.fhirType());
}
}

private MeasureScoring getMeasureScoring(Measure measure) {
return MeasureScoring.fromCode(measure.getScoring().getCodingFirstRep().getCode());
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package org.opencds.cqf.fhir.cr.measure.dstu3;

import java.util.Map;
import java.util.Optional;
import org.hl7.fhir.dstu3.model.MeasureReport;
import org.hl7.fhir.dstu3.model.MeasureReport.MeasureReportGroupComponent;
Expand All @@ -8,15 +9,19 @@
import org.hl7.fhir.dstu3.model.MeasureReport.StratifierGroupComponent;
import org.hl7.fhir.dstu3.model.MeasureReport.StratifierGroupPopulationComponent;
import org.opencds.cqf.fhir.cr.measure.common.BaseMeasureReportScorer;
import org.opencds.cqf.fhir.cr.measure.common.GroupDef;
import org.opencds.cqf.fhir.cr.measure.common.MeasurePopulationType;
import org.opencds.cqf.fhir.cr.measure.common.MeasureScoring;

public class Dstu3MeasureReportScorer extends BaseMeasureReportScorer<MeasureReport> {

@Override
public void score(MeasureScoring measureScoring, MeasureReport measureReport) {
public void score(Map<GroupDef, MeasureScoring> measureScoring, MeasureReport measureReport) {
// Dstu3 only has MeasureScoring defined on Measure level,
// use that for all groups
var scoring = measureScoring.entrySet().stream().findFirst().get().getValue();
for (MeasureReportGroupComponent mrgc : measureReport.getGroup()) {
scoreGroup(measureScoring, mrgc);
scoreGroup(scoring, mrgc);
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
package org.opencds.cqf.fhir.cr.measure.r4;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.apache.commons.lang3.StringUtils;
import org.hl7.fhir.r4.model.CodeableConcept;
import org.hl7.fhir.r4.model.Coding;
Expand Down Expand Up @@ -37,6 +39,8 @@ public R4MeasureDefBuilder(boolean enforceIds) {
this.enforceIds = enforceIds;
}

public final String CQFM_SCORING_EXT_URL = "http://hl7.org/fhir/us/cqfmeasures/StructureDefinition/cqfm-scoring";

@Override
public MeasureDef build(Measure measure) {
checkId(measure);
Expand All @@ -51,10 +55,27 @@ public MeasureDef build(Measure measure) {
}

// Groups
var measureLevelMeasureScoring = getMeasureScoring(measure);
List<GroupDef> groups = new ArrayList<>();
Map<GroupDef, MeasureScoring> groupMeasureScoring = new HashMap<>();
for (MeasureGroupComponent group : measure.getGroup()) {
checkId(group);

// Group MeasureScoring
if (measureLevelMeasureScoring == null && group.getExtensionByUrl(CQFM_SCORING_EXT_URL) == null) {
throw new IllegalStateException("MeasureScoring must be specified on Group or Measure");
}
MeasureScoring groupMeasureScoringCode = null;
if (group.getExtensionByUrl(CQFM_SCORING_EXT_URL) != null) {
CodeableConcept coding = (CodeableConcept)
group.getExtensionByUrl(CQFM_SCORING_EXT_URL).getValue();
groupMeasureScoringCode =
getGroupMeasureScoring(coding.getCodingFirstRep().getCode());
}
if (group.getExtensionByUrl(CQFM_SCORING_EXT_URL) == null && measureLevelMeasureScoring != null) {
groupMeasureScoringCode = measureLevelMeasureScoring;
}

// Populations
List<PopulationDef> populations = new ArrayList<>();
for (MeasureGroupPopulationComponent pop : group.getPopulation()) {
Expand Down Expand Up @@ -94,17 +115,13 @@ public MeasureDef build(Measure measure) {

stratifiers.add(stratifierDef);
}

groups.add(new GroupDef(group.getId(), conceptToConceptDef(group.getCode()), stratifiers, populations));
var groupDef = new GroupDef(group.getId(), conceptToConceptDef(group.getCode()), stratifiers, populations);
groups.add(groupDef);
groupMeasureScoring.put(groupDef, groupMeasureScoringCode);
}

return new MeasureDef(
measure.getId(),
measure.getUrl(),
measure.getVersion(),
MeasureScoring.fromCode(measure.getScoring().getCodingFirstRep().getCode()),
groups,
sdes);
measure.getId(), measure.getUrl(), measure.getVersion(), groupMeasureScoring, groups, sdes);
}

private ConceptDef conceptToConceptDef(CodeableConcept codeable) {
Expand Down Expand Up @@ -135,4 +152,12 @@ private void checkId(Resource r) {
throw new NullPointerException("id is required on all Resources of type: " + r.fhirType());
}
}

private MeasureScoring getMeasureScoring(Measure measure) {
return MeasureScoring.fromCode(measure.getScoring().getCodingFirstRep().getCode());
}

private MeasureScoring getGroupMeasureScoring(String scoringCode) {
return MeasureScoring.fromCode(scoringCode);
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package org.opencds.cqf.fhir.cr.measure.r4;

import java.util.Map;
import java.util.Optional;
import org.hl7.fhir.r4.model.MeasureReport;
import org.hl7.fhir.r4.model.MeasureReport.MeasureReportGroupComponent;
Expand All @@ -9,18 +10,60 @@
import org.hl7.fhir.r4.model.MeasureReport.StratifierGroupPopulationComponent;
import org.hl7.fhir.r4.model.Quantity;
import org.opencds.cqf.fhir.cr.measure.common.BaseMeasureReportScorer;
import org.opencds.cqf.fhir.cr.measure.common.GroupDef;
import org.opencds.cqf.fhir.cr.measure.common.MeasurePopulationType;
import org.opencds.cqf.fhir.cr.measure.common.MeasureScoring;
import org.opencds.cqf.fhir.cr.measure.common.PopulationDef;

public class R4MeasureReportScorer extends BaseMeasureReportScorer<MeasureReport> {

@Override
public void score(MeasureScoring measureScoring, MeasureReport measureReport) {
public void score(Map<GroupDef, MeasureScoring> measureScoring, MeasureReport measureReport) {
for (MeasureReportGroupComponent mrgc : measureReport.getGroup()) {
scoreGroup(measureScoring, mrgc);
scoreGroup(getGroupMeasureScoring(mrgc, measureScoring), mrgc);
}
}

protected MeasureScoring getGroupMeasureScoring(
MeasureReportGroupComponent mrgc, Map<GroupDef, MeasureScoring> measureScoring) {
MeasureScoring measureScoringFromGroup = null;
// cycle through available Group Definitions to retrieve appropriate MeasureScoring
for (Map.Entry<GroupDef, MeasureScoring> entry : measureScoring.entrySet()) {
// Take only MeasureScoring available
if (measureScoring.size() == 1) {
measureScoringFromGroup = entry.getValue();
break;
}
// Match by group id if available
if (mrgc.getId() != null && entry.getKey().id() != null) {
if (entry.getKey().id().equals(mrgc.getId())) {
measureScoringFromGroup = entry.getValue();
break;
}
}
// Match by group's population id
if (mrgc.getPopulation().size() == entry.getKey().populations().size()) {
int i = 0;
for (MeasureReportGroupPopulationComponent popId : mrgc.getPopulation()) {
for (PopulationDef popDefEntry : entry.getKey().populations()) {
if (popId.getId().equals(popDefEntry.id())) {
i++;
break;
}
}
}
// Check all populations found a match
if (i == mrgc.getPopulation().size()) {
measureScoringFromGroup = entry.getValue();
}
}
}
if (measureScoringFromGroup == null) {
throw new IllegalStateException("No MeasureScoring value set");
}
return measureScoringFromGroup;
}

protected void scoreGroup(MeasureScoring measureScoring, MeasureReportGroupComponent mrgc) {
switch (measureScoring) {
case PROPORTION:
Expand Down
Loading

0 comments on commit 6ee71b7

Please sign in to comment.