quality-measure-engine 1.0.3 → 1.0.4
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 +2 -1
- data/VERSION +1 -1
- data/js/map_reduce_utils.js +1 -1
- data/lib/qme/importer/patient_importer.rb +6 -1
- data/lib/qme/map/map_reduce_builder.rb +10 -0
- data/lib/qme/map/map_reduce_executor.rb +54 -25
- data/lib/qme/randomizer/patient_randomizer.rb +78 -0
- metadata +2 -2
data/Gemfile
CHANGED
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
1.0.
|
1
|
+
1.0.4
|
data/js/map_reduce_utils.js
CHANGED
@@ -124,7 +124,7 @@ function() {
|
|
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,
|
127
|
-
race: record.race, ethnicity: record.ethnicity};
|
127
|
+
race: record.race, ethnicity: record.ethnicity, languages: record.languages};
|
128
128
|
if (population()) {
|
129
129
|
value.population = true;
|
130
130
|
if (denominator()) {
|
@@ -64,7 +64,7 @@ module QME
|
|
64
64
|
"./cda:consumable/cda:manufacturedProduct/cda:manufacturedMaterial/cda:code/cda:originalText/cda:reference[@value]")
|
65
65
|
@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",
|
66
66
|
"./cda:value",
|
67
|
-
"./cda:entryRelationship/cda:observation[cda:templateId/@root='2.16.840.1.
|
67
|
+
"./cda:entryRelationship/cda:observation[cda:templateId/@root='2.16.840.1.113883.10.20.1.50']/cda:value",
|
68
68
|
"./cda:text/cda:reference[@value]")
|
69
69
|
@section_importers[:social_history] = SectionImporter.new("//cda:observation[cda:templateId/@root='2.16.840.1.113883.3.88.11.83.19']")
|
70
70
|
@section_importers[:care_goals] = SectionImporter.new("//cda:observation[cda:templateId/@root='2.16.840.1.113883.10.20.1.25']")
|
@@ -123,6 +123,7 @@ module QME
|
|
123
123
|
patient_record['birthdate'] = patient_hash['birthdate']
|
124
124
|
patient_record['race'] = patient_hash['race']
|
125
125
|
patient_record['ethnicity'] = patient_hash['ethnicity']
|
126
|
+
patient_record['languages'] = patient_hash['languages']
|
126
127
|
patient_record['addresses'] = patient_hash['addresses']
|
127
128
|
event_hash = {}
|
128
129
|
patient_hash['events'].each do |key, value|
|
@@ -215,6 +216,10 @@ module QME
|
|
215
216
|
patient['race'] = race_node['code'] if race_node
|
216
217
|
ethnicity_node = doc.at_xpath('/cda:ClinicalDocument/cda:recordTarget/cda:patientRole/cda:patient/cda:ethnicGroupCode')
|
217
218
|
patient['ethnicity'] = ethnicity_node['code'] if ethnicity_node
|
219
|
+
|
220
|
+
languages = doc.at_xpath('/cda:ClinicalDocument/cda:recordTarget/cda:patientRole/cda:patient').search('languageCommunication').map {|lc| lc.at_xpath('cda:languageCode')['code'] }
|
221
|
+
patient['languages'] = languages unless languages.empty?
|
222
|
+
|
218
223
|
id_node = doc.at_xpath('/cda:ClinicalDocument/cda:recordTarget/cda:patientRole/cda:id')
|
219
224
|
patient['patient_id'] = id_node['extension']
|
220
225
|
end
|
@@ -93,6 +93,16 @@ module QME
|
|
93
93
|
end
|
94
94
|
|
95
95
|
reduce += "patient.effective_date = #{@params['effective_date']};
|
96
|
+
if (patient.provider_performances) {
|
97
|
+
var tmp = [];
|
98
|
+
for(var i=0; i<patient.provider_performances.length; i++) {
|
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))
|
101
|
+
tmp.push(value);
|
102
|
+
}
|
103
|
+
if (tmp.length == 0) tmp = null;
|
104
|
+
patient.provider_performances = tmp;
|
105
|
+
}
|
96
106
|
return patient;}"
|
97
107
|
|
98
108
|
reduce
|
@@ -25,30 +25,42 @@ module QME
|
|
25
25
|
# @return [Hash] measure groups (like numerator) as keys, counts as values
|
26
26
|
def count_records_in_measure_groups
|
27
27
|
patient_cache = get_db.collection('patient_cache')
|
28
|
-
|
29
|
-
|
30
|
-
|
28
|
+
base_query = {'value.measure_id' => @measure_id, 'value.sub_id' => @sub_id,
|
29
|
+
'value.effective_date' => @parameter_values['effective_date'],
|
30
|
+
'value.test_id' => @parameter_values['test_id']}
|
31
|
+
|
32
|
+
base_query.merge!(filter_parameters)
|
33
|
+
|
34
|
+
query = base_query.clone
|
31
35
|
|
32
|
-
query.merge!(
|
36
|
+
query.merge!({'value.manual_exclusion'=>{'$ne'=>true}})
|
33
37
|
|
34
38
|
result = {:measure_id => @measure_id, :sub_id => @sub_id,
|
35
39
|
:effective_date => @parameter_values['effective_date'],
|
36
40
|
:test_id => @parameter_values['test_id'], :filters => @parameter_values['filters']}
|
37
41
|
|
38
42
|
aggregate = patient_cache.group({cond: query,
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
#
|
43
|
+
initial: {population: 0, denominator: 0, numerator: 0, antinumerator: 0, exclusions: 0, considered: 0},
|
44
|
+
reduce: "function(record,sums) {
|
45
|
+
for (var key in sums) {
|
46
|
+
sums[key] += (record['value'][key] || key == 'considered') ? 1 : 0
|
47
|
+
}
|
48
|
+
}"}).first
|
49
|
+
|
50
|
+
aggregate ||= {"population"=>0, "denominator"=>0, "numerator"=>0, "antinumerator"=>0, "exclusions"=>0}
|
51
|
+
aggregate.each {|key, value| aggregate[key] = value.to_i}
|
52
|
+
aggregate['exclusions'] += patient_cache.find(base_query.merge({'value.manual_exclusion'=>true})).count
|
53
|
+
result.merge!(aggregate)
|
54
|
+
|
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)
|
52
64
|
|
53
65
|
result.merge!(execution_time: (Time.now.to_i - @parameter_values['start_time'].to_i)) if @parameter_values['start_time']
|
54
66
|
|
@@ -67,6 +79,7 @@ module QME
|
|
67
79
|
:out => {:reduce => 'patient_cache'},
|
68
80
|
:finalize => measure.finalize_function,
|
69
81
|
:query => {:test_id => @parameter_values['test_id']})
|
82
|
+
apply_manual_exclusions
|
70
83
|
end
|
71
84
|
|
72
85
|
# This method runs the MapReduce job for the measure and a specific patient.
|
@@ -80,6 +93,19 @@ module QME
|
|
80
93
|
:out => {:reduce => 'patient_cache'},
|
81
94
|
:finalize => measure.finalize_function,
|
82
95
|
:query => {:patient_id => patient_id, :test_id => @parameter_values['test_id']})
|
96
|
+
apply_manual_exclusions
|
97
|
+
end
|
98
|
+
|
99
|
+
# This records collects the set of manual exclusions from the manual_exclusions collections
|
100
|
+
# and sets a flag in each cached patient result for patients that have been excluded from the
|
101
|
+
# current measure
|
102
|
+
def apply_manual_exclusions
|
103
|
+
exclusions = get_db.collection('manual_exclusions').find({'measure_id'=>@measure_id, 'sub_id'=>@sub_id}).to_a.map do |exclusion|
|
104
|
+
exclusion['medical_record_id']
|
105
|
+
end
|
106
|
+
get_db.collection('patient_cache').update(
|
107
|
+
{'value.measure_id'=>@measure_id, 'value.sub_id'=>@sub_id, 'value.medical_record_id'=>{'$in'=>exclusions} },
|
108
|
+
{'$set'=>{'value.manual_exclusion'=>true}}, :multi=>true)
|
83
109
|
end
|
84
110
|
|
85
111
|
def filter_parameters
|
@@ -87,8 +113,9 @@ module QME
|
|
87
113
|
conditions = []
|
88
114
|
if(filters = @parameter_values['filters'])
|
89
115
|
if (filters['providers'] && filters['providers'].size > 0)
|
90
|
-
providers = filters['providers'].map {|provider_id| BSON::ObjectId(provider_id) if provider_id }
|
91
|
-
|
116
|
+
providers = filters['providers'].map {|provider_id| BSON::ObjectId(provider_id) if (provider_id and provider_id != 'null') }
|
117
|
+
# provider_performances have already been filtered by start and end date in map_reduce_builder as part of the finalize
|
118
|
+
conditions << {'value.provider_performances.provider_id' => {'$in' => providers}}
|
92
119
|
end
|
93
120
|
if (filters['races'] && filters['races'].size > 0)
|
94
121
|
conditions << {'value.race.code' => {'$in' => filters['races']}}
|
@@ -99,16 +126,18 @@ module QME
|
|
99
126
|
if (filters['genders'] && filters['genders'].size > 0)
|
100
127
|
conditions << {'value.gender' => {'$in' => filters['genders']}}
|
101
128
|
end
|
129
|
+
if (filters['languages'] && filters['languages'].size > 0)
|
130
|
+
languages = filters['languages'].clone
|
131
|
+
has_unspecified = languages.delete('null')
|
132
|
+
or_clauses = []
|
133
|
+
or_clauses << {'value.languages'=>{'$regex'=>Regexp.new("(#{languages.join("|")})-..")}} if languages.length > 0
|
134
|
+
or_clauses << {'value.languages'=>nil} if (has_unspecified)
|
135
|
+
conditions << {'$or'=>or_clauses}
|
136
|
+
end
|
102
137
|
end
|
103
138
|
results.merge!({'$and'=>conditions}) if conditions.length > 0
|
104
139
|
results
|
105
140
|
end
|
106
|
-
def provider_queries(provider_ids, effective_date)
|
107
|
-
{'$or' => [provider_query(provider_ids, effective_date,effective_date), provider_query(provider_ids, nil,effective_date), provider_query(provider_ids, effective_date,nil)]}
|
108
|
-
end
|
109
|
-
def provider_query(provider_ids, start_before, end_after)
|
110
|
-
{'value.provider_performances' => {'$elemMatch' => {'provider_id' => {'$in' => provider_ids}, 'start_date'=> {'$lt'=>start_before}, 'end_date'=> {'$gt'=>end_after} } }}
|
111
|
-
end
|
112
141
|
end
|
113
142
|
end
|
114
143
|
end
|
@@ -80,6 +80,84 @@ module QME
|
|
80
80
|
{race: '2131-1', ethnicity: '2186-5'}
|
81
81
|
end
|
82
82
|
end
|
83
|
+
|
84
|
+
# Picks spoken language based on 2010 census estamates
|
85
|
+
# 80.3% english
|
86
|
+
# 12.3% spanish
|
87
|
+
# 00.9% chinese
|
88
|
+
# 00.7% french
|
89
|
+
# 00.4% german
|
90
|
+
# 00.4% korean
|
91
|
+
# 00.4% vietnamese
|
92
|
+
# 00.3% italian
|
93
|
+
# 00.3% portuguese
|
94
|
+
# 00.3% russian
|
95
|
+
# 00.2% japanese
|
96
|
+
# 00.2% polish
|
97
|
+
# 00.1% greek
|
98
|
+
# 00.1% persian
|
99
|
+
# 00.1% us sign
|
100
|
+
# 03.0% other
|
101
|
+
#
|
102
|
+
def language
|
103
|
+
language_percent = rand(999)
|
104
|
+
case language_percent
|
105
|
+
when 0..802
|
106
|
+
# english
|
107
|
+
'en-US'
|
108
|
+
when 802..925
|
109
|
+
# spanish
|
110
|
+
'es-US'
|
111
|
+
when 926..932
|
112
|
+
# french
|
113
|
+
'fr-US'
|
114
|
+
when 933..935
|
115
|
+
# italian
|
116
|
+
'it-US'
|
117
|
+
when 936..938
|
118
|
+
# portuguese
|
119
|
+
'pt-US'
|
120
|
+
when 939..942
|
121
|
+
# german
|
122
|
+
'de-US'
|
123
|
+
when 943..943
|
124
|
+
# greek
|
125
|
+
'el-US'
|
126
|
+
when 944..946
|
127
|
+
# russian
|
128
|
+
'ru-US'
|
129
|
+
when 947..948
|
130
|
+
# polish
|
131
|
+
'pl-US'
|
132
|
+
when 949..949
|
133
|
+
# persian
|
134
|
+
'fa-US'
|
135
|
+
when 950..958
|
136
|
+
# chinese
|
137
|
+
'zh-US'
|
138
|
+
when 959..960
|
139
|
+
# japanese
|
140
|
+
'ja-US'
|
141
|
+
when 961..964
|
142
|
+
# korean
|
143
|
+
'ko-US'
|
144
|
+
when 965..968
|
145
|
+
# vietnamese
|
146
|
+
'vi-US'
|
147
|
+
when 969..969
|
148
|
+
# us sign
|
149
|
+
'sgn-US'
|
150
|
+
when 970..999
|
151
|
+
# other
|
152
|
+
other = ["aa","ab","ae","af","ak","am","an","ar","as","av","ay","az","ba","be","bg","bh","bi","bm","bn","bo","br","bs","ca","ce","ch","co","cr","cs","cu","cv","cy","da",
|
153
|
+
"dv","dz","ee","eo","et","eu","ff","fi","fj","fo","fy","ga","gd","gl","gn","gu","gv","ha","he","hi","ho","hr","ht","hu","hy","hz","ia","id","ie","ig","ii","ik",
|
154
|
+
"io","is","iu","jv","ka","kg","ki","kj","kk","kl","km","kn","kr","ks","ku","kv","kw","ky","la","lb","lg","li","ln","lo","lt","lu","lv","mg","mh","mi","mk","ml",
|
155
|
+
"mn","mr","ms","mt","my","na","nb","nd","ne","ng","nl","nn","no","nr","nv","ny","oc","oj","om","or","os","pa","pi","ps","qu","rm","rn","ro","rw","sa","sc","sd",
|
156
|
+
"se","sg","si","sk","sl","sm","sn","so","sq","sr","ss","st","su","sv","sw","ta","te","tg","th","ti","tk","tl","tn","to","tr","ts","tt","tw","ty","ug","uk","ur",
|
157
|
+
"uz","ve","vo","wa","wo","xh","yi","yo","za","zu"].sample
|
158
|
+
"#{other}-US"
|
159
|
+
end
|
160
|
+
end
|
83
161
|
|
84
162
|
# Pick a forename at random appropriate for the supplied gender
|
85
163
|
# @param [String] gender the gender 'M' or 'F'
|
metadata
CHANGED
@@ -2,7 +2,7 @@
|
|
2
2
|
name: quality-measure-engine
|
3
3
|
version: !ruby/object:Gem::Version
|
4
4
|
prerelease:
|
5
|
-
version: 1.0.
|
5
|
+
version: 1.0.4
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
8
8
|
- Marc Hadley
|
@@ -12,7 +12,7 @@ autorequire:
|
|
12
12
|
bindir: bin
|
13
13
|
cert_chain: []
|
14
14
|
|
15
|
-
date: 2011-12-
|
15
|
+
date: 2011-12-16 00:00:00 -05:00
|
16
16
|
default_executable:
|
17
17
|
dependencies:
|
18
18
|
- !ruby/object:Gem::Dependency
|