quality-measure-engine 1.1.5 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/.gitignore +12 -0
- data/.travis.yml +16 -0
- data/Gemfile +5 -21
- data/Gemfile.lock +126 -0
- data/LICENSE.txt +13 -0
- data/README.md +23 -44
- data/Rakefile +6 -29
- data/lib/qme/bundle/bundle.rb +34 -0
- data/lib/qme/bundle/importer.rb +69 -0
- data/lib/qme/database_access.rb +7 -11
- data/lib/qme/map/map_reduce_builder.rb +4 -1
- data/lib/qme/map/map_reduce_executor.rb +55 -43
- data/lib/qme/map/measure_calculation_job.rb +24 -23
- data/lib/qme/quality_measure.rb +5 -5
- data/lib/qme/quality_report.rb +37 -19
- data/lib/qme/railtie.rb +7 -0
- data/lib/qme/tasks/bundle.rake +14 -0
- data/lib/qme/version.rb +3 -0
- data/lib/quality-measure-engine.rb +13 -25
- data/quality-measure-engine.gemspec +28 -0
- data/test/fixtures/bundles/just_measure_0002.zip +0 -0
- data/test/fixtures/delayed_backend_mongoid_jobs/queued_job.json +9 -0
- data/test/fixtures/measures/measure_metadata.json +52 -0
- data/test/fixtures/records/barry_berry.json +471 -0
- data/test/fixtures/records/billy_jones_ipp.json +78 -0
- data/test/fixtures/records/jane_jones_numerator.json +120 -0
- data/test/fixtures/records/jill_jones_denominator.json +78 -0
- data/test/simplecov_setup.rb +18 -0
- data/test/test_helper.rb +26 -0
- data/test/unit/qme/map/map_reduce_builder_test.rb +38 -0
- data/test/unit/qme/map/map_reduce_executor_test.rb +56 -0
- data/test/unit/qme/map/measure_calculation_job_test.rb +22 -0
- data/test/unit/qme/quality_measure_test.rb +14 -0
- data/{spec/qme/quality_report_spec.rb → test/unit/qme/quality_report_test.rb} +32 -20
- metadata +91 -115
- data/VERSION +0 -1
- data/js/map_reduce_utils.js +0 -173
- data/js/underscore_min.js +0 -25
- data/lib/qme/ext/record.rb +0 -43
- data/lib/qme/importer/entry.rb +0 -126
- data/lib/qme/importer/generic_importer.rb +0 -117
- data/lib/qme/importer/measure_properties_generator.rb +0 -39
- data/lib/qme/importer/property_matcher.rb +0 -110
- data/lib/qme/measure/database_loader.rb +0 -83
- data/lib/qme/measure/measure_loader.rb +0 -174
- data/lib/qme/measure/properties_builder.rb +0 -184
- data/lib/qme/measure/properties_converter.rb +0 -27
- data/lib/qme/randomizer/patient_randomization_job.rb +0 -47
- data/lib/qme/randomizer/patient_randomizer.rb +0 -250
- data/lib/qme/randomizer/random_patient_creator.rb +0 -47
- data/lib/qme_test.rb +0 -13
- data/lib/tasks/fixtures.rake +0 -91
- data/lib/tasks/measure.rake +0 -110
- data/lib/tasks/mongo.rake +0 -68
- data/lib/tasks/patient_random.rake +0 -45
- data/spec/qme/bundle_spec.rb +0 -37
- data/spec/qme/importer/generic_importer_spec.rb +0 -73
- data/spec/qme/importer/measure_properties_generator_spec.rb +0 -15
- data/spec/qme/importer/property_matcher_spec.rb +0 -174
- data/spec/qme/map/map_reduce_builder_spec.rb +0 -38
- data/spec/qme/map/measures_spec.rb +0 -38
- data/spec/qme/map/patient_mapper_spec.rb +0 -11
- data/spec/qme/measure_loader_spec.rb +0 -12
- data/spec/qme/properties_builder_spec.rb +0 -61
- data/spec/spec_helper.rb +0 -120
- data/spec/validate_measures_spec.rb +0 -21
@@ -1,38 +0,0 @@
|
|
1
|
-
describe QME::MapReduce::Builder do
|
2
|
-
|
3
|
-
before do
|
4
|
-
@loader = QME::Database::Loader.new('test')
|
5
|
-
raw_measure_json = File.read(File.join('fixtures', 'entry', 'sample.json'))
|
6
|
-
@measure_json = JSON.parse(raw_measure_json)
|
7
|
-
end
|
8
|
-
|
9
|
-
it 'should extract the measure metadata' do
|
10
|
-
measure = QME::MapReduce::Builder.new(@loader.get_db, @measure_json, 'effective_date'=>Time.gm(2010, 9, 19).to_i)
|
11
|
-
measure.id.should eql('0043')
|
12
|
-
end
|
13
|
-
it 'should extract one parameter for measure 0043' do
|
14
|
-
time = Time.gm(2010, 9, 19).to_i
|
15
|
-
measure = QME::MapReduce::Builder.new(@loader.get_db, @measure_json, 'effective_date'=>time)
|
16
|
-
measure.params.size.should eql(1)
|
17
|
-
measure.params.should have_key('effective_date')
|
18
|
-
measure.params['effective_date'].should eql(time)
|
19
|
-
end
|
20
|
-
it 'should raise a RuntimeError if not passed all the parameters' do
|
21
|
-
lambda { QME::MapReduce::Builder.new(@measure_json) }.should
|
22
|
-
raise_error(RuntimeError, 'No value supplied for measure parameter: effective_date')
|
23
|
-
end
|
24
|
-
end
|
25
|
-
|
26
|
-
describe QME::MapReduce::Builder::Context do
|
27
|
-
before do
|
28
|
-
@loader = QME::Database::Loader.new('test')
|
29
|
-
end
|
30
|
-
|
31
|
-
it 'should set instance methods from the supplied hash' do
|
32
|
-
vars = {'a'=>10, 'b'=>20}
|
33
|
-
context = QME::MapReduce::Builder::Context.new(@loader.get_db, vars)
|
34
|
-
binding = context.get_binding
|
35
|
-
eval("a",binding).should eql(10)
|
36
|
-
eval("b",binding).should eql(20)
|
37
|
-
end
|
38
|
-
end
|
@@ -1,38 +0,0 @@
|
|
1
|
-
describe QME::MapReduce::Executor do
|
2
|
-
|
3
|
-
before do
|
4
|
-
@loader = QME::Database::Loader.new('test')
|
5
|
-
if ENV['MEASURE_DIR']
|
6
|
-
@measures = Dir.glob(File.join(ENV['MEASURE_DIR'], '*'))
|
7
|
-
else
|
8
|
-
@measures = Dir.glob(File.join('measures', '*'))
|
9
|
-
end
|
10
|
-
|
11
|
-
# define custom matchers
|
12
|
-
RSpec::Matchers.define :match_population do |population|
|
13
|
-
match do |value|
|
14
|
-
value == population
|
15
|
-
end
|
16
|
-
end
|
17
|
-
RSpec::Matchers.define :match_denominator do |denominator|
|
18
|
-
match do |value|
|
19
|
-
value == denominator
|
20
|
-
end
|
21
|
-
end
|
22
|
-
RSpec::Matchers.define :match_numerator do |numerator|
|
23
|
-
match do |value|
|
24
|
-
value == numerator
|
25
|
-
end
|
26
|
-
end
|
27
|
-
RSpec::Matchers.define :match_exclusions do |exclusions|
|
28
|
-
match do |value|
|
29
|
-
value == exclusions
|
30
|
-
end
|
31
|
-
end
|
32
|
-
end
|
33
|
-
|
34
|
-
it 'should produce the expected results for each measure' do
|
35
|
-
validate_measures(@measures,@loader)
|
36
|
-
end
|
37
|
-
|
38
|
-
end
|
@@ -1,12 +0,0 @@
|
|
1
|
-
describe QME::Measure::Loader do
|
2
|
-
before do
|
3
|
-
@measure_def_dir = File.join('fixtures', 'measure_defs', 'sample_single_from_multi_xls')
|
4
|
-
end
|
5
|
-
|
6
|
-
it 'Should load the sample measure correctly' do
|
7
|
-
measure = QME::Measure::Loader.load_measure(@measure_def_dir)
|
8
|
-
measure = measure[0]
|
9
|
-
measure['measure'].should have_key('eyes')
|
10
|
-
measure['measure'].should have_key('esrd_diagnosis_active')
|
11
|
-
end
|
12
|
-
end
|
@@ -1,61 +0,0 @@
|
|
1
|
-
require 'ap'
|
2
|
-
|
3
|
-
describe QME::Measure::PropertiesBuilder do
|
4
|
-
before do
|
5
|
-
@json_file = File.join('fixtures', 'measure_props', 'props2.xlsx.json')
|
6
|
-
@properties = JSON.parse(File.read(@json_file))
|
7
|
-
end
|
8
|
-
|
9
|
-
it 'Should patch the definition' do
|
10
|
-
@properties['N_1810']['value'].should be_nil
|
11
|
-
patched_properties = QME::Measure::PropertiesBuilder.patch_properties(@properties, @json_file)
|
12
|
-
patched_properties['N_1810']['value'].should have_key('type')
|
13
|
-
patched_properties['N_1472']['group_type'].should eql('abstract')
|
14
|
-
end
|
15
|
-
|
16
|
-
it 'Should group properties' do
|
17
|
-
@properties.values.select { |value| value['standard_concept_id']=='N_c102' }.should_not be_empty
|
18
|
-
patched_properties = QME::Measure::PropertiesBuilder.patch_properties(@properties, @json_file)
|
19
|
-
grouped_properties = QME::Measure::PropertiesBuilder.build_groups(patched_properties)
|
20
|
-
grouped_properties.select { |key| key['standard_concept_id']=='N_c102' }.should be_empty
|
21
|
-
grouped_properties.select { |key| key['standard_concept_id']=='N_c190' }.should_not be_empty
|
22
|
-
end
|
23
|
-
|
24
|
-
it 'Should create the expected properties' do
|
25
|
-
result = QME::Measure::PropertiesBuilder.build_properties(@properties, @json_file)
|
26
|
-
result['measure'].should have_key('diastolic_blood_pressure_physical_exam_finding')
|
27
|
-
result['measure']['diastolic_blood_pressure_physical_exam_finding'].should have_key('standard_concept')
|
28
|
-
result['measure']['diastolic_blood_pressure_physical_exam_finding']['standard_concept'].should eql('diastolic_blood_pressure')
|
29
|
-
result['measure']['diastolic_blood_pressure_physical_exam_finding'].should have_key('standard_category')
|
30
|
-
result['measure']['diastolic_blood_pressure_physical_exam_finding']['standard_category'].should eql('physical_exam')
|
31
|
-
result['measure']['diastolic_blood_pressure_physical_exam_finding']['items']['type'].should eql('object')
|
32
|
-
result['measure']['diastolic_blood_pressure_physical_exam_finding'].should have_key('codes')
|
33
|
-
result['measure']['diastolic_blood_pressure_physical_exam_finding']['codes'].length.should eql(1)
|
34
|
-
result['measure'].should have_key('encounter_outpatient_encounter')
|
35
|
-
result['measure']['encounter_outpatient_encounter']['items']['type'].should eql('number')
|
36
|
-
result['measure'].should have_key('esrd_diagnosis_active')
|
37
|
-
result['measure']['esrd_diagnosis_active']['standard_category'].should eql('diagnosis_condition_problem')
|
38
|
-
result['measure']['esrd_diagnosis_active']['codes'].length.should eql(3)
|
39
|
-
end
|
40
|
-
|
41
|
-
it 'Should not create the excluded properties' do
|
42
|
-
result = QME::Measure::PropertiesBuilder.build_properties(@properties, @json_file)
|
43
|
-
result['measure'].select { |key| QME::Measure::PropertiesBuilder::PROPERTIES_TO_IGNORE.include?(key['standard_concept']) }.should be_empty
|
44
|
-
end
|
45
|
-
|
46
|
-
it 'Should expand code ranges for CPT codes' do
|
47
|
-
result = QME::Measure::PropertiesBuilder.extract_code_values("1, 2-5, 6 ", "SNOMED-CT")
|
48
|
-
result.length.should eql(3)
|
49
|
-
result.should include("1")
|
50
|
-
result.should include("2-5")
|
51
|
-
result.should include("6")
|
52
|
-
result = QME::Measure::PropertiesBuilder.extract_code_values("1, 2-5, 6 ", "CPT")
|
53
|
-
result.length.should eql(6)
|
54
|
-
result.should include("1")
|
55
|
-
result.should include("2")
|
56
|
-
result.should include("3")
|
57
|
-
result.should include("4")
|
58
|
-
result.should include("5")
|
59
|
-
result.should include("6")
|
60
|
-
end
|
61
|
-
end
|
data/spec/spec_helper.rb
DELETED
@@ -1,120 +0,0 @@
|
|
1
|
-
begin
|
2
|
-
require 'cover_me'
|
3
|
-
rescue LoadError
|
4
|
-
puts 'cover_me unavailable, running without code coverage measurement'
|
5
|
-
end
|
6
|
-
require 'bundler/setup'
|
7
|
-
|
8
|
-
#require 'pry'
|
9
|
-
|
10
|
-
PROJECT_ROOT = File.dirname(__FILE__) + '/../'
|
11
|
-
|
12
|
-
require PROJECT_ROOT + 'lib/quality-measure-engine'
|
13
|
-
|
14
|
-
Bundler.require(:test)
|
15
|
-
ENV['DB_NAME'] = 'test'
|
16
|
-
|
17
|
-
def reload_bundle(bundle_dir='.', measure_dir=ENV['MEASURE_DIR'] || 'measures')
|
18
|
-
loader = QME::Database::Loader.new
|
19
|
-
loader.drop_collection('bundles')
|
20
|
-
loader.drop_collection('measures')
|
21
|
-
loader.drop_collection('manual_exclusions')
|
22
|
-
loader.save_bundle(bundle_dir, measure_dir)
|
23
|
-
loader
|
24
|
-
end
|
25
|
-
|
26
|
-
def validate_measures(measure_dirs, loader)
|
27
|
-
|
28
|
-
reload_bundle
|
29
|
-
|
30
|
-
loader.get_db.collection('manual_exclusions').save({'measure_id'=>'test1', 'medical_record_id'=>'1234567890'})
|
31
|
-
|
32
|
-
measure_dirs.each do |dir|
|
33
|
-
# check for sample data
|
34
|
-
fixture_dir = File.join('fixtures', 'measures', File.basename(dir))
|
35
|
-
patient_files = Dir.glob(File.join(fixture_dir, 'patients', '*.json'))
|
36
|
-
if patient_files.length==0
|
37
|
-
puts "Skipping #{dir}, no sample data in #{fixture_dir}"
|
38
|
-
next
|
39
|
-
end
|
40
|
-
|
41
|
-
puts "Parsing #{dir}"
|
42
|
-
|
43
|
-
loader.drop_collection('records')
|
44
|
-
loader.drop_collection('query_cache')
|
45
|
-
loader.drop_collection('patient_cache')
|
46
|
-
|
47
|
-
# load measure from file system
|
48
|
-
# this is innefficient, could just load it from DB as its already stored there
|
49
|
-
measures = QME::Measure::Loader.load_measure(dir)
|
50
|
-
|
51
|
-
# patients can include an optional test_id population identifier, extract this if present
|
52
|
-
test_id = nil
|
53
|
-
|
54
|
-
# load db with sample patient records
|
55
|
-
patient_files.each do |patient_file|
|
56
|
-
patient = JSON.parse(File.read(patient_file))
|
57
|
-
test_id ||= patient['test_id']
|
58
|
-
loader.save('records', patient)
|
59
|
-
end
|
60
|
-
|
61
|
-
# load expected results
|
62
|
-
result_file = File.join('fixtures', 'measures', File.basename(dir), 'result.json')
|
63
|
-
expected = JSON.parse(File.read(result_file))
|
64
|
-
|
65
|
-
# evaulate measure using Map/Reduce and validate results
|
66
|
-
measures.each do |measure|
|
67
|
-
measure_id = measure['id']
|
68
|
-
sub_id = measure['sub_id']
|
69
|
-
puts "Validating measure #{measure_id}#{sub_id}"
|
70
|
-
executor = QME::MapReduce::Executor.new(measure_id, sub_id,
|
71
|
-
'effective_date'=>Time.gm(2010, 9, 19).to_i,
|
72
|
-
'test_id'=>test_id)
|
73
|
-
executor.map_records_into_measure_groups
|
74
|
-
result = executor.count_records_in_measure_groups
|
75
|
-
if expected['initialPopulation'] == nil
|
76
|
-
# multiple results for multi numerator/denominator measure
|
77
|
-
# loop through list of results to find the matching one
|
78
|
-
expected['results'].each do |expect|
|
79
|
-
if expect['id'].eql?(measure_id) && (sub_id==nil || expect['sub_id'].eql?(sub_id))
|
80
|
-
result['population'].should match_population(expect['initialPopulation'])
|
81
|
-
result['numerator'].should match_numerator(expect['numerator'])
|
82
|
-
result['denominator'].should match_denominator(expect['denominator'])
|
83
|
-
result['exclusions'].should match_exclusions(expect['exclusions'])
|
84
|
-
(result['numerator']+result['antinumerator']).should eql(expect['denominator'])
|
85
|
-
break
|
86
|
-
end
|
87
|
-
end
|
88
|
-
else
|
89
|
-
result['population'].should match_population(expected['initialPopulation'])
|
90
|
-
result['numerator'].should match_numerator(expected['numerator'])
|
91
|
-
result['denominator'].should match_denominator(expected['denominator'])
|
92
|
-
result['exclusions'].should match_exclusions(expected['exclusions'])
|
93
|
-
(result['numerator']+result['antinumerator']).should eql(expected['denominator'])
|
94
|
-
end
|
95
|
-
end
|
96
|
-
puts ' - done'
|
97
|
-
end
|
98
|
-
|
99
|
-
end
|
100
|
-
|
101
|
-
def validate_patient_mapping(loader)
|
102
|
-
reload_bundle
|
103
|
-
loader.drop_collection('records')
|
104
|
-
loader.drop_collection('query_cache')
|
105
|
-
loader.drop_collection('patient_cache')
|
106
|
-
|
107
|
-
patient_file = File.join('fixtures', 'mapping', 'test1_numerator.json')
|
108
|
-
patient = JSON.parse(File.read(patient_file))
|
109
|
-
loader.save('records', patient)
|
110
|
-
|
111
|
-
executor = QME::MapReduce::Executor.new('test1', nil,
|
112
|
-
'effective_date'=>Time.gm(2010, 9, 19).to_i)
|
113
|
-
result = executor.get_patient_result('patient1')
|
114
|
-
|
115
|
-
result['population'].should be(true)
|
116
|
-
result['numerator'].should be(true)
|
117
|
-
result['denominator'].should be(true)
|
118
|
-
result['exclusions'].should be(false)
|
119
|
-
result['antinumerator'].should be(false)
|
120
|
-
end
|
@@ -1,21 +0,0 @@
|
|
1
|
-
# Validate the measure specifications and patient samples
|
2
|
-
|
3
|
-
require 'json'
|
4
|
-
|
5
|
-
describe JSON, 'All measure specifications' do
|
6
|
-
it 'should be valid JSON' do
|
7
|
-
Dir.glob(File.join('measures', '*', '*.json')).each do |measure_file|
|
8
|
-
measure = File.read(measure_file)
|
9
|
-
json = JSON.parse(measure)
|
10
|
-
end
|
11
|
-
end
|
12
|
-
end
|
13
|
-
|
14
|
-
describe JSON, 'All patient samples' do
|
15
|
-
it 'should be valid JSON' do
|
16
|
-
Dir.glob(File.join('fixtures', 'measures', '*', 'patients', '*.json')).each do |measure_file|
|
17
|
-
measure = File.read(measure_file)
|
18
|
-
json = JSON.parse(measure)
|
19
|
-
end
|
20
|
-
end
|
21
|
-
end
|