quality-measure-engine 1.0.3 → 1.0.4
Sign up to get free protection for your applications and to get access to all the features.
- 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
|