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_warningsObject (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

#ageObject

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



218
219
220
# File 'lib/quby/answers/services/score_calculator.rb', line 218

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.



303
304
305
306
307
308
309
310
311
312
313
314
# File 'lib/quby/answers/services/score_calculator.rb', line 303

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

#genderObject

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

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



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

def gender
  @patient.gender
end

#get_subscore(score_key, subscore_key) ⇒ Object



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

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]

  # 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



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

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.



142
143
144
145
146
# File 'lib/quby/answers/services/score_calculator.rb', line 142

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



153
154
155
# File 'lib/quby/answers/services/score_calculator.rb', line 153

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



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

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

#observation_timeObject

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



213
214
215
# File 'lib/quby/answers/services/score_calculator.rb', line 213

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)`



208
209
210
# File 'lib/quby/answers/services/score_calculator.rb', line 208

def observed_on_or_after(date)
  @observation_time >= date
end

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



259
260
261
262
# File 'lib/quby/answers/services/score_calculator.rb', line 259

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

#referenced_valuesObject



255
256
257
# File 'lib/quby/answers/services/score_calculator.rb', line 255

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

#respondent_typeObject

Public: Returns the type of the respondent



230
231
232
# File 'lib/quby/answers/services/score_calculator.rb', line 230

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.



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

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



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

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



172
173
174
175
176
177
# File 'lib/quby/answers/services/score_calculator.rb', line 172

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



184
185
186
# File 'lib/quby/answers/services/score_calculator.rb', line 184

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

#table_lookup(table_key, parameters) ⇒ Object



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

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.



108
109
110
# File 'lib/quby/answers/services/score_calculator.rb', line 108

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

#values(*keys) ⇒ Object

Public: Get values for given question keys

*keys - A list or array 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. Returns hash of all values if no keys are given.

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



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

def values(*keys)
  keys = keys.flatten(1).map(&:to_s)
  ensure_answer_values_for(keys)
  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.



121
122
123
124
125
126
127
128
129
130
131
132
133
# File 'lib/quby/answers/services/score_calculator.rb', line 121

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: []) ⇒ 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.

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.



91
92
93
94
95
96
97
98
99
# File 'lib/quby/answers/services/score_calculator.rb', line 91

def values_without_missings(*keys, minimum_present: 1, missing_values: [])
  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)
  values_with_nils(keys).reject do |v|
    missing_value?(v, missing_values: missing_values)
  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. 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)



274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
# File 'lib/quby/answers/services/score_calculator.rb', line 274

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!
  else
    yield
  end
end