Files
nairobi/app/models/concerns/statisticable.rb
Javi Martín 1d85a63e7c Calculate age stats based on the participation date
We were calculating the age stats based on the age of the users who
participated... at the moment where we were calculating the stats. That
means that, if 20 years ago, 1000 people who were 16 years old
participated, they would be shown as having 36 years in the stats.

Instead, we want to show the stats at the time when the process took
place, so we're implementing a `participation_date` method.

Note that, for polls, we could actually use the `age` column in the
`poll_voters` table. However, doing so would be harder, would only work
for polls but not for budgets, and it wouldn't be statistically very
relevant, since the stats are shown by age groups, and only a small
percentage of people would change their age group (and only to the
nearest one) between the time they participate and the time the process
ends.

We might use the `poll_voters` table in the future, though, since we
have a similar issue with geozones and genders, and using the
information in `poll_voters` would solve it as well (only for polls,
though).

Also note that we're using the `ends_at` dates because some people but
be too young to vote when a process starts but old enough to vote when
the process ends.

Finally, note that we might need to change the way we calculate the
participation date for a budget, since some budgets might not enabled
every phase. Not sure how stats work in that scenario (even before these
changes).
2024-05-13 15:42:37 +02:00

193 lines
4.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
stats_methods.each { |stat_name| send(stat_name) }
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
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
geozone_stats.to_h do |stats|
[
stats.name,
{
count: stats.count,
percentage: stats.percentage
}
]
end
end
def calculate_percentage(fraction, total)
PercentageCalculator.calculate(fraction, total)
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 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 participants_between_ages(from, to)
participants.between_ages(from, to)
end
def geozones
Geozone.order("name")
end
def geozone_stats
geozones.map { |geozone| GeozoneStats.new(geozone, participants) }
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