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.
Files changed (66) hide show
  1. data/.gitignore +12 -0
  2. data/.travis.yml +16 -0
  3. data/Gemfile +5 -21
  4. data/Gemfile.lock +126 -0
  5. data/LICENSE.txt +13 -0
  6. data/README.md +23 -44
  7. data/Rakefile +6 -29
  8. data/lib/qme/bundle/bundle.rb +34 -0
  9. data/lib/qme/bundle/importer.rb +69 -0
  10. data/lib/qme/database_access.rb +7 -11
  11. data/lib/qme/map/map_reduce_builder.rb +4 -1
  12. data/lib/qme/map/map_reduce_executor.rb +55 -43
  13. data/lib/qme/map/measure_calculation_job.rb +24 -23
  14. data/lib/qme/quality_measure.rb +5 -5
  15. data/lib/qme/quality_report.rb +37 -19
  16. data/lib/qme/railtie.rb +7 -0
  17. data/lib/qme/tasks/bundle.rake +14 -0
  18. data/lib/qme/version.rb +3 -0
  19. data/lib/quality-measure-engine.rb +13 -25
  20. data/quality-measure-engine.gemspec +28 -0
  21. data/test/fixtures/bundles/just_measure_0002.zip +0 -0
  22. data/test/fixtures/delayed_backend_mongoid_jobs/queued_job.json +9 -0
  23. data/test/fixtures/measures/measure_metadata.json +52 -0
  24. data/test/fixtures/records/barry_berry.json +471 -0
  25. data/test/fixtures/records/billy_jones_ipp.json +78 -0
  26. data/test/fixtures/records/jane_jones_numerator.json +120 -0
  27. data/test/fixtures/records/jill_jones_denominator.json +78 -0
  28. data/test/simplecov_setup.rb +18 -0
  29. data/test/test_helper.rb +26 -0
  30. data/test/unit/qme/map/map_reduce_builder_test.rb +38 -0
  31. data/test/unit/qme/map/map_reduce_executor_test.rb +56 -0
  32. data/test/unit/qme/map/measure_calculation_job_test.rb +22 -0
  33. data/test/unit/qme/quality_measure_test.rb +14 -0
  34. data/{spec/qme/quality_report_spec.rb → test/unit/qme/quality_report_test.rb} +32 -20
  35. metadata +91 -115
  36. data/VERSION +0 -1
  37. data/js/map_reduce_utils.js +0 -173
  38. data/js/underscore_min.js +0 -25
  39. data/lib/qme/ext/record.rb +0 -43
  40. data/lib/qme/importer/entry.rb +0 -126
  41. data/lib/qme/importer/generic_importer.rb +0 -117
  42. data/lib/qme/importer/measure_properties_generator.rb +0 -39
  43. data/lib/qme/importer/property_matcher.rb +0 -110
  44. data/lib/qme/measure/database_loader.rb +0 -83
  45. data/lib/qme/measure/measure_loader.rb +0 -174
  46. data/lib/qme/measure/properties_builder.rb +0 -184
  47. data/lib/qme/measure/properties_converter.rb +0 -27
  48. data/lib/qme/randomizer/patient_randomization_job.rb +0 -47
  49. data/lib/qme/randomizer/patient_randomizer.rb +0 -250
  50. data/lib/qme/randomizer/random_patient_creator.rb +0 -47
  51. data/lib/qme_test.rb +0 -13
  52. data/lib/tasks/fixtures.rake +0 -91
  53. data/lib/tasks/measure.rake +0 -110
  54. data/lib/tasks/mongo.rake +0 -68
  55. data/lib/tasks/patient_random.rake +0 -45
  56. data/spec/qme/bundle_spec.rb +0 -37
  57. data/spec/qme/importer/generic_importer_spec.rb +0 -73
  58. data/spec/qme/importer/measure_properties_generator_spec.rb +0 -15
  59. data/spec/qme/importer/property_matcher_spec.rb +0 -174
  60. data/spec/qme/map/map_reduce_builder_spec.rb +0 -38
  61. data/spec/qme/map/measures_spec.rb +0 -38
  62. data/spec/qme/map/patient_mapper_spec.rb +0 -11
  63. data/spec/qme/measure_loader_spec.rb +0 -12
  64. data/spec/qme/properties_builder_spec.rb +0 -61
  65. data/spec/spec_helper.rb +0 -120
  66. data/spec/validate_measures_spec.rb +0 -21
@@ -1,110 +0,0 @@
1
- path = File.dirname(__FILE__)
2
- path = path.index('lib') == 0 ? "./#{path}" : path
3
- require 'json'
4
- require 'zlib'
5
- gem 'rubyzip'
6
- require 'zip/zip'
7
- require 'zip/zipfilesystem'
8
- require File.join(path,'../quality-measure-engine')
9
- measures_dir = ENV['MEASURE_DIR'] || 'measures'
10
- bundle_dir = ENV['BUNDLE_DIR'] || './'
11
- xls_dir = ENV['XLS_DIR'] || 'xls'
12
- db_name = ENV['DB_NAME'] || 'test'
13
-
14
- namespace :measures do
15
-
16
- desc 'Build all measures to tmp directory'
17
- task :build do
18
- puts "Loading measures from #{measures_dir}"
19
- dest_dir = File.join('.', 'tmp')
20
- Dir.mkdir(dest_dir) if !Dir.exist?(dest_dir)
21
- Dir.glob(File.join(measures_dir, '*')).each do |measure_dir|
22
- measures = QME::Measure::Loader.load_measure(measure_dir)
23
- measures.each do |measure|
24
- id = measure['id']
25
- sub_id = measure['sub_id']
26
- json = JSON.pretty_generate(measure)
27
- file_name = File.join(dest_dir, "#{id}#{sub_id}.json")
28
- file = File.new(file_name, "w")
29
- file.write(json)
30
- file.close
31
- end
32
- end
33
- end
34
-
35
- desc "run the map_test tool"
36
- task :map_tool do
37
- puts "Loading measures from #{measures_dir}"
38
- require File.join(path,"../../map_test/map_test.rb")
39
- end
40
-
41
- desc 'Take a snapshot of the current measures, system.js and bundles collections and store as a ZIP file'
42
- task :snapshot do
43
- tmp = File.join('.', 'tmp')
44
- dest_dir = File.join(tmp, 'bundle')
45
- FileUtils.rm_r dest_dir, :force=>true
46
- FileUtils.mkdir_p(dest_dir)
47
- system("mongodump --db #{db_name} --collection bundles --out - > #{dest_dir}/bundles.bson")
48
- system("mongodump --db #{db_name} --collection system.js --out - > #{dest_dir}/system.js.bson")
49
- system("mongodump --db #{db_name} --collection measures --out - > #{dest_dir}/measures.bson")
50
- read_me = <<EOF
51
- Load the included files into Mongo as follows:
52
-
53
- mongorestore --db dbname --drop measures.bson
54
- mongorestore --db dbname --drop bundles.bson
55
- mongorestore --db dbname --drop system.js.bson
56
-
57
- Where dbname is the name of the database you want to load the measures into. For a
58
- production system this will typically be pophealth-production. For a development
59
- system it will typically be pophealth-development.
60
-
61
- Note that the existing contents of the destination database's measures, system.js and
62
- bundles collections will be lost.
63
- EOF
64
- File.open(File.join(dest_dir, 'README.txt'), 'w') {|f| f.write(read_me) }
65
-
66
- archive = File.join(tmp, 'bundle.zip')
67
- puts "Snapshot saved to #{archive}"
68
- FileUtils.rm archive, :force=>true
69
-
70
- Zip::ZipFile.open(archive, 'w') do |zipfile|
71
- Dir["#{dest_dir}/*"].each do |file|
72
- zipfile.add(File.basename(file),file)
73
- end
74
- end
75
- FileUtils.rm_r dest_dir, :force=>true
76
- end
77
-
78
- desc "convert NQF Excel spreadsheets to JSON"
79
- task :convert do
80
- require LIB + '/qme/measure/properties_builder'
81
- require LIB + '/qme/measure/properties_converter'
82
- dest_dir = File.join('.', 'tmp')
83
- Dir.mkdir(dest_dir) if !Dir.exist?(dest_dir)
84
- Dir.glob(File.join(xls_dir, '*.xlsx')).each do |measure|
85
- properties = QME::Measure::Converter.from_xls(measure)
86
- json = JSON.pretty_generate(properties)
87
- file_name = File.join(dest_dir, "#{File.basename(measure)}.json")
88
- file = File.new(file_name, "w")
89
- file.write(json)
90
- file.close
91
- end
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
-
109
-
110
- end
data/lib/tasks/mongo.rake DELETED
@@ -1,68 +0,0 @@
1
- path = File.dirname(__FILE__)
2
- path = path.index('lib') == 0 ? "./#{path}" : path
3
- require 'mongo'
4
- require 'json'
5
- require 'resque'
6
- require File.join(path,'../quality-measure-engine')
7
-
8
- measures_dir = ENV['MEASURE_DIR'] || 'measures'
9
- bundle_dir = ENV['BUNDLE_DIR'] || '.'
10
- fixtures_dir = ENV['FIXTURE_DIR'] || File.join('fixtures', 'measures')
11
- db_name = ENV['DB_NAME'] || 'test'
12
- loader = QME::Database::Loader.new()
13
-
14
- namespace :mongo do
15
-
16
- desc 'Removed cached measure results'
17
- task :drop_cache do
18
- loader.drop_collection('query_cache')
19
- loader.drop_collection('patient_cache')
20
- end
21
-
22
- desc 'Remove the patient records collection'
23
- task :drop_records => :drop_cache do
24
- loader.drop_collection('records')
25
- end
26
-
27
- desc 'Remove the measures and bundles collection'
28
- task :drop_bundle do
29
- loader.drop_collection('bundles')
30
- loader.drop_collection('measures')
31
- end
32
-
33
- desc 'Remove all patient records and reload'
34
- task :reload_records => :drop_records do
35
- load_files(loader, File.join(fixtures_dir,'*','patients','*.json'), 'records')
36
- end
37
-
38
- desc 'Remove all patient records and reload'
39
- task :reload_bundle => [:drop_bundle] do
40
- loader.save_bundle(bundle_dir, measures_dir)
41
- end
42
-
43
- desc 'Clear database and road each measure and its sample patient files'
44
- task :reload => [:reload_records, :reload_bundle]
45
-
46
- desc 'Seed the query cache by calculating the results for all measures'
47
- task :seed_cache, [:year, :month, :day] do |t, args|
48
- db = loader.get_db
49
- patient_cache = db['patient_cache']
50
- patient_cache.create_index([['value.measure_id', Mongo::ASCENDING],
51
- ['value.sub_id', Mongo::ASCENDING],
52
- ['value.effective_date', Mongo::ASCENDING]])
53
- year = args.year.to_i>0 ? args.year.to_i : 2010
54
- month = args.month.to_i>0 ? args.month.to_i : 9
55
- day = args.day.to_i>0 ? args.day.to_i : 19
56
- QME::QualityMeasure.all.each_value do |measure_def|
57
- QME::MapReduce::MeasureCalculationJob.create(:measure_id => measure_def['id'], :sub_id => measure_def['sub_id'], :effective_date => Time.gm(year, month, day).to_i)
58
- end
59
- end
60
-
61
- def load_files(loader, file_pattern, collection_name)
62
- Dir.glob(file_pattern).each do |file|
63
- json = JSON.parse(File.read(file))
64
- loader.save(collection_name, json)
65
- end
66
- end
67
-
68
- end
@@ -1,45 +0,0 @@
1
- path = File.dirname(__FILE__)
2
- path = path.index('lib') == 0 ? "./#{path}" : path
3
- require 'mongo'
4
- require 'json'
5
- require File.join(path,'../quality-measure-engine')
6
-
7
- patient_template_dir = ENV['PATIENT_TEMPLATE_DIR'] || File.join('fixtures', 'patient_templates')
8
- db_name = ENV['DB_NAME'] || 'test'
9
- loader = QME::Database::Loader.new(db_name)
10
-
11
- namespace :patient do
12
-
13
- desc 'Generate n (default 10) random patient records and save them in the database'
14
- task :random, [:n] => ['mongo:drop_records'] do |t, args|
15
- n = args.n.to_i>0 ? args.n.to_i : 10
16
-
17
- templates = []
18
- Dir.glob(File.join(patient_template_dir, '*.json.erb')).each do |file|
19
- templates << File.read(file)
20
- end
21
-
22
- if templates.length<1
23
- puts "No patient templates in #{patient_template_dir}"
24
- return
25
- end
26
-
27
- processed_measures = {}
28
- QME::QualityMeasure.all.each_value do |measure_def|
29
- measure_id = measure_def['id']
30
- if !processed_measures[measure_id]
31
- QME::Importer::MeasurePropertiesGenerator.instance.add_measure(measure_id, QME::Importer::GenericImporter.new(measure_def))
32
- processed_measures[measure_id]=true
33
- end
34
- end
35
-
36
- n.times do
37
- template = templates[rand(templates.length)]
38
- generator = QME::Randomizer::Patient.new(template)
39
- json = JSON.parse(generator.get())
40
- patient_record = QME::Randomizer::RandomPatientCreator.parse_hash(json)
41
- patient_record.save!
42
- end
43
- end
44
-
45
- end
@@ -1,37 +0,0 @@
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
@@ -1,73 +0,0 @@
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
@@ -1,15 +0,0 @@
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
@@ -1,174 +0,0 @@
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