Here are two ways:
data = AssociationThread[{"a", "b", "c"}, #] & /@ Tuples[{1, 2}, {3}]
(* {<|"a" -> 1, "b" -> 1, "c" -> 1|>, <|"a" -> 1, "b" -> 1, "c" -> 2|>,
<|"a" -> 1, "b" -> 2, "c" -> 1|>, <|"a" -> 1, "b" -> 2, "c" -> 2|>,
<|"a" -> 2, "b" -> 1, "c" -> 1|>, <|"a" -> 2, "b" -> 1, "c" -> 2|>,
<|"a" -> 2, "b" -> 2, "c" -> 1|>, <|"a" -> 2, "b" -> 2, "c" -> 2|>} *)
GroupBy[data, First -> Rest, Merge[Apply@Construct]@*Prepend[<|"b" -> b, "c" -> c|>]]
(* <|1 -> <|"b" -> b[1, 1, 2, 2], "c" -> c[1, 2, 1, 2]|>,
2 -> <|"b" -> b[1, 1, 2, 2], "c" -> c[1, 2, 1, 2]|>|> *)
GroupBy[data, First -> Rest, Query[{"b" -> b, "c" -> c}]@*Merge[Identity]]
(* <|1 -> <|"b" -> b[{1, 1, 2, 2}], "c" -> c[{1, 2, 1, 2}]|>,
2 -> <|"b" -> b[{1, 1, 2, 2}], "c" -> c[{1, 2, 1, 2}]|>|> *)
The first one prepends an additional association to the associations to be merged. It then uses Construct
to apply the function from the first association to the elements of the other associations.
The second approach merges the associations with Identity
, and uses Query
to apply the appropriate post-processing to each entry. In my opinion, this one is more readable. It also has the advantage that it doesn't break if the associations have keys not present in your "reduction list":
GroupBy[data, First -> Rest, Merge[Apply@Construct]@*Prepend[<|"b" -> b|>]]
(* <|1 -> <|"b" -> b[1, 1, 2, 2], "c" -> 1[2, 1, 2]|>,
2 -> <|"b" -> b[1, 1, 2, 2], "c" -> 1[2, 1, 2]|>|> *)
GroupBy[data, First -> Rest, Query[{"b" -> b}]@*Merge[Identity]]
(* <|1 -> <|"b" -> b[{1, 1, 2, 2}], "c" -> {1, 2, 1, 2}|>,
2 -> <|"b" -> b[{1, 1, 2, 2}], "c" -> {1, 2, 1, 2}|>|> *)
Notice how the first output contains expressions like 1[2, 1, 2]
(since we didn't supply a function in the first association, Construct
simply took the element from the first "proper" association for the head - in this case a 1
). The second output simply contains {1, 2, 1, 2}
in that place, since the output from Merge
has just been left untouched.