quality-measure-engine 0.1.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/.gitignore ADDED
@@ -0,0 +1,6 @@
1
+ .bundle
2
+ *.DS_Store
3
+ vendor
4
+ nbproject
5
+ pkg
6
+ .redcar
data/Gemfile ADDED
@@ -0,0 +1,18 @@
1
+ source :gemcutter
2
+
3
+ gem 'mongo'
4
+ gem 'mongomatic'
5
+ gem 'bson_ext'
6
+ gem 'rake'
7
+ gem 'therubyracer', :require => 'v8'
8
+
9
+ group :test do
10
+ gem 'rspec'
11
+ gem 'jsonschema'
12
+ gem 'awesome_print', :require => 'ap'
13
+ end
14
+
15
+ group :build do
16
+ gem 'jeweler'
17
+ end
18
+
data/Gemfile.lock ADDED
@@ -0,0 +1,50 @@
1
+ GEM
2
+ remote: http://rubygems.org/
3
+ specs:
4
+ activesupport (3.0.1)
5
+ awesome_print (0.2.1)
6
+ bson (1.1)
7
+ bson_ext (1.1.1)
8
+ diff-lcs (1.1.2)
9
+ gemcutter (0.6.1)
10
+ git (1.2.5)
11
+ jeweler (1.4.0)
12
+ gemcutter (>= 0.1.0)
13
+ git (>= 1.2.5)
14
+ rubyforge (>= 2.0.0)
15
+ json_pure (1.4.6)
16
+ jsonschema (2.0.0)
17
+ mongo (1.1)
18
+ bson (>= 1.0.5)
19
+ mongomatic (0.5.8)
20
+ activesupport (>= 2.3.5)
21
+ bson (= 1.1)
22
+ mongo (= 1.1)
23
+ rake (0.8.7)
24
+ rspec (2.0.0)
25
+ rspec-core (= 2.0.0)
26
+ rspec-expectations (= 2.0.0)
27
+ rspec-mocks (= 2.0.0)
28
+ rspec-core (2.0.0)
29
+ rspec-expectations (2.0.0)
30
+ diff-lcs (>= 1.1.2)
31
+ rspec-mocks (2.0.0)
32
+ rspec-core (= 2.0.0)
33
+ rspec-expectations (= 2.0.0)
34
+ rubyforge (2.0.4)
35
+ json_pure (>= 1.1.7)
36
+ therubyracer (0.7.5)
37
+
38
+ PLATFORMS
39
+ ruby
40
+
41
+ DEPENDENCIES
42
+ awesome_print
43
+ bson_ext
44
+ jeweler
45
+ jsonschema
46
+ mongo
47
+ mongomatic
48
+ rake
49
+ rspec
50
+ therubyracer
data/README.md ADDED
@@ -0,0 +1,24 @@
1
+ This project will provide a library that can ingest HITSP C32's and ASTM CCR's and extract values needed to compute heath quality measures for a population. It will then be able to query over a population to compute how many people within a population conform to the measure.
2
+
3
+ Environment
4
+ -----------
5
+
6
+ This project currently uses Ruby 1.9.2 and is built using [Bundler](http://gembundler.com/). To get all of the dependencies for the project, first install bundler:
7
+
8
+ gem install bundler
9
+
10
+ Then run bundler to grab all of the necessay gems:
11
+
12
+ bundle install
13
+
14
+ Testing
15
+ -------
16
+
17
+ This project uses [RSpec](http://github.com/rspec/rspec-core) for testing. To run the suite, just enter the following:
18
+
19
+ rake spec
20
+
21
+ Project Practices
22
+ ------------------
23
+
24
+ Please try to follow our [Coding Style Guides](http://github.com/eedrummer/styleguide). Additionally, we will be using git in a pattern similar to [Vincent Driessen's workflow](http://nvie.com/posts/a-successful-git-branching-model/). While feature branches are encouraged, they are not required to work on the project.
data/Rakefile ADDED
@@ -0,0 +1,26 @@
1
+ require 'rspec/core/rake_task'
2
+ require 'jeweler'
3
+
4
+ RSpec::Core::RakeTask.new do |t|
5
+ t.rspec_opts = ["-c", "-f progress", "-r ./spec/spec_helper.rb"]
6
+ t.pattern = 'spec/**/*_spec.rb'
7
+ end
8
+
9
+ Jeweler::Tasks.new do |gem|
10
+ gem.name = "quality-measure-engine"
11
+ gem.summary = "A library for extracting quality measure information from HITSP C32's and ASTM CCR's"
12
+ gem.description = "A library for extracting quality measure information from HITSP C32's and ASTM CCR's"
13
+ gem.email = "talk@projectpophealth.org"
14
+ gem.homepage = "http://github.com/pophealth/quality-measure-engine"
15
+ gem.authors = ["Marc Hadley", "Andy Gregorowicz"]
16
+
17
+ gem.add_dependency 'mongo', '~> 1.1'
18
+ gem.add_dependency 'mongomatic', '~> 0.5.8'
19
+ gem.add_dependency 'therubyracer', '~> 0.7.5'
20
+ gem.add_dependency 'bson_ext', '~> 1.1.1'
21
+
22
+ gem.add_development_dependency "jsonschema", "~> 2.0.0"
23
+ gem.add_development_dependency "rspec", "~> 2.0.0"
24
+ gem.add_development_dependency "awesome_print", "~> 0.2.1"
25
+ gem.add_development_dependency "jeweler", "~> 1.4.0"
26
+ end
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.1.0
@@ -0,0 +1,36 @@
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
+ }
@@ -0,0 +1,6 @@
1
+ {
2
+ "initialPopulation": 50,
3
+ "denominator": 45,
4
+ "numerator": [35],
5
+ "exclusions": [5]
6
+ }
data/lib/patches/v8.rb ADDED
@@ -0,0 +1,20 @@
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
@@ -0,0 +1,169 @@
1
+ module QME
2
+ module MapReduce
3
+ class Builder
4
+ attr_reader :id, :parameters
5
+
6
+ YEAR_IN_SECONDS = 365*24*60*60
7
+
8
+ def initialize(measure_def, params)
9
+ @measure_def = measure_def
10
+ @id = measure_def['id']
11
+ @parameters = {}
12
+ measure_def['parameters'] ||= {}
13
+ measure_def['parameters'].each do |parameter, value|
14
+ if !params.has_key?(parameter.intern)
15
+ raise "No value supplied for measure parameter: #{parameter}"
16
+ end
17
+ @parameters[parameter.intern] = params[parameter.intern]
18
+ end
19
+ ctx = V8::Context.new
20
+ ctx['year']=YEAR_IN_SECONDS
21
+ @parameters.each do |key, param|
22
+ ctx[key]=param
23
+ end
24
+ measure_def['calculated_dates'] ||= {}
25
+ measure_def['calculated_dates'].each do |parameter, value|
26
+ @parameters[parameter.intern]=ctx.eval(value)
27
+ end
28
+ @property_prefix = 'this.measures["'+@id+'"].'
29
+ end
30
+
31
+ def map_function
32
+ "function () {\n" +
33
+ " var value = {i: 0, d: 0, n: 0, e: 0};\n" +
34
+ " if #{population} {\n" +
35
+ " value.i++;\n" +
36
+ " if #{denominator} {\n" +
37
+ " value.d++;\n" +
38
+ " if #{numerator} {\n" +
39
+ " value.n++;\n" +
40
+ " } else if #{exception} {\n" +
41
+ " value.e++;\n" +
42
+ " value.d--;\n" +
43
+ " }\n" +
44
+ " }\n" +
45
+ " }\n" +
46
+ " emit(null, value);\n" +
47
+ "};\n"
48
+ end
49
+
50
+ REDUCE_FUNCTION = <<END_OF_REDUCE_FN
51
+ function (key, values) {
52
+ var total = {i: 0, d: 0, n: 0, e: 0};
53
+ for (var i = 0; i < values.length; i++) {
54
+ total.i += values[i].i;
55
+ total.d += values[i].d;
56
+ total.n += values[i].n;
57
+ total.e += values[i].e;
58
+ }
59
+ return total;
60
+ };
61
+ END_OF_REDUCE_FN
62
+
63
+ def reduce_function
64
+ REDUCE_FUNCTION
65
+ end
66
+
67
+ def population
68
+ javascript(@measure_def['population'])
69
+ end
70
+
71
+ def denominator
72
+ javascript(@measure_def['denominator'])
73
+ end
74
+
75
+ def numerator
76
+ javascript(@measure_def['numerator'])
77
+ end
78
+
79
+ def exception
80
+ javascript(@measure_def['exception'])
81
+ end
82
+
83
+ def javascript(expr)
84
+ if expr.has_key?('query')
85
+ # leaf node
86
+ query = expr['query']
87
+ triple = leaf_expr(query)
88
+ property_name = munge_property_name(triple[0])
89
+ '('+property_name+triple[1]+triple[2]+')'
90
+ elsif expr.size==1
91
+ operator = expr.keys[0]
92
+ result = logical_expr(operator, expr[operator])
93
+ operator = result.shift
94
+ js = '('
95
+ result.each_with_index do |operand,index|
96
+ if index>0
97
+ js+=operator
98
+ end
99
+ js+=operand
100
+ end
101
+ js+=')'
102
+ js
103
+ elsif expr.size==0
104
+ '(false)'
105
+ else
106
+ throw "Unexpected number of keys in: #{expr}"
107
+ end
108
+ end
109
+
110
+ def munge_property_name(name)
111
+ if name=='birthdate'
112
+ 'this.'+name
113
+ else
114
+ @property_prefix+name
115
+ end
116
+ end
117
+
118
+ def logical_expr(operator, args)
119
+ operands = args.collect { |arg| javascript(arg) }
120
+ [get_operator(operator)].concat(operands)
121
+ end
122
+
123
+ def leaf_expr(query)
124
+ property_name = query.keys[0]
125
+ property_value_expression = query[property_name]
126
+ if property_value_expression.kind_of?(Hash)
127
+ operator = property_value_expression.keys[0]
128
+ value = property_value_expression[operator]
129
+ [property_name, get_operator(operator), get_value(value)]
130
+ else
131
+ [property_name, '==', get_value(property_value_expression)]
132
+ end
133
+ end
134
+
135
+ def get_operator(operator)
136
+ case operator
137
+ when '_eql'
138
+ '=='
139
+ when '_gt'
140
+ '>'
141
+ when '_gte'
142
+ '>='
143
+ when '_lt'
144
+ '<'
145
+ when '_lte'
146
+ '<='
147
+ when 'and'
148
+ '&&'
149
+ when 'or'
150
+ '||'
151
+ else
152
+ throw "Unknown operator: #{operator}"
153
+ end
154
+ end
155
+
156
+ def get_value(value)
157
+ if value.kind_of?(String) && value[0]=='@'
158
+ @parameters[value[1..-1].intern].to_s
159
+ elsif value.kind_of?(String)
160
+ '"'+value+'"'
161
+ elsif value==nil
162
+ 'null'
163
+ else
164
+ value.to_s
165
+ end
166
+ end
167
+ end
168
+ end
169
+ end
@@ -0,0 +1,31 @@
1
+ module QME
2
+ module MapReduce
3
+ class Executor
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
+
15
+ measure = Builder.new(measure_def(measure_id), parameter_values)
16
+
17
+ records = @db.collection('records')
18
+ results = records.map_reduce(measure.map_function, measure.reduce_function)
19
+ result = results.find.to_a[0]
20
+ value = result['value']
21
+
22
+ {
23
+ :population=>value['i'].to_i,
24
+ :denominator=> value['d'].to_i,
25
+ :numerator=> value['n'].to_i,
26
+ :exceptions=> value['e'].to_i
27
+ }
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,124 @@
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
+ ctx = V8::Context.new
22
+ @parameters.each_pair do |key, value|
23
+ ctx[key] = value
24
+ end
25
+
26
+ ctx['year'] = 365 * 24 * 60 * 60 # TODO: Replace this with a js file that has all constants
27
+
28
+ @calculated_dates = {}
29
+ @measure_json["calculated_dates"].each_pair do |key, value|
30
+ @calculated_dates[key] = ctx.eval(value)
31
+ end
32
+ end
33
+
34
+ def numerator_query
35
+ create_query(@measure_json['numerator']).merge(denominator_query)
36
+ end
37
+
38
+ def denominator_query
39
+ create_query(@measure_json['denominator']).merge(initial_population_query)
40
+ end
41
+
42
+ def initial_population_query
43
+ create_query(@measure_json['population'])
44
+ end
45
+
46
+ def exclusions_query
47
+ create_query(@measure_json['exception'])
48
+ end
49
+
50
+ # Creates the appropriate JSON document to query MongoDB based on
51
+ # a measure definition passed in. This method calls itself recursively
52
+ # to walk the tree and get all possible logical operators available
53
+ # in a measure definition
54
+ def create_query(definition_json, args={})
55
+ if definition_json.has_key?('and')
56
+ definition_json['and'].each do |operand|
57
+ create_query(operand, args)
58
+ end
59
+ elsif definition_json.has_key?('or')
60
+ operands = []
61
+ definition_json['or'].each do |operand|
62
+ operands << create_query(operand)
63
+ end
64
+ args['$or'] = operands
65
+ elsif definition_json.has_key?('query')
66
+ process_query(definition_json['query'], args)
67
+ end
68
+
69
+ args
70
+ end
71
+
72
+ # Called by create_query to process leaf nodes in a measure
73
+ # definition
74
+ def process_query(definition_json, args)
75
+ if definition_json.size > 1
76
+ raise 'A query should have only one property'
77
+ end
78
+
79
+ query_property = definition_json.keys.first
80
+ document_key = transform_query_property(query_property)
81
+ document_value = nil
82
+ query_value = definition_json[query_property]
83
+ if query_value.kind_of?(Hash)
84
+ if query_value.size > 1
85
+ raise 'A query value should only have one property'
86
+ end
87
+
88
+ document_value = {query_value.keys.first.gsub('_', '$') =>
89
+ substitute_variables(query_value.values.first)}
90
+ if args[document_key]
91
+ args[document_key].merge!(document_value)
92
+ else
93
+ args[document_key] = document_value
94
+ end
95
+ else
96
+ document_value = substitute_variables(query_value)
97
+ args[document_key] = document_value
98
+ end
99
+ end
100
+
101
+ # Takes a query property name and transforms it into
102
+ # a name of a key in a MongoDB document
103
+ def transform_query_property(property_name)
104
+ #TODO What do we do with special case fields - the stuff we are keeping at the patient level?
105
+ if ['birthdate'].include?(property_name)
106
+ property_name
107
+ else
108
+ "measures.#{@measure_id}.#{property_name}"
109
+ end
110
+ end
111
+
112
+ # Finds strings that start with "@" and replaces them
113
+ # with the calculated date
114
+ def substitute_variables(value)
115
+ if value.kind_of?(String) && value[0] == '@'
116
+ variable_name = value[1..-1]
117
+ @calculated_dates[variable_name]
118
+ else
119
+ value
120
+ end
121
+ end
122
+ end
123
+ end
124
+ end