quality-measure-engine 1.1.1 → 1.1.2

Sign up to get free protection for your applications and to get access to all the features.
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: