Class: Quby::Answers::Services::ScoreCalculator

Inherits:
Object
  • Object
show all
Defined in:
lib/quby/answers/services/score_calculator.rb

Defined Under Namespace

Classes: MissingAnswerValues, UnknownFieldsReferenced

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(questionnaire:, values:, observation_time:, patient_attrs: {}, respondent_attrs: {}) ⇒ ScoreCalculator

Public: Initialize a new ScoreCalculator

values - The Hash values describes the keys of questions and the values of the answer given to that question. observation_time - The Time to be used to calculate the age of the patient. patient_attrs - A Hash describing extra patient information (default: {}) :birthyear - The Integer birthyear of the patient to be used in score calculation (optional) :gender - The Symbol gender of the patient, must be one of: :male, :female or :unknown (optional) respondent_attrs - A Hash describing respondent information (default: {}) :respondent_type - The Symbol or String type of respondent



51
52
53
54
55
56
57
58
59
60
# File 'lib/quby/answers/services/score_calculator.rb', line 51

def initialize(questionnaire:, values:, observation_time:, patient_attrs: {}, respondent_attrs: {})
  @questionnaire = questionnaire
  @values = values
  @observation_time = observation_time
  @patient = Entities::Patient.new(patient_attrs)
  @respondent = Entities::Respondent.new(respondent_attrs)
  @score = {}
  @referenced_values = []
  @outcome_warnings = []
end

Instance Attribute Details

#outcome_warnings ⇒ Object (readonly)

Returns the value of attribute outcome_warnings.



23
24
25
# File 'lib/quby/answers/services/score_calculator.rb', line 23

def outcome_warnings
  @outcome_warnings
end

Class Method Details

.calculate(**kwargs, &block) ⇒ Object

Evaluates block within the context of a new calculator instance. All instance methods are accessible.



27
28
29
30
31
32
33
34
35
36
37
# File 'lib/quby/answers/services/score_calculator.rb', line 27

def self.calculate(**kwargs, &block)
  instance = new(**kwargs)
  result = instance.instance_eval(&block)
  if result.respond_to?(:merge)
    result = result.merge({
      referenced_values: instance.referenced_values,
      outcome_warnings: instance.outcome_warnings.presence
    }.compact)
  end
  result
end

Instance Method Details

#age ⇒ Object

Public: Returns the Integer age of the patient, or nil if it's not known.



226
227
228
# File 'lib/quby/answers/services/score_calculator.rb', line 226

def age
  @patient.age_at @observation_time
end

#ensure_answer_values_for(*keys, minimum_present: keys.flatten(1).size, missing_values: []) ⇒ Object

Public: Ensure given question_keys have answers. Strings with nothing but whitespace are not considered answered.

*keys - A list of keys to check if an answer is given *minimum_present - defaults to all *missing_values - extra values to consider missing.



313
314
315
316
317
318
319
320
321
322
323
324
# File 'lib/quby/answers/services/score_calculator.rb', line 313

def ensure_answer_values_for(*keys, minimum_present: keys.flatten(1).size, missing_values: [])
  keys = keys.flatten(1).map(&:to_s)
  # we also consider '' and whitespace to be not filled in, as well as nil values or missing keys
  unanswered_keys = keys.select { |key| missing_value?(@values[key], missing_values: missing_values) }

  if unanswered_keys.size > keys.size - minimum_present
    raise MissingAnswerValues.new \
            questionnaire_key: @questionnaire.key,
            values: @values,
            missing: unanswered_keys
  end
end

#gender ⇒ Object

Public: Returns the Symbol describing the gender of the patient.

The symbol :unknown is returned when gender is not known.



233
234
235
# File 'lib/quby/answers/services/score_calculator.rb', line 233

def gender
  @patient.gender
end

#get_subscore(score_key, subscore_key) ⇒ Object



252
253
254
255
256
257
258
259
260
261
# File 'lib/quby/answers/services/score_calculator.rb', line 252

def get_subscore(score_key, subscore_key)
  fail "Score #{score_key} does not exist" unless @questionnaire.score_schemas.key?(score_key)
  fail "Subscore #{subscore_key} for #{score_key} does not exist" unless @questionnaire.score_schemas[score_key].subscore(subscore_key)

  # Must match with QubyCompilers ScoreSchemaBuilder internals.
  # Annoying and ugly but all of these `variable :foo do` blocks in the definitions are too,
  # and because score() will check the existence, this shouldn't fail silently
  calculation_key = :"_#{score_key}.#{subscore_key}"
  score(calculation_key)
end

#max(*values) ⇒ Object

Public: Max of values

values - an Array or list of Numerics

Returns the highest value of the given values



210
211
212
# File 'lib/quby/answers/services/score_calculator.rb', line 210

def max(*values)
  values.flatten.compact.max
end

#mean(values, ignoring: [], minimum_present: 1) ⇒ Object

Public: Gives mean of values

values - An Array of Numerics ignoring - An array of values to remove before taking the mean. minimum_present - return nil if less values than this are left after filtering

Returns the mean of the given values or nil if minimum_present is not met.



150
151
152
153
154
# File 'lib/quby/answers/services/score_calculator.rb', line 150

def mean(values, ignoring: [], minimum_present: 1)
  compacted_values = values.reject { |v| ignoring.include? v }
  return nil if compacted_values.blank? || compacted_values.length < minimum_present
  sum(compacted_values).to_f / compacted_values.length
end

#mean_ignoring_nils(values) ⇒ Object

Public: Gives mean of values, ignoring nil values

values - An Array of Numerics

Returns the mean of the given values



161
162
163
# File 'lib/quby/answers/services/score_calculator.rb', line 161

def mean_ignoring_nils(values)
  mean(values, ignoring: [nil])
end

#mean_ignoring_nils_80_pct(values) ⇒ Object

Public: Gives mean of values, ignoring nil values if >= 80% is filled in

values - An Array of Numerics

Returns the mean of the given values, or nil if less than 80% is present



170
171
172
# File 'lib/quby/answers/services/score_calculator.rb', line 170

def mean_ignoring_nils_80_pct(values)
  mean(values, ignoring: [nil], minimum_present: values.length * 0.8)
end

#observation_time ⇒ Object

Public: Returns initial completion date, for manual comparisons. Preferably use observed_on_or_after instead,



221
222
223
# File 'lib/quby/answers/services/score_calculator.rb', line 221

def observation_time
  @observation_time
end

#observed_on_or_after(date) ⇒ Object

Public: Returns false if response was completed before the given date, true if on or after completed_on_or_after input could be a Date, e.g. Date::new(2020,1,30)



216
217
218
# File 'lib/quby/answers/services/score_calculator.rb', line 216

def observed_on_or_after(date)
  @observation_time >= date
end

#opencpu(package, function, parameters = {}) ⇒ Object



267
268
269
270
# File 'lib/quby/answers/services/score_calculator.rb', line 267

def opencpu(package, function, parameters = {})
  client = ::OpenCPU.client
  client.execute(package, function, parameters)
end

#referenced_values ⇒ Object



263
264
265
# File 'lib/quby/answers/services/score_calculator.rb', line 263

def referenced_values
  @values.keys.select { |key| @referenced_values.include? key }
end

#respondent_type ⇒ Object

Public: Returns the type of the respondent



238
239
240
# File 'lib/quby/answers/services/score_calculator.rb', line 238

def respondent_type
  @respondent.type
end

#score(key) ⇒ Object

Public: Runs another score calculation or variable and returns its result

key - The Symbol of another score.



245
246
247
248
249
250
# File 'lib/quby/answers/services/score_calculator.rb', line 245

def score(key)
  fail "Score #{key.inspect} does not exist." unless @questionnaire.score_calculations.key? key

  calculation = @questionnaire.score_calculations.fetch(key)
  instance_eval(&calculation.calculation)
end

#sum(values) ⇒ Object

Public: Sums values

values - An Array of Numerics

Returns the sum of the given values



201
202
203
# File 'lib/quby/answers/services/score_calculator.rb', line 201

def sum(values)
  values.reduce(0, &:+)
end

#sum_extrapolate(values, minimum_present) ⇒ Object

Public: Sums values, extrapolating nils to be valued as the mean of the present values

values - An Array of Numerics minimum_answered - The minimum of values needed to be present, returns nil otherwise

Returns the sum of the given values, or nil if minimum_present is not met



180
181
182
183
184
185
# File 'lib/quby/answers/services/score_calculator.rb', line 180

def sum_extrapolate(values, minimum_present)
  return nil if values.reject(&:blank?).length < minimum_present
  mean = mean_ignoring_nils(values)
  values = values.map { |value| value ? value : mean }
  sum(values)
end

#sum_extrapolate_80_pct(values) ⇒ Object

Public: Sums values, extrapolating nils to be valued as the mean of the present values

values - An Array of Numerics

Returns the sum of the given values, or nil if less than 80% is present



192
193
194
# File 'lib/quby/answers/services/score_calculator.rb', line 192

def sum_extrapolate_80_pct(values)
  sum_extrapolate(values, values.length * 0.8)
end

#table_lookup(table_key, parameters) ⇒ Object



272
273
274
# File 'lib/quby/answers/services/score_calculator.rb', line 272

def table_lookup(table_key, parameters)
  @questionnaire.lookup_tables.fetch(table_key).lookup(parameters)
end

#value(key) ⇒ Object

Public: Get value for given question key

key - A key for which to return a value

Returns the value.

Raises MissingAnswerValues if the keys doesn't have a value.



116
117
118
# File 'lib/quby/answers/services/score_calculator.rb', line 116

def value(key)
  values(key).first
end

#values(*keys, missing_values: []) ⇒ Object

Public: Get values for given question keys

*keys - A list or array of keys for which to return values *missing_values - extra values to consider missing.

Returns an Array of values. Values are whatever they may be defined as, usually they are either Integers of Floats, but remember that no such restriction is placed. And for open questions the value will probably be a String. Returns hash of all values if no keys are given.

Raises MissingAnswerValues if one or more keys doesn't have a value.



74
75
76
77
78
# File 'lib/quby/answers/services/score_calculator.rb', line 74

def values(*keys, missing_values: [])
  keys = keys.flatten(1).map(&:to_s)
  ensure_answer_values_for(keys, missing_values:)
  values_with_nils(keys)
end

#values_with_nils(*keys) ⇒ Object

Public: Get values for given question keys, or nil if the question is not filled in

*keys - A list of keys for which to return values

Returns an Array of values. Values are whatever they may be defined as, usually they are either Integers of Floats, but remember that no such restriction is placed. And for open questions the value will probably be a String. If the question is not filled in or the question key is unknown, nil will be returned for that question.



129
130
131
132
133
134
135
136
137
138
139
140
141
# File 'lib/quby/answers/services/score_calculator.rb', line 129

def values_with_nils(*keys)
  keys = keys.flatten(1).map(&:to_s)
  ensure_defined_question_keys(keys)
  ensure_no_duplicate_keys(keys)

  if keys.empty?
    remember_usage_of_value_keys(@values.keys)
    @values
  else
    remember_usage_of_value_keys(keys)
    @values.values_at(*keys)
  end
end

#values_without_missings(*keys, minimum_present: 1, missing_values: [], replace_missings_with: :remove) ⇒ Object

Public: Get values for given question keys removing any missing keys.

*keys - A list or array of keys for which to return values - required. *minimum_present - see Raises. *missing_values - extra values to consider missing. replace_missings_with - instead of removing, replace missing values with this one (can replace with nil, default :remove)

Returns an Array of values. Values are whatever they may be defined as, usually they are either Integers of Floats, but remember that no such restriction is placed. And for open questions the value will probably be a String.

Raises MissingAnswerValues when less than minimum_present keys have a value.



93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
# File 'lib/quby/answers/services/score_calculator.rb', line 93

def values_without_missings(*keys, minimum_present: 1, missing_values: [], replace_missings_with: :remove)
  keys = keys.flatten(1).map(&:to_s)
  fail ArgumentError, 'keys empty' unless keys.present?

  ensure_answer_values_for(keys, minimum_present: minimum_present, missing_values: missing_values)
  if replace_missings_with == :remove
    values_with_nils(keys).reject { |v|
      missing_value?(v, missing_values: missing_values)
    }
  else
    values_with_nils(keys).map { |v| 
      missing_value?(v, missing_values: missing_values) ? replace_missings_with : v
    }
  end
end

#when_demographics_match(ages: nil, genders: nil) ⇒ Object

Way to test and warn about age/gender requirements for subscore calculation. If all given arguments are matched, the block is yielded. If an argument is given but does not match an outcome_warning is added to the score. Returns value of block if a match, nil otherwise May be called multiple times per score. ages - range or array of Ranges of valid ages genders - array of Symbols for genders we can calculate for (:male, :female, :unknown)



283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
# File 'lib/quby/answers/services/score_calculator.rb', line 283

def when_demographics_match(ages: nil, genders: nil) # &block
  warnings = []
  unless ages.nil?
    if age.nil?
      warnings.push "No age given, some subscores can't be calculated"
    elsif Array.wrap(ages).none? { |range| range.cover?(age) }
      warnings.push "Some subscores can't be calculated, for recorded age"
    end
  end

  unless genders.nil?
    unless genders.include?(gender)
      warnings.push "Some subscores can't be calculated for gender #{gender}"
    end
  end

  if warnings.present?
    @outcome_warnings.concat(warnings).uniq!
    nil
  else
    yield
  end
end