Group Users by Age Range in Ruby

Group Users by Age Range in ruby

You want to build some SQL that looks like this:

select count(*),
case
when age between 10 and 14 then '10 - 14'
when age between 15 and 21 then '15 - 21'
-- ...
end as age_range
from users
where age between 10 and 120
group by age_range

In ActiveRecord terms, that would be:

# First build the big ugly CASE, we can also figure out the
# overall max and min ages along the way.
min = nil
max = nil
cases = AGE_RANGES.map do |r|
min = [r[:min], min || r[:min]].min
max = [r[:max], max || r[:max]].max
"when age between #{r[:min]} and #{r[:max]} then '#{r[:min]} - #{r[:max]}'"
end

# Then away we go...
age_ranges = Users.select("count(*) as n, case #{cases.join(' ')} end as age_range")
.where(:age => min .. max)
.group('age_range')
.all

That will leave you with an array of objects in age_ranges and those objects will have n and age_range methods. If you want a Hash out of that, then:

age_ranges = Hash[age_ranges.map { |r| [r.age_range, r.n] }]

That won't include ranges that don't have any people in them of course; I'll leave that as an exercise for the reader.

Group users by age range in rails

I guess you could at least build the cache once then use it in your loop. The following code is not pretty, it's just to illustrate what I mean:

def build_year_cache(index, rjust_str)
first_number = 5 * index
second_number = index % 2 == 0 ? (5 * index + 4) : ((5 * index) + 4)
first_year = Date.today.year - second_number.to_s.rjust(4, rjust_str).to_i
second_year = Date.today.year - first_number.to_s.rjust(4, rjust_str).to_i
"#{first_year}-#{second_year}"
end

def build_years_cache
cache = {}
years = (0..99).to_a

[
[0..19, '1900'],
[20..39, '2000'],
[40..59, '2100']
].each do |range, rjust_str|
birth_year = []
20.times do |index|
birth_year.append(build_year_cache(index, rjust_str))
end
multiplied_birth_years = ([birth_year] * 5).inject(&:zip).flatten
cache[range] = Hash[years.zip multiplied_birth_years]
end

cache
end

def years(pesel, cache)
year = pesel[0..1].to_i
month = pesel[2..3].to_i
range = cache.keys.find { |k, v| k.include?(month) }
cache[range].fetch(year)
end

def grouped_by_age
cache = build_years_cache
@yearsbook = @study_participations.includes(user: :profile).group_by do |study_participation|
years(study_participation.user.profile.pesel, cache)
end
end

How to list the ages of users from their DOB

  1. create instance method to compute age of a user app/models/user.rb:

    def age
    now = Time.now.utc.to_date
    now.year - birthdate.year - ((now.month > birthdate.month || (now.month == birthdate.month && now.day >= birthdate.day)) ? 0 : 1)
    end
  2. Create object representing an age group (I'll go with tableless model, but it can be pure ruby object or even struct) app/models/age_group.rb:

    class AgeGroup
    include ActiveModel::Model # not really necessary, but will add some AM functionality which could be nice later
    attr_accessor :from, :to, :count
    end
  3. Create service object for computing age groups (Choosing service object is just my personal preference. You can create a helper or whatever you think will fit your needs the best). app/services/age_groups_service.rb:

    class AgeGroupService
    # @params
    # ranges - an array of age group ranges. E.g.: [[0, 18], [19, 24], [25, 34], ...]
    # users - an array of users from which the age groups will be computed. Defaults to all users
    def initialize(ranges = [], users = User.all.to_a)
    @ranges = ranges
    @users = users
    @age_groups = []
    end

    # Count users in each age group range
    # @return
    # an array of age groups. E.g.: [{ from: 0, to: 18, count: 12 }, ...]
    def call
    @ranges.each do |range|
    users = @users.select { |user| user.age >= range[0] && user.age <= range[1] }
    @age_groups << AgeGroup.new(from: range[0], to: range[1], count: users.length)
    end
    @age_groups
    end
    end
  4. Assign value returned by call to the AgeGroupsService to an instance variable in your controller:

    age_group_service = AgeGroupsService.new([[18, 24], [25, 34], [35, 44], [45, 100]])
    @age_groups = age_group_service.call
  5. Print out the results in a view:

    <ul>
    <% @age_groups.each do |age_group| %>
    <li><%= age_group.from %> - <%= age_group.to %>: <b><%= age_group.count %></b></li>
    <% end %>
    </ul>

Some further ideas / notes:

  • AgeGroup model and AgeGroupsService aren't necessary. You can use PORO and helpers instead or you can even put all the code into User model and relevant controller. Anyway I would strongly recommend against this as it will go messy and it just feels wrong. Age groups seem like something deserving it's own place.
  • This is very simple solution. It will work fine if you don't have a lot of users. This implementation would be problematic in a production app with thousands of users. The problem here is the computational complexity of the algorithm assigning users into age groups. The algorithm iterates through every user for each age group range. You could solve this problem in different ways: 1. improve the algorithm by reducing its complexity or number of users it has to iterate over. My first try would be to make sure the iteration over the ranges will stop as soon as it's clear there aren't any more users for the current range and iterations over all following ranges won't iterate over users already assigned to a range. 2. use SQL for the computation if you aren't afraid of using raw SQL and don't mind loosing the DB agnosticity. You can create a combined query or even a DB view which will return the count of all users with birthday between two dates. 3. Make it work asynchronously. Move the computation into backround (so it won't block the controller) and then load it using async call in Javascript.

Ruby on Rails, Select Users with age range from Active Record

First of all: use a date as type for birthdate in the database.

Then you can just use:

User.where(birthdate: 65.years.ago..25.years.ago)

If you can't change the birthdate type convert it using SQL (example with PostrgeSQL):

User.where('to_date(birthdate, 'MM/YYYY') between ? and ?', 65.years.ago, 25.years.ago)

But you may still have to correct it since you don't have the exact day and only the month.

How to group records in ranges of their created_at age in Rails

You don't need count in the end, you query should like this

Deal.select(sql).group(:day_range)

see the example result in here

making a pie-chart of the user age in rails

So for thousands of users I'd definitely do this in the database. While the labels will probably need some massaging, you can start with a query like this:

User.group("date_trunc('year', age(dob))").count

This will result in a hash with entries that look something like this:

{
"00:00:00" => 3,
"1 year" => 5,
"2 years" => 8,
...
}

You can then do relabelling and group the year results into bins (e.g. 10-20) as needed. I wouldn't try to do this all in one line in your view - I'd make this a method either on the model or on a dedicated query object.

search for users age based on date of birth

This should do it:

# view
<%= select @object, :min_age, (18..75).to_a %>
to
<%= select @object, :max_age, (18..75).to_a %>

# controller
def show
@search = Search.find(params[:id])
@users = @search.users
end

# Search model
def find_users
users = User.order(:id)
users = users.where(gender: gender) if gender.present?
users = users.where(zip_code: zip_code) if zip_code.present?
users = users.where(children: children) if children.present?
users = users.where(religion: religion) if religion.present?
users = users.where(ethnicity: ethnicity) if ethnicity.present?

if min_age.present? && max_age.present?
min = [ min_age, max_age ].min
max = [ min_age, max_age ].max
min_date = Date.today - min.years
max_date = Date.today - max.years
users = users.where("birthday BETWEEN ? AND ?", max_date, min_date)
end
users
end


Related Topics



Leave a reply



Submit