Skip to content

Commit

Permalink
appliesResult to StratifierResult in detailed results output (#308)
Browse files Browse the repository at this point in the history
* appliesTo functionality in the detailed results

* Add additional attribute appliesResult to StratifierResults

* Pulled out handling stratifier results

* Remove console.logs, unnecessary function

* Go through stratifierResults instead of group stratifiers

* handleStratifierValues after individual episode results

* Add all populations to stratum.population in measure report

* Add stratifier info to readme

* Update test/unit/MeasureReportBuilder.test.ts

Co-authored-by: lmd59 <laurend@mitre.org>

---------

Co-authored-by: lmd59 <laurend@mitre.org>
  • Loading branch information
elsaperelli and lmd59 authored Aug 9, 2024
1 parent 0b752a1 commit b0241d9
Show file tree
Hide file tree
Showing 6 changed files with 227 additions and 22 deletions.
42 changes: 42 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -520,6 +520,48 @@ fqm-execution reports -m /path/to/measure/bundle.json -p /path/to/patient1/bundl

# Guides and Concepts

## Stratification

The results for each stratifier on a Measure (if they exist) are reported on the DetailedResults array for each group. The StratifierResult object contains two result attributes: `result` and `appliesResult`. `result` is simply the raw result of the stratifier and `appliesResult` is the same unless that stratifier contains a [cqfm-appliesTo extension](https://hl7.org/fhir/us/cqfmeasures/STU4/StructureDefinition-cqfm-appliesTo.html). In the case that a stratifier applies to a specified population, the `appliesResult` is the result of the stratifier result AND the result of the specified population. The following is an example of what the DetailedResults would look like for a Measure whose single stratifier has a result of `true` but appliesTo the numerator population that has a result of `false`.

```typescript
[
{
patientId: 'patient-1',
detailedResults: [
{
groupId: 'group-1',
populationResults: [
{
populationType: 'initial-population',
criteriaExpression: 'Initial Population',
result: false
},
{
populationType: 'denominator',
criteriaExpression: 'Denominator',
result: false
},
{
populationType: 'numerator',
criteriaExpression: 'Numerator',
result: false
}
],
stratifierResults: [
{
strataCode: 'strata-1',
result: true,
appliesResult: false,
strataId: 'strata-1'
}
]
}
]
}
];
```

## Measures with Observation Functions

For [Continuous Variable Measures](https://build.fhir.org/ig/HL7/cqf-measures/measure-conformance.html#continuous-variable-measure) and [Ratio Measures](https://build.fhir.org/ig/HL7/cqf-measures/measure-conformance.html#ratio-measures), some of the measure populations can have an associated "measure observation", which is a CQL function that will return some result based on some information present on the data during calculation. In the case of these measures, `fqm-execution` will include an `observations` property on the `populationResult` object for each measure observation population. This `observations` property
Expand Down
49 changes: 48 additions & 1 deletion src/calculation/DetailedResultsBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,9 @@ export function createPopulationValues(
populationResults = popAndStratResults.populationResults;
stratifierResults = popAndStratResults.stratifierResults;
populationResults = handlePopulationValues(populationResults, populationGroup, measureScoringCode);
if (stratifierResults) {
stratifierResults = handleStratificationValues(populationGroup, populationResults, stratifierResults);
}
} else {
// episode of care based measure
// collect results per episode
Expand All @@ -45,6 +48,9 @@ export function createPopulationValues(
const popAndStratResults = createPatientPopulationValues(populationGroup, patientResults);
populationResults = popAndStratResults.populationResults;
stratifierResults = popAndStratResults.stratifierResults;
if (stratifierResults) {
stratifierResults = handleStratificationValues(populationGroup, populationResults, stratifierResults);
}
} else {
populationResults = [];
stratifierResults = [];
Expand Down Expand Up @@ -92,11 +98,15 @@ export function createPopulationValues(
stratifierResults?.push({
result: strat.result,
strataCode: strat.strataCode,
appliesResult: strat.result,
...(strat.strataId ? { strataId: strat.strataId } : {})
});
}
});
});
if (stratifierResults) {
stratifierResults = handleStratificationValues(populationGroup, populationResults, stratifierResults);
}
}
}
const detailedResult: DetailedPopulationGroupResult = {
Expand Down Expand Up @@ -305,9 +315,11 @@ export function createPatientPopulationValues(
if (strata.criteria?.expression) {
const value = patientResults[strata.criteria?.expression];
const result = isStatementValueTruthy(value);

stratifierResults?.push({
strataCode: strata.code?.text ?? strata.id ?? `strata-${strataIndex++}`,
result,
appliesResult: result,
...(strata.id ? { strataId: strata.id } : {})
});
}
Expand All @@ -320,6 +332,37 @@ export function createPatientPopulationValues(
};
}

export function handleStratificationValues(
populationGroup: fhir4.MeasureGroup,
populationResults: PopulationResult[],
stratifierResults: StratifierResult[]
): StratifierResult[] {
if (populationGroup.stratifier) {
stratifierResults.forEach(stratRes => {
const strata =
populationGroup.stratifier?.find(s => s.id === stratRes.strataCode) ||
populationGroup.stratifier?.find(s => s.code && s.code.text === stratRes.strataCode);
if (strata) {
// if the cqfm-appliesTo extension is present, then we want to consider the result of that
// population in our stratifier result
const appliesToExtension = strata.extension?.find(
e => e.url === 'http://hl7.org/fhir/us/cqfmeasures/StructureDefinition/cqfm-appliesTo'
);

let popValue = true;
if (appliesToExtension) {
const popCode = appliesToExtension.valueCodeableConcept?.coding?.[0].code;
if (popCode) {
popValue = populationResults.find(pr => pr.populationType === popCode)?.result ?? true;
}
}
stratRes.appliesResult = popValue && stratRes.result;
}
});
}
return stratifierResults;
}

function isStatementValueTruthy(value: any): boolean {
if (Array.isArray(value) && value.length > 0) {
return true;
Expand Down Expand Up @@ -441,6 +484,9 @@ export function createEpisodePopulationValues(
populationGroup,
measureScoringCode
);
if (episodeResults.stratifierResults) {
handleStratificationValues(populationGroup, episodeResults.populationResults, episodeResults.stratifierResults);
}
});

// TODO: Remove any episode that don't fall in any populations or stratifications after the above code
Expand Down Expand Up @@ -521,7 +567,8 @@ function createOrSetValueOfEpisodes(
newEpisodeResults.stratifierResults?.push({
...(strataId ? { strataId } : {}),
strataCode: newStrataCode,
result: newStrataCode === strataCode ? true : false
result: newStrataCode === strataCode ? true : false,
appliesResult: newStrataCode === strataCode ? true : false
});
});
}
Expand Down
20 changes: 3 additions & 17 deletions src/calculation/MeasureReportBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -140,23 +140,9 @@ export default class MeasureReportBuilder<T extends PopulationGroupResult> exten
const strat = <fhir4.MeasureReportGroupStratifierStratum>{};
// use existing populations, but reduce count as appropriate
// Deep copy population with matching attributes but different interface
// if a stratifier has a cqfm-appliesTo extension, then we only want to
// include that population. If none is specified, the stratification applies
// to all populations in a group
const appliesToExtension = s.extension?.find(
e => e.url === 'http://hl7.org/fhir/us/cqfmeasures/StructureDefinition/cqfm-appliesTo'
strat.population = <fhir4.MeasureReportGroupStratifierStratumPopulation[]>(
JSON.parse(JSON.stringify(group.population))
);
if (appliesToExtension) {
const popCode = appliesToExtension.valueCodeableConcept?.coding?.[0].code;
const matchingPop = group.population?.find(p => p.code?.coding?.[0].code === popCode);
strat.population = <fhir4.MeasureReportGroupStratifierStratumPopulation[]>(
JSON.parse(JSON.stringify([matchingPop]))
);
} else {
strat.population = <fhir4.MeasureReportGroupStratifierStratumPopulation[]>(
JSON.parse(JSON.stringify(group.population))
);
}

reportStratifier.stratum = [strat];
group.stratifier?.push(reportStratifier);
Expand Down Expand Up @@ -278,7 +264,7 @@ export default class MeasureReportBuilder<T extends PopulationGroupResult> exten
if (group.stratifier) {
groupResults.stratifierResults?.forEach(stratResults => {
// only add to results if this patient is in the strata
if (stratResults.result) {
if (stratResults.appliesResult) {
// the strataCode has the potential to be a couple of things, either s.code[0].text (previous measures)
// or s.id (newer measures)
const strata: MeasureReportGroupStratifier | undefined =
Expand Down
6 changes: 6 additions & 0 deletions src/types/Calculator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,12 @@ export interface StratifierResult {
* True if patient or episode is in stratifier. False if not.
*/
result: boolean;
/**
* True if patient or episode is in stratifier AND the population
* result it appliesTo is true. False if not. Only implemented for
* patient based measures currently.
*/
appliesResult: boolean;
strataId?: string;
}

Expand Down
118 changes: 117 additions & 1 deletion test/unit/DetailedResultsBuilder.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import * as DetailedResultsBuilder from '../../src/calculation/DetailedResultsBu
import { getJSONFixture } from './helpers/testHelpers';
import { MeasureScoreType, PopulationType } from '../../src/types/Enums';
import { StatementResults } from '../../src/types/CQLTypes';
import { PopulationResult, EpisodeResults } from '../../src/types/Calculator';
import { PopulationResult, EpisodeResults, StratifierResult } from '../../src/types/Calculator';
import { ELMExpressionRef, ELMQuery, ELMTuple, ELMFunctionRef } from '../../src/types/ELMTypes';

type MeasureWithGroup = fhir4.Measure & {
Expand Down Expand Up @@ -1111,6 +1111,122 @@ describe('DetailedResultsBuilder', () => {
});
});

describe('handleStratificationValues', () => {
test('it should take population result into consider when appliesTo extension exists', () => {
const populationGroup: fhir4.MeasureGroup = {
stratifier: [
{
id: 'example-strata-id',
extension: [
{
url: 'http://hl7.org/fhir/us/cqfmeasures/StructureDefinition/cqfm-appliesTo',
valueCodeableConcept: {
coding: [
{
system: 'http://terminology.hl7.org/CodeSystem/measure-population',
code: 'initial-population',
display: 'Initial Population'
}
]
}
}
],
criteria: {
expression: 'strat1',
language: 'text/cql'
}
}
]
};

const populationResults: PopulationResult[] = [
{
populationType: PopulationType.IPP,
criteriaExpression: 'Initial Population',
result: false,
populationId: 'exampleId'
}
];

const stratifierResults: StratifierResult[] = [
{
strataCode: 'example-strata-id',
result: true,
appliesResult: true,
strataId: 'example-strata-id'
}
];

const newStratifierResults = DetailedResultsBuilder.handleStratificationValues(
populationGroup,
populationResults,
stratifierResults
);

expect(newStratifierResults).toBeDefined();
expect(newStratifierResults).toHaveLength(1);
expect(newStratifierResults).toEqual(
expect.arrayContaining([
expect.objectContaining({
strataId: 'example-strata-id',
result: true,
appliesResult: false
})
])
);
});

test('it should not take population result into consideration when appliesTo extension does not exist', () => {
const populationGroup: fhir4.MeasureGroup = {
stratifier: [
{
id: 'example-strata-id',
criteria: {
expression: 'strat1',
language: 'text/cql'
}
}
]
};

const populationResults: PopulationResult[] = [
{
populationType: PopulationType.IPP,
criteriaExpression: 'Initial Population',
result: false,
populationId: 'exampleId'
}
];

const stratifierResults: StratifierResult[] = [
{
strataCode: 'example-strata-id',
result: true,
appliesResult: true,
strataId: 'example-strata-id'
}
];

const newStratifierResults = DetailedResultsBuilder.handleStratificationValues(
populationGroup,
populationResults,
stratifierResults
);

expect(newStratifierResults).toBeDefined();
expect(newStratifierResults).toHaveLength(1);
expect(newStratifierResults).toEqual(
expect.arrayContaining([
expect.objectContaining({
strataId: 'example-strata-id',
result: true,
appliesResult: true
})
])
);
});
});

describe('ELM JSON Function', () => {
test('should properly generate episode-based ELM JSON given name and parameter', () => {
const exampleFunctionName = 'exampleFunction';
Expand Down
Loading

0 comments on commit b0241d9

Please sign in to comment.