class GekanatorQuestionExample < ApplicationRecord ANSWERS = GekanatorQuestionSuggestion::ANSWERS NON_UNKNOWN_ANSWERS = ANSWERS - ['unknown'] SOURCES = ['initial_suggestion', 'post_game_extra'].freeze belongs_to :gekanator_question belongs_to :post belongs_to :user belongs_to :gekanator_game, optional: true validates :answer, presence: true, inclusion: { in: ANSWERS } validates :answer_counts, presence: true validates :sample_count, presence: true, numericality: { only_integer: true, greater_than: 0 } validates :source, presence: true, inclusion: { in: SOURCES } validates :weight, presence: true, numericality: { greater_than: 0 } before_validation :normalize_learning_state def record_answer!(answer:, source:, gekanator_game: nil) answer = answer.to_s raise ArgumentError, 'invalid answer' unless ANSWERS.include?(answer) counts = normalized_answer_counts counts[answer] += 1 self.answer_counts = counts self.sample_count = counts.values.sum self.gekanator_game = gekanator_game if gekanator_game.present? self.source = source if new_record? apply_aggregated_answer!(preferred_answer: answer) self end private def normalize_learning_state counts = normalized_answer_counts if counts.values.sum.zero? && answer.present? counts[answer] = 1 end self.answer_counts = counts self.sample_count = counts.values.sum apply_aggregated_answer! end def apply_aggregated_answer!(preferred_answer: nil) counts = normalized_answer_counts known_counts = counts.slice(*NON_UNKNOWN_ANSWERS) known_total = known_counts.values.sum if known_total.zero? self.answer = 'unknown' self.weight = 0.1 return else max_count = known_counts.values.max candidates = known_counts.select { |_answer, count| count == max_count }.keys self.answer = if preferred_answer.present? && candidates.include?(preferred_answer) preferred_answer elsif answer.present? && candidates.include?(answer) answer else candidates.first end end consensus = max_count.to_f / known_total self.weight = Math.sqrt(known_total) * consensus end def normalized_answer_counts base = ANSWERS.index_with(0) answer_counts.to_h.each do |key, value| answer_key = key.to_s next unless ANSWERS.include?(answer_key) base[answer_key] = value.to_i end base end end