quality-measure-engine 1.0.4 → 1.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/Gemfile CHANGED
@@ -5,11 +5,12 @@ gemspec :development_group => :test
5
5
  gem 'mongo', '1.5.1'
6
6
  gem 'bson_ext', '1.5.1', :platforms => :mri
7
7
  gem 'rake'
8
- gem 'pry', :require => true
8
+ #gem 'pry', :require => true
9
+ #gem 'health-data-standards', :git => 'https://github.com/projectcypress/health-data-standards.git', :branch => 'master'
10
+ gem 'health-data-standards', '0.7.0'
9
11
 
10
12
  group :test do
11
13
  gem 'cover_me', '>= 1.0.0.rc5', :platforms => :ruby_19
12
- gem 'metric_fu'
13
14
  gem 'sinatra'
14
15
  end
15
16
 
data/Rakefile CHANGED
@@ -1,6 +1,5 @@
1
1
  require 'rspec/core/rake_task'
2
2
  require 'yard'
3
- require 'metric_fu'
4
3
  require 'resque/tasks'
5
4
 
6
5
  ENV['MEASURE_DIR'] = ENV['MEASURE_DIR'] || File.join('fixtures', 'measure_defs')
@@ -29,11 +28,4 @@ end
29
28
  task :coverage do
30
29
  Rake::Task['spec'].invoke
31
30
  Rake::Task['cover_me:report'].invoke
32
- end
33
-
34
- MetricFu::Configuration.run do |config|
35
- #define which metrics you want to use
36
- config.metrics = [:roodi, :reek, :churn, :flog, :flay]
37
- config.graphs = [:flog, :flay]
38
- config.flay ={:dirs_to_flay => []} #Flay doesn't seem to be handling CLI arguments well... so this config squashes them
39
31
  end
data/VERSION CHANGED
@@ -1 +1 @@
1
- 1.0.4
1
+ 1.1.0
@@ -120,7 +120,7 @@ function() {
120
120
  root.map = function(record, population, denominator, numerator, exclusion) {
121
121
  var value = {population: false, denominator: false, numerator: false,
122
122
  exclusions: false, antinumerator: false, patient_id: record._id,
123
- medical_record_id: record.patient_id,
123
+ medical_record_id: record.medical_record_number,
124
124
  first: record.first, last: record.last, gender: record.gender,
125
125
  birthdate: record.birthdate, test_id: record.test_id,
126
126
  provider_performances: record.provider_performances,
@@ -24,6 +24,10 @@ module QME
24
24
  if @db == nil
25
25
  @db = Mongo::Connection.new(@db_host, @db_port).db(@db_name)
26
26
  end
27
+ Mongoid.configure do |config|
28
+ config.master = Mongo::Connection.new(@db_host, @db_port).db(@db_name)
29
+ end
30
+
27
31
  @db
28
32
  end
29
33
  end
@@ -0,0 +1,49 @@
1
+ # Extensions to the Record model in health-data-standards to support
2
+ # quality measure calculation
3
+ class Record
4
+ extend ActiveSupport::Memoizable
5
+
6
+ def procedure_results
7
+ results.to_a + vital_signs.to_a + procedures.to_a
8
+ end
9
+
10
+ def laboratory_tests
11
+ results.to_a + vital_signs.to_a
12
+ end
13
+
14
+ def all_meds
15
+ medications.to_a + immunizations.to_a
16
+ end
17
+
18
+ def active_diagnosis
19
+ conditions.any_of({:status => 'active'}, {:status => nil}).to_a +
20
+ social_history.any_of({:status => 'active'}, {:status => nil}).to_a
21
+ end
22
+
23
+ def inactive_diagnosis
24
+ conditions.any_of({:status => 'inactive'}, {:status => nil}).to_a +
25
+ social_history.any_of({:status => 'inactive'}, {:status => nil}).to_a
26
+ end
27
+
28
+ def resolved_diagnosis
29
+ conditions.any_of({:status => 'resolved'}, {:status => nil}).to_a +
30
+ social_history.any_of({:status => 'resolved'}, {:status => nil}).to_a
31
+ end
32
+
33
+ def all_problems
34
+ conditions.to_a + social_history.to_a
35
+ end
36
+
37
+ def all_devices
38
+ conditions.to_a + procedures.to_a + care_goals.to_a + medical_equipment.to_a
39
+ end
40
+
41
+ memoize :procedure_results
42
+ memoize :laboratory_tests
43
+ memoize :all_meds
44
+ memoize :active_diagnosis
45
+ memoize :inactive_diagnosis
46
+ memoize :resolved_diagnosis
47
+ memoize :all_problems
48
+ memoize :all_devices
49
+ end
@@ -91,7 +91,7 @@ module QME
91
91
  # Creates a Hash for this Entry
92
92
  # @return [Hash] a Hash representing the Entry
93
93
  def to_hash
94
- entry_hash = {}
94
+ entry_hash = {'_id' => BSON::ObjectId.new}
95
95
  entry_hash['codes'] = @codes
96
96
  unless @value.empty?
97
97
  entry_hash['value'] = @value
@@ -23,13 +23,12 @@ module QME
23
23
  #
24
24
  # @param [Hash] patient_hash representation of a patient
25
25
  # @return [Hash] measure information
26
- def parse(patient_hash)
26
+ def parse(patient)
27
27
  measure_info = {}
28
28
  @definition['measure'].each_pair do |property, description|
29
29
  raise "No standard_category for #{property}" if !description['standard_category']
30
30
  matcher = PropertyMatcher.new(description)
31
- enrty_filter = filter_for_property(description['standard_category'], description['qds_data_type'])
32
- entry_list = enrty_filter.call(patient_hash)
31
+ entry_list = filter_for_property(description['standard_category'], description['qds_data_type'], patient)
33
32
  if ! entry_list.empty?
34
33
  matched_list = matcher.match(entry_list)
35
34
  measure_info[property] = matched_list if matched_list.length > 0
@@ -40,89 +39,67 @@ module QME
40
39
 
41
40
  private
42
41
 
43
- def create_section_filter(*sections)
44
- Proc.new do |patient_hash|
45
- sections.map do |section|
46
- if patient_hash[section]
47
- patient_hash[section]
48
- else
49
- []
50
- end
51
- end.flatten
52
- end
53
- end
54
-
55
- def create_section_and_status_filter(status, *sections)
56
- section_filter = create_section_filter(*sections)
57
- Proc.new do |patient_hash|
58
- entry_list = section_filter.call(patient_hash)
59
- entry_list.select do |entry|
60
- entry.status.nil? || entry.status == status
61
- end
62
- end
63
- end
64
-
65
- def filter_for_property(standard_category, qds_data_type)
42
+ def filter_for_property(standard_category, qds_data_type, patient)
66
43
  # Currently unsupported categories: negation_rationale, risk_category_assessment
67
44
  case standard_category
68
45
  when 'encounter'
69
- create_section_filter(:encounters)
46
+ patient.encounters
70
47
  when 'procedure'
71
48
  case qds_data_type
72
49
  when 'procedure_performed'
73
- create_section_filter(:procedures)
50
+ patient.procedures
74
51
  when 'procedure_adverse_event', 'procedure_intolerance'
75
- create_section_filter(:allergies)
52
+ patient.allergies
76
53
  when 'procedure_result'
77
- create_section_filter(:procedures, :results, :vital_signs)
54
+ patient.procedure_results
78
55
  end
79
56
  when 'risk_category_assessment'
80
- create_section_filter(:procedures)
57
+ patient.procedures
81
58
  when 'communication'
82
- create_section_filter(:procedures)
59
+ patient.procedures
83
60
  when 'laboratory_test'
84
- create_section_filter(:results, :vital_signs)
61
+ patient.laboratory_tests
85
62
  when 'physical_exam'
86
- create_section_filter(:vital_signs)
63
+ patient.vital_signs
87
64
  when 'medication'
88
65
  case qds_data_type
89
66
  when 'medication_dispensed', 'medication_order', 'medication_active', 'medication_administered'
90
- create_section_filter(:medications, :immunizations)
67
+ patient.all_meds
91
68
  when 'medication_allergy', 'medication_intolerance', 'medication_adverse_event'
92
- create_section_filter(:allergies)
69
+ patient.allergies
93
70
  end
94
71
  when 'diagnosis_condition_problem'
95
72
  case qds_data_type
96
73
  when 'diagnosis_active'
97
- create_section_and_status_filter(:active, :conditions, :social_history)
74
+ patient.active_diagnosis
98
75
  when 'diagnosis_inactive'
99
- create_section_and_status_filter(:inactive, :conditions, :social_history)
76
+ patient.inactive_diagnosis
100
77
  when 'diagnosis_resolved'
101
- create_section_and_status_filter(:resolved, :conditions, :social_history)
78
+ patient.resolved_diagnosis
102
79
  end
103
80
  when 'symptom'
104
- create_section_filter(:conditions, :social_history)
81
+ patient.all_problems
105
82
  when 'individual_characteristic'
106
- create_section_filter(:conditions, :social_history)
83
+ patient.all_problems
107
84
  when 'device'
108
85
  case qds_data_type
109
86
  when 'device_applied'
110
- create_section_filter(:conditions, :procedures, :care_goals, :medical_equipment)
87
+ patient.all_devices
111
88
  when 'device_allergy'
112
- create_section_filter(:allergies)
89
+ patient.allergies
113
90
  end
114
91
  when 'care_goal'
115
- create_section_filter(:care_goals)
92
+ patient.care_goals
116
93
  when 'diagnostic_study'
117
- create_section_filter(:procedures)
94
+ patient.procedures
118
95
  when 'substance'
119
- create_section_filter(:allergies)
96
+ patient.allergies
120
97
  else
121
98
  unless self.class.warnings.include?(standard_category)
122
99
  puts "Warning: Unsupported standard_category (#{standard_category})"
123
100
  self.class.warnings << standard_category
124
101
  end
125
- Proc.new {[]} # A proc that returns an empty array
102
+ []
126
103
  end
127
104
  end
128
105
  end
@@ -0,0 +1,39 @@
1
+ module QME
2
+ module Importer
3
+ class MeasurePropertiesGenerator
4
+ include Singleton
5
+
6
+ def initialize
7
+ @measure_importers = {}
8
+ end
9
+
10
+ # Adds a GenericImporter that will be used to extract
11
+ # information about a measure from a patient Record.
12
+ def add_measure(measure_id, importer)
13
+ @measure_importers[measure_id] = importer
14
+ end
15
+
16
+ # Generates denormalized measure information
17
+ # Measure information is contained in a Hash that hash
18
+ # the measure id as a key, and the denormalized
19
+ # measure information as a value
20
+ #
21
+ # @param [Record] patient - populated patient record
22
+ # @returns Hash with denormalized measure information
23
+ def generate_properties(patient)
24
+ measures = {}
25
+ @measure_importers.each_pair do |measure_id, importer|
26
+ measures[measure_id] = importer.parse(patient)
27
+ end
28
+ measures
29
+ end
30
+
31
+ # The same as generate_properties but addes the denormalized
32
+ # measure information into the Record and saves it.
33
+ def generate_properties!(patient)
34
+ patient['measures'] = generate_properties(patient)
35
+ patient.save!
36
+ end
37
+ end
38
+ end
39
+ end
@@ -11,16 +11,19 @@ module QME
11
11
  # @param [Nokogiri::XML::Document] doc It is expected that the root node of this document
12
12
  # will have the "cda" namespace registered to "urn:hl7-org:v3"
13
13
  # @return [Array] an array of providers found in the document
14
- def extract_providers(doc)
15
-
16
- performers = doc.xpath("//cda:documentationOf/cda:serviceEvent/cda:performer")
14
+ def extract_providers(doc, use_encounters=false)
17
15
 
16
+
17
+ xpath_base = use_encounters ? "//cda:encounter/cda:performer" : "//cda:documentationOf/cda:serviceEvent/cda:performer"
18
+
19
+ performers = doc.xpath(xpath_base)
20
+
18
21
  providers = performers.map do |performer|
19
22
  provider = {}
20
23
  entity = performer.xpath(performer, "./cda:assignedEntity")
21
24
  name = entity.xpath("./cda:assignedPerson/cda:name")
22
25
  provider[:title] = extract_data(name, "./cda:prefix")
23
- provider[:given_name] = extract_data(name, "./cda:given")
26
+ provider[:given_name] = extract_data(name, "./cda:given[1]")
24
27
  provider[:family_name] = extract_data(name, "./cda:family")
25
28
  provider[:phone] = extract_data(entity, "./cda:telecom/@value") { |text| text.gsub("tel:", "") }
26
29
  provider[:organization] = extract_data(entity, "./cda:representedOrganization/cda:name")
@@ -97,7 +97,7 @@ module QME
97
97
  var tmp = [];
98
98
  for(var i=0; i<patient.provider_performances.length; i++) {
99
99
  var value = patient.provider_performances[i];
100
- if (value['start_date'] <= #{@params['effective_date']} && (value['end_date'] >= #{@params['effective_date']} || value['end_date'] == null))
100
+ if ((value['start_date'] <= #{@params['effective_date']} || value['start_date'] == null) && (value['end_date'] >= #{@params['effective_date']} || value['end_date'] == null))
101
101
  tmp.push(value);
102
102
  }
103
103
  if (tmp.length == 0) tmp = null;
@@ -32,36 +32,24 @@ module QME
32
32
  base_query.merge!(filter_parameters)
33
33
 
34
34
  query = base_query.clone
35
-
36
- query.merge!({'value.manual_exclusion'=>{'$ne'=>true}})
35
+
36
+ query.merge!({'value.manual_exclusion' => {'$in' => [nil, false]}})
37
37
 
38
38
  result = {:measure_id => @measure_id, :sub_id => @sub_id,
39
39
  :effective_date => @parameter_values['effective_date'],
40
40
  :test_id => @parameter_values['test_id'], :filters => @parameter_values['filters']}
41
41
 
42
- aggregate = patient_cache.group({cond: query,
43
- initial: {population: 0, denominator: 0, numerator: 0, antinumerator: 0, exclusions: 0, considered: 0},
44
- reduce: "function(record,sums) {
45
- for (var key in sums) {
46
- sums[key] += (record['value'][key] || key == 'considered') ? 1 : 0
47
- }
48
- }"}).first
49
-
50
- aggregate ||= {"population"=>0, "denominator"=>0, "numerator"=>0, "antinumerator"=>0, "exclusions"=>0}
51
- aggregate.each {|key, value| aggregate[key] = value.to_i}
52
- aggregate['exclusions'] += patient_cache.find(base_query.merge({'value.manual_exclusion'=>true})).count
42
+ # need to time the old way agains the single query to verify that the single query is more performant
43
+ aggregate = {"population"=>0, "denominator"=>0, "numerator"=>0, "antinumerator"=>0, "exclusions"=>0}
44
+ %w(population denominator numerator antinumerator exclusions).each do |measure_group|
45
+ patient_cache.find(query.merge("value.#{measure_group}" => true)) do |cursor|
46
+ aggregate[measure_group] = cursor.count
47
+ end
48
+ end
49
+ aggregate["considered"] = patient_cache.find(query).count
50
+ aggregate["exclusions"] += patient_cache.find(base_query.merge({'value.manual_exclusion'=>true})).count
53
51
  result.merge!(aggregate)
54
52
 
55
- # # need to time the old way agains the single query to verify that the single query is more performant
56
- # aggregate = {population: 0, denominator: 0, numerator: 0, antinumerator: 0, exclusions: 0}
57
- # %w(population denominator numerator antinumerator exclusions).each do |measure_group|
58
- # patient_cache.find(query.merge("value.#{measure_group}" => true)) do |cursor|
59
- # aggregate[measure_group] = cursor.count
60
- # end
61
- # end
62
- # aggregate[:considered] = patient_cache.find(query).count
63
- # result.merge!(aggregate)
64
-
65
53
  result.merge!(execution_time: (Time.now.to_i - @parameter_values['start_time'].to_i)) if @parameter_values['start_time']
66
54
 
67
55
  get_db.collection("query_cache").save(result, safe: true)
@@ -92,11 +80,27 @@ module QME
92
80
  records.map_reduce(measure.map_function, "function(key, values){return values;}",
93
81
  :out => {:reduce => 'patient_cache'},
94
82
  :finalize => measure.finalize_function,
95
- :query => {:patient_id => patient_id, :test_id => @parameter_values['test_id']})
83
+ :query => {:medical_record_number => patient_id, :test_id => @parameter_values['test_id']})
96
84
  apply_manual_exclusions
97
85
  end
98
86
 
99
- # This records collects the set of manual exclusions from the manual_exclusions collections
87
+ # This method runs the MapReduce job for the measure and a specific patient.
88
+ # This will *not* create a document in the patient_cache collection, instead the
89
+ # result is returned directly.
90
+ def get_patient_result(patient_id)
91
+ qm = QualityMeasure.new(@measure_id, @sub_id)
92
+ measure = Builder.new(get_db, qm.definition, @parameter_values)
93
+ records = get_db.collection('records')
94
+ result = records.map_reduce(measure.map_function, "function(key, values){return values;}",
95
+ :out => {:inline => true},
96
+ :raw => true,
97
+ :finalize => measure.finalize_function,
98
+ :query => {:medical_record_number => patient_id, :test_id => @parameter_values['test_id']})
99
+ raise result['err'] if result['ok']!=1
100
+ result['results'][0]['value']
101
+ end
102
+
103
+ # This collects the set of manual exclusions from the manual_exclusions collections
100
104
  # and sets a flag in each cached patient result for patients that have been excluded from the
101
105
  # current measure
102
106
  def apply_manual_exclusions
@@ -24,21 +24,20 @@ module QME
24
24
  QME::QualityMeasure.all.each_value do |measure_def|
25
25
  measure_id = measure_def['id']
26
26
  if !processed_measures[measure_id]
27
- QME::Importer::PatientImporter.instance.add_measure(measure_id, QME::Importer::GenericImporter.new(measure_def))
28
- processed_measures[measure_id]=true
27
+ QME::Importer::MeasurePropertiesGenerator.instance.add_measure(measure_id, QME::Importer::GenericImporter.new(measure_def))
28
+ processed_measures[measure_id] = true
29
29
  end
30
30
  end
31
31
 
32
- loader = QME::Database::Loader.new()
33
32
  tick('Generating patients')
34
33
  count.times do |i|
35
34
  at(i, count, "Generating patient #{i} of #{count}")
36
35
  template = templates[rand(templates.length)]
37
36
  generator = QME::Randomizer::Patient.new(template)
38
37
  json = JSON.parse(generator.get())
39
- patient_record_hash = QME::Importer::PatientImporter.instance.parse_hash(json)
40
- patient_record_hash['test_id'] = test_id
41
- loader.save('records', patient_record_hash)
38
+ patient_record = RandomPatientCreator.parse_hash(json)
39
+ patient_record.test_id = test_id
40
+ patient_record.save!
42
41
  end
43
42
 
44
43
  completed
@@ -0,0 +1,47 @@
1
+ module QME
2
+ module Randomizer
3
+ class RandomPatientCreator
4
+
5
+ # Parses a patient hash containing demographic and event information
6
+ #
7
+ # @param [Hash] patient_hash patient data
8
+ # @return [Record] a representation of the patient that can be inserted into MongoDB
9
+ def self.parse_hash(patient_hash)
10
+ patient_record = Record.new
11
+ patient_record.first = patient_hash['first']
12
+ patient_record.last = patient_hash['last']
13
+ patient_record.gender = patient_hash['gender']
14
+ patient_record.medical_record_number = patient_hash['medical_record_number']
15
+ patient_record.birthdate = patient_hash['birthdate']
16
+ patient_record.race = patient_hash['race']
17
+ patient_record.ethnicity = patient_hash['ethnicity']
18
+ # pophealth needs languages... please do not remove
19
+ patient_record.languages = patient_hash['languages']
20
+ #patient_record['addresses'] = patient_hash['addresses']
21
+ patient_hash['events'].each do |key, value|
22
+ patient_record.send("#{key}=".to_sym, parse_events(value))
23
+ end
24
+ patient_record['measures'] = QME::Importer::MeasurePropertiesGenerator.instance.generate_properties(patient_record)
25
+
26
+ patient_record
27
+ end
28
+
29
+ # Parses a list of event hashes into an array of Entry objects
30
+ #
31
+ # @param [Array] event_list list of event hashes
32
+ # @return [Array] array of Entry objects
33
+ def self.parse_events(event_list)
34
+ event_list.collect do |event|
35
+ if event.class==String.class
36
+ # skip String elements in the event list, patient randomization templates
37
+ # introduce String elements to simplify tailing-comma handling when generating
38
+ # JSON using ERb
39
+ nil
40
+ else
41
+ Entry.from_event_hash(event)
42
+ end
43
+ end.compact
44
+ end
45
+ end
46
+ end
47
+ end