quality-measure-engine 0.1.2 → 0.2.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 +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
|
-
}
|