quality-measure-engine 1.0.4 → 1.1.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.
- data/Gemfile +3 -2
- data/Rakefile +0 -8
- data/VERSION +1 -1
- data/js/map_reduce_utils.js +1 -1
- data/lib/qme/database_access.rb +4 -0
- data/lib/qme/ext/record.rb +49 -0
- data/lib/qme/importer/entry.rb +1 -1
- data/lib/qme/importer/generic_importer.rb +24 -47
- data/lib/qme/importer/measure_properties_generator.rb +39 -0
- data/lib/qme/importer/provider_importer.rb +7 -4
- data/lib/qme/map/map_reduce_builder.rb +1 -1
- data/lib/qme/map/map_reduce_executor.rb +29 -25
- data/lib/qme/randomizer/patient_randomization_job.rb +5 -6
- data/lib/qme/randomizer/random_patient_creator.rb +47 -0
- data/lib/quality-measure-engine.rb +5 -8
- data/lib/tasks/patient_random.rake +3 -3
- metadata +99 -107
- data/lib/qme/ext/string.rb +0 -5
- data/lib/qme/importer/code_system_helper.rb +0 -41
- data/lib/qme/importer/hl7_helper.rb +0 -27
- data/lib/qme/importer/patient_importer.rb +0 -228
- data/lib/qme/importer/section_importer.rb +0 -138
data/Gemfile
CHANGED
@@ -5,11 +5,12 @@ gemspec :development_group => :test
|
|
5
5
|
gem 'mongo', '1.5.1'
|
6
6
|
gem 'bson_ext', '1.5.1', :platforms => :mri
|
7
7
|
gem 'rake'
|
8
|
-
gem 'pry', :require => true
|
8
|
+
#gem 'pry', :require => true
|
9
|
+
#gem 'health-data-standards', :git => 'https://github.com/projectcypress/health-data-standards.git', :branch => 'master'
|
10
|
+
gem 'health-data-standards', '0.7.0'
|
9
11
|
|
10
12
|
group :test do
|
11
13
|
gem 'cover_me', '>= 1.0.0.rc5', :platforms => :ruby_19
|
12
|
-
gem 'metric_fu'
|
13
14
|
gem 'sinatra'
|
14
15
|
end
|
15
16
|
|
data/Rakefile
CHANGED
@@ -1,6 +1,5 @@
|
|
1
1
|
require 'rspec/core/rake_task'
|
2
2
|
require 'yard'
|
3
|
-
require 'metric_fu'
|
4
3
|
require 'resque/tasks'
|
5
4
|
|
6
5
|
ENV['MEASURE_DIR'] = ENV['MEASURE_DIR'] || File.join('fixtures', 'measure_defs')
|
@@ -29,11 +28,4 @@ end
|
|
29
28
|
task :coverage do
|
30
29
|
Rake::Task['spec'].invoke
|
31
30
|
Rake::Task['cover_me:report'].invoke
|
32
|
-
end
|
33
|
-
|
34
|
-
MetricFu::Configuration.run do |config|
|
35
|
-
#define which metrics you want to use
|
36
|
-
config.metrics = [:roodi, :reek, :churn, :flog, :flay]
|
37
|
-
config.graphs = [:flog, :flay]
|
38
|
-
config.flay ={:dirs_to_flay => []} #Flay doesn't seem to be handling CLI arguments well... so this config squashes them
|
39
31
|
end
|
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
1.0
|
1
|
+
1.1.0
|
data/js/map_reduce_utils.js
CHANGED
@@ -120,7 +120,7 @@ function() {
|
|
120
120
|
root.map = function(record, population, denominator, numerator, exclusion) {
|
121
121
|
var value = {population: false, denominator: false, numerator: false,
|
122
122
|
exclusions: false, antinumerator: false, patient_id: record._id,
|
123
|
-
medical_record_id: record.
|
123
|
+
medical_record_id: record.medical_record_number,
|
124
124
|
first: record.first, last: record.last, gender: record.gender,
|
125
125
|
birthdate: record.birthdate, test_id: record.test_id,
|
126
126
|
provider_performances: record.provider_performances,
|
data/lib/qme/database_access.rb
CHANGED
@@ -0,0 +1,49 @@
|
|
1
|
+
# Extensions to the Record model in health-data-standards to support
|
2
|
+
# quality measure calculation
|
3
|
+
class Record
|
4
|
+
extend ActiveSupport::Memoizable
|
5
|
+
|
6
|
+
def procedure_results
|
7
|
+
results.to_a + vital_signs.to_a + procedures.to_a
|
8
|
+
end
|
9
|
+
|
10
|
+
def laboratory_tests
|
11
|
+
results.to_a + vital_signs.to_a
|
12
|
+
end
|
13
|
+
|
14
|
+
def all_meds
|
15
|
+
medications.to_a + immunizations.to_a
|
16
|
+
end
|
17
|
+
|
18
|
+
def active_diagnosis
|
19
|
+
conditions.any_of({:status => 'active'}, {:status => nil}).to_a +
|
20
|
+
social_history.any_of({:status => 'active'}, {:status => nil}).to_a
|
21
|
+
end
|
22
|
+
|
23
|
+
def inactive_diagnosis
|
24
|
+
conditions.any_of({:status => 'inactive'}, {:status => nil}).to_a +
|
25
|
+
social_history.any_of({:status => 'inactive'}, {:status => nil}).to_a
|
26
|
+
end
|
27
|
+
|
28
|
+
def resolved_diagnosis
|
29
|
+
conditions.any_of({:status => 'resolved'}, {:status => nil}).to_a +
|
30
|
+
social_history.any_of({:status => 'resolved'}, {:status => nil}).to_a
|
31
|
+
end
|
32
|
+
|
33
|
+
def all_problems
|
34
|
+
conditions.to_a + social_history.to_a
|
35
|
+
end
|
36
|
+
|
37
|
+
def all_devices
|
38
|
+
conditions.to_a + procedures.to_a + care_goals.to_a + medical_equipment.to_a
|
39
|
+
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
|
+
end
|
data/lib/qme/importer/entry.rb
CHANGED
@@ -91,7 +91,7 @@ module QME
|
|
91
91
|
# Creates a Hash for this Entry
|
92
92
|
# @return [Hash] a Hash representing the Entry
|
93
93
|
def to_hash
|
94
|
-
entry_hash = {}
|
94
|
+
entry_hash = {'_id' => BSON::ObjectId.new}
|
95
95
|
entry_hash['codes'] = @codes
|
96
96
|
unless @value.empty?
|
97
97
|
entry_hash['value'] = @value
|
@@ -23,13 +23,12 @@ module QME
|
|
23
23
|
#
|
24
24
|
# @param [Hash] patient_hash representation of a patient
|
25
25
|
# @return [Hash] measure information
|
26
|
-
def parse(
|
26
|
+
def parse(patient)
|
27
27
|
measure_info = {}
|
28
28
|
@definition['measure'].each_pair do |property, description|
|
29
29
|
raise "No standard_category for #{property}" if !description['standard_category']
|
30
30
|
matcher = PropertyMatcher.new(description)
|
31
|
-
|
32
|
-
entry_list = enrty_filter.call(patient_hash)
|
31
|
+
entry_list = filter_for_property(description['standard_category'], description['qds_data_type'], patient)
|
33
32
|
if ! entry_list.empty?
|
34
33
|
matched_list = matcher.match(entry_list)
|
35
34
|
measure_info[property] = matched_list if matched_list.length > 0
|
@@ -40,89 +39,67 @@ module QME
|
|
40
39
|
|
41
40
|
private
|
42
41
|
|
43
|
-
def
|
44
|
-
Proc.new do |patient_hash|
|
45
|
-
sections.map do |section|
|
46
|
-
if patient_hash[section]
|
47
|
-
patient_hash[section]
|
48
|
-
else
|
49
|
-
[]
|
50
|
-
end
|
51
|
-
end.flatten
|
52
|
-
end
|
53
|
-
end
|
54
|
-
|
55
|
-
def create_section_and_status_filter(status, *sections)
|
56
|
-
section_filter = create_section_filter(*sections)
|
57
|
-
Proc.new do |patient_hash|
|
58
|
-
entry_list = section_filter.call(patient_hash)
|
59
|
-
entry_list.select do |entry|
|
60
|
-
entry.status.nil? || entry.status == status
|
61
|
-
end
|
62
|
-
end
|
63
|
-
end
|
64
|
-
|
65
|
-
def filter_for_property(standard_category, qds_data_type)
|
42
|
+
def filter_for_property(standard_category, qds_data_type, patient)
|
66
43
|
# Currently unsupported categories: negation_rationale, risk_category_assessment
|
67
44
|
case standard_category
|
68
45
|
when 'encounter'
|
69
|
-
|
46
|
+
patient.encounters
|
70
47
|
when 'procedure'
|
71
48
|
case qds_data_type
|
72
49
|
when 'procedure_performed'
|
73
|
-
|
50
|
+
patient.procedures
|
74
51
|
when 'procedure_adverse_event', 'procedure_intolerance'
|
75
|
-
|
52
|
+
patient.allergies
|
76
53
|
when 'procedure_result'
|
77
|
-
|
54
|
+
patient.procedure_results
|
78
55
|
end
|
79
56
|
when 'risk_category_assessment'
|
80
|
-
|
57
|
+
patient.procedures
|
81
58
|
when 'communication'
|
82
|
-
|
59
|
+
patient.procedures
|
83
60
|
when 'laboratory_test'
|
84
|
-
|
61
|
+
patient.laboratory_tests
|
85
62
|
when 'physical_exam'
|
86
|
-
|
63
|
+
patient.vital_signs
|
87
64
|
when 'medication'
|
88
65
|
case qds_data_type
|
89
66
|
when 'medication_dispensed', 'medication_order', 'medication_active', 'medication_administered'
|
90
|
-
|
67
|
+
patient.all_meds
|
91
68
|
when 'medication_allergy', 'medication_intolerance', 'medication_adverse_event'
|
92
|
-
|
69
|
+
patient.allergies
|
93
70
|
end
|
94
71
|
when 'diagnosis_condition_problem'
|
95
72
|
case qds_data_type
|
96
73
|
when 'diagnosis_active'
|
97
|
-
|
74
|
+
patient.active_diagnosis
|
98
75
|
when 'diagnosis_inactive'
|
99
|
-
|
76
|
+
patient.inactive_diagnosis
|
100
77
|
when 'diagnosis_resolved'
|
101
|
-
|
78
|
+
patient.resolved_diagnosis
|
102
79
|
end
|
103
80
|
when 'symptom'
|
104
|
-
|
81
|
+
patient.all_problems
|
105
82
|
when 'individual_characteristic'
|
106
|
-
|
83
|
+
patient.all_problems
|
107
84
|
when 'device'
|
108
85
|
case qds_data_type
|
109
86
|
when 'device_applied'
|
110
|
-
|
87
|
+
patient.all_devices
|
111
88
|
when 'device_allergy'
|
112
|
-
|
89
|
+
patient.allergies
|
113
90
|
end
|
114
91
|
when 'care_goal'
|
115
|
-
|
92
|
+
patient.care_goals
|
116
93
|
when 'diagnostic_study'
|
117
|
-
|
94
|
+
patient.procedures
|
118
95
|
when 'substance'
|
119
|
-
|
96
|
+
patient.allergies
|
120
97
|
else
|
121
98
|
unless self.class.warnings.include?(standard_category)
|
122
99
|
puts "Warning: Unsupported standard_category (#{standard_category})"
|
123
100
|
self.class.warnings << standard_category
|
124
101
|
end
|
125
|
-
|
102
|
+
[]
|
126
103
|
end
|
127
104
|
end
|
128
105
|
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
module QME
|
2
|
+
module Importer
|
3
|
+
class MeasurePropertiesGenerator
|
4
|
+
include Singleton
|
5
|
+
|
6
|
+
def initialize
|
7
|
+
@measure_importers = {}
|
8
|
+
end
|
9
|
+
|
10
|
+
# Adds a GenericImporter that will be used to extract
|
11
|
+
# information about a measure from a patient Record.
|
12
|
+
def add_measure(measure_id, importer)
|
13
|
+
@measure_importers[measure_id] = importer
|
14
|
+
end
|
15
|
+
|
16
|
+
# Generates denormalized measure information
|
17
|
+
# Measure information is contained in a Hash that hash
|
18
|
+
# the measure id as a key, and the denormalized
|
19
|
+
# measure information as a value
|
20
|
+
#
|
21
|
+
# @param [Record] patient - populated patient record
|
22
|
+
# @returns Hash with denormalized measure information
|
23
|
+
def generate_properties(patient)
|
24
|
+
measures = {}
|
25
|
+
@measure_importers.each_pair do |measure_id, importer|
|
26
|
+
measures[measure_id] = importer.parse(patient)
|
27
|
+
end
|
28
|
+
measures
|
29
|
+
end
|
30
|
+
|
31
|
+
# The same as generate_properties but addes the denormalized
|
32
|
+
# measure information into the Record and saves it.
|
33
|
+
def generate_properties!(patient)
|
34
|
+
patient['measures'] = generate_properties(patient)
|
35
|
+
patient.save!
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -11,16 +11,19 @@ module QME
|
|
11
11
|
# @param [Nokogiri::XML::Document] doc It is expected that the root node of this document
|
12
12
|
# will have the "cda" namespace registered to "urn:hl7-org:v3"
|
13
13
|
# @return [Array] an array of providers found in the document
|
14
|
-
def extract_providers(doc)
|
15
|
-
|
16
|
-
performers = doc.xpath("//cda:documentationOf/cda:serviceEvent/cda:performer")
|
14
|
+
def extract_providers(doc, use_encounters=false)
|
17
15
|
|
16
|
+
|
17
|
+
xpath_base = use_encounters ? "//cda:encounter/cda:performer" : "//cda:documentationOf/cda:serviceEvent/cda:performer"
|
18
|
+
|
19
|
+
performers = doc.xpath(xpath_base)
|
20
|
+
|
18
21
|
providers = performers.map do |performer|
|
19
22
|
provider = {}
|
20
23
|
entity = performer.xpath(performer, "./cda:assignedEntity")
|
21
24
|
name = entity.xpath("./cda:assignedPerson/cda:name")
|
22
25
|
provider[:title] = extract_data(name, "./cda:prefix")
|
23
|
-
provider[:given_name] = extract_data(name, "./cda:given")
|
26
|
+
provider[:given_name] = extract_data(name, "./cda:given[1]")
|
24
27
|
provider[:family_name] = extract_data(name, "./cda:family")
|
25
28
|
provider[:phone] = extract_data(entity, "./cda:telecom/@value") { |text| text.gsub("tel:", "") }
|
26
29
|
provider[:organization] = extract_data(entity, "./cda:representedOrganization/cda:name")
|
@@ -97,7 +97,7 @@ module QME
|
|
97
97
|
var tmp = [];
|
98
98
|
for(var i=0; i<patient.provider_performances.length; i++) {
|
99
99
|
var value = patient.provider_performances[i];
|
100
|
-
if (value['start_date'] <= #{@params['effective_date']} && (value['end_date'] >= #{@params['effective_date']} || value['end_date'] == null))
|
100
|
+
if ((value['start_date'] <= #{@params['effective_date']} || value['start_date'] == null) && (value['end_date'] >= #{@params['effective_date']} || value['end_date'] == null))
|
101
101
|
tmp.push(value);
|
102
102
|
}
|
103
103
|
if (tmp.length == 0) tmp = null;
|
@@ -32,36 +32,24 @@ module QME
|
|
32
32
|
base_query.merge!(filter_parameters)
|
33
33
|
|
34
34
|
query = base_query.clone
|
35
|
-
|
36
|
-
query.merge!({'value.manual_exclusion'=>{'$
|
35
|
+
|
36
|
+
query.merge!({'value.manual_exclusion' => {'$in' => [nil, false]}})
|
37
37
|
|
38
38
|
result = {:measure_id => @measure_id, :sub_id => @sub_id,
|
39
39
|
:effective_date => @parameter_values['effective_date'],
|
40
40
|
:test_id => @parameter_values['test_id'], :filters => @parameter_values['filters']}
|
41
41
|
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
aggregate
|
51
|
-
aggregate.each {|key, value| aggregate[key] = value.to_i}
|
52
|
-
aggregate['exclusions'] += patient_cache.find(base_query.merge({'value.manual_exclusion'=>true})).count
|
42
|
+
# need to time the old way agains the single query to verify that the single query is more performant
|
43
|
+
aggregate = {"population"=>0, "denominator"=>0, "numerator"=>0, "antinumerator"=>0, "exclusions"=>0}
|
44
|
+
%w(population denominator numerator antinumerator exclusions).each do |measure_group|
|
45
|
+
patient_cache.find(query.merge("value.#{measure_group}" => true)) do |cursor|
|
46
|
+
aggregate[measure_group] = cursor.count
|
47
|
+
end
|
48
|
+
end
|
49
|
+
aggregate["considered"] = patient_cache.find(query).count
|
50
|
+
aggregate["exclusions"] += patient_cache.find(base_query.merge({'value.manual_exclusion'=>true})).count
|
53
51
|
result.merge!(aggregate)
|
54
52
|
|
55
|
-
# # need to time the old way agains the single query to verify that the single query is more performant
|
56
|
-
# aggregate = {population: 0, denominator: 0, numerator: 0, antinumerator: 0, exclusions: 0}
|
57
|
-
# %w(population denominator numerator antinumerator exclusions).each do |measure_group|
|
58
|
-
# patient_cache.find(query.merge("value.#{measure_group}" => true)) do |cursor|
|
59
|
-
# aggregate[measure_group] = cursor.count
|
60
|
-
# end
|
61
|
-
# end
|
62
|
-
# aggregate[:considered] = patient_cache.find(query).count
|
63
|
-
# result.merge!(aggregate)
|
64
|
-
|
65
53
|
result.merge!(execution_time: (Time.now.to_i - @parameter_values['start_time'].to_i)) if @parameter_values['start_time']
|
66
54
|
|
67
55
|
get_db.collection("query_cache").save(result, safe: true)
|
@@ -92,11 +80,27 @@ module QME
|
|
92
80
|
records.map_reduce(measure.map_function, "function(key, values){return values;}",
|
93
81
|
:out => {:reduce => 'patient_cache'},
|
94
82
|
:finalize => measure.finalize_function,
|
95
|
-
:query => {:
|
83
|
+
:query => {:medical_record_number => patient_id, :test_id => @parameter_values['test_id']})
|
96
84
|
apply_manual_exclusions
|
97
85
|
end
|
98
86
|
|
99
|
-
# This
|
87
|
+
# This method runs the MapReduce job for the measure and a specific patient.
|
88
|
+
# This will *not* create a document in the patient_cache collection, instead the
|
89
|
+
# result is returned directly.
|
90
|
+
def get_patient_result(patient_id)
|
91
|
+
qm = QualityMeasure.new(@measure_id, @sub_id)
|
92
|
+
measure = Builder.new(get_db, qm.definition, @parameter_values)
|
93
|
+
records = get_db.collection('records')
|
94
|
+
result = records.map_reduce(measure.map_function, "function(key, values){return values;}",
|
95
|
+
:out => {:inline => true},
|
96
|
+
:raw => true,
|
97
|
+
:finalize => measure.finalize_function,
|
98
|
+
:query => {:medical_record_number => patient_id, :test_id => @parameter_values['test_id']})
|
99
|
+
raise result['err'] if result['ok']!=1
|
100
|
+
result['results'][0]['value']
|
101
|
+
end
|
102
|
+
|
103
|
+
# This collects the set of manual exclusions from the manual_exclusions collections
|
100
104
|
# and sets a flag in each cached patient result for patients that have been excluded from the
|
101
105
|
# current measure
|
102
106
|
def apply_manual_exclusions
|
@@ -24,21 +24,20 @@ module QME
|
|
24
24
|
QME::QualityMeasure.all.each_value do |measure_def|
|
25
25
|
measure_id = measure_def['id']
|
26
26
|
if !processed_measures[measure_id]
|
27
|
-
QME::Importer::
|
28
|
-
processed_measures[measure_id]=true
|
27
|
+
QME::Importer::MeasurePropertiesGenerator.instance.add_measure(measure_id, QME::Importer::GenericImporter.new(measure_def))
|
28
|
+
processed_measures[measure_id] = true
|
29
29
|
end
|
30
30
|
end
|
31
31
|
|
32
|
-
loader = QME::Database::Loader.new()
|
33
32
|
tick('Generating patients')
|
34
33
|
count.times do |i|
|
35
34
|
at(i, count, "Generating patient #{i} of #{count}")
|
36
35
|
template = templates[rand(templates.length)]
|
37
36
|
generator = QME::Randomizer::Patient.new(template)
|
38
37
|
json = JSON.parse(generator.get())
|
39
|
-
|
40
|
-
|
41
|
-
|
38
|
+
patient_record = RandomPatientCreator.parse_hash(json)
|
39
|
+
patient_record.test_id = test_id
|
40
|
+
patient_record.save!
|
42
41
|
end
|
43
42
|
|
44
43
|
completed
|
@@ -0,0 +1,47 @@
|
|
1
|
+
module QME
|
2
|
+
module Randomizer
|
3
|
+
class RandomPatientCreator
|
4
|
+
|
5
|
+
# Parses a patient hash containing demographic and event information
|
6
|
+
#
|
7
|
+
# @param [Hash] patient_hash patient data
|
8
|
+
# @return [Record] a representation of the patient that can be inserted into MongoDB
|
9
|
+
def self.parse_hash(patient_hash)
|
10
|
+
patient_record = Record.new
|
11
|
+
patient_record.first = patient_hash['first']
|
12
|
+
patient_record.last = patient_hash['last']
|
13
|
+
patient_record.gender = patient_hash['gender']
|
14
|
+
patient_record.medical_record_number = patient_hash['medical_record_number']
|
15
|
+
patient_record.birthdate = patient_hash['birthdate']
|
16
|
+
patient_record.race = patient_hash['race']
|
17
|
+
patient_record.ethnicity = patient_hash['ethnicity']
|
18
|
+
# pophealth needs languages... please do not remove
|
19
|
+
patient_record.languages = patient_hash['languages']
|
20
|
+
#patient_record['addresses'] = patient_hash['addresses']
|
21
|
+
patient_hash['events'].each do |key, value|
|
22
|
+
patient_record.send("#{key}=".to_sym, parse_events(value))
|
23
|
+
end
|
24
|
+
patient_record['measures'] = QME::Importer::MeasurePropertiesGenerator.instance.generate_properties(patient_record)
|
25
|
+
|
26
|
+
patient_record
|
27
|
+
end
|
28
|
+
|
29
|
+
# Parses a list of event hashes into an array of Entry objects
|
30
|
+
#
|
31
|
+
# @param [Array] event_list list of event hashes
|
32
|
+
# @return [Array] array of Entry objects
|
33
|
+
def self.parse_events(event_list)
|
34
|
+
event_list.collect do |event|
|
35
|
+
if event.class==String.class
|
36
|
+
# skip String elements in the event list, patient randomization templates
|
37
|
+
# introduce String elements to simplify tailing-comma handling when generating
|
38
|
+
# JSON using ERb
|
39
|
+
nil
|
40
|
+
else
|
41
|
+
Entry.from_event_hash(event)
|
42
|
+
end
|
43
|
+
end.compact
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|