quality-measure-engine 1.1.1 → 1.1.2

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/Gemfile CHANGED
@@ -7,7 +7,7 @@ gem 'bson_ext', '1.5.1', :platforms => :mri
7
7
  gem 'rake'
8
8
  #gem 'pry', :require => true
9
9
  #gem 'health-data-standards', :git => 'https://github.com/projectcypress/health-data-standards.git', :branch => 'master'
10
- gem 'health-data-standards', '0.7.1'
10
+ gem 'health-data-standards', '0.8.0'
11
11
 
12
12
  group :test do
13
13
  gem 'cover_me', '>= 1.0.0.rc5', :platforms => :ruby_19
@@ -1,49 +1,43 @@
1
1
  # Extensions to the Record model in health-data-standards to support
2
2
  # quality measure calculation
3
3
  class Record
4
- extend ActiveSupport::Memoizable
5
-
4
+
5
+ def procedures_performed
6
+ @procedures_performed = procedures.to_a + immunizations.to_a
7
+ end
8
+
6
9
  def procedure_results
7
- results.to_a + vital_signs.to_a + procedures.to_a
10
+ @procedure_results ||= results.to_a + vital_signs.to_a + procedures.to_a
8
11
  end
9
12
 
10
13
  def laboratory_tests
11
- results.to_a + vital_signs.to_a
14
+ @laboratory_tests ||= results.to_a + vital_signs.to_a
12
15
  end
13
16
 
14
17
  def all_meds
15
- medications.to_a + immunizations.to_a
18
+ @all_meds ||= medications.to_a + immunizations.to_a
16
19
  end
17
20
 
18
21
  def active_diagnosis
19
- conditions.any_of({:status => 'active'}, {:status => nil}).to_a +
22
+ @active_diagnosis ||= conditions.any_of({:status => 'active'}, {:status => nil}).to_a +
20
23
  social_history.any_of({:status => 'active'}, {:status => nil}).to_a
21
24
  end
22
25
 
23
26
  def inactive_diagnosis
24
- conditions.any_of({:status => 'inactive'}, {:status => nil}).to_a +
27
+ @inactive_diagnosis ||= conditions.any_of({:status => 'inactive'}, {:status => nil}).to_a +
25
28
  social_history.any_of({:status => 'inactive'}, {:status => nil}).to_a
26
29
  end
27
30
 
28
31
  def resolved_diagnosis
29
- conditions.any_of({:status => 'resolved'}, {:status => nil}).to_a +
32
+ @resolved_diagnosis ||= conditions.any_of({:status => 'resolved'}, {:status => nil}).to_a +
30
33
  social_history.any_of({:status => 'resolved'}, {:status => nil}).to_a
31
34
  end
32
35
 
33
36
  def all_problems
34
- conditions.to_a + social_history.to_a
37
+ @all_problems ||= conditions.to_a + social_history.to_a
35
38
  end
36
39
 
37
40
  def all_devices
38
- conditions.to_a + procedures.to_a + care_goals.to_a + medical_equipment.to_a
41
+ @all_devices ||= conditions.to_a + procedures.to_a + care_goals.to_a + medical_equipment.to_a
39
42
  end
40
-
41
- memoize :procedure_results
42
- memoize :laboratory_tests
43
- memoize :all_meds
44
- memoize :active_diagnosis
45
- memoize :inactive_diagnosis
46
- memoize :resolved_diagnosis
47
- memoize :all_problems
48
- memoize :all_devices
49
43
  end
@@ -44,10 +44,12 @@ module QME
44
44
  case standard_category
45
45
  when 'encounter'
46
46
  patient.encounters
47
+ when 'immunization'
48
+ patient.immunizations
47
49
  when 'procedure'
48
50
  case qds_data_type
49
51
  when 'procedure_performed'
50
- patient.procedures
52
+ patient.procedures_performed
51
53
  when 'procedure_adverse_event', 'procedure_intolerance'
52
54
  patient.allergies
53
55
  when 'procedure_result'
@@ -60,7 +62,7 @@ module QME
60
62
  when 'laboratory_test'
61
63
  patient.laboratory_tests
62
64
  when 'physical_exam'
63
- patient.vital_signs
65
+ patient.procedure_results
64
66
  when 'medication'
65
67
  case qds_data_type
66
68
  when 'medication_dispensed', 'medication_order', 'medication_active', 'medication_administered'
@@ -18,6 +18,10 @@ module QME
18
18
  result
19
19
  end
20
20
 
21
+ def self.get_measures(measure_ids)
22
+ get_db.collection('measures').find('id' => {"$in" => measure_ids})
23
+ end
24
+
21
25
  # Creates a new QualityMeasure
22
26
  # @param [String] measure_id value of the measure's id field
23
27
  # @param [String] sub_id value of the measure's sub_id field, may be nil for measures with only a single numerator and denominator
@@ -6,7 +6,6 @@ gem 'rubyzip'
6
6
  require 'zip/zip'
7
7
  require 'zip/zipfilesystem'
8
8
  require File.join(path,'../quality-measure-engine')
9
-
10
9
  measures_dir = ENV['MEASURE_DIR'] || 'measures'
11
10
  bundle_dir = ENV['BUNDLE_DIR'] || './'
12
11
  xls_dir = ENV['XLS_DIR'] || 'xls'
@@ -91,5 +90,21 @@ EOF
91
90
  file.close
92
91
  end
93
92
  end
93
+
94
+ desc "export results of measures (expects date in YYYY-MM-DD format)"
95
+ task :export_results, :effective_date do |t, args|
96
+ measure_ids = ENV["MEASURES"].split(",")
97
+ measures = QME::QualityMeasure.get_measures(measure_ids)
98
+ effective_date_s = args[:effective_date] || "2010-12-31"
99
+ effective_date = Time.parse(effective_date_s).to_i
100
+ measures.each do |measure|
101
+ qr = QME::QualityReport.new(measure['id'], measure['sub_id'], 'effective_date' => effective_date)
102
+ qr.calculate(false) unless qr.calculated?
103
+ r = qr.result
104
+
105
+ puts "#{measure['id']}#{measure['sub_id']}: #{r['numerator']}/#{r['denominator']}/#{r['population']} (#{r['exclusions']})"
106
+ end
107
+ end
108
+
94
109
 
95
110
  end
@@ -0,0 +1,37 @@
1
+ describe QME::MapReduce::Executor do
2
+
3
+ before :all do
4
+ @bundle_dir = File.join(File.dirname(__FILE__),'../../fixtures/bundle')
5
+ @measure_dir = 'measures'
6
+ end
7
+
8
+ before do
9
+ @loader = QME::Database::Loader.new('test')
10
+ @loader.get_db.drop_collection('measures')
11
+ @loader.get_db.drop_collection('bundles')
12
+ end
13
+
14
+ it 'Should be able to load a bundle' do
15
+ bundle = @loader.save_bundle(@bundle_dir, @measure_dir)
16
+ bundle[:measures].length.should == 1
17
+ bundle[:bundle_data][:extensions].length.should == 3
18
+ bundle[:bundle_data]['name'].should == "test_bundle"
19
+ @loader.get_db['bundles'].count.should == 1
20
+ @loader.get_db['bundles'].find_one['name'].should == 'test_bundle'
21
+ end
22
+
23
+
24
+ it 'should be able to remove a bundle' do
25
+ bundle = @loader.save_bundle(@bundle_dir, @measure_dir)
26
+ bundle_measures_count = bundle[:measures].length
27
+ @loader.get_db['bundles'].count.should == 1
28
+ measures = @loader.get_db['measures'].count
29
+
30
+ @loader.remove_bundle(bundle[:bundle_data]['_id'])
31
+ @loader.get_db['bundles'].count.should == 0
32
+ measures = @loader.get_db['measures'].count.should == (measures - bundle_measures_count)
33
+
34
+
35
+ end
36
+
37
+ end
@@ -0,0 +1,73 @@
1
+ describe QME::Importer::GenericImporter do
2
+
3
+ it "should properly handle devices" do
4
+ measure_def = {'measure' => {"cardiac_pacer" => {
5
+ "standard_category" => "device",
6
+ "qds_data_type" => "device_applied",
7
+ "type" => "array",
8
+ "items" => {
9
+ "type" => "number",
10
+ "format" => "utc-sec"
11
+ },
12
+ "codes" => [
13
+ {
14
+ "set" => "SNOMED-CT",
15
+ "values" => [
16
+ "14106009",
17
+ "56961003"
18
+ ]
19
+ }
20
+ ]
21
+ }}}
22
+
23
+ entry = Entry.new
24
+ entry.add_code('14106009', 'SNOMED-CT')
25
+ entry.start_time = 1026777600
26
+
27
+ patient = Record.new
28
+ patient.medical_equipment = [entry]
29
+
30
+ gi = QME::Importer::GenericImporter.new(measure_def)
31
+ measure_info = gi.parse(patient)
32
+ measure_info['cardiac_pacer'].should include(1026777600)
33
+ end
34
+
35
+ it "should handle active conditions" do
36
+ measure_def = {'measure' => {"silliness" => {
37
+ "standard_category" => "diagnosis_condition_problem",
38
+ "qds_data_type" => "diagnosis_active",
39
+ "type" => "array",
40
+ "items" => {
41
+ "type" => "number",
42
+ "format" => "utc-sec"
43
+ },
44
+ "codes" => [
45
+ {
46
+ "set" => "SNOMED-CT",
47
+ "values" => [
48
+ "14106009",
49
+ "56961003"
50
+ ]
51
+ }
52
+ ]
53
+ }}}
54
+
55
+ entry1 = Entry.new
56
+ entry1.add_code('14106009', 'SNOMED-CT')
57
+ entry1.start_time = 1026777600
58
+ entry1.status = 'active'
59
+
60
+ entry2 = Entry.new
61
+ entry2.add_code('14106009', 'SNOMED-CT')
62
+ entry2.start_time = 1026777601
63
+ entry2.status = 'inactive'
64
+
65
+ patient = Record.new
66
+ patient.conditions = [entry1, entry2]
67
+
68
+ gi = QME::Importer::GenericImporter.new(measure_def)
69
+ measure_info = gi.parse(patient)
70
+ measure_info['silliness'].should include(1026777600)
71
+ measure_info['silliness'].should_not include(1026777601)
72
+ end
73
+ end
@@ -0,0 +1,15 @@
1
+ describe QME::Importer::MeasurePropertiesGenerator do
2
+ it 'should generate measure properties' do
3
+ doc = Nokogiri::XML(File.new('fixtures/c32_fragments/0032/numerator.xml'))
4
+ doc.root.add_namespace_definition('cda', 'urn:hl7-org:v3')
5
+
6
+ measure_json = JSON.parse(File.read(File.join('fixtures', 'entry', 'sample.json')))
7
+ QME::Importer::MeasurePropertiesGenerator.instance.add_measure('0043', QME::Importer::GenericImporter.new(measure_json))
8
+
9
+ patient = HealthDataStandards::Import::C32::PatientImporter.instance.parse_c32(doc)
10
+
11
+ measure_properties = QME::Importer::MeasurePropertiesGenerator.instance.generate_properties(patient)
12
+
13
+ measure_properties['0043']['encounter'].should include(1270598400)
14
+ end
15
+ end
@@ -0,0 +1,174 @@
1
+ describe QME::Importer::PropertyMatcher do
2
+
3
+ it "should raise an error when it can't determine the property schema" do
4
+ property_description = {
5
+ "type" => "cheese",
6
+ "description" => "A cheesey example"
7
+ }
8
+
9
+ pm = QME::Importer::PropertyMatcher.new(property_description)
10
+ expect {pm.match([])}.to raise_error
11
+ end
12
+
13
+ it "should be able to extract a date list property" do
14
+ property_description = {
15
+ "type" => "array",
16
+ "items" => {
17
+ "type" => "number"
18
+ },
19
+ "codes" => [
20
+ {
21
+ "set" => "SNOMED-CT",
22
+ "values" => ["314443004"]
23
+ }
24
+ ]
25
+ }
26
+
27
+ pm = QME::Importer::PropertyMatcher.new(property_description)
28
+
29
+ entry = Entry.new
30
+ entry.add_code('314443004', 'SNOMED-CT')
31
+ entry.start_time = 1026777600
32
+
33
+ result = pm.match([entry])
34
+ result.should include(1026777600)
35
+ end
36
+
37
+
38
+ describe 'when extracting a date/value list property' do
39
+ it "should be able to deal with number values" do
40
+ property_description = {
41
+ "type" => "array",
42
+ "items" => {
43
+ "type" => "object",
44
+ "properties" => {
45
+ "value" => {
46
+ "type" => "number"
47
+ },
48
+ "date" => {
49
+ "type" => "number"
50
+ }
51
+ }
52
+ },
53
+ "codes" => [
54
+ {
55
+ "set" => "SNOMED-CT",
56
+ "values" => ["314443004"]
57
+ }
58
+ ]
59
+ }
60
+
61
+ pm = QME::Importer::PropertyMatcher.new(property_description)
62
+
63
+ entry = Entry.new
64
+ entry.add_code('314443004', 'SNOMED-CT')
65
+ entry.set_value('11.45')
66
+ entry.start_time = 1026777600
67
+
68
+ result = pm.match([entry])
69
+ result.should include({'date' => 1026777600, 'value' => 11.45})
70
+ result.length.should == 1
71
+ end
72
+
73
+ it "should be able to deal with boolean values" do
74
+ property_description = {
75
+ "type" => "array",
76
+ "items" => {
77
+ "type" => "object",
78
+ "properties" => {
79
+ "value" => {
80
+ "type" => "boolean"
81
+ },
82
+ "date" => {
83
+ "type" => "number"
84
+ }
85
+ }
86
+ },
87
+ "codes" => [
88
+ {
89
+ "set" => "SNOMED-CT",
90
+ "values" => ["314443004"]
91
+ }
92
+ ]
93
+ }
94
+
95
+ pm = QME::Importer::PropertyMatcher.new(property_description)
96
+
97
+ entry = Entry.new
98
+ entry.add_code('314443004', 'SNOMED-CT')
99
+ entry.set_value('true')
100
+ entry.start_time = 1026777600
101
+
102
+ result = pm.match([entry])
103
+ result.should include({'date' => 1026777600, 'value' => true})
104
+ result.length.should == 1
105
+ end
106
+
107
+ it "should be able to deal with string" do
108
+ property_description = {
109
+ "type" => "array",
110
+ "items" => {
111
+ "type" => "object",
112
+ "properties" => {
113
+ "value" => {
114
+ "type" => "string"
115
+ },
116
+ "date" => {
117
+ "type" => "number"
118
+ }
119
+ }
120
+ },
121
+ "codes" => [
122
+ {
123
+ "set" => "SNOMED-CT",
124
+ "values" => ["314443004"]
125
+ }
126
+ ]
127
+ }
128
+
129
+ pm = QME::Importer::PropertyMatcher.new(property_description)
130
+
131
+ entry = Entry.new
132
+ entry.add_code('314443004', 'SNOMED-CT')
133
+ entry.set_value('super critical')
134
+ entry.start_time = 1026777600
135
+
136
+ result = pm.match([entry])
137
+ result.should include({'date' => 1026777600, 'value' => 'super critical'})
138
+ result.length.should == 1
139
+ end
140
+ end
141
+
142
+ it "should be able to extract a date range property" do
143
+ property_description = {
144
+ "type" => "array",
145
+ "items" => {
146
+ "type" => "object",
147
+ "properties" => {
148
+ "start" => {
149
+ "type" => "number"
150
+ },
151
+ "end" => {
152
+ "type" => "number"
153
+ }
154
+ }
155
+ },
156
+ "codes" => [
157
+ {
158
+ "set" => "SNOMED-CT",
159
+ "values" => ["194774006"]
160
+ }
161
+ ]
162
+ }
163
+
164
+ pm = QME::Importer::PropertyMatcher.new(property_description)
165
+
166
+ entry = Entry.new
167
+ entry.add_code('194774006', 'SNOMED-CT')
168
+ entry.start_time = 1026777600
169
+ entry.end_time = 1189814400
170
+
171
+ result = pm.match([entry])
172
+ result.should include({'start' => 1026777600, 'end' => 1189814400})
173
+ end
174
+ end
@@ -0,0 +1,38 @@
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
@@ -0,0 +1,38 @@
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
@@ -0,0 +1,11 @@
1
+ describe QME::MapReduce::Executor do
2
+
3
+ before do
4
+ @loader = QME::Database::Loader.new('test')
5
+ end
6
+
7
+ it 'should map patients as expected' do
8
+ validate_patient_mapping(@loader)
9
+ end
10
+
11
+ end
@@ -0,0 +1,12 @@
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
@@ -0,0 +1,61 @@
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
@@ -0,0 +1,74 @@
1
+ describe QME::QualityReport do
2
+ before do
3
+ loader = QME::Database::Loader.new
4
+ loader.drop_collection('query_cache')
5
+ loader.drop_collection('patient_cache')
6
+ loader.get_db['query_cache'].save(
7
+ "measure_id" => "test2",
8
+ "sub_id" => "b",
9
+ "initialPopulation" => 4,
10
+ "numerator" => 1,
11
+ "denominator" => 2,
12
+ "exclusions" => 1,
13
+ "effective_date" => Time.gm(2010, 9, 19).to_i
14
+ )
15
+ loader.get_db['patient_cache'].save(
16
+ "value" => {
17
+ "population" => false,
18
+ "denominator" => false,
19
+ "numerator" => false,
20
+ "exclusions" => false,
21
+ "antinumerator" => false,
22
+ "medical_record_id" => "0616911582",
23
+ "first" => "Mary",
24
+ "last" => "Edwards",
25
+ "gender" => "F",
26
+ "birthdate" => Time.gm(1940, 9, 19).to_i,
27
+ "test_id" => nil,
28
+ "provider_performances" => nil,
29
+ "race" => {
30
+ "code" => "2106-3",
31
+ "code_set" => "CDC-RE"
32
+ },
33
+ "ethnicity" => {
34
+ "code" => "2135-2",
35
+ "code_set" => "CDC-RE"
36
+ },
37
+ "measure_id" => "test2",
38
+ "sub_id" => "b",
39
+ "effective_date" => Time.gm(2010, 9, 19).to_i
40
+ }
41
+ )
42
+ end
43
+
44
+ it "should be able to determine if it has been calculated" do
45
+ qr = QME::QualityReport.new('test2', 'b', "effective_date" => Time.gm(2010, 9, 19).to_i)
46
+ qr.calculated?.should be_true
47
+
48
+ qr = QME::QualityReport.new('test2', 'b', "effective_date" => Time.gm(2010, 9, 20).to_i)
49
+ qr.calculated?.should be_false
50
+ end
51
+
52
+ it "should return the result of a calculated quality measure" do
53
+ qr = QME::QualityReport.new('test2', 'b', "effective_date" => Time.gm(2010, 9, 19).to_i)
54
+ result = qr.result
55
+
56
+ result['numerator'].should == 1
57
+ end
58
+
59
+ it "should be able to clear all of the quality reports" do
60
+ QME::QualityReport.destroy_all
61
+
62
+ qr = QME::QualityReport.new('test2', 'b', "effective_date" => Time.gm(2010, 9, 19).to_i)
63
+ qr.calculated?.should be_false
64
+ end
65
+
66
+ it "should be remove results for updated patients" do
67
+ qr = QME::QualityReport.new('test2', 'b', "effective_date" => Time.gm(2010, 9, 19).to_i)
68
+ qr.calculated?.should be_true
69
+ qr.patients_cached?.should be_true
70
+ QME::QualityReport.update_patient_results("0616911582")
71
+ qr.calculated?.should be_false
72
+ qr.patients_cached?.should be_false
73
+ end
74
+ end
@@ -0,0 +1,120 @@
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
@@ -0,0 +1,21 @@
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
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: quality-measure-engine
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.1.1
4
+ version: 1.1.2
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -11,12 +11,11 @@ authors:
11
11
  autorequire:
12
12
  bindir: bin
13
13
  cert_chain: []
14
- date: 2012-02-17 00:00:00.000000000 -05:00
15
- default_executable:
14
+ date: 2012-05-10 00:00:00.000000000Z
16
15
  dependencies:
17
16
  - !ruby/object:Gem::Dependency
18
17
  name: mongo
19
- requirement: &2157284660 !ruby/object:Gem::Requirement
18
+ requirement: &2158244120 !ruby/object:Gem::Requirement
20
19
  none: false
21
20
  requirements:
22
21
  - - ~>
@@ -24,10 +23,10 @@ dependencies:
24
23
  version: '1.3'
25
24
  type: :runtime
26
25
  prerelease: false
27
- version_requirements: *2157284660
26
+ version_requirements: *2158244120
28
27
  - !ruby/object:Gem::Dependency
29
28
  name: rubyzip
30
- requirement: &2157272260 !ruby/object:Gem::Requirement
29
+ requirement: &2158243320 !ruby/object:Gem::Requirement
31
30
  none: false
32
31
  requirements:
33
32
  - - ~>
@@ -35,10 +34,10 @@ dependencies:
35
34
  version: 0.9.4
36
35
  type: :runtime
37
36
  prerelease: false
38
- version_requirements: *2157272260
37
+ version_requirements: *2158243320
39
38
  - !ruby/object:Gem::Dependency
40
39
  name: nokogiri
41
- requirement: &2157271520 !ruby/object:Gem::Requirement
40
+ requirement: &2158242720 !ruby/object:Gem::Requirement
42
41
  none: false
43
42
  requirements:
44
43
  - - ~>
@@ -46,10 +45,10 @@ dependencies:
46
45
  version: 1.4.4
47
46
  type: :runtime
48
47
  prerelease: false
49
- version_requirements: *2157271520
48
+ version_requirements: *2158242720
50
49
  - !ruby/object:Gem::Dependency
51
50
  name: resque
52
- requirement: &2157270840 !ruby/object:Gem::Requirement
51
+ requirement: &2158242260 !ruby/object:Gem::Requirement
53
52
  none: false
54
53
  requirements:
55
54
  - - ~>
@@ -57,10 +56,10 @@ dependencies:
57
56
  version: 1.15.0
58
57
  type: :runtime
59
58
  prerelease: false
60
- version_requirements: *2157270840
59
+ version_requirements: *2158242260
61
60
  - !ruby/object:Gem::Dependency
62
61
  name: resque-status
63
- requirement: &2157270100 !ruby/object:Gem::Requirement
62
+ requirement: &2158235580 !ruby/object:Gem::Requirement
64
63
  none: false
65
64
  requirements:
66
65
  - - ~>
@@ -68,10 +67,10 @@ dependencies:
68
67
  version: 0.2.3
69
68
  type: :runtime
70
69
  prerelease: false
71
- version_requirements: *2157270100
70
+ version_requirements: *2158235580
72
71
  - !ruby/object:Gem::Dependency
73
72
  name: jsonschema
74
- requirement: &2157269380 !ruby/object:Gem::Requirement
73
+ requirement: &2158235040 !ruby/object:Gem::Requirement
75
74
  none: false
76
75
  requirements:
77
76
  - - ~>
@@ -79,10 +78,10 @@ dependencies:
79
78
  version: 2.0.0
80
79
  type: :development
81
80
  prerelease: false
82
- version_requirements: *2157269380
81
+ version_requirements: *2158235040
83
82
  - !ruby/object:Gem::Dependency
84
83
  name: rspec
85
- requirement: &2157268580 !ruby/object:Gem::Requirement
84
+ requirement: &2158234480 !ruby/object:Gem::Requirement
86
85
  none: false
87
86
  requirements:
88
87
  - - ~>
@@ -90,10 +89,10 @@ dependencies:
90
89
  version: 2.5.0
91
90
  type: :development
92
91
  prerelease: false
93
- version_requirements: *2157268580
92
+ version_requirements: *2158234480
94
93
  - !ruby/object:Gem::Dependency
95
94
  name: awesome_print
96
- requirement: &2157268080 !ruby/object:Gem::Requirement
95
+ requirement: &2158233760 !ruby/object:Gem::Requirement
97
96
  none: false
98
97
  requirements:
99
98
  - - ~>
@@ -101,10 +100,10 @@ dependencies:
101
100
  version: '0.3'
102
101
  type: :development
103
102
  prerelease: false
104
- version_requirements: *2157268080
103
+ version_requirements: *2158233760
105
104
  - !ruby/object:Gem::Dependency
106
105
  name: roo
107
- requirement: &2157267620 !ruby/object:Gem::Requirement
106
+ requirement: &2158232780 !ruby/object:Gem::Requirement
108
107
  none: false
109
108
  requirements:
110
109
  - - ~>
@@ -112,10 +111,10 @@ dependencies:
112
111
  version: 1.9.3
113
112
  type: :development
114
113
  prerelease: false
115
- version_requirements: *2157267620
114
+ version_requirements: *2158232780
116
115
  - !ruby/object:Gem::Dependency
117
116
  name: builder
118
- requirement: &2157267100 !ruby/object:Gem::Requirement
117
+ requirement: &2158231380 !ruby/object:Gem::Requirement
119
118
  none: false
120
119
  requirements:
121
120
  - - ~>
@@ -123,10 +122,10 @@ dependencies:
123
122
  version: 3.0.0
124
123
  type: :development
125
124
  prerelease: false
126
- version_requirements: *2157267100
125
+ version_requirements: *2158231380
127
126
  - !ruby/object:Gem::Dependency
128
127
  name: spreadsheet
129
- requirement: &2157266420 !ruby/object:Gem::Requirement
128
+ requirement: &2158230480 !ruby/object:Gem::Requirement
130
129
  none: false
131
130
  requirements:
132
131
  - - ~>
@@ -134,10 +133,10 @@ dependencies:
134
133
  version: 0.6.5.2
135
134
  type: :development
136
135
  prerelease: false
137
- version_requirements: *2157266420
136
+ version_requirements: *2158230480
138
137
  - !ruby/object:Gem::Dependency
139
138
  name: google-spreadsheet-ruby
140
- requirement: &2157265900 !ruby/object:Gem::Requirement
139
+ requirement: &2158229620 !ruby/object:Gem::Requirement
141
140
  none: false
142
141
  requirements:
143
142
  - - ~>
@@ -145,7 +144,7 @@ dependencies:
145
144
  version: 0.1.2
146
145
  type: :development
147
146
  prerelease: false
148
- version_requirements: *2157265900
147
+ version_requirements: *2158229620
149
148
  description: A library for extracting quality measure information from HITSP C32's
150
149
  and ASTM CCR's
151
150
  email: talk@projectpophealth.org
@@ -179,11 +178,22 @@ files:
179
178
  - lib/tasks/patient_random.rake
180
179
  - js/map_reduce_utils.js
181
180
  - js/underscore_min.js
181
+ - spec/qme/bundle_spec.rb
182
+ - spec/qme/importer/generic_importer_spec.rb
183
+ - spec/qme/importer/measure_properties_generator_spec.rb
184
+ - spec/qme/importer/property_matcher_spec.rb
185
+ - spec/qme/map/map_reduce_builder_spec.rb
186
+ - spec/qme/map/measures_spec.rb
187
+ - spec/qme/map/patient_mapper_spec.rb
188
+ - spec/qme/measure_loader_spec.rb
189
+ - spec/qme/properties_builder_spec.rb
190
+ - spec/qme/quality_report_spec.rb
191
+ - spec/spec_helper.rb
192
+ - spec/validate_measures_spec.rb
182
193
  - Gemfile
183
194
  - README.md
184
195
  - Rakefile
185
196
  - VERSION
186
- has_rdoc: true
187
197
  homepage: http://github.com/pophealth/quality-measure-engine
188
198
  licenses: []
189
199
  post_install_message:
@@ -204,9 +214,10 @@ required_rubygems_version: !ruby/object:Gem::Requirement
204
214
  version: '0'
205
215
  requirements: []
206
216
  rubyforge_project:
207
- rubygems_version: 1.6.2
217
+ rubygems_version: 1.8.10
208
218
  signing_key:
209
219
  specification_version: 3
210
220
  summary: A library for extracting quality measure information from HITSP C32's and
211
221
  ASTM CCR's
212
222
  test_files: []
223
+ has_rdoc: