Class: Quby::Answers::Services::ScoreCalculator
- Inherits:
-
Object
- Object
- Quby::Answers::Services::ScoreCalculator
- Defined in:
- lib/quby/answers/services/score_calculator.rb
Defined Under Namespace
Classes: MissingAnswerValues, UnknownFieldsReferenced
Instance Attribute Summary collapse
-
#outcome_warnings ⇒ Object
readonly
Returns the value of attribute outcome_warnings.
Class Method Summary collapse
-
.calculate(**kwargs, &block) ⇒ Object
Evaluates block within the context of a new calculator instance.
Instance Method Summary collapse
-
#age ⇒ Object
Public: Returns the Integer age of the patient, or nil if it's not known.
-
#ensure_answer_values_for(*keys, minimum_present: keys.flatten(1).size, missing_values: []) ⇒ Object
Public: Ensure given question_keys have answers.
-
#gender ⇒ Object
Public: Returns the Symbol describing the gender of the patient.
- #get_subscore(score_key, subscore_key) ⇒ Object
-
#initialize(questionnaire:, values:, observation_time:, patient_attrs: {}, respondent_attrs: {}) ⇒ ScoreCalculator
constructor
Public: Initialize a new ScoreCalculator.
-
#max(*values) ⇒ Object
Public: Max of values.
-
#mean(values, ignoring: [], minimum_present: 1) ⇒ Object
Public: Gives mean of values.
-
#mean_ignoring_nils(values) ⇒ Object
Public: Gives mean of values, ignoring nil values.
-
#mean_ignoring_nils_80_pct(values) ⇒ Object
Public: Gives mean of values, ignoring nil values if >= 80% is filled in.
-
#observation_time ⇒ Object
Public: Returns initial completion date, for manual comparisons.
-
#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.
- #opencpu(package, function, parameters = {}) ⇒ Object
- #referenced_values ⇒ Object
-
#respondent_type ⇒ Object
Public: Returns the type of the respondent.
-
#score(key) ⇒ Object
Public: Runs another score calculation or variable and returns its result.
-
#sum(values) ⇒ Object
Public: Sums values.
-
#sum_extrapolate(values, minimum_present) ⇒ Object
Public: Sums values, extrapolating nils to be valued as the mean of the present values.
-
#sum_extrapolate_80_pct(values) ⇒ Object
Public: Sums values, extrapolating nils to be valued as the mean of the present values.
- #table_lookup(table_key, parameters) ⇒ Object
-
#value(key) ⇒ Object
Public: Get value for given question key.
-
#values(*keys, missing_values: []) ⇒ Object
Public: Get values for given question keys.
-
#values_with_nils(*keys) ⇒ Object
Public: Get values for given question keys, or nil if the question is not filled in.
-
#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.
-
#when_demographics_match(ages: nil, genders: nil) ⇒ Object
Way to test and warn about age/gender requirements for subscore calculation.
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 |