Find N number of common attributes in an array of structs

Viewed 86

I have the following struct:

Name = Struct.new(:first_name, :last_name) do
  def greeting
    "Hello #{first_name}!"
  end
end

I am adding these objects to an array like this:

full_names << Name.new(first_name, last_name)

Now, I'd like to find the N most common first names.

5 Answers

You can use group_by to create a hash mapping each first name to an array of all of its occurrences in the array, transform_values to turn each value array of strings into a count, then max_by to extract the largest n counts.

Name = Struct.new(:first_name, :last_name) do
  def greeting
    "Hello #{first_name}!"
  end
end

full_names = [
  Name.new("a", "b"),
  Name.new("b", "c"),
  Name.new("d", "c"),
  Name.new("c", "d"),
  Name.new("d", "c"),
  Name.new("b", "b"),
  Name.new("b", "e")
]
n = 2

p full_names
  .group_by(&:first_name)
  .transform_values(&:size)
  .max_by(n, &:last)

Output:

[["b", 3], ["d", 2]]

If you only want the first names and not the counts, append .map(&:first) to the chain.

full_names = [
  Name.new("Bob", "Feller"),
  Name.new("Hank", "Jones"),
  Name.new("Annie", "Oakley"),
  Name.new("Cher", ""),
  Name.new("Annie", "Hall"),
  Name.new("Melba", "Toast"),
  Name.new("Bob", "Dylan"),
  Name.new("Hank", "Wiliams"),
  Name.new("Bob", "Marley")
]

nbr_most_common = 3

full_names.each_with_object(Hash.new(0)) { |i,h| h[i[:first_name]] += 1 }.
           max_by(nbr_most_common, &:last).
           map(&:first)
  #=> ["Bob", "Hank", "Annie"]

If you wish to also display the frequency, change the last line to to_h:

full_names.each_with_object(Hash.new(0)) { |i,h| h[i[:first_name]] += 1 }.
           max_by(nbr_most_common, &:last).
           to_h
   #=> {"Bob"=>3, "Hank"=>2, "Annie"=>2} 

See the version of Hash::new that creates a default value (here zero) and Enumerable#max_by.

You can use Enumerable#tally in 2.7

Name = Struct.new(:first_name, :last_name) do
  def greeting
    "Hello #{first_name}!"
  end
end

full_names = [
  Name.new("Yui", "Yoko"),
  Name.new("Bob", "Feller"),
  Name.new("Hank", "Jones"),
  Name.new("Annie", "Oakley"),
  Name.new("Cher", ""),
  Name.new("Annie", "Hall"),
  Name.new("Melba", "Toast"),
  Name.new("Bob", "Dylan"),
  Name.new("Hank", "Wiliams"),
  Name.new("Bob", "Marley")
]

full_names.map(&:first_name).tally.max_by(3, &:last) 
#=> [["Bob", 3], ["Annie", 2], ["Hank", 2]]

Using the same example:

Name = Struct.new(:first_name, :last_name) do
  def greeting
    "Hello #{first_name}!"
  end
end

full_names = [
  Name.new("a", "b"),
  Name.new("b", "c"),
  Name.new("d", "c"),
  Name.new("c", "d"),
  Name.new("d", "c"),
  Name.new("b", "b"),
  Name.new("b", "e")
]

We can use inject to create a Hash with first_name => frequency:

first_names = full_names.map(&:first_name)
first_names #["a", "b", "d", "c", "d", "b", "b"]
hash_of_frequency = first_names.inject(Hash.new(0)) { |h,v| h[v] += 1; h }
hash_of_frequency # [["b", 3], ["d", 2], ["a", 1], ["c", 1]]

Now we just execute a sort_by and get your results:

hash_of_frequency.sort_by{|k, v| -v}.first(number_desired)

The result is the same, but I've preferred to use inject

Here was a solution I came up with based on Sophie's answer HERE.

First, this gives me a hash of first names and the number of occurrences of each:

freq_of_first_names = full_names.map(&:first_name).inject(Hash.new(0)) { |h,v| h[v] += 1; h }
 => "Haley"=>122, "Auer"=>119, "Lakin"=>96, "Macejkovic"=>98...

Now I can sort this with the .sort_by method, and also adding the .last method along with the number of records I want returned (in this case 10).

freq_of_first_names.sort_by {|_key, value| value}.last(10)
Related