quality-measure-engine 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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