quality-measure-engine 1.1.5 → 2.0.0

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