Repository: holoviz/holoviews
Pull request #6499 (2025-01-27 13:08:18)

fix: heatmap aggregation when kdims are in a Pandas multi-index
The optimizations recently done in HoloViews and hvPlot to avoid unnecessary Pandas copies have exposed this bug reported first on hvPlot's issue tracker https://github.com/holoviz/hvplot/issues/1467.

```python
import xarray as xr
import pandas as pd
import holoviews as hv
hv.extension('bokeh')

da = xr.DataArray([[0.5, 0.2], [-0.1, 0.5]], dims=["A", "B"], name="C")
df = da.to_dataframe()
heatmap = hv.HeatMap(df, ['A', 'B'], ['C'])
hv.render(heatmap)
```

Error: `KeyError: "None of ['A', 'B'] are in the columns"`.


closes https://github.com/holoviz/hvplot/issues/1467


codecov[bot]: ## [Codecov](https://app.codecov.io/gh/holoviz/holoviews/pull/6499?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=holoviz) Report
All modified and coverable lines are covered by tests :white_check_mark:
> Project coverage is 88.79%. Comparing base [(`d9861ae`)](https://app.codecov.io/gh/holoviz/holoviews/commit/d9861ae9279d4be1b762878e8af82c8a7d2709ce?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=holoviz) to head [(`8199a9c`)](https://app.codecov.io/gh/holoviz/holoviews/commit/8199a9cea8db1fe81109cc551e04a9ed7a209420?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=holoviz).

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##             main    #6499      +/-   ##
==========================================
- Coverage   88.79%   88.79%   -0.01%     
==========================================
  Files         323      323              
  Lines       68731    68742      +11     
==========================================
+ Hits        61028    61037       +9     
- Misses       7703     7705       +2     
```

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/holoviz/holoviews/pull/6499?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=holoviz).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=holoviz).


Patch:
diff --git a/holoviews/element/util.py b/holoviews/element/util.py
index 130be0c0c..bde652c71 100644
--- a/holoviews/element/util.py
+++ b/holoviews/element/util.py
@@ -212,7 +212,10 @@ class categorical_aggregate2d(Operation):
         groupby_kwargs = {"sort": False}
         if PANDAS_GE_2_1_0:
             groupby_kwargs["observed"] = False
-        df = obj.data.set_index(index_cols).groupby(index_cols, **groupby_kwargs).first()
+        df = obj.data
+        if not all(c in df.index.names for c in index_cols):
+            df = df.set_index(index_cols)
+        df = df.groupby(index_cols, **groupby_kwargs).first()
         label = 'unique' if len(df) == len(obj) else 'non-unique'
         levels = self._get_coords(obj)
         index = pd.MultiIndex.from_product(levels, names=df.index.names)


Test patch:
diff --git a/holoviews/tests/plotting/bokeh/test_heatmapplot.py b/holoviews/tests/plotting/bokeh/test_heatmapplot.py
index 67f315692..bbd820fb8 100644
--- a/holoviews/tests/plotting/bokeh/test_heatmapplot.py
+++ b/holoviews/tests/plotting/bokeh/test_heatmapplot.py
@@ -156,3 +156,15 @@ class TestHeatMapPlot(TestBokehPlot):
         np.testing.assert_array_equal(data["X"], df["X"])
         np.testing.assert_array_equal(data["Y"], df["Y"])
         np.testing.assert_array_equal(data["zvalues"], df["Z"])
+
+    def test_heatmap_pandas_multiindex(self):
+        df = pd.DataFrame(
+            data={'C': [5, 2, -1, 5]},
+            index=pd.MultiIndex.from_product([(0, 1), (0, 1)], names=['A', 'B']),
+        )
+        hm = HeatMap(df, ['A', 'B'], 'C')
+        plot = bokeh_renderer.get_plot(hm)
+        data = plot.handles["cds"].data
+        np.testing.assert_array_equal(data['A'], df.index.get_level_values('A'))
+        np.testing.assert_array_equal(data['B'], df.index.get_level_values('B'))
+        np.testing.assert_array_equal(data['zvalues'], df['C'])