qpp-measures-data
Advanced tools
Comparing version 1.0.0-alpha.14 to 1.0.0-alpha.15
{ | ||
"name": "qpp-measures-data", | ||
"version": "1.0.0-alpha.14", | ||
"version": "1.0.0-alpha.15", | ||
"description": "Quality Payment Program Measures Data Repository", | ||
@@ -5,0 +5,0 @@ "repository": { |
@@ -8,4 +8,6 @@ const fs = require('fs'); | ||
const qpp = fs.readFileSync(path.join(__dirname, '../../staging/measures-data.json'), 'utf8'); | ||
fs.writeFileSync(path.join(__dirname, '../../measures/measures-data.json'), enrichMeasures(JSON.parse(qpp))); | ||
const measuresDataPath = process.argv[2]; | ||
const outputPath = process.argv[3]; | ||
const qpp = fs.readFileSync(path.join(__dirname, measuresDataPath), 'utf8'); | ||
fs.writeFileSync(path.join(__dirname, outputPath), enrichMeasures(JSON.parse(qpp))); | ||
@@ -12,0 +14,0 @@ function enrichMeasures(measures) { |
const parse = require('csv-parse/lib/sync'); | ||
const _ = require('lodash'); | ||
const fs = require('fs'); | ||
const path = require('path'); | ||
/** | ||
* `import-qcdr-measures` reads a QCDR CSV file and outputs valid measures | ||
* using `convertCsvToMeasures` and a config object. | ||
* | ||
* example: | ||
* $ cat util/measures/20170825-PIMMS-non-mips_measure_specifications.csv | node ./scripts/measures/import-qcdr-measures.js | ||
* | ||
* test: | ||
* $ cat util/measures/20170825-PIMMS-non-mips_measure_specifications.csv | node ./scripts/measures/import-qcdr-measures.js | node scripts/validate-data.js measures | ||
*/ | ||
* `import-qcdr-measures` reads a QCDR CSV file and creates valid measures, | ||
* then merges the result into an existing set of measures, throwing an error | ||
* if any existing measures with the same measureId has different values | ||
* for any existing properties. | ||
*/ | ||
@@ -28,3 +27,2 @@ /** | ||
lastPerformanceYear: null, | ||
metricType: 'singlePerformanceRate', | ||
eMeasureId: null, | ||
@@ -38,7 +36,6 @@ nqfEMeasureId: null, | ||
], | ||
measureSets: [] | ||
measureSets: [], | ||
isRegistryMeasure: true | ||
}, | ||
sourced_fields: { | ||
vendorId: 0, | ||
primarySteward: 1, | ||
measureId: 2, | ||
@@ -87,3 +84,6 @@ title: 3, | ||
} | ||
} | ||
}, | ||
primarySteward: 22 | ||
// `metricType` is a sourced field but not represented here since it maps from | ||
// multiple columns-- you can find it by searching in the code below | ||
} | ||
@@ -97,2 +97,4 @@ }; | ||
* @return {array} Returns an array of measures objects | ||
* | ||
* We trim all data sourced from CSVs because people sometimes unintentionally include spaces or linebreaks | ||
*/ | ||
@@ -111,7 +113,7 @@ const convertCsvToMeasures = function(records, config) { | ||
// measure data maps directly to data in csv | ||
newMeasure[measureKey] = record[columnObject]; | ||
newMeasure[measureKey] = _.trim(record[columnObject]); | ||
} | ||
} else { | ||
// measure data requires mapping CSV data to new value, e.g. Y, N -> true, false | ||
const mappedValue = columnObject.mappings[record[columnObject.index]]; | ||
const mappedValue = columnObject.mappings[_.trim(record[columnObject.index])]; | ||
newMeasure[measureKey] = mappedValue || columnObject.mappings['default']; | ||
@@ -123,2 +125,18 @@ } | ||
}); | ||
// If the 'proportion' column (col 17) is Y and the other two columns | ||
// (continuous and ratio, cols 18 and 19) are N, metricType should be | ||
// 'singlePerformanceRate'. Otherwise it should be 'nonProportion' | ||
// | ||
// Note: if the 'proportion' column is Y *and* there are multiple | ||
// strata, then the metricType should be 'multiPerformanceRate' | ||
// TODO(kalvin): implement multiPerformanceRate; | ||
if (record[17] === 'Y' && | ||
record[18] === 'N' && | ||
record[19] === 'N') { | ||
newMeasure['metricType'] = 'singlePerformanceRate'; | ||
} else { | ||
newMeasure['metricType'] = 'nonProportion'; | ||
} | ||
return newMeasure; | ||
@@ -130,18 +148,86 @@ }); | ||
let csvFile = ''; | ||
function mergeMeasures(allMeasures, qcdrMeasures, measuresDataPath) { | ||
const addedMeasureIds = []; | ||
const modifiedMeasureIds = []; | ||
process.stdin.setEncoding('utf8'); | ||
// If measure exists already, merge in keys from QCDR measures. If any existing | ||
// non-empty keys have a different value, throw an error. | ||
// If measure doesn't exist, create it. | ||
_.each(qcdrMeasures, function(measure) { | ||
const id = measure.measureId; | ||
const existingMeasure = _.find(allMeasures, {'measureId': id}); | ||
process.stdin.on('readable', function() { | ||
var chunk = process.stdin.read(); | ||
if (chunk !== null) { | ||
csvFile += chunk; | ||
if (existingMeasure && !_.isEqual(existingMeasure, measure)) { | ||
const conflictingValues = _.reduce(measure, function(result, value, key) { | ||
const existingValue = existingMeasure[key]; | ||
// isEqual does a deep comparison so this works with strata as well | ||
if (!_.isNil(existingValue) && !_.isEqual(value, existingValue)) { | ||
result.push({ | ||
'existingKey': key, | ||
'existingValue': existingValue, | ||
'conflictingQcdrValue': value | ||
}); | ||
} | ||
return result; | ||
}, []); | ||
if (!_.isEmpty(conflictingValues)) { | ||
throw TypeError('QCDR measure with measureId: "' + id + '" conflicts' + | ||
' with existing measure. See below:\n' + JSON.stringify(conflictingValues, null, 2)); | ||
} else { | ||
_.assign(existingMeasure, measure); | ||
modifiedMeasureIds.push(id); | ||
} | ||
} else if (!existingMeasure) { | ||
allMeasures.push(measure); | ||
addedMeasureIds.push(id); | ||
} | ||
}); | ||
if (_.isEmpty(addedMeasureIds) && _.isEmpty(modifiedMeasureIds)) { | ||
console.log('Import complete. No measures added to or modified in ' + measuresDataPath); | ||
} else { | ||
console.log('Added measures with the following ids: ' + | ||
addedMeasureIds + '\n'); | ||
console.log('Modified measures with the following ids: ' + | ||
modifiedMeasureIds + '\n'); | ||
console.log('Successfully merged QCDR measures into ' + measuresDataPath); | ||
} | ||
}); | ||
process.stdin.on('end', function() { | ||
const records = parse(csvFile, 'utf8'); | ||
return allMeasures; | ||
} | ||
// We want to add the new isRegistryMeasure field to all quality measures, | ||
// not just the measures where it's true (aka qcdr measures) | ||
function addMissingRegistryFlags(measures) { | ||
_.each(measures, function(measure) { | ||
if (measure.category === 'quality' && !_.isBoolean(measure.isRegistryMeasure)) { | ||
measure.isRegistryMeasure = false; | ||
} | ||
}); | ||
return measures; | ||
} | ||
function importMeasures(measuresDataPath, qcdrMeasuresDataPath, outputPath) { | ||
const qpp = fs.readFileSync(path.join(__dirname, measuresDataPath), 'utf8'); | ||
const allMeasures = JSON.parse(qpp); | ||
const csv = fs.readFileSync(path.join(__dirname, qcdrMeasuresDataPath), 'utf8'); | ||
const records = parse(csv, 'utf8'); | ||
// remove header | ||
records.shift(); | ||
process.stdout.write(JSON.stringify(convertCsvToMeasures(records, config), null, 2)); | ||
}); | ||
// If there's more than one QCDR measure with the same measure, we can | ||
// arbitrarily pick one and ignore the others (they should all be | ||
// identical except for the QCDR Organization Name which we don't care about) | ||
const qcdrMeasures = _.uniqBy(convertCsvToMeasures(records, config), 'measureId'); | ||
const mergedMeasures = mergeMeasures(allMeasures, qcdrMeasures, outputPath); | ||
return JSON.stringify(addMissingRegistryFlags(mergedMeasures), null, 2); | ||
} | ||
const measuresDataPath = process.argv[2]; | ||
const qcdrMeasuresDataPath = process.argv[3]; | ||
const outputPath = process.argv[4]; | ||
const newMeasures = importMeasures(measuresDataPath, qcdrMeasuresDataPath, outputPath); | ||
fs.writeFileSync(path.join(__dirname, outputPath), newMeasures); |
@@ -16,2 +16,3 @@ [ | ||
"isInverse": false, | ||
"isRegistryMeasure": false, | ||
"strata": [ | ||
@@ -18,0 +19,0 @@ { |
@@ -15,2 +15,3 @@ [ | ||
"nqfId": "0102", | ||
"isRegistryMeasure": false, | ||
"isInverse": false, | ||
@@ -146,2 +147,2 @@ "strata": [ | ||
} | ||
] | ||
] |
@@ -15,2 +15,3 @@ [ | ||
"nqfId": "0563", | ||
"isRegistryMeasure": false, | ||
"isInverse": false, | ||
@@ -141,2 +142,2 @@ "strata": [ | ||
} | ||
] | ||
] |
@@ -15,2 +15,3 @@ [ | ||
"nqfId": "0068", | ||
"isRegistryMeasure": false, | ||
"isInverse": false, | ||
@@ -196,2 +197,2 @@ "strata": [ | ||
} | ||
] | ||
] |
@@ -15,2 +15,3 @@ [ | ||
"nqfId": "0651", | ||
"isRegistryMeasure": false, | ||
"isInverse": false, | ||
@@ -165,3 +166,3 @@ "strata": [ | ||
"code": "G8807" | ||
} | ||
} | ||
] | ||
@@ -182,2 +183,2 @@ }, | ||
} | ||
] | ||
] |
@@ -14,3 +14,3 @@ [ | ||
"title": "Asthma: Assessment of Asthma Control – Ambulatory Care Setting", | ||
"description": "Percentage of patients aged 5 years and older with a diagnosis of asthma who were evaluated at least once during the measurement period for asthma control (comprising asthma impairment and asthma risk). National Quality Strategy Domain: Effective Clinical Care Process Measure ", | ||
"description": "Percentage of patients aged 5 years and older with a diagnosis of asthma who were evaluated at least once during the measurement period for asthma control (comprising asthma impairment and asthma risk). National Quality Strategy Domain: Effective Clinical Care Process Measure", | ||
"nationalQualityStrategyDomain": "Effective Clinical Care", | ||
@@ -20,3 +20,4 @@ "measureType": "process", | ||
"isInverse": false, | ||
"isRiskAdjusted": false | ||
"isRiskAdjusted": false, | ||
"isRegistryMeasure": true | ||
}, | ||
@@ -35,3 +36,3 @@ { | ||
"title": "Allergen Immunotherapy Treatment: Allergen Specific Immunoglobulin E (IgE) Sensitivity Assessed and Documented Prior to Treatment", | ||
"description": "Percentage of patients aged 5 years and older who were assessed for IgE sensitivity to allergens prior to initiating allergen immunotherapy AND results documented in the medical record.National Quality Strategy Domain: Patient Safety Process Measure ", | ||
"description": "Percentage of patients aged 5 years and older who were assessed for IgE sensitivity to allergens prior to initiating allergen immunotherapy AND results documented in the medical record.National Quality Strategy Domain: Patient Safety Process Measure", | ||
"nationalQualityStrategyDomain": "Patient Safety", | ||
@@ -41,4 +42,5 @@ "measureType": "process", | ||
"isInverse": false, | ||
"isRiskAdjusted": false | ||
"isRiskAdjusted": false, | ||
"isRegistryMeasure": true | ||
} | ||
] | ||
] |
@@ -5,6 +5,12 @@ // Libraries | ||
const { execSync } = require('child_process'); | ||
const fs = require('fs'); | ||
const path = require('path'); | ||
// Test data | ||
const testCsv = './test/scripts/measures/fixtures/test-qcdr.csv'; | ||
const testCsv2Cols = './test/scripts/measures/fixtures/test-qcdr-2cols.csv'; | ||
const testMeasures = '../../test/scripts/measures/fixtures/test-measures-data.json'; | ||
const testMeasures2 = '../../test/scripts/measures/fixtures/test-measures-data2.json'; | ||
const testCsv = '../../test/scripts/measures/fixtures/test-qcdr.csv'; | ||
const testCsv2Cols = '../../test/scripts/measures/fixtures/test-qcdr-2cols.csv'; | ||
const outputArg = '../../test/scripts/measures/fixtures/test-measures-data-output.json'; | ||
const output = '../' + outputArg; | ||
@@ -14,20 +20,24 @@ // Expected new measures | ||
// Function which executes script and converts output to a JS object. | ||
const runTest = function(file) { | ||
const cmd = 'cat ' + file + ' | node ./scripts/measures/import-qcdr-measures.js'; | ||
const measuresJson = execSync(cmd, {stdio: 'pipe'}).toString(); | ||
return JSON.parse(measuresJson); | ||
// Function which executes script and reads in output file to a JS object. | ||
const runTest = function(measuresFile, measuresCsv) { | ||
const cmd = 'node ./scripts/measures/import-qcdr-measures.js ' + | ||
measuresFile + ' ' + measuresCsv + ' ' + outputArg; | ||
console.log(execSync(cmd, {stdio: 'pipe'}).toString()); | ||
const qpp = fs.readFileSync(path.join(__dirname, output), 'utf8'); | ||
return JSON.parse(qpp); | ||
}; | ||
describe('convertCsvToMeasures', function() { | ||
it('should create new measures', () => { | ||
const newMeasures = runTest(testCsv); | ||
assert.equal(newMeasures.length, 2); | ||
describe('import measures', function() { | ||
it('should create new measures and ignore duplicate measureIds', () => { | ||
const measures = runTest(testMeasures, testCsv); | ||
assert.equal(measures.length, 2); | ||
}); | ||
it('should overwrite fields with the right csv data', () => { | ||
const newMeasures = runTest(testCsv); | ||
const measures = runTest(testMeasures, testCsv); | ||
expectedMeasures.forEach(function(expectedMeasure, measureIdx) { | ||
Object.entries(expectedMeasure).forEach(function([measureKey, measureValue]) { | ||
assert.deepEqual(newMeasures[measureIdx][measureKey], measureValue); | ||
assert.deepEqual(measures[measureIdx][measureKey], measureValue); | ||
}); | ||
@@ -40,5 +50,25 @@ }); | ||
// assert.throws expects a function as its first parameter | ||
const errFunc = () => { runTest(testCsv2Cols); }; | ||
const errFunc = () => { | ||
runTest(testMeasures, testCsv2Cols); | ||
}; | ||
assert.throws(errFunc, Error, errorMessage); | ||
}); | ||
it('throws an informative error when a value in the qcdr csv for an existing ' + | ||
'measureId conflicts with an existing measure', function() { | ||
const errorMessage = /conflicts with existing measure/; | ||
// assert.throws expects a function as its first parameter | ||
const errFunc = () => { | ||
runTest(testMeasures2, testCsv); | ||
}; | ||
assert.throws(errFunc, Error, errorMessage); | ||
}); | ||
}); | ||
after(function() { | ||
try { | ||
fs.unlinkSync(path.join(__dirname, output)); | ||
} catch (err) { | ||
console.log('Warning: ', output, ' should have been deleted but was not.'); | ||
} | ||
}); |
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
5734100
67
53734
15