Repository: holoviz/holoviews

Issue:
Pandas `observed=False` warnings
The warnings:

```
[/Users/mliquet/dev/holoviews/holoviews/core/data/pandas.py:298](http://localhost:8889/Users/mliquet/dev/holoviews/holoviews/core/data/pandas.py#line=297): FutureWarning: The default of observed=False is deprecated and will be changed to True in a future version of pandas. Pass observed=False to retain current behavior or observed=True to adopt the future default and silence this warning.
  grouped = reindexed.groupby(cols, sort=False)
[/Users/mliquet/dev/holoviews/holoviews/element/util.py:203](http://localhost:8889/Users/mliquet/dev/holoviews/holoviews/element/util.py#line=202): FutureWarning: The default of observed=False is deprecated and will be changed to True in a future version of pandas. Pass observed=False to retain current behavior or observed=True to adopt the future default and silence this warning.
  df = obj.data.set_index(index_cols).groupby(index_cols, sort=False).first()
```

```python
import holoviews as hv
import numpy as np
import pandas as pd

hv.extension('bokeh')

df = pd.DataFrame({
    'X': pd.Series(['a', 'a', 'a', 'b', 'b', 'b', 'c', 'c', 'c',], dtype='category'),
    'Y': pd.Series(['O', 'P', 'Q', 'O', 'P', 'Q', 'O', 'P', 'Q',], dtype='category'),
    'Z': [1, 2, 3, 4, 5, 6, 7, 8, 9],
})

hv.HeatMap(df, ['X', 'Y'], 'Z').aggregate(function=np.mean)
```

![image](https://github.com/holoviz/holoviews/assets/35924738/9dac32aa-71cd-4759-b623-4c1ba195f20f)


Discussion:
codecov[bot]: ## [Codecov](https://app.codecov.io/gh/holoviz/holoviews/pull/6346?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.50%. Comparing base [(`55856e1`)](https://app.codecov.io/gh/holoviz/holoviews/commit/55856e1b17850129144b9dd0d27fcc2b770c76a3?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=holoviz) to head [(`3368ebf`)](https://app.codecov.io/gh/holoviz/holoviews/commit/3368ebffa841cfdf3e7e96bbb528776060b7435c?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=holoviz).
> Report is 3 commits behind head on main.

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


```diff
@@            Coverage Diff             @@
##             main    #6346      +/-   ##
==========================================
- Coverage   88.51%   88.50%   -0.02%     
==========================================
  Files         323      323              
  Lines       68098    68110      +12     
==========================================
+ Hits        60276    60279       +3     
- Misses       7822     7831       +9     
```

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/holoviz/holoviews/pull/6346?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/core/data/pandas.py b/holoviews/core/data/pandas.py
index 300391cbf..c09b0348e 100644
--- a/holoviews/core/data/pandas.py
+++ b/holoviews/core/data/pandas.py
@@ -7,6 +7,7 @@ from .. import util
 from ..dimension import Dimension, dimension_name
 from ..element import Element
 from ..ndmapping import NdMapping, item_check, sorted_context
+from ..util import PANDAS_GE_210
 from .interface import DataError, Interface
 from .util import finite_range
 
@@ -256,10 +257,9 @@ class PandasInterface(Interface, PandasAPI):
             # when iterating over a groupby with a grouper equal to a list of length 1.
             # Don't supply a list with a single grouper to avoid this warning.
             group_by = group_by[0]
-        if util.pandas_version >= Version("2.1.0"):
-            groupby_kwargs = {"sort": False, "observed": False}
-        else:
-            groupby_kwargs = {"sort": False}
+        groupby_kwargs = {"sort": False}
+        if PANDAS_GE_210:
+            groupby_kwargs["observed"] = False
         data = [(k, group_type(v, **group_kwargs)) for k, v in
                 dataset.data.groupby(group_by, **groupby_kwargs)]
         if issubclass(container_type, NdMapping):
@@ -295,7 +295,10 @@ class PandasInterface(Interface, PandasAPI):
                     c for c, d in zip(reindexed.columns, reindexed.dtypes)
                     if is_numeric_dtype(d) and c not in cols
                 ]
-            grouped = reindexed.groupby(cols, sort=False)
+            groupby_kwargs = {"sort": False}
+            if PANDAS_GE_210:
+                groupby_kwargs["observed"] = False
+            grouped = reindexed.groupby(cols, **groupby_kwargs)
             df = grouped[numeric_cols].aggregate(fn, **kwargs).reset_index()
         else:
             agg = reindexed.apply(fn, **kwargs)
diff --git a/holoviews/core/util.py b/holoviews/core/util.py
index 24e95e271..590b7ab6e 100644
--- a/holoviews/core/util.py
+++ b/holoviews/core/util.py
@@ -43,6 +43,7 @@ param_version = Version(param.__version__)
 pandas_version = Version(pd.__version__)
 
 NUMPY_GE_200 = numpy_version >= Version("2")
+PANDAS_GE_210 = pandas_version >= Version("2.1")
 
 # Types
 generator_types = (zip, range, types.GeneratorType)
diff --git a/holoviews/element/util.py b/holoviews/element/util.py
index 19d47ba5e..6e6f9edce 100644
--- a/holoviews/element/util.py
+++ b/holoviews/element/util.py
@@ -10,6 +10,7 @@ from ..core.data import PandasInterface, default_datatype
 from ..core.operation import Operation
 from ..core.sheetcoords import Slice
 from ..core.util import (
+    PANDAS_GE_210,
     cartesian_product,
     datetime_types,
     is_cyclic,
@@ -200,7 +201,10 @@ class categorical_aggregate2d(Operation):
 
     def _aggregate_dataset_pandas(self, obj):
         index_cols = [d.name for d in obj.kdims]
-        df = obj.data.set_index(index_cols).groupby(index_cols, sort=False).first()
+        groupby_kwargs = {"sort": False}
+        if PANDAS_GE_210:
+            groupby_kwargs["observed"] = False
+        df = obj.data.set_index(index_cols).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 f44e69e61..67f315692 100644
--- a/holoviews/tests/plotting/bokeh/test_heatmapplot.py
+++ b/holoviews/tests/plotting/bokeh/test_heatmapplot.py
@@ -1,4 +1,5 @@
 import numpy as np
+import pandas as pd
 from bokeh.models import FactorRange, HoverTool, Range1d
 
 from holoviews.element import HeatMap, Image, Points
@@ -140,3 +141,18 @@ class TestHeatMapPlot(TestBokehPlot):
         self.assertEqual(cds.data['col'], np.array([1, 1, 2, 2]))
         self.assertEqual(cds.data['alpha'], np.array([0, 1, 0, 0]))
         self.assertEqual(cds.data['zvalues'], np.array([0.5, 0.1, 0.2, 0.6]))
+
+    def test_heatmap_pandas_categorial(self):
+        # Test for https://github.com/holoviz/holoviews/issues/6313
+        df = pd.DataFrame({
+            'X': pd.Series(['a', 'a', 'a', 'b', 'b', 'b', 'c', 'c', 'c',], dtype='category'),
+            'Y': pd.Series(['O', 'P', 'Q', 'O', 'P', 'Q', 'O', 'P', 'Q',], dtype='category'),
+            'Z': [1, 2, 3, 4, 5, 6, 7, 8, 9],
+        })
+
+        hm = HeatMap(df, ['X', 'Y'], 'Z').aggregate(function=np.mean)
+        plot = bokeh_renderer.get_plot(hm)
+        data = plot.handles["cds"].data
+        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"])