quality-measure-engine 0.1.2 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- data/Gemfile +9 -9
- data/README.md +39 -2
- data/Rakefile +25 -44
- data/js/map-reduce-utils.js +174 -0
- data/js/underscore-min.js +24 -0
- data/lib/qme/importer/code_system_helper.rb +26 -0
- data/lib/qme/importer/entry.rb +89 -0
- data/lib/qme/importer/generic_importer.rb +71 -0
- data/lib/qme/importer/hl7_helper.rb +27 -0
- data/lib/qme/importer/patient_importer.rb +150 -0
- data/lib/qme/importer/property_matcher.rb +103 -0
- data/lib/qme/importer/section_importer.rb +82 -0
- data/lib/qme/map/map_reduce_builder.rb +77 -147
- data/lib/qme/map/map_reduce_executor.rb +97 -13
- data/lib/qme/measure/database_loader.rb +90 -0
- data/lib/qme/measure/measure_loader.rb +141 -0
- data/lib/qme/mongo_helpers.rb +15 -0
- data/lib/qme/randomizer/patient_randomizer.rb +95 -0
- data/lib/qme_test.rb +13 -0
- data/lib/quality-measure-engine.rb +20 -4
- data/lib/tasks/measure.rake +76 -0
- data/lib/tasks/mongo.rake +74 -0
- data/lib/tasks/patient_random.rake +46 -0
- metadata +110 -156
- data/.gitignore +0 -6
- data/Gemfile.lock +0 -44
- data/fixtures/complex_measure.json +0 -36
- data/fixtures/result_example.json +0 -6
- data/lib/patches/v8.rb +0 -20
- data/lib/qme/query/json_document_builder.rb +0 -130
- data/lib/qme/query/json_query_executor.rb +0 -44
- data/measures/0032/0032_NQF_Cervical_Cancer_Screening.json +0 -171
- data/measures/0032/patients/denominator1.json +0 -10
- data/measures/0032/patients/denominator2.json +0 -10
- data/measures/0032/patients/numerator1.json +0 -11
- data/measures/0032/patients/population1.json +0 -9
- data/measures/0032/patients/population2.json +0 -11
- data/measures/0032/result/result.json +0 -6
- data/measures/0043/0043_NQF_PneumoniaVaccinationStatusForOlderAdults.json +0 -71
- data/measures/0043/patients/denominator.json +0 -11
- data/measures/0043/patients/numerator.json +0 -11
- data/measures/0043/patients/population.json +0 -10
- data/measures/0043/result/result.json +0 -6
- data/quality-measure-engine.gemspec +0 -97
- data/schema/result.json +0 -28
- data/schema/schema.json +0 -143
- data/spec/qme/map/map_reduce_builder_spec.rb +0 -64
- data/spec/qme/measures_spec.rb +0 -50
- data/spec/qme/query/json_document_builder_spec.rb +0 -56
- data/spec/schema_spec.rb +0 -21
- data/spec/spec_helper.rb +0 -7
- data/spec/validate_measures_spec.rb +0 -21
data/.gitignore
DELETED
data/Gemfile.lock
DELETED
@@ -1,44 +0,0 @@
|
|
1
|
-
GEM
|
2
|
-
remote: http://rubygems.org/
|
3
|
-
specs:
|
4
|
-
awesome_print (0.2.1)
|
5
|
-
bson (1.1)
|
6
|
-
bson_ext (1.1.1)
|
7
|
-
diff-lcs (1.1.2)
|
8
|
-
gemcutter (0.6.1)
|
9
|
-
git (1.2.5)
|
10
|
-
jeweler (1.4.0)
|
11
|
-
gemcutter (>= 0.1.0)
|
12
|
-
git (>= 1.2.5)
|
13
|
-
rubyforge (>= 2.0.0)
|
14
|
-
json_pure (1.4.6)
|
15
|
-
jsonschema (2.0.0)
|
16
|
-
mongo (1.1)
|
17
|
-
bson (>= 1.0.5)
|
18
|
-
rake (0.8.7)
|
19
|
-
rspec (2.0.0)
|
20
|
-
rspec-core (= 2.0.0)
|
21
|
-
rspec-expectations (= 2.0.0)
|
22
|
-
rspec-mocks (= 2.0.0)
|
23
|
-
rspec-core (2.0.0)
|
24
|
-
rspec-expectations (2.0.0)
|
25
|
-
diff-lcs (>= 1.1.2)
|
26
|
-
rspec-mocks (2.0.0)
|
27
|
-
rspec-core (= 2.0.0)
|
28
|
-
rspec-expectations (= 2.0.0)
|
29
|
-
rubyforge (2.0.4)
|
30
|
-
json_pure (>= 1.1.7)
|
31
|
-
therubyracer (0.7.5)
|
32
|
-
|
33
|
-
PLATFORMS
|
34
|
-
ruby
|
35
|
-
|
36
|
-
DEPENDENCIES
|
37
|
-
awesome_print
|
38
|
-
bson_ext
|
39
|
-
jeweler
|
40
|
-
jsonschema
|
41
|
-
mongo
|
42
|
-
rake
|
43
|
-
rspec
|
44
|
-
therubyracer
|
@@ -1,36 +0,0 @@
|
|
1
|
-
{
|
2
|
-
"id": "0043",
|
3
|
-
"name": "Pneumonia Vaccination Status for Older Adults",
|
4
|
-
"steward": "NCQA",
|
5
|
-
"population": {
|
6
|
-
"and": [
|
7
|
-
{
|
8
|
-
"category": "Patient Characteristic",
|
9
|
-
"title": "Age > 17 before measure period",
|
10
|
-
"query": {"age": {"_gt": 17}}
|
11
|
-
},
|
12
|
-
{
|
13
|
-
"category": "Patient Characteristic",
|
14
|
-
"title": "Age < 75 before measure period",
|
15
|
-
"query": {"age": {"_lt": 75}}
|
16
|
-
},
|
17
|
-
{
|
18
|
-
"or": [
|
19
|
-
{
|
20
|
-
"category": "Patient Characteristic",
|
21
|
-
"title": "Male",
|
22
|
-
"query": {"sex": "male"}
|
23
|
-
},
|
24
|
-
{
|
25
|
-
"category": "Patient Characteristic",
|
26
|
-
"title": "Female",
|
27
|
-
"query": {"sex": "female"}
|
28
|
-
}
|
29
|
-
]
|
30
|
-
}
|
31
|
-
]
|
32
|
-
},
|
33
|
-
"denominator": {},
|
34
|
-
"numerator": {},
|
35
|
-
"exception": {}
|
36
|
-
}
|
data/lib/patches/v8.rb
DELETED
@@ -1,20 +0,0 @@
|
|
1
|
-
# Patching V8::Object because it is the entry point for conversions
|
2
|
-
# between Ruby and JavaScript types. We are using seconds since the
|
3
|
-
# epoch to represent dates. On 32 bit architectures, for recent dates
|
4
|
-
# this will be too large for Fixnum and become a Bignum. Ruby Racer
|
5
|
-
# worn't properly convert a Bignum to JavaScript, but it will work
|
6
|
-
# just fine for a Float. Because of this, we will convert all Bignums
|
7
|
-
# passed into a JavaScript context to a Float
|
8
|
-
module V8
|
9
|
-
class Object
|
10
|
-
alias :old_index_setter :'[]='
|
11
|
-
|
12
|
-
def []=(key, value)
|
13
|
-
if value.kind_of?(Bignum)
|
14
|
-
old_index_setter(key, value.to_f)
|
15
|
-
else
|
16
|
-
old_index_setter(key, value)
|
17
|
-
end
|
18
|
-
end
|
19
|
-
end
|
20
|
-
end
|
@@ -1,130 +0,0 @@
|
|
1
|
-
module QME
|
2
|
-
module Query
|
3
|
-
class JSONDocumentBuilder
|
4
|
-
attr_accessor :parameters, :calculated_dates
|
5
|
-
|
6
|
-
# Creates the JSONDocumentBuilder. Will calculate dates if parameters
|
7
|
-
# are passed in.
|
8
|
-
def initialize(measure_json, parameters={})
|
9
|
-
@measure_json = measure_json
|
10
|
-
@parameters = parameters
|
11
|
-
@measure_id = measure_json['id']
|
12
|
-
|
13
|
-
if ! parameters.empty?
|
14
|
-
calculate_dates
|
15
|
-
end
|
16
|
-
end
|
17
|
-
|
18
|
-
# Calculates all dates necessary to create a query for this measure
|
19
|
-
# This will be run by the constructor if params were passed in
|
20
|
-
def calculate_dates
|
21
|
-
@calculated_dates = {}
|
22
|
-
|
23
|
-
ctx = V8::Context.new
|
24
|
-
@parameters.each_pair do |key, value|
|
25
|
-
ctx[key] = value
|
26
|
-
@calculated_dates[key] = value
|
27
|
-
end
|
28
|
-
|
29
|
-
ctx['year'] = 365 * 24 * 60 * 60 # TODO: Replace this with a js file that has all constants
|
30
|
-
|
31
|
-
@measure_json["calculated_dates"].each_pair do |key, value|
|
32
|
-
@calculated_dates[key] = ctx.eval(value)
|
33
|
-
end
|
34
|
-
end
|
35
|
-
|
36
|
-
def numerator_query
|
37
|
-
create_query(@measure_json['numerator']).merge(denominator_query)
|
38
|
-
end
|
39
|
-
|
40
|
-
def denominator_query
|
41
|
-
create_query(@measure_json['denominator']).merge(initial_population_query)
|
42
|
-
end
|
43
|
-
|
44
|
-
def initial_population_query
|
45
|
-
create_query(@measure_json['population'])
|
46
|
-
end
|
47
|
-
|
48
|
-
def exclusions_query
|
49
|
-
create_query(@measure_json['exception'])
|
50
|
-
end
|
51
|
-
|
52
|
-
# Creates the appropriate JSON document to query MongoDB based on
|
53
|
-
# a measure definition passed in. This method calls itself recursively
|
54
|
-
# to walk the tree and get all possible logical operators available
|
55
|
-
# in a measure definition
|
56
|
-
def create_query(definition_json, args={})
|
57
|
-
if definition_json.has_key?('and')
|
58
|
-
definition_json['and'].each do |operand|
|
59
|
-
create_query(operand, args)
|
60
|
-
end
|
61
|
-
elsif definition_json.has_key?('or')
|
62
|
-
operands = []
|
63
|
-
definition_json['or'].each do |operand|
|
64
|
-
operands << create_query(operand)
|
65
|
-
end
|
66
|
-
if args['$or']
|
67
|
-
args['$ne'] = {'$or' => operands}
|
68
|
-
else
|
69
|
-
args['$or'] = operands
|
70
|
-
end
|
71
|
-
elsif definition_json.has_key?('query')
|
72
|
-
process_query(definition_json['query'], args)
|
73
|
-
end
|
74
|
-
|
75
|
-
args
|
76
|
-
end
|
77
|
-
|
78
|
-
# Called by create_query to process leaf nodes in a measure
|
79
|
-
# definition
|
80
|
-
def process_query(definition_json, args)
|
81
|
-
if definition_json.size > 1
|
82
|
-
raise 'A query should have only one property'
|
83
|
-
end
|
84
|
-
|
85
|
-
query_property = definition_json.keys.first
|
86
|
-
document_key = transform_query_property(query_property)
|
87
|
-
document_value = nil
|
88
|
-
query_value = definition_json[query_property]
|
89
|
-
if query_value.kind_of?(Hash)
|
90
|
-
if query_value.size > 1
|
91
|
-
raise 'A query value should only have one property'
|
92
|
-
end
|
93
|
-
|
94
|
-
document_value = {query_value.keys.first.gsub('_', '$') =>
|
95
|
-
substitute_variables(query_value.values.first)}
|
96
|
-
if args[document_key]
|
97
|
-
args[document_key].merge!(document_value)
|
98
|
-
else
|
99
|
-
args[document_key] = document_value
|
100
|
-
end
|
101
|
-
else
|
102
|
-
document_value = substitute_variables(query_value)
|
103
|
-
args[document_key] = document_value
|
104
|
-
end
|
105
|
-
end
|
106
|
-
|
107
|
-
# Takes a query property name and transforms it into
|
108
|
-
# a name of a key in a MongoDB document
|
109
|
-
def transform_query_property(property_name)
|
110
|
-
#TODO What do we do with special case fields - the stuff we are keeping at the patient level?
|
111
|
-
if ['birthdate'].include?(property_name)
|
112
|
-
property_name
|
113
|
-
else
|
114
|
-
"measures.#{@measure_id}.#{property_name}"
|
115
|
-
end
|
116
|
-
end
|
117
|
-
|
118
|
-
# Finds strings that start with "@" and replaces them
|
119
|
-
# with the calculated date
|
120
|
-
def substitute_variables(value)
|
121
|
-
if value.kind_of?(String) && value[0] == '@'
|
122
|
-
variable_name = value[1..-1]
|
123
|
-
@calculated_dates[variable_name]
|
124
|
-
else
|
125
|
-
value
|
126
|
-
end
|
127
|
-
end
|
128
|
-
end
|
129
|
-
end
|
130
|
-
end
|
@@ -1,44 +0,0 @@
|
|
1
|
-
module QME
|
2
|
-
module Query
|
3
|
-
class JsonQueryExecutor
|
4
|
-
def initialize(db)
|
5
|
-
@db = db
|
6
|
-
end
|
7
|
-
|
8
|
-
def measure_def(measure_id)
|
9
|
-
measures = @db.collection('measures')
|
10
|
-
measures.find({'id'=> "#{measure_id}"}).to_a[0]
|
11
|
-
end
|
12
|
-
|
13
|
-
def measure_result(measure_id, parameter_values)
|
14
|
-
jdb = JSONDocumentBuilder.new(measure_def(measure_id),
|
15
|
-
parameter_values)
|
16
|
-
|
17
|
-
collection = @db.collection('records')
|
18
|
-
result = {}
|
19
|
-
|
20
|
-
collection.find(jdb.numerator_query) do |cursor|
|
21
|
-
result[:numerator] = cursor.count
|
22
|
-
end
|
23
|
-
|
24
|
-
collection.find(jdb.denominator_query) do |cursor|
|
25
|
-
result[:denominator] = cursor.count
|
26
|
-
end
|
27
|
-
collection.find(jdb.initial_population_query) do |cursor|
|
28
|
-
result[:population] = cursor.count
|
29
|
-
end
|
30
|
-
|
31
|
-
exclusions_query = jdb.exclusions_query
|
32
|
-
if exclusions_query.empty?
|
33
|
-
result[:exceptions] = 0
|
34
|
-
else
|
35
|
-
collection.find(exclusions_query) do |cursor|
|
36
|
-
result[:exceptions] = cursor.count
|
37
|
-
end
|
38
|
-
end
|
39
|
-
|
40
|
-
result
|
41
|
-
end
|
42
|
-
end
|
43
|
-
end
|
44
|
-
end
|
@@ -1,171 +0,0 @@
|
|
1
|
-
{
|
2
|
-
"id": "0032",
|
3
|
-
"name": "Cervical Cancer Screening",
|
4
|
-
"steward": "NCQA",
|
5
|
-
"parameters": {
|
6
|
-
"effective_date": {
|
7
|
-
"name": "Effective end date for measure",
|
8
|
-
"type": "long"
|
9
|
-
}
|
10
|
-
},
|
11
|
-
"properties": {
|
12
|
-
"birthdate": {
|
13
|
-
"name": "Date of birth",
|
14
|
-
"type": "long",
|
15
|
-
"codes": [
|
16
|
-
{
|
17
|
-
"set": "HL7",
|
18
|
-
"version": "3.0",
|
19
|
-
"values": ["00110"]
|
20
|
-
}
|
21
|
-
]
|
22
|
-
},
|
23
|
-
"encounter_outpatient": {
|
24
|
-
"name": "Date of outpatient encounter",
|
25
|
-
"type": "long",
|
26
|
-
"codes": [
|
27
|
-
{
|
28
|
-
"set": "CPT",
|
29
|
-
"version": "06/2009",
|
30
|
-
"values": ["99201", "99202", "99203", "99204", "99205", "99211", "99212", "99213", "99214", "99215", "99217", "99218", "99219", "99220", "99241", "99242", "99243", "99244", "99245", "99341", "99342", "99343", "99344", "99345", "99347-99350", "99384", "99385", "99386", "99387", "99394", "99395", "99396", "99397", "99401", "99402", "99403", "99404", "99411", "99412", "99420", "99429", "99455", "99456"]
|
31
|
-
},
|
32
|
-
{
|
33
|
-
"set": "ICD-9-CM",
|
34
|
-
"version": "06/2009",
|
35
|
-
"values": ["V70.0", "V70.3", "V70.5", "V70.6", "V70.8", "V70.9"]
|
36
|
-
}
|
37
|
-
]
|
38
|
-
},
|
39
|
-
"encounter_obgyn": {
|
40
|
-
"name": "Date of ObGyn encounter",
|
41
|
-
"type": "long",
|
42
|
-
"codes": [
|
43
|
-
{
|
44
|
-
"set": "ICD-9-CM",
|
45
|
-
"version": "06/2009",
|
46
|
-
"values": ["V24", "V25", "V26", "V27", "V28", "V45.5", "V61.5", "V61.6", "V61.7", "V69.2", "V72.3", "V72.4"]
|
47
|
-
}
|
48
|
-
]
|
49
|
-
},
|
50
|
-
"hysterectomy": {
|
51
|
-
"name": "Hysterectomy performed",
|
52
|
-
"type": "long",
|
53
|
-
"codes": [
|
54
|
-
{
|
55
|
-
"set": "CPT",
|
56
|
-
"version": "06/2009",
|
57
|
-
"values": ["51925", "56308", "58150", "58152", "58200", "58210", "58240", "58260", "58262", "58263", "58267", "58270", "58275", "58280", "58285", "58290", "58291", "58292", "58293", "58294", "58550", "58552", "58553", "58554", "58570", "58571", "58572", "58573", "58951", "58953", "58954", "58956", "59135"]
|
58
|
-
},
|
59
|
-
{
|
60
|
-
"set": "ICD-9-CM",
|
61
|
-
"version": "06/2009",
|
62
|
-
"values": ["618.5", "68.4", "68.41", "68.49", "68.5", "68.51", "68.59", "68.6", "68.61", "68.69", "68.7", "68.71", "68.79", "68.8", "V67.01", "V76.47", "V88.01", "V88.03"]
|
63
|
-
},
|
64
|
-
{
|
65
|
-
"set": "ICD-10-CM",
|
66
|
-
"version": "06/2009",
|
67
|
-
"values": ["N81.81", "Z12.72", "Z90.71", "Z90.710", "Z90.7112"]
|
68
|
-
},
|
69
|
-
{
|
70
|
-
"set": "SNOMED-CT",
|
71
|
-
"version": "07/2009",
|
72
|
-
"values": ["116140006", "116142003", "116143008", "116144002", "236886002", "236888001", "236891001", "27950001", "307771009", "31545000", "35955002", "361222003", "361223008", "414575003", "59750000", "79095000", "86477000", "88144003"]
|
73
|
-
}
|
74
|
-
]
|
75
|
-
},
|
76
|
-
"pap_test": {
|
77
|
-
"name": "Pap test",
|
78
|
-
"type": "long",
|
79
|
-
"codes": [
|
80
|
-
{
|
81
|
-
"set": "CPT",
|
82
|
-
"version": "06/2009",
|
83
|
-
"values": ["88141", "88142", "88143", "88147", "88148", "88150", "88152", "88153", "88154", "88155", "88164", "88165", "88166", "88167", "88174", "88175"]
|
84
|
-
},
|
85
|
-
{
|
86
|
-
"set": "HCPCS",
|
87
|
-
"version": "06/2009",
|
88
|
-
"values": ["G0123", "G0124", "G0141", "G0143", "G0144", "G0145", "G0147", "G0148", "P3000", "P3001", "Q0091"]
|
89
|
-
},
|
90
|
-
{
|
91
|
-
"set": "ICD-10-CM",
|
92
|
-
"version": "06/2009",
|
93
|
-
"values": ["Z12.4", "Z12.72"]
|
94
|
-
},
|
95
|
-
{
|
96
|
-
"set": "ICD-9-CM",
|
97
|
-
"version": "06/2009",
|
98
|
-
"values": ["91.46", "V72.32"]
|
99
|
-
},
|
100
|
-
{
|
101
|
-
"set": "LOINC",
|
102
|
-
"version": "06/2009",
|
103
|
-
"values": ["10524-7", "18500-9", "19762-4", "19764-0", "19765-7", "19766-5", "19774-9", "33717-0", "47527-7", "47528-5"]
|
104
|
-
},
|
105
|
-
{
|
106
|
-
"set": "SNOMED-CT",
|
107
|
-
"version": "07/2009",
|
108
|
-
"values": ["439958008", "440615002", "440623000"]
|
109
|
-
}
|
110
|
-
]
|
111
|
-
}
|
112
|
-
},
|
113
|
-
"calculated_dates": {
|
114
|
-
"earliest_birthdate": "effective_date - 64*year",
|
115
|
-
"latest_birthdate": "effective_date - 23*year",
|
116
|
-
"earliest_encounter": "effective_date - 2*year",
|
117
|
-
"earliest_pap": "effective_date - 3*year"
|
118
|
-
},
|
119
|
-
"population": {
|
120
|
-
"and": [
|
121
|
-
{
|
122
|
-
"category": "Patient Characteristic",
|
123
|
-
"title": "Age >= 23 before measure period",
|
124
|
-
"query": {"birthdate": {"_lte": "@latest_birthdate"}}
|
125
|
-
},
|
126
|
-
{
|
127
|
-
"category": "Patient Characteristic",
|
128
|
-
"title": "Age <= 64 before measure period",
|
129
|
-
"query": {"birthdate": {"_gte": "@earliest_birthdate"}}
|
130
|
-
}
|
131
|
-
]
|
132
|
-
},
|
133
|
-
"denominator": {
|
134
|
-
"and": [
|
135
|
-
{
|
136
|
-
"or": [
|
137
|
-
{
|
138
|
-
"category": "Outpatient Encounter",
|
139
|
-
"title": "Outpatient encounter within last three years",
|
140
|
-
"query": {"encounter_outpatient": {"_gte": "@earliest_encounter"}}
|
141
|
-
},
|
142
|
-
{
|
143
|
-
"category": "ObGyn Encounter",
|
144
|
-
"title": "ObGyn encounter within last three years",
|
145
|
-
"query": {"encounter_obgyn": {"_gte": "@earliest_encounter"}}
|
146
|
-
}
|
147
|
-
]
|
148
|
-
},
|
149
|
-
{
|
150
|
-
"or": [
|
151
|
-
{
|
152
|
-
"category": "Procedure not performed",
|
153
|
-
"title": "Hysterectomy",
|
154
|
-
"query": {"hysterectomy": null}
|
155
|
-
},
|
156
|
-
{
|
157
|
-
"category": "Procedure performed",
|
158
|
-
"title": "Hysterectomy after effective date",
|
159
|
-
"query": {"hysterectomy": {"_gte": "@effective_date"}}
|
160
|
-
}
|
161
|
-
]
|
162
|
-
}
|
163
|
-
]
|
164
|
-
},
|
165
|
-
"numerator": {
|
166
|
-
"category": "Laboratory test result",
|
167
|
-
"title": "Pap test",
|
168
|
-
"query": {"pap_test": {"_gte": "@earliest_pap"}}
|
169
|
-
},
|
170
|
-
"exception": {}
|
171
|
-
}
|