quality-measure-engine 0.2.0 → 0.8.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.
@@ -0,0 +1,27 @@
1
+ require 'roo'
2
+
3
+ module QME
4
+ module Measure
5
+
6
+ # Utility class for converting NQF XLS files to JSON
7
+ class Converter
8
+
9
+ #Convert an NQF XLS file to a hash
10
+ def self.from_xls(file)
11
+ xls =Excelx.new(file)
12
+ xls.default_sheet='Measure_QDS'
13
+ result = {}
14
+ (xls.header_line+1..xls.last_row).each do |row|
15
+ entry = {}
16
+ (xls.first_column..xls.last_column).each do |column|
17
+ entry[xls.cell(xls.header_line, column)] = xls.cell(row, column)
18
+ end
19
+ result[entry['QDS_id']] = entry
20
+ end
21
+ result
22
+ end
23
+
24
+ end
25
+
26
+ end
27
+ end
@@ -0,0 +1,41 @@
1
+ module QME
2
+ class QualityMeasure
3
+ include DatabaseAccess
4
+ extend DatabaseAccess
5
+ determine_connection_information
6
+
7
+ # Return a list of the measures in the database
8
+ # @return [Hash] an hash of measure definitions
9
+ def self.all
10
+ result = {}
11
+ measures = get_db.collection('measures')
12
+ measures.find().each do |measure|
13
+ id = measure['id']
14
+ sub_id = measure['sub_id']
15
+ measure_id = "#{id}#{sub_id}.json"
16
+ result[measure_id] ||= measure
17
+ end
18
+ result
19
+ end
20
+
21
+ # Creates a new QualityMeasure
22
+ # @param [String] measure_id value of the measure's id field
23
+ # @param [String] sub_id value of the measure's sub_id field, may be nil for measures with only a single numerator and denominator
24
+ def initialize(measure_id, sub_id = nil)
25
+ @measure_id = measure_id
26
+ @sub_id = sub_id
27
+ determine_connection_information
28
+ end
29
+
30
+ # Retrieve a measure definition from the database
31
+ # @return [Hash] a JSON hash of the encoded measure
32
+ def definition
33
+ measures = get_db.collection('measures')
34
+ if @sub_id
35
+ measures.find_one({'id' => @measure_id, 'sub_id' => @sub_id})
36
+ else
37
+ measures.find_one({'id' => @measure_id})
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,61 @@
1
+ module QME
2
+ # A class that allows you to create and obtain the results of running a
3
+ # quality measure against a set of patient records.
4
+ class QualityReport
5
+ include DatabaseAccess
6
+ extend DatabaseAccess
7
+ determine_connection_information
8
+
9
+ # Gets rid of all calculated QualityReports by dropping the patient_cache
10
+ # and query_cache collections
11
+ def self.destroy_all
12
+ determine_connection_information
13
+ get_db.collection("query_cache").drop
14
+ get_db.collection("patient_cache").drop
15
+ end
16
+
17
+ # Creates a new QualityReport
18
+ # @param [String] measure_id value of the measure's id field
19
+ # @param [String] sub_id value of the measure's sub_id field, may be nil
20
+ # for measures with only a single numerator and denominator
21
+ # @param [Hash] parameter_values slots in the measure definition that need to
22
+ # be filled in.
23
+ def initialize(measure_id, sub_id, parameter_values)
24
+ @measure_id = measure_id
25
+ @sub_id = sub_id
26
+ @parameter_values = parameter_values
27
+ determine_connection_information
28
+ end
29
+
30
+ # Determines whether the quality report has been calculated for the given
31
+ # measure and parameters
32
+ # @return [true|false]
33
+ def calculated?
34
+ ! result().nil?
35
+ end
36
+
37
+ # Kicks off a background job to calculate the measure
38
+ # @return a unique id for the measure calculation job
39
+ def calculate
40
+ MapReduce::MeasureCalculationJob.create(:measure_id => @measure_id, :sub_id => @sub_id,
41
+ :effective_date => @parameter_values['effective_date'])
42
+ end
43
+
44
+ # Returns the status of a measure calculation job
45
+ # @param job_id the id of the job to check on
46
+ # @return [Hash] containing status information on the measure calculation job
47
+ def status(job_id)
48
+ Resque::Status.get(job_id)
49
+ end
50
+
51
+ # Gets the result of running a quality measure
52
+ # @return [Hash] measure groups (like numerator) as keys, counts as values or nil if
53
+ # the measure has not yet been calculated
54
+ def result
55
+ cache = get_db.collection("query_cache")
56
+ query = {:measure_id => @measure_id, :sub_id => @sub_id,
57
+ :effective_date => @parameter_values['effective_date']}
58
+ cache.find_one(query)
59
+ end
60
+ end
61
+ end
@@ -1,29 +1,32 @@
1
1
  Bundler.require(:default)
2
2
 
3
- LIB = File.dirname(__FILE__)
3
+ require 'resque/job_with_status'
4
4
 
5
- require LIB + '/qme/map/map_reduce_builder'
6
- require LIB + '/qme/map/map_reduce_executor'
5
+ require_relative 'qme/database_access'
6
+ require_relative 'qme/quality_measure'
7
7
 
8
- require LIB + '/qme/randomizer/patient_randomizer'
8
+ require_relative 'qme/map/map_reduce_builder'
9
+ require_relative 'qme/map/map_reduce_executor'
10
+ require_relative 'qme/map/measure_calculation_job'
9
11
 
10
- require 'singleton'
12
+ require_relative 'qme/quality_report'
13
+
14
+ require_relative 'qme/randomizer/patient_randomizer'
11
15
 
12
- require LIB + '/qme/importer/entry'
13
- require LIB + '/qme/importer/property_matcher'
14
- require LIB + '/qme/importer/patient_importer'
15
- require LIB + '/qme/importer/code_system_helper'
16
- require LIB + '/qme/importer/hl7_helper'
16
+ require 'singleton'
17
17
 
18
- require LIB + '/qme/importer/section_importer'
19
- require LIB + '/qme/importer/generic_importer'
18
+ require_relative 'qme/importer/entry'
19
+ require_relative 'qme/importer/property_matcher'
20
+ require_relative 'qme/importer/patient_importer'
21
+ require_relative 'qme/importer/code_system_helper'
22
+ require_relative 'qme/importer/hl7_helper'
20
23
 
21
- require LIB + '/qme/mongo_helpers'
24
+ require_relative 'qme/importer/section_importer'
25
+ require_relative 'qme/importer/generic_importer'
22
26
 
23
27
  require 'json'
24
28
  require 'mongo'
25
29
  require 'nokogiri'
26
30
 
27
- require LIB + '/qme/measure/measure_loader'
28
- require LIB + '/qme/measure/database_loader'
29
-
31
+ require_relative 'qme/measure/measure_loader'
32
+ require_relative 'qme/measure/database_loader'
@@ -0,0 +1,91 @@
1
+ require 'json'
2
+ require 'erb'
3
+
4
+ fixtures_dir = ENV['FIXTURE_DIR'] || File.join('fixtures', 'measures')
5
+ output_dir = ENV['HTML_OUTPUT_DIR'] || File.join('tmp', 'html')
6
+
7
+ def epoch_pp(seconds_since_epoch)
8
+ if seconds_since_epoch.kind_of? Fixnum
9
+ Time.at(seconds_since_epoch).utc.strftime('%B %d, %Y')
10
+ else
11
+ seconds_since_epoch
12
+ end
13
+ end
14
+
15
+ def extract_dates(thingy, dates)
16
+ if thingy.kind_of? Fixnum
17
+ dates[thingy] = epoch_pp(thingy)
18
+ elsif thingy.kind_of? Array
19
+ thingy.each {|inner_thingy| extract_dates(inner_thingy, dates)}
20
+ elsif thingy.kind_of? Hash
21
+ thingy.values.each {|inner_thingy| extract_dates(inner_thingy, dates)}
22
+ else
23
+ # Must be a string
24
+ end
25
+ end
26
+
27
+ namespace :fixtures do
28
+
29
+ # NOTE: This will create invalid JSON fixture files, since JSON does not have the ability add comments.
30
+ # However, most JSON parsers handle this just fine.
31
+ desc 'Place a comment header at the top of each fixture file with a table of seconds since epoch to month, day, year'
32
+ task :time_comments do
33
+ Dir.glob(File.join(fixtures_dir,'*','patients','*.json')) do |file|
34
+ fixture_person = JSON.parse(File.read(file))
35
+ dates = {}
36
+ extract_dates(fixture_person, dates)
37
+
38
+ fixture = File.open(file, 'w')
39
+ fixture.write("//\n// Dates used in this fixture\n//\n")
40
+ dates.each_pair do |key, value|
41
+ fixture.write("// #{key} - #{value}\n")
42
+ end
43
+ fixture.write(JSON.pretty_generate(fixture_person))
44
+ fixture.close
45
+ end
46
+ end
47
+
48
+ desc 'Generate HTML versions of the patient fixtures'
49
+ task :html_doc do
50
+ patient_template = File.read(File.join(File.dirname(__FILE__), 'patient_fixture.erb'))
51
+ patient_erb = ERB.new(patient_template)
52
+
53
+ result_template = File.read(File.join(File.dirname(__FILE__), 'result.erb'))
54
+ result_erb = ERB.new(result_template)
55
+
56
+ fixtures_path = Pathname.new(fixtures_dir)
57
+ output_path = Pathname.new(output_dir)
58
+
59
+ Dir.glob(File.join(fixtures_dir,'*','patients','*.json')) do |file|
60
+ @patient = JSON.parse(File.read(file))
61
+ @filename = file
62
+ html = patient_erb.result(binding)
63
+
64
+ fixture_path = Pathname.new(file)
65
+ relative_fixture_path = fixture_path.relative_path_from(fixtures_path)
66
+ measure_output_path = output_path + relative_fixture_path
67
+ FileUtils.mkdir_p(measure_output_path.dirname)
68
+ File.open(measure_output_path.sub_ext('.html'), 'w') do |html_out|
69
+ html_out.write(html)
70
+ end
71
+ end
72
+
73
+ Dir.glob(File.join(fixtures_dir,'*','result.json')) do |file|
74
+ result_json = JSON.parse(File.read(file))
75
+ if result_json["results"]
76
+ @results = result_json["results"]
77
+ else
78
+ @results = [result_json]
79
+ end
80
+ html = result_erb.result(binding)
81
+
82
+ result_path = Pathname.new(file)
83
+ relative_result_path = result_path.relative_path_from(fixtures_path)
84
+ result_output_path = output_path + relative_result_path
85
+
86
+ File.open(result_output_path.sub_ext('.html'), 'w') do |html_out|
87
+ html_out.write(html)
88
+ end
89
+ end
90
+ end
91
+ end
@@ -5,19 +5,18 @@ require 'zlib'
5
5
  gem 'rubyzip'
6
6
  require 'zip/zip'
7
7
  require 'zip/zipfilesystem'
8
-
9
-
10
8
  require File.join(path,'../quality-measure-engine')
11
9
 
12
-
13
10
  measures_dir = ENV['MEASURE_DIR'] || 'measures'
14
- puts "Loading measures from #{measures_dir}"
15
-
16
11
  bundle_dir = ENV['BUNDLE_DIR'] || './'
12
+ xls_dir = ENV['XLS_DIR'] || 'xls'
13
+ db_name = ENV['DB_NAME'] || 'test'
14
+
17
15
  namespace :measures do
18
16
 
19
17
  desc 'Build all measures to tmp directory'
20
18
  task :build do
19
+ puts "Loading measures from #{measures_dir}"
21
20
  dest_dir = File.join('.', 'tmp')
22
21
  Dir.mkdir(dest_dir) if !Dir.exist?(dest_dir)
23
22
  Dir.glob(File.join(measures_dir, '*')).each do |measure_dir|
@@ -34,43 +33,63 @@ namespace :measures do
34
33
  end
35
34
  end
36
35
 
37
-
38
-
39
36
  desc "run the map_test tool"
40
37
  task :map_tool do
38
+ puts "Loading measures from #{measures_dir}"
41
39
  require File.join(path,"../../map_test/map_test.rb")
42
40
  end
43
41
 
44
-
45
- desc "bundle measures into a compressed file for deployment"
46
- task :bundle do
42
+ desc 'Take a snapshot of the current measures, system.js and bundles collections and store as a ZIP file'
43
+ task :snapshot do
44
+ tmp = File.join('.', 'tmp')
45
+ dest_dir = File.join(tmp, 'bundle')
46
+ FileUtils.rm_r dest_dir, :force=>true
47
+ FileUtils.mkdir_p(dest_dir)
48
+ system("mongodump --db #{db_name} --collection bundles --out - > #{dest_dir}/bundles.bson")
49
+ system("mongodump --db #{db_name} --collection system.js --out - > #{dest_dir}/system.js.bson")
50
+ system("mongodump --db #{db_name} --collection measures --out - > #{dest_dir}/measures.bson")
51
+ read_me = <<EOF
52
+ Load the included files into Mongo as follows:
53
+
54
+ mongorestore --db dbname --drop measures.bson
55
+ mongorestore --db dbname --drop bundles.bson
56
+ mongorestore --db dbname --drop system.js.bson
57
+
58
+ Where dbname is the name of the database you want to load the measures into. For a
59
+ production system this will typically be pophealth-production. For a development
60
+ system it will typically be pophealth-development.
61
+
62
+ Note that the existing contents of the destination database's measures, system.js and
63
+ bundles collections will be lost.
64
+ EOF
65
+ File.open(File.join(dest_dir, 'README.txt'), 'w') {|f| f.write(read_me) }
47
66
 
48
- md = File.join(bundle_dir,measures_dir)
49
- js = File.join(bundle_dir,'js')
50
- bf = File.join(bundle_dir,'bundle.js')
51
-
52
- tmp = './tmp'
53
- bundle_tmp = File.join(tmp,'bundle')
54
-
55
- md.sub!(%r[/$],'')
56
- FileUtils.rm bundle_tmp, :force=>true
57
- FileUtils.mkdir_p(bundle_tmp)
58
- archive = File.join(tmp,'bundle.zip')
59
- FileUtils.rm archive, :force=>true
60
-
61
- Zip::ZipFile.open(archive, 'w') do |zipfile|
62
- Dir["#{md}/**/**"].reject{|f|f==archive}.each do |file|
63
- zipfile.add(file.sub(bundle_dir,''),file)
64
- end
65
-
66
- Dir["#{js}/**/**"].reject{|f|f==archive}.each do |file|
67
- zipfile.add(file.sub(bundle_dir,''),file)
68
- end
69
-
70
- if File.exists?(bf)
71
- zipfile.add(bf.sub(bundle_dir,''),bf)
72
- end
73
- end
67
+ archive = File.join(tmp, 'bundle.zip')
68
+ puts "Snapshot saved to #{archive}"
69
+ FileUtils.rm archive, :force=>true
70
+
71
+ Zip::ZipFile.open(archive, 'w') do |zipfile|
72
+ Dir["#{dest_dir}/*"].each do |file|
73
+ zipfile.add(File.basename(file),file)
74
+ end
75
+ end
76
+ FileUtils.rm_r dest_dir, :force=>true
77
+ end
78
+
79
+ desc "convert NQF Excel spreadsheets to JSON"
80
+ task :convert do
81
+ require LIB + '/qme/measure/properties_builder'
82
+ require LIB + '/qme/measure/properties_converter'
83
+ dest_dir = File.join('.', 'tmp')
84
+ Dir.mkdir(dest_dir) if !Dir.exist?(dest_dir)
85
+ Dir.glob(File.join(xls_dir, '*.xlsx')).each do |measure|
86
+ properties = QME::Measure::Converter.from_xls(measure)
87
+ json = JSON.pretty_generate(properties)
88
+ file_name = File.join(dest_dir, "#{File.basename(measure)}.json")
89
+ file = File.new(file_name, "w")
90
+ file.write(json)
91
+ file.close
92
+ end
74
93
  end
75
94
 
76
95
  end
data/lib/tasks/mongo.rake CHANGED
@@ -2,19 +2,21 @@ path = File.dirname(__FILE__)
2
2
  path = path.index('lib') == 0 ? "./#{path}" : path
3
3
  require 'mongo'
4
4
  require 'json'
5
+ require 'resque'
5
6
  require File.join(path,'../quality-measure-engine')
6
7
 
7
8
  measures_dir = ENV['MEASURE_DIR'] || 'measures'
8
9
  bundle_dir = ENV['BUNDLE_DIR'] || '.'
9
10
  fixtures_dir = ENV['FIXTURE_DIR'] || File.join('fixtures', 'measures')
10
11
  db_name = ENV['DB_NAME'] || 'test'
11
- loader = QME::Database::Loader.new(db_name)
12
+ loader = QME::Database::Loader.new()
12
13
 
13
14
  namespace :mongo do
14
15
 
15
16
  desc 'Removed cached measure results'
16
17
  task :drop_cache do
17
18
  loader.drop_collection('query_cache')
19
+ loader.drop_collection('patient_cache')
18
20
  end
19
21
 
20
22
  desc 'Remove the patient records collection'
@@ -23,22 +25,11 @@ namespace :mongo do
23
25
  end
24
26
 
25
27
  desc 'Remove the measures and bundles collection'
26
- task :drop_bundle => :drop_measures do
28
+ task :drop_bundle do
27
29
  loader.drop_collection('bundles')
28
- end
29
-
30
- desc 'Remove the measures collection'
31
- task :drop_measures => :drop_cache do
32
30
  loader.drop_collection('measures')
33
31
  end
34
32
 
35
- desc 'Remove all measures and reload'
36
- task :reload_measures => :drop_measures do
37
- Dir.glob(File.join(measures_dir, '*')).each do |measure_dir|
38
- loader.save_measure(measure_dir, 'measures')
39
- end
40
- end
41
-
42
33
  desc 'Remove all patient records and reload'
43
34
  task :reload_records => :drop_records do
44
35
  load_files(loader, File.join(fixtures_dir,'*','patients','*.json'), 'records')
@@ -46,21 +37,24 @@ namespace :mongo do
46
37
 
47
38
  desc 'Remove all patient records and reload'
48
39
  task :reload_bundle => [:drop_bundle] do
49
- loader.save_bundle(bundle_dir,'bundles')
40
+ loader.save_bundle(bundle_dir, measures_dir)
50
41
  end
51
42
 
52
43
  desc 'Clear database and road each measure and its sample patient files'
53
- task :reload => [:reload_records, :reload_measures]
44
+ task :reload => [:reload_records, :reload_bundle]
54
45
 
55
46
  desc 'Seed the query cache by calculating the results for all measures'
56
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]])
57
53
  year = args.year.to_i>0 ? args.year.to_i : 2010
58
54
  month = args.month.to_i>0 ? args.month.to_i : 9
59
55
  day = args.day.to_i>0 ? args.day.to_i : 19
60
- map = QME::MapReduce::Executor.new(loader.get_db)
61
- map.all_measures.each_value do |measure_def|
62
- result = map.measure_result(measure_def['id'], measure_def['sub_id'], :effective_date=>Time.gm(year, month, day).to_i)
63
- puts "#{measure_def['id']}#{measure_def['sub_id']}: p#{result[:population]}, d#{result[:denominator]}, n#{result[:numerator]}, e#{result[:exclusions]}"
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)
64
58
  end
65
59
  end
66
60