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.
Files changed (52) hide show
  1. data/Gemfile +9 -9
  2. data/README.md +39 -2
  3. data/Rakefile +25 -44
  4. data/js/map-reduce-utils.js +174 -0
  5. data/js/underscore-min.js +24 -0
  6. data/lib/qme/importer/code_system_helper.rb +26 -0
  7. data/lib/qme/importer/entry.rb +89 -0
  8. data/lib/qme/importer/generic_importer.rb +71 -0
  9. data/lib/qme/importer/hl7_helper.rb +27 -0
  10. data/lib/qme/importer/patient_importer.rb +150 -0
  11. data/lib/qme/importer/property_matcher.rb +103 -0
  12. data/lib/qme/importer/section_importer.rb +82 -0
  13. data/lib/qme/map/map_reduce_builder.rb +77 -147
  14. data/lib/qme/map/map_reduce_executor.rb +97 -13
  15. data/lib/qme/measure/database_loader.rb +90 -0
  16. data/lib/qme/measure/measure_loader.rb +141 -0
  17. data/lib/qme/mongo_helpers.rb +15 -0
  18. data/lib/qme/randomizer/patient_randomizer.rb +95 -0
  19. data/lib/qme_test.rb +13 -0
  20. data/lib/quality-measure-engine.rb +20 -4
  21. data/lib/tasks/measure.rake +76 -0
  22. data/lib/tasks/mongo.rake +74 -0
  23. data/lib/tasks/patient_random.rake +46 -0
  24. metadata +110 -156
  25. data/.gitignore +0 -6
  26. data/Gemfile.lock +0 -44
  27. data/fixtures/complex_measure.json +0 -36
  28. data/fixtures/result_example.json +0 -6
  29. data/lib/patches/v8.rb +0 -20
  30. data/lib/qme/query/json_document_builder.rb +0 -130
  31. data/lib/qme/query/json_query_executor.rb +0 -44
  32. data/measures/0032/0032_NQF_Cervical_Cancer_Screening.json +0 -171
  33. data/measures/0032/patients/denominator1.json +0 -10
  34. data/measures/0032/patients/denominator2.json +0 -10
  35. data/measures/0032/patients/numerator1.json +0 -11
  36. data/measures/0032/patients/population1.json +0 -9
  37. data/measures/0032/patients/population2.json +0 -11
  38. data/measures/0032/result/result.json +0 -6
  39. data/measures/0043/0043_NQF_PneumoniaVaccinationStatusForOlderAdults.json +0 -71
  40. data/measures/0043/patients/denominator.json +0 -11
  41. data/measures/0043/patients/numerator.json +0 -11
  42. data/measures/0043/patients/population.json +0 -10
  43. data/measures/0043/result/result.json +0 -6
  44. data/quality-measure-engine.gemspec +0 -97
  45. data/schema/result.json +0 -28
  46. data/schema/schema.json +0 -143
  47. data/spec/qme/map/map_reduce_builder_spec.rb +0 -64
  48. data/spec/qme/measures_spec.rb +0 -50
  49. data/spec/qme/query/json_document_builder_spec.rb +0 -56
  50. data/spec/schema_spec.rb +0 -21
  51. data/spec/spec_helper.rb +0 -7
  52. data/spec/validate_measures_spec.rb +0 -21
data/.gitignore DELETED
@@ -1,6 +0,0 @@
1
- .bundle
2
- *.DS_Store
3
- vendor
4
- nbproject
5
- pkg
6
- .redcar
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
- }
@@ -1,6 +0,0 @@
1
- {
2
- "initialPopulation": 50,
3
- "denominator": 45,
4
- "numerator": [35],
5
- "exclusions": [5]
6
- }
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
- }