按二维分组并在 Elixir 中计数

Group by two dimensions and count in Elixir

我正在研究二维分组,然后计算每个类别/子类别中每个元素的数量。

data = [
  %{brand: "Mercedes", color: "blue"},
  %{brand: "Mercedes", color: "blue"},
  %{brand: "BMW", color: "blue"},
  %{brand: "BMW", color: "blue"},
  %{brand: "BMW", color: "blue"},
  %{brand: "Lada", color: "blue"},
  %{brand: "Mercedes", color: "red"},
  %{brand: "Mercedes", color: "red"},
  %{brand: "Mercedes", color: "red"},
  %{brand: "Mercedes", color: "red"},
  %{brand: "BMW", color: "black"}
]

预期结果:

[
  %{"Mercedes": ["blue": 2, "red": 4]},
  %{"Lada": ["blue": 1]},
  %{"BMW": ["blue": 3, "black": 1]}
]

我有一些接近结果的东西,但这不是我想要的:

per_brands = Enum.group_by(data, fn (item) -> [item[:brand]] end)
Enum.map(
  per_brands,
  fn (cars_per_brand) ->
    %{
      "#{elem(cars_per_brand, 0)}": (
        Enum.map(
          Enum.group_by(elem(cars_per_brand, 1), fn (car) -> car[:color] end),
          fn (cars_per_brands_per_colors) ->
            %{"#{elem(cars_per_brands_per_colors, 0)}": Enum.count(elem(cars_per_brands_per_colors, 1))}
          end
        ))
    }
  end
)

# Result: (not exactly what I want)
[
  %{BMW: [%{black: 1}, %{blue: 3}]},
  %{Lada: [%{blue: 1}]},
  %{Mercedes: [%{blue: 2}, %{red: 4}]}
]

我相信 reduce/3 可以做一些更聪明更好的事情,但我无法理解。

你可以像这样用 reduce 来做:

Enum.reduce(data, %{}, fn %{brand: brand, color: color}, acc ->
  colors = Map.get(acc, brand, %{}) |> Map.update(color, 1, &(&1 + 1))
  Map.put(acc, brand, colors)
end)

对于每 brand/color 对,建立地图的地图,在找到它们时递增颜色计数。

结果:

%{
  'BMW' => %{'black' => 1, 'blue' => 3},
  'Lada' => %{'blue' => 1},
  'Mercedes' => %{'blue' => 2, 'red' => 4}
}

我设法用 Map.ReduceAccess.key/2 做了一些比我最初的方法更好的事情:

Enum.reduce(
  data,
  %{},
  fn (item, acc) ->
    new_value = if (acc[item[:brand]][item[:color]]) do
      # The value already exists
      acc[item[:brand]][item[:color]] + 1
    else
      # The value does not exist, initialize to 1
      1
    end

    acc = put_in(acc, [Access.key(item[:brand], %{}), item[:color]], new_value)
  end
)

不过我认为@adam 的解决方案更简洁。