Files
nairobi/app/models/concerns/statisticable.rb
Javi Martín 62cd4c8d7b Add indices to stats temporary tables
Since we're doing many queries to get stats for each age group and each
geozone, testing shows these indices make stats calculation about 25%
faster on processes with 100,000 participants.
2024-05-17 16:08:08 +02:00

228 lines
5.1 KiB
Ruby

module Statisticable
extend ActiveSupport::Concern
PARTICIPATIONS = %w[gender age geozone].freeze
included do
attr_reader :resource
end
class_methods do
def stats_methods
base_stats_methods + gender_methods + age_methods + geozone_methods
end
def base_stats_methods
%i[total_participants participations] + participation_check_methods
end
def participation_check_methods
PARTICIPATIONS.map { |participation| :"#{participation}?" }
end
def gender_methods
%i[total_male_participants total_female_participants male_percentage female_percentage]
end
def age_methods
[:participants_by_age]
end
def geozone_methods
%i[participants_by_geozone total_no_demographic_data]
end
def stats_cache(*method_names)
method_names.each do |method_name|
alias_method :"raw_#{method_name}", method_name
define_method method_name do
stats_cache(method_name) { send(:"raw_#{method_name}") }
end
end
end
end
def initialize(resource)
@resource = resource
end
def generate
User.transaction do
create_participants_table
begin
define_singleton_method :participants do
participants_class.all
end
stats_methods.each { |stat_name| send(stat_name) }
ensure
define_singleton_method :participants do
participants_from_original_table
end
end
drop_participants_table
end
end
def stats_methods
base_stats_methods + participation_methods
end
def participations
PARTICIPATIONS.select { |participation| send("#{participation}?") }
end
def gender?
participants.male.any? || participants.female.any?
end
def age?
participants.between_ages(
age_groups.flatten.min,
age_groups.flatten.max,
at_time: participation_date
).any?
end
def geozone?
participants.where(geozone: geozones).any?
end
def participants
@participants ||= User.unscoped.where(id: participant_ids)
end
alias_method :participants_from_original_table, :participants
def total_male_participants
participants.male.count
end
def total_female_participants
participants.female.count
end
def total_no_demographic_data
participants.where("gender IS NULL OR date_of_birth IS NULL OR geozone_id IS NULL").count
end
def male_percentage
calculate_percentage(total_male_participants, total_participants_with_gender)
end
def female_percentage
calculate_percentage(total_female_participants, total_participants_with_gender)
end
def participants_by_age
age_groups.to_h do |start, finish|
count = participants.between_ages(start, finish, at_time: participation_date).count
[
"#{start} - #{finish}",
{
range: range_description(start, finish),
count: count,
percentage: calculate_percentage(count, total_participants)
}
]
end
end
def participants_by_geozone
geozones.to_h do |geozone|
count = participants.where(geozone: geozone).count
[
geozone.name,
{
count: count,
percentage: calculate_percentage(count, total_participants)
}
]
end
end
def calculate_percentage(fraction, total)
return 0.0 if total.zero?
(fraction * 100.0 / total).round(3)
end
def version
"v#{resource.find_or_create_stats_version.updated_at.to_i}"
end
def advanced?
resource.advanced_stats_enabled?
end
private
def base_stats_methods
self.class.base_stats_methods
end
def participation_methods
participations.map { |participation| self.class.send("#{participation}_methods") }.flatten
end
def create_participants_table
User.connection.create_table(
participants_table_name,
temporary: true,
as: participants_from_original_table.to_sql
)
User.connection.add_index participants_table_name, :date_of_birth
User.connection.add_index participants_table_name, :geozone_id
end
def drop_participants_table
User.connection.drop_table(participants_table_name, if_exists: true, temporary: true)
end
def participants_table_name
@participants_table_name ||= "participants_#{resource.class.table_name}_#{resource.id}"
end
def participants_class
@participants_class ||= Class.new(User).tap { |klass| klass.table_name = participants_table_name }
end
def total_participants_with_gender
@total_participants_with_gender ||= participants.where.not(gender: nil).distinct.count
end
def age_groups
[[16, 19],
[20, 24],
[25, 29],
[30, 34],
[35, 39],
[40, 44],
[45, 49],
[50, 54],
[55, 59],
[60, 64],
[65, 69],
[70, 74],
[75, 79],
[80, 84],
[85, 89],
[90, 300]]
end
def geozones
Geozone.order("name")
end
def range_description(start, finish)
if finish > 200
I18n.t("stats.age_more_than", start: start)
else
I18n.t("stats.age_range", start: start, finish: finish)
end
end
end