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.
- data/README.md +34 -6
- data/Rakefile +2 -0
- data/VERSION +1 -1
- data/js/{map-reduce-utils.js → map_reduce_utils.js} +50 -85
- data/js/{underscore-min.js → underscore_min.js} +3 -2
- data/lib/qme/database_access.rb +30 -0
- data/lib/qme/importer/code_system_helper.rb +3 -1
- data/lib/qme/importer/entry.rb +12 -22
- data/lib/qme/importer/generic_importer.rb +89 -30
- data/lib/qme/importer/patient_importer.rb +37 -21
- data/lib/qme/importer/property_matcher.rb +7 -2
- data/lib/qme/importer/section_importer.rb +31 -9
- data/lib/qme/map/map_reduce_builder.rb +14 -12
- data/lib/qme/map/map_reduce_executor.rb +34 -98
- data/lib/qme/map/measure_calculation_job.rb +33 -0
- data/lib/qme/measure/database_loader.rb +18 -25
- data/lib/qme/measure/measure_loader.rb +70 -37
- data/lib/qme/measure/properties_builder.rb +184 -0
- data/lib/qme/measure/properties_converter.rb +27 -0
- data/lib/qme/quality_measure.rb +41 -0
- data/lib/qme/quality_report.rb +61 -0
- data/lib/quality-measure-engine.rb +19 -16
- data/lib/tasks/fixtures.rake +91 -0
- data/lib/tasks/measure.rake +55 -36
- data/lib/tasks/mongo.rake +13 -19
- data/lib/tasks/patient_random.rake +2 -3
- metadata +174 -102
- data/lib/qme/mongo_helpers.rb +0 -15
@@ -5,65 +5,124 @@ module QME
|
|
5
5
|
# it can then be passed a C32 document and will return a Hash with all of the information needed to calculate the measure.
|
6
6
|
class GenericImporter
|
7
7
|
|
8
|
-
|
8
|
+
class << self
|
9
|
+
attr_accessor :warnings
|
10
|
+
end
|
11
|
+
|
12
|
+
@warnings = []
|
9
13
|
|
10
14
|
# Creates a generic importer for any quality measure.
|
11
15
|
#
|
12
16
|
# @param [Hash] definition A measure definition described in JSON
|
13
17
|
def initialize(definition)
|
14
18
|
@definition = definition
|
19
|
+
@warnings = []
|
15
20
|
end
|
16
|
-
|
21
|
+
|
17
22
|
# Parses a HITSP C32 document and returns a Hash of information related to the measure
|
18
23
|
#
|
19
24
|
# @param [Hash] patient_hash representation of a patient
|
20
25
|
# @return [Hash] measure information
|
21
26
|
def parse(patient_hash)
|
22
27
|
measure_info = {}
|
23
|
-
|
24
28
|
@definition['measure'].each_pair do |property, description|
|
25
29
|
raise "No standard_category for #{property}" if !description['standard_category']
|
26
30
|
matcher = PropertyMatcher.new(description)
|
27
|
-
|
31
|
+
enrty_filter = filter_for_property(description['standard_category'], description['qds_data_type'])
|
32
|
+
entry_list = enrty_filter.call(patient_hash)
|
33
|
+
if ! entry_list.empty?
|
34
|
+
matched_list = matcher.match(entry_list)
|
35
|
+
measure_info[property] = matched_list if matched_list.length > 0
|
36
|
+
end
|
37
|
+
end
|
38
|
+
measure_info
|
39
|
+
end
|
40
|
+
|
41
|
+
private
|
42
|
+
|
43
|
+
def create_section_filter(*sections)
|
44
|
+
Proc.new do |patient_hash|
|
45
|
+
sections.map do |section|
|
28
46
|
if patient_hash[section]
|
29
47
|
patient_hash[section]
|
30
48
|
else
|
31
49
|
[]
|
32
50
|
end
|
33
51
|
end.flatten
|
34
|
-
|
35
|
-
|
36
|
-
|
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
|
37
61
|
end
|
38
62
|
end
|
39
|
-
|
40
|
-
measure_info
|
41
63
|
end
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
def symbols_for_category(standard_category)
|
46
|
-
# Currently unsupported categories:
|
47
|
-
# characteristic, substance_allergy, medication_allergy, negation_rationale,
|
48
|
-
# diagnostic_study
|
64
|
+
|
65
|
+
def filter_for_property(standard_category, qds_data_type)
|
66
|
+
# Currently unsupported categories: negation_rationale, risk_category_assessment
|
49
67
|
case standard_category
|
50
|
-
when 'encounter'
|
51
|
-
|
52
|
-
when '
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
68
|
+
when 'encounter'
|
69
|
+
create_section_filter(:encounters)
|
70
|
+
when 'procedure'
|
71
|
+
case qds_data_type
|
72
|
+
when 'procedure_performed'
|
73
|
+
create_section_filter(:procedures)
|
74
|
+
when 'procedure_adverse_event', 'procedure_intolerance'
|
75
|
+
create_section_filter(:allergies)
|
76
|
+
when 'procedure_result'
|
77
|
+
create_section_filter(:procedures, :results, :vital_signs)
|
78
|
+
end
|
79
|
+
when 'risk_category_assessment'
|
80
|
+
create_section_filter(:procedures)
|
81
|
+
when 'communication'
|
82
|
+
create_section_filter(:procedures)
|
83
|
+
when 'laboratory_test'
|
84
|
+
create_section_filter(:results, :vital_signs)
|
85
|
+
when 'physical_exam'
|
86
|
+
create_section_filter(:vital_signs)
|
87
|
+
when 'medication'
|
88
|
+
case qds_data_type
|
89
|
+
when 'medication_dispensed', 'medication_order', 'medication_active', 'medication_administered'
|
90
|
+
create_section_filter(:medications, :immunizations)
|
91
|
+
when 'medication_allergy', 'medication_intolerance', 'medication_adverse_event'
|
92
|
+
create_section_filter(:allergies)
|
93
|
+
end
|
94
|
+
when 'diagnosis_condition_problem'
|
95
|
+
case qds_data_type
|
96
|
+
when 'diagnosis_active'
|
97
|
+
create_section_and_status_filter(:active, :conditions, :social_history)
|
98
|
+
when 'diagnosis_inactive'
|
99
|
+
create_section_and_status_filter(:inactive, :conditions, :social_history)
|
100
|
+
when 'diagnosis_resolved'
|
101
|
+
create_section_and_status_filter(:resolved, :conditions, :social_history)
|
102
|
+
end
|
103
|
+
when 'symptom'
|
104
|
+
create_section_filter(:conditions, :social_history)
|
105
|
+
when 'individual_characteristic'
|
106
|
+
create_section_filter(:conditions, :social_history)
|
107
|
+
when 'device'
|
108
|
+
case qds_data_type
|
109
|
+
when 'device_applied'
|
110
|
+
create_section_filter(:conditions, :procedures, :care_goals, :medical_equipment)
|
111
|
+
when 'device_allergy'
|
112
|
+
create_section_filter(:allergies)
|
113
|
+
end
|
114
|
+
when 'care_goal'
|
115
|
+
create_section_filter(:care_goals)
|
116
|
+
when 'diagnostic_study'
|
117
|
+
create_section_filter(:procedures)
|
118
|
+
when 'substance'
|
119
|
+
create_section_filter(:allergies)
|
61
120
|
else
|
62
|
-
|
121
|
+
unless self.class.warnings.include?(standard_category)
|
63
122
|
puts "Warning: Unsupported standard_category (#{standard_category})"
|
64
|
-
|
123
|
+
self.class.warnings << standard_category
|
65
124
|
end
|
66
|
-
[]
|
125
|
+
Proc.new {[]} # A proc that returns an empty array
|
67
126
|
end
|
68
127
|
end
|
69
128
|
end
|
@@ -1,14 +1,15 @@
|
|
1
1
|
module QME
|
2
2
|
module Importer
|
3
|
-
|
3
|
+
|
4
4
|
# This class is the central location for taking a HITSP C32 XML document and converting it
|
5
5
|
# into the processed form we store in MongoDB. The class does this by running each measure
|
6
6
|
# independently on the XML document
|
7
7
|
#
|
8
8
|
# This class is a Singleton. It should be accessed by calling PatientImporter.instance
|
9
9
|
class PatientImporter
|
10
|
+
|
10
11
|
include Singleton
|
11
|
-
|
12
|
+
|
12
13
|
# Creates a new PatientImporter with the following XPath expressions used to find content in
|
13
14
|
# a HITSP C32:
|
14
15
|
#
|
@@ -18,8 +19,8 @@ module QME
|
|
18
19
|
# Procedure entries
|
19
20
|
# //cda:procedure[cda:templateId/@root='2.16.840.1.113883.10.20.1.29']
|
20
21
|
#
|
21
|
-
# Result entries
|
22
|
-
# //cda:observation[cda:templateId/@root='2.16.840.1.113883.3.88.11.83.15']
|
22
|
+
# Result entries - There seems to be some confusion around the correct templateId, so the code checks for both
|
23
|
+
# //cda:observation[cda:templateId/@root='2.16.840.1.113883.3.88.11.83.15.1'] | //cda:observation[cda:templateId/@root='2.16.840.1.113883.3.88.11.83.15']
|
23
24
|
#
|
24
25
|
# Vital sign entries
|
25
26
|
# //cda:observation[cda:templateId/@root='2.16.840.1.113883.3.88.11.83.14']
|
@@ -33,33 +34,44 @@ module QME
|
|
33
34
|
# Condition entries
|
34
35
|
# //cda:section[cda:templateId/@root='2.16.840.1.113883.3.88.11.83.103']/cda:entry/cda:act/cda:entryRelationship/cda:observation
|
35
36
|
#
|
37
|
+
# Codes for conditions are determined by examining the value child element as opposed to the code child element
|
38
|
+
#
|
36
39
|
# Social History entries (non-C32 section, specified in the HL7 CCD)
|
37
40
|
# //cda:observation[cda:templateId/@root='2.16.840.1.113883.3.88.11.83.19']
|
38
41
|
#
|
39
42
|
# Care Goal entries(non-C32 section, specified in the HL7 CCD)
|
40
43
|
# //cda:observation[cda:templateId/@root='2.16.840.1.113883.10.20.1.25']
|
41
44
|
#
|
42
|
-
#
|
45
|
+
# Allergy entries
|
46
|
+
# //cda:observation[cda:templateId/@root='2.16.840.1.113883.10.20.1.18']
|
47
|
+
#
|
48
|
+
# Immunization entries
|
49
|
+
# //cda:substanceAdministration[cda:templateId/@root='2.16.840.1.113883.10.20.1.24']
|
50
|
+
#
|
51
|
+
# Codes for immunizations are found in the substanceAdministration with the following relative XPath
|
52
|
+
# ./cda:consumable/cda:manufacturedProduct/cda:manufacturedMaterial/cda:code
|
43
53
|
def initialize
|
44
54
|
@measure_importers = {}
|
45
|
-
|
46
55
|
@section_importers = {}
|
47
56
|
@section_importers[:encounters] = SectionImporter.new("//cda:section[cda:templateId/@root='2.16.840.1.113883.3.88.11.83.127']/cda:entry/cda:encounter")
|
48
57
|
@section_importers[:procedures] = SectionImporter.new("//cda:procedure[cda:templateId/@root='2.16.840.1.113883.10.20.1.29']")
|
49
|
-
@section_importers[:results] = SectionImporter.new("//cda:observation[cda:templateId/@root='2.16.840.1.113883.3.88.11.83.15']")
|
58
|
+
@section_importers[:results] = SectionImporter.new("//cda:observation[cda:templateId/@root='2.16.840.1.113883.3.88.11.83.15.1'] | //cda:observation[cda:templateId/@root='2.16.840.1.113883.3.88.11.83.15']")
|
50
59
|
@section_importers[:vital_signs] = SectionImporter.new("//cda:observation[cda:templateId/@root='2.16.840.1.113883.3.88.11.83.14']")
|
51
60
|
@section_importers[:medications] = SectionImporter.new("//cda:section[cda:templateId/@root='2.16.840.1.113883.3.88.11.83.112']/cda:entry/cda:substanceAdministration",
|
52
61
|
"./cda:consumable/cda:manufacturedProduct/cda:manufacturedMaterial/cda:code")
|
53
|
-
|
54
62
|
@section_importers[:conditions] = SectionImporter.new("//cda:section[cda:templateId/@root='2.16.840.1.113883.3.88.11.83.103']/cda:entry/cda:act/cda:entryRelationship/cda:observation",
|
55
|
-
"./cda:value"
|
56
|
-
|
63
|
+
"./cda:value",
|
64
|
+
"./cda:entryRelationship/cda:observation[cda:templateId/@root='2.16.840.1.1 13883.10.20.1.50']/cda:value")
|
57
65
|
@section_importers[:social_history] = SectionImporter.new("//cda:observation[cda:templateId/@root='2.16.840.1.113883.3.88.11.83.19']")
|
58
66
|
@section_importers[:care_goals] = SectionImporter.new("//cda:observation[cda:templateId/@root='2.16.840.1.113883.10.20.1.25']")
|
59
67
|
@section_importers[:medical_equipment] = SectionImporter.new("//cda:section[cda:templateId/@root='2.16.840.1.113883.3.88.11.83.128']/cda:entry/cda:supply",
|
60
68
|
"./cda:participant/cda:participantRole/cda:playingDevice/cda:code")
|
69
|
+
@section_importers[:allergies] = SectionImporter.new("//cda:observation[cda:templateId/@root='2.16.840.1.113883.10.20.1.18']",
|
70
|
+
"./cda:participant/cda:participantRole/cda:playingEntity/cda:code")
|
71
|
+
@section_importers[:immunizations] = SectionImporter.new("//cda:substanceAdministration[cda:templateId/@root='2.16.840.1.113883.10.20.1.24']",
|
72
|
+
"./cda:consumable/cda:manufacturedProduct/cda:manufacturedMaterial/cda:code")
|
61
73
|
end
|
62
|
-
|
74
|
+
|
63
75
|
# Parses a HITSP C32 document and returns a Hash of of the patient.
|
64
76
|
#
|
65
77
|
# @param [Nokogiri::XML::Document] doc It is expected that the root node of this document
|
@@ -71,7 +83,7 @@ module QME
|
|
71
83
|
get_demographics(patient_record, doc)
|
72
84
|
process_events(patient_record, c32_patient)
|
73
85
|
end
|
74
|
-
|
86
|
+
|
75
87
|
# Parses a patient hash containing demongraphic and event information
|
76
88
|
#
|
77
89
|
# @param [Hash] patient_hash patient data
|
@@ -88,27 +100,32 @@ module QME
|
|
88
100
|
end
|
89
101
|
process_events(patient_record, event_hash)
|
90
102
|
end
|
91
|
-
|
103
|
+
|
92
104
|
def process_events(patient_record, event_hash)
|
93
105
|
patient_record['measures'] = {}
|
94
106
|
@measure_importers.each_pair do |measure_id, importer|
|
95
107
|
patient_record['measures'][measure_id] = importer.parse(event_hash)
|
96
108
|
end
|
97
|
-
|
98
109
|
patient_record
|
99
110
|
end
|
100
|
-
|
111
|
+
|
101
112
|
# Parses a list of event hashes into an array of Entry objects
|
102
113
|
#
|
103
114
|
# @param [Array] event_list list of event hashes
|
104
115
|
# @return [Array] array of Entry objects
|
105
116
|
def parse_events(event_list)
|
106
117
|
event_list.collect do |event|
|
107
|
-
|
108
|
-
|
118
|
+
if event.class==String.class
|
119
|
+
# skip String elements in the event list, patient randomization templates
|
120
|
+
# introduce String elements to simplify tailing-comma handling when generating
|
121
|
+
# JSON using ERb
|
122
|
+
nil
|
123
|
+
else
|
124
|
+
QME::Importer::Entry.from_event_hash(event)
|
125
|
+
end
|
109
126
|
end.compact
|
110
127
|
end
|
111
|
-
|
128
|
+
|
112
129
|
# Adds a measure to run on a C32 that is passed in
|
113
130
|
#
|
114
131
|
# @param [MeasureBase] measure an Class that can extract information from a C32 that is necessary
|
@@ -116,7 +133,7 @@ module QME
|
|
116
133
|
def add_measure(measure_id, importer)
|
117
134
|
@measure_importers[measure_id] = importer
|
118
135
|
end
|
119
|
-
|
136
|
+
|
120
137
|
# Create a simple representation of the patient from a HITSP C32
|
121
138
|
#
|
122
139
|
# @param [Nokogiri::XML::Document] doc It is expected that the root node of this document
|
@@ -127,10 +144,9 @@ module QME
|
|
127
144
|
@section_importers.each_pair do |section, importer|
|
128
145
|
c32_patient[section] = importer.create_entries(doc)
|
129
146
|
end
|
130
|
-
|
131
147
|
c32_patient
|
132
148
|
end
|
133
|
-
|
149
|
+
|
134
150
|
# Inspects a C32 document and populates the patient Hash with first name, last name
|
135
151
|
# birth date and gender.
|
136
152
|
#
|
@@ -42,8 +42,13 @@ module QME
|
|
42
42
|
# Hashes will have a "value" and "date" property containing the respective data
|
43
43
|
def extract_value_date_list(entry_list)
|
44
44
|
basic_extractor(entry_list) do |entry, matching_values|
|
45
|
-
|
46
|
-
|
45
|
+
value = entry.value[:scalar]
|
46
|
+
if value
|
47
|
+
if @property_description['items']['properties']['value']['type'] == 'number'
|
48
|
+
value = value.to_f
|
49
|
+
end
|
50
|
+
|
51
|
+
matching_values << {'date' => entry.as_point_in_time, 'value' => value}
|
47
52
|
end
|
48
53
|
end
|
49
54
|
end
|
@@ -8,11 +8,15 @@ module QME
|
|
8
8
|
# @param [String] entry_xpath An XPath expression that can be used to find the desired entries
|
9
9
|
# @param [String] code_xpath XPath expression to find the code element as a child of the desired CDA entry.
|
10
10
|
# Defaults to "./cda:code"
|
11
|
-
|
11
|
+
# @param [String] status_xpath XPath expression to find the status element as a child of the desired CDA
|
12
|
+
# entry. Defaults to nil. If not provided, a status will not be checked for since it is not applicable
|
13
|
+
# to all enrty types
|
14
|
+
def initialize(entry_xpath, code_xpath="./cda:code", status_xpath=nil)
|
12
15
|
@entry_xpath = entry_xpath
|
13
16
|
@code_xpath = code_xpath
|
17
|
+
@status_xpath = status_xpath
|
14
18
|
end
|
15
|
-
|
19
|
+
|
16
20
|
# Traverses that HITSP C32 document passed in using XPath and creates an Array of Entry
|
17
21
|
# objects based on what it finds
|
18
22
|
# @param [Nokogiri::XML::Document] doc It is expected that the root node of this document
|
@@ -27,34 +31,49 @@ module QME
|
|
27
31
|
extract_codes(entry_element, entry)
|
28
32
|
extract_dates(entry_element, entry)
|
29
33
|
extract_value(entry_element, entry)
|
34
|
+
if @status_xpath
|
35
|
+
extract_status(entry_element, entry)
|
36
|
+
end
|
30
37
|
if entry.usable?
|
31
38
|
entry_list << entry
|
32
39
|
end
|
33
40
|
end
|
34
|
-
|
35
41
|
entry_list
|
36
42
|
end
|
37
|
-
|
43
|
+
|
38
44
|
private
|
39
|
-
|
45
|
+
|
46
|
+
def extract_status(parent_element, entry)
|
47
|
+
status_element = parent_element.at_xpath(@status_xpath)
|
48
|
+
if status_element
|
49
|
+
case status_element['code']
|
50
|
+
when '55561003'
|
51
|
+
entry.status = :active
|
52
|
+
when '73425007'
|
53
|
+
entry.status = :inactive
|
54
|
+
when '413322009'
|
55
|
+
entry.status = :resolved
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
40
60
|
def extract_codes(parent_element, entry)
|
41
61
|
code_elements = parent_element.xpath(@code_xpath)
|
42
62
|
code_elements.each do |code_element|
|
43
63
|
add_code_if_present(code_element, entry)
|
44
|
-
|
45
64
|
translations = code_element.xpath('cda:translation')
|
46
65
|
translations.each do |translation|
|
47
66
|
add_code_if_present(translation, entry)
|
48
67
|
end
|
49
68
|
end
|
50
69
|
end
|
51
|
-
|
70
|
+
|
52
71
|
def add_code_if_present(code_element, entry)
|
53
72
|
if code_element['codeSystem'] && code_element['code']
|
54
73
|
entry.add_code(code_element['code'], CodeSystemHelper.code_system_for(code_element['codeSystem']))
|
55
74
|
end
|
56
75
|
end
|
57
|
-
|
76
|
+
|
58
77
|
def extract_dates(parent_element, entry)
|
59
78
|
if parent_element.at_xpath('cda:effectiveTime')
|
60
79
|
entry.time = HL7Helper.timestamp_to_integer(parent_element.at_xpath('cda:effectiveTime')['value'])
|
@@ -65,8 +84,11 @@ module QME
|
|
65
84
|
if parent_element.at_xpath('cda:effectiveTime/cda:high')
|
66
85
|
entry.end_time = HL7Helper.timestamp_to_integer(parent_element.at_xpath('cda:effectiveTime/cda:high')['value'])
|
67
86
|
end
|
87
|
+
if parent_element.at_xpath('cda:effectiveTime/cda:center')
|
88
|
+
entry.time = HL7Helper.timestamp_to_integer(parent_element.at_xpath('cda:effectiveTime/cda:center')['value'])
|
89
|
+
end
|
68
90
|
end
|
69
|
-
|
91
|
+
|
70
92
|
def extract_value(parent_element, entry)
|
71
93
|
value_element = parent_element.at_xpath('cda:value')
|
72
94
|
if value_element
|
@@ -31,18 +31,9 @@ module QME
|
|
31
31
|
result = ''
|
32
32
|
result << 'if (typeof(map)=="undefined") {'
|
33
33
|
result << "\n"
|
34
|
-
Dir.glob(File.join(File.dirname(__FILE__), '../../../js/*.js')).each do |js_file|
|
35
|
-
result << File.read(js_file)
|
36
|
-
result << "\n"
|
37
|
-
end
|
38
|
-
Dir.glob(File.join('./js/*.js')).each do |js_file|
|
39
|
-
result << File.read(js_file)
|
40
|
-
result << "\n"
|
41
|
-
end
|
42
34
|
@db['bundles'].find.each do |bundle|
|
43
35
|
(bundle['extensions'] || []).each do |ext|
|
44
|
-
result << ext
|
45
|
-
result << "\n"
|
36
|
+
result << "#{ext}();\n"
|
46
37
|
end
|
47
38
|
end
|
48
39
|
result << "}\n"
|
@@ -89,8 +80,19 @@ module QME
|
|
89
80
|
# wrapper for the reduce utility function specified in
|
90
81
|
# map-reduce-utils.js
|
91
82
|
# @return [String] the reduce function
|
92
|
-
def
|
93
|
-
|
83
|
+
def finalize_function
|
84
|
+
reduce =
|
85
|
+
"function (key, value) {
|
86
|
+
var patient = value;
|
87
|
+
patient.measure_id = \"#{@measure_def['id']}\";\n"
|
88
|
+
if @measure_def['sub_id']
|
89
|
+
reduce += " patient.sub_id = \"#{@measure_def['sub_id']}\";\n"
|
90
|
+
end
|
91
|
+
|
92
|
+
reduce += "patient.effective_date = #{@params['effective_date']};
|
93
|
+
return patient;}"
|
94
|
+
|
95
|
+
reduce
|
94
96
|
end
|
95
97
|
|
96
98
|
|
@@ -4,112 +4,48 @@ module QME
|
|
4
4
|
# Computes the value of quality measures based on the current set of patient
|
5
5
|
# records in the database
|
6
6
|
class Executor
|
7
|
-
|
8
|
-
|
9
|
-
def initialize(
|
10
|
-
@
|
7
|
+
include DatabaseAccess
|
8
|
+
|
9
|
+
def initialize(measure_id, sub_id, parameter_values)
|
10
|
+
@measure_id = measure_id
|
11
|
+
@sub_id = sub_id
|
12
|
+
@parameter_values = parameter_values
|
13
|
+
determine_connection_information
|
11
14
|
end
|
12
15
|
|
13
|
-
#
|
14
|
-
#
|
15
|
-
#
|
16
|
-
# @return [Hash]
|
17
|
-
def
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
16
|
+
# Examines the patient_cache collection and generates a total of all groups
|
17
|
+
# for the measure. The totals are placed in a document in the query_cache
|
18
|
+
# collection.
|
19
|
+
# @return [Hash] measure groups (like numerator) as keys, counts as values
|
20
|
+
def count_records_in_measure_groups
|
21
|
+
patient_cache = get_db.collection('patient_cache')
|
22
|
+
query = {'value.measure_id' => @measure_id, 'value.sub_id' => @sub_id,
|
23
|
+
'value.effective_date' => @parameter_values['effective_date']}
|
24
|
+
result = {:measure_id => @measure_id, :sub_id => @sub_id,
|
25
|
+
:effective_date => @parameter_values['effective_date']}
|
26
|
+
|
27
|
+
%w(population denominator numerator antinumerator exclusions).each do |measure_group|
|
28
|
+
patient_cache.find(query.merge("value.#{measure_group}" => true)) do |cursor|
|
29
|
+
result[measure_group] = cursor.count
|
30
|
+
end
|
23
31
|
end
|
24
|
-
end
|
25
|
-
|
26
|
-
# Compute the specified measure
|
27
|
-
# @param [String] measure_id value of the measure's id field
|
28
|
-
# @param [String] sub_id value of the measure's sub_id field, may be nil for measures with only a single numerator and denominator
|
29
|
-
# @param [Hash] parameter_values a hash of the measure parameter values. Keys may be either a String or Symbol
|
30
|
-
# @return [Hash] a hash of the measure result with Symbol keys: population, denominator, numerator, antinumerator and exclusions whose values are the count of patient records that meet the criteria for each component of the measure. Also included are keys: population_members, denominator_members, numerator_members, antinumerator_members and exclusions_members whose values are arrays of the patient record identifiers that meet the criteria for each component of the measure.
|
31
|
-
def measure_result(measure_id, sub_id, parameter_values)
|
32
32
|
|
33
|
-
|
34
|
-
cache_q = {:measure_id=>measure_id, :sub_id=>sub_id,:effective_date=>parameter_values[:effective_date]}
|
35
|
-
result = cache.find_one(cache_q)
|
36
|
-
unless result
|
37
|
-
measure = Builder.new(@db, measure_def(measure_id, sub_id), parameter_values)
|
38
|
-
|
39
|
-
records = @db.collection('records')
|
40
|
-
results = records.map_reduce(measure.map_function, measure.reduce_function)
|
41
|
-
result = results.find_one # only one key is used by map fn
|
42
|
-
result['value']['cache_name'] = cache_name(measure_id,sub_id,parameter_values[:effective_date])
|
43
|
-
cache_q["value"] =result['value']
|
44
|
-
cache << cache_q
|
45
|
-
|
46
|
-
cache_measure_patients(measure_id,sub_id,parameter_values[:effective_date],result['value'])
|
47
|
-
end
|
33
|
+
get_db.collection("query_cache").save(result)
|
48
34
|
|
49
|
-
value = result['value']
|
50
|
-
summary = {}
|
51
|
-
%w(population denominator numerator antinumerator exclusions).each do |field|
|
52
|
-
summary[field.intern] = value[field].length
|
53
|
-
summary[(field+'_members').intern] = value[field]
|
54
|
-
end
|
55
|
-
summary
|
56
|
-
|
57
|
-
end
|
58
|
-
|
59
|
-
# Return a list of the measures in the database
|
60
|
-
# @return [Hash] an hash of measure definitions
|
61
|
-
def all_measures
|
62
|
-
result = {}
|
63
|
-
measures = @db.collection('measures')
|
64
|
-
measures.find().each do |measure|
|
65
|
-
id = measure['id']
|
66
|
-
sub_id = measure['sub_id']
|
67
|
-
measure_id = "#{id}#{sub_id}.json"
|
68
|
-
result[measure_id] ||= measure
|
69
|
-
end
|
70
35
|
result
|
71
36
|
end
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
# @param [String] sub_id value of the measure's sub_id field, may be nil for measures with only a single numerator and denominator
|
84
|
-
# @param [Integer] effective_date the effective date used by the map reduce function to calculate popultion, denominator .......
|
85
|
-
def cache_measure_patients(measure_id, sub_id, effective_date, results,drop=true)
|
86
|
-
col_name =cache_name(measure_id, sub_id, effective_date)
|
87
|
-
@db.collection(col_name).drop if drop
|
88
|
-
cached_patients = @db.collection(col_name)
|
89
|
-
population = results['population']
|
90
|
-
if population
|
91
|
-
records = @db.collection("records").find('_id' => {'$in' => results['population']})
|
92
|
-
records.each do |record|
|
93
|
-
record_id = record['_id']
|
94
|
-
new_record = {:first=>record['first'],
|
95
|
-
:last=>record['last'],
|
96
|
-
:birthdate=>record['birthdate'],
|
97
|
-
:gender=>record['gender'],
|
98
|
-
:measure_id => measure_id,
|
99
|
-
:sub_id=>sub_id,
|
100
|
-
:effective_date => effective_date,
|
101
|
-
:measure_data => record[measure_id],
|
102
|
-
:numerator=>!results['numerator'].index(record_id).nil?,
|
103
|
-
:denominator=>!results['denominator'].index(record_id).nil?,
|
104
|
-
:exclusion=>!results['exclusions'].index(record_id).nil?,
|
105
|
-
:antinumerator=>!results['antinumerator'].index(record_id).nil?}
|
106
|
-
|
107
|
-
cached_patients << new_record
|
108
|
-
|
109
|
-
end
|
110
|
-
end
|
37
|
+
|
38
|
+
# This method runs the MapReduce job for the measure which will create documents
|
39
|
+
# in the patient_cache collection. These documents will state the measure groups
|
40
|
+
# that the record belongs to, such as numerator, etc.
|
41
|
+
def map_records_into_measure_groups
|
42
|
+
qm = QualityMeasure.new(@measure_id, @sub_id)
|
43
|
+
measure = Builder.new(get_db, qm.definition, @parameter_values)
|
44
|
+
records = get_db.collection('records')
|
45
|
+
records.map_reduce(measure.map_function, "function(key, values){return values;}",
|
46
|
+
:out => {:reduce => 'patient_cache'},
|
47
|
+
:finalize => measure.finalize_function)
|
111
48
|
end
|
112
|
-
|
113
49
|
end
|
114
50
|
end
|
115
51
|
end
|