Hash#group_by
Overview
group_by is defined on Enumerable, so it works on hashes as well as arrays. When called on a hash, it iterates over each key-value pair, applies the block to derive a group key, and returns a structure where each group key maps to an array of all entries that produced that key.
The critical thing to understand: Hash#group_by on a hash returns a structure with arrays of [key, value] pairs as values, not a hash of hashes. If you want the latter, you need to transform the result.
That shape is useful when you want to keep the original entries intact while grouping them by some derived label. The grouped result is easy to inspect, and you can convert it into a different structure only if the next step needs that.
Signature
group_by { |(key, value)| block } -> Hash
group_by -> Enumerator
When called without a block, group_by returns an Enumerator that you can chain with other methods or pass around as a lazy computation. The block receives each key-value pair as a two-element array, which you can destructure directly in the block arguments.
Return Value
Returns a hash where:
- Keys are the return values of the block
- Values are arrays of
[key, value]pairs that produced each key
Basic Usage
The return value is always a hash whose keys are the block’s return values and whose values are arrays of the original elements that produced each key. The original collection is not modified. Because group_by builds a completely new structure, you can call it on frozen collections or pass the result through a pipeline without side effects.
Grouping by a simple condition
numbers = [1, 2, 3, 4, 5, 6]
numbers.group_by(&:odd?) # { true => [1, 3, 5], false => [2, 4, 6] }
The shorthand (&:odd?) passes the odd? method as the grouping block, which produces two groups keyed by true and false. This pattern works for any single-argument predicate: group_by(&:even?), group_by(&:zero?), or any method that returns a value suitable as a hash key.
Grouping a hash by value condition
users = {
alice: 28,
bob: 17,
carol: 35,
dave: 22
}
users.group_by { |_name, age| age >= 25 ? "adult" : "minor" }
# { "adult" => [[:alice, 28], [:carol, 35]], "minor" => [[:bob, 17], [:dave, 22]] }
When called on a hash, each element yielded to the block is a [key, value] array. The result groups those arrays by the computed label. To turn each group into a nested hash instead of an array of pairs, chain .transform_values(&:to_h) after group_by — that conversion is often the next step in real code.
The block destructures the pair array into separate _name and age variables using the vertical bar syntax: |_name, age|. This destructuring is a Ruby idiom that keeps the block readable even when the original collection is a hash rather than a flat array.
Grouping by string length
words = ["apple", "apricot", "banana", "blueberry", "cherry"]
words.group_by(&:length)
# { 5 => ["apple"], 7 => ["apricot", "banana"], 9 => ["blueberry", "cherry"] }
Common use cases
Counting by category
The examples so far show grouping by simple criteria — a boolean predicate, a threshold, and a string length. Each produces a different kind of group label, which influences how you work with the result downstream. The boolean labels create exactly two groups, while the length-based labels can produce many.
orders = [
{ product: "Widget", status: "shipped" },
{ product: "Gadget", status: "pending" },
{ product: "Gizmo", status: "shipped" },
{ product: "Widget", status: "processing" }
]
orders.group_by { |o| o[:status] }.transform_values(&:length)
# { "shipped" => 2, "pending" => 1, "processing" => 1 }
Chaining group_by with transform_values(&:length) collapses each group into its size, producing a compact summary from the original rows. This is a common reporting pattern: group first to collect related entries, then reduce each group to the metric that matters — count, sum, average, or some other aggregate.
Partitioning data with labels
temperatures = {
monday: 22,
tuesday: 19,
wednesday: 25,
thursday: 18,
friday: 27,
saturday: 21,
sunday: 23
}
temperatures.group_by { |_day, temp| temp > 22 ? "warm" : "mild" }
# { "mild" => [[:tuesday, 19], [:thursday, 18], [:saturday, 21]],
# "warm" => [[:monday, 22], [:wednesday, 25], [:friday, 27], [:sunday, 23]] }
Label-based grouping is a good fit when the categories are descriptive rather than numeric. It keeps the classification rule in one block and leaves the resulting groups easy to scan. The block only needs to return a label string, and the method handles collecting and partitioning the entries behind the scenes.
Grouping by date component
events = {
event_a: Time.new(2024, 3, 15),
event_b: Time.new(2024, 3, 15),
event_c: Time.new(2024, 3, 22),
event_d: Time.new(2024, 3, 22),
event_e: Time.new(2024, 4, 1)
}
events.group_by { |_name, time| time.strftime("%Y-%m") }
# { "2024-03" => [[:event_a, ...], [:event_b, ...], [:event_c, ...], [:event_d, ...]],
# "2024-04" => [[:event_e, ...]] }
Time-based grouping is a common way to roll up event records by month or week. The block stays simple, and the grouped result gives you a ready-made report structure. Using strftime in the block lets you control the granularity — "%Y-%m" for monthly, "%Y-%W" for weekly, or "%Y" for annual grouping.
Grouping with transformation
products = {
sku_001: { name: "Widget", category: "A", price: 10 },
sku_002: { name: "Gadget", category: "B", price: 25 },
sku_003: { name: "Gizmo", category: "A", price: 15 },
sku_004: { name: "Doohickey", category: "B", price: 30 }
}
# Group by category, but keep only product names in each group
products.group_by { |_sku, attrs| attrs[:category] }
.transform_values { |entries| entries.map { |_k, v| v[:name] } }
# { "A" => ["Widget", "Gizmo"], "B" => ["Gadget", "Doohickey"] }
This version is useful when you want the group labels to come from one field but only keep a smaller projection of each entry. It keeps the grouping and trimming steps close together, which makes the transformation easier to follow. The transform_values block receives an array of [key, value] pairs, so the inner map extracts the nested value before collecting the trimmed results.
Result Structure
The return value is always a Hash, with arrays of two-element arrays as values:
h = { a: 1, b: 2, c: 3 }
result = h.group_by { |_k, v| v.odd? ? "odd" : "even" }
# { "odd" => [[:a, 1], [:c, 3]], "even" => [[:b, 2]] }
# Accessing the grouped data
result["odd"].to_h # => { a: 1, c: 3 } (convert back to hash)
Calling .to_h on a group converts the array of pairs back into a hash, which is convenient when you need to pass a specific group to code that expects a hash rather than an array of pairs. Each group is independent, so you can convert some groups while leaving others as arrays.
Chaining with other enumerable methods
# Group, then filter groups by size
data = [:a, :b, :c, :d, :e, :f]
data.group_by { |x| x.odd? ? "odd" : "even" }
.select { |_label, items| items.size > 2 }
# { "odd" => [:a, :c, :e] }
# Group, then sum values within each group
scores = { alice: 95, bob: 82, carol: 78, dave: 91 }
scores.group_by { |_name, score| (score / 10).to_s + "0s" }
.transform_values { |entries| entries.map { |_k, v| v }.sum }
# { "90s" => 186, "80s" => 82, "70s" => 78 }
Chaining is useful when grouping is only one step in a larger pipeline. Once you have the grouping, you can filter, reduce, or reshape it without starting over from scratch. The intermediate result from group_by is a plain hash, so any hash method — select, reject, transform_values, map — works directly on it without conversion. Keeping each step small and named makes the pipeline easier to test: you can verify the grouping separately from the aggregation that follows it.
Gotchas
Returns arrays of pairs, not hashes. When you iterate a structure with group_by, each group contains [key, value] pairs. You often need .to_h or .transform_values to get the structure you want:
# What you get (array of pairs)
h = { a: 1, b: 2 }
h.group_by { |k, v| v.odd? }
# { true => [[:a, 1]], false => [[:b, 2]] }
# What you might have wanted (hash of hashes or hash of sub-hashes)
h.group_by { |k, v| v.odd? }.transform_values(&:to_h)
# { true => { a: 1 }, false => { b: 2 } }
Block argument order. When iterating a hash, the block receives the key first, then the value:
Block argument order is a frequent source of bugs when chaining. The block always receives the key first and the value second, regardless of whether you destructure them or access them positionally. If the key is not needed, use an underscore prefix to signal the intent clearly.
h.each { |k, v| puts "#{k} => #{v}" } # a => 1, b => 2
Performance. group_by makes a pass over the entire collection. For very large datasets, consider whether you need the full grouped structure or can process in a single pass with each_with_object.
See Also
- /reference/enumerable/enumerable-group-by/ — the Enumerable version, works on arrays and hashes
- /reference/hash-methods/select/ — filter a hash by key or value conditions
- /guides/ruby-hash-tricks/ — practical patterns for transforming and grouping hashes