quality-measure-engine 1.0.4 → 1.1.0

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