digiefel · GitHub

Description

plotnine currently errors when given a pandas Categorical whose categories are Interval objects.

python version: 3.13.5
plotnine version: 0.14.6
pandas version: 2.3.1
numpy version: 2.3.1

Minimal reproducible example

import pandas as pd
from plotnine import ggplot, aes, geom_boxplot
# Create a numeric column and bin it into intervals:
df = pd. DataFrame({'value': range(100)})
df['bin'] = pd.cut(df['value'], bins=[0, 20, 40, 60, 80, 100])
# 'bin' is a Categorical of pandas Interval objects:
print(df['bin'].cat.categories)
# → IntervalIndex([(0, 20], (20, 40], (40, 60], (60, 80], (80, 100]], dtype='interval[int64, right]')

This works fine:

ggplot(df, aes('bin', 'value', fill='value')) + geom_point()

Image

but neither of these do:

ggplot(df, aes('bin', 'value', fill='bin')) + geom_point()
# TypeError: '<' not supported between instances of 'pandas._libs.interval.Interval' and 'float'
ggplot(df, aes('bin', 'value', fill='factor(value)')) + geom_point()
# TypeError: '<' not supported between instances of 'pandas._libs.interval.Interval' and 'float'
Traceback
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
File ~/work/freezapy/.venv/lib/python3.13/site-packages/IPython/core/formatters.py:925, in IPythonDisplayFormatter.__call__(self, obj)
    923 method = get_real_method(obj, self.print_method)
    924 if method is not None:
--> 925     method()
    926     return True
File ~/work/freezapy/.venv/lib/python3.13/site-packages/plotnine/ggplot.py:141, in ggplot._ipython_display_(self)
    134 def _ipython_display_(self):
    135     """
    136     Display plot in the output of the cell
    137 
    138     This method will always be called when a ggplot object is the
    139     last in the cell.
    140     """
--> 141     self._display()
File ~/work/freezapy/.venv/lib/python3.13/site-packages/plotnine/ggplot.py:181, in ggplot._display(self)
    179 figure_size_px = self.theme._figure_size_px
    180 buf = BytesIO()
--> 181 self.save(buf, format=save_format, verbose=False)
    182 display_func = get_display_function(format, figure_size_px)
    183 display_func(buf.getvalue())
File ~/work/freezapy/.venv/lib/python3.13/site-packages/plotnine/ggplot.py:673, in ggplot.save(self, filename, format, path, width, height, units, dpi, limitsize, verbose, **kwargs)
    624 def save(
    625     self,
    626     filename: Optional[str | Path | BytesIO] = None,
   (...)
    635     **kwargs: Any,
    636 ):
    637     """
    638     Save a ggplot object as an image file
    639 
   (...)
    671         Additional arguments to pass to matplotlib `savefig()`.
    672     """
--> 673     sv = self.save_helper(
    674         filename=filename,
    675         format=format,
    676         path=path,
    677         width=width,
    678         height=height,
    679         units=units,
    680         dpi=dpi,
    681         limitsize=limitsize,
    682         verbose=verbose,
    683         **kwargs,
    684     )
    686     with plot_context(self).rc_context:
    687         sv.figure.savefig(**sv.kwargs)
File ~/work/freezapy/.venv/lib/python3.13/site-packages/plotnine/ggplot.py:621, in ggplot.save_helper(self, filename, format, path, width, height, units, dpi, limitsize, verbose, **kwargs)
    618 if dpi is not None:
    619     self.theme = self.theme + theme(dpi=dpi)
--> 621 figure = self.draw(show=False)
    622 return mpl_save_view(figure, fig_kwargs)
File ~/work/freezapy/.venv/lib/python3.13/site-packages/plotnine/ggplot.py:278, in ggplot.draw(self, show)
    276 self = deepcopy(self)
    277 with plot_context(self, show=show):
--> 278     self._build()
    280     # setup
    281     self.figure, self.axs = self.facet.setup(self)
File ~/work/freezapy/.venv/lib/python3.13/site-packages/plotnine/ggplot.py:368, in ggplot._build(self)
    364 layout.setup(layers, self)
    366 # Compute aesthetics to produce data with generalised
    367 # variable names
--> 368 layers.compute_aesthetics(self)
    370 # Transform data using all scales
    371 layers.transform(scales)
File ~/work/freezapy/.venv/lib/python3.13/site-packages/plotnine/layer.py:468, in Layers.compute_aesthetics(self, plot)
    466 def compute_aesthetics(self, plot: ggplot):
    467     for l in self:
--> 468         l.compute_aesthetics(plot)
File ~/work/freezapy/.venv/lib/python3.13/site-packages/plotnine/layer.py:270, in layer.compute_aesthetics(self, plot)
    267 else:
    268     evaled["PANEL"] = self.data["PANEL"]
--> 270 data = add_group(evaled)
    271 self.data = data.sort_values("PANEL", kind="mergesort")
File ~/work/freezapy/.venv/lib/python3.13/site-packages/plotnine/layer.py:521, in add_group(data)
    519 disc = discrete_columns(data, ignore=ignore)
    520 if disc:
--> 521     data["group"] = ninteraction(data[disc], drop=True)
    522 else:
    523     data["group"] = NO_GROUP
File ~/work/freezapy/.venv/lib/python3.13/site-packages/plotnine/_utils/__init__.py:309, in ninteraction(df, drop)
    306 def len_unique(x):
    307     return len(np.unique(x))
--> 309 ndistinct: IntArray = ids.apply(len_unique, axis=0).to_numpy()
    311 combs = np.array(np.hstack([1, np.cumprod(ndistinct[:-1])]))
    312 mat = np.array(ids)
File ~/work/freezapy/.venv/lib/python3.13/site-packages/pandas/core/frame.py:10381, in DataFrame.apply(self, func, axis, raw, result_type, args, by_row, engine, engine_kwargs, **kwargs)
  10367 from pandas.core.apply import frame_apply
  10369 op = frame_apply(
  10370     self,
  10371     func=func,
   (...)
  10379     kwargs=kwargs,
  10380 )
> 10381 return op.apply().__finalize__(self, method="apply")
File ~/work/freezapy/.venv/lib/python3.13/site-packages/pandas/core/apply.py:916, in FrameApply.apply(self)
    913 elif self.raw:
    914     return self.apply_raw(engine=self.engine, engine_kwargs=self.engine_kwargs)
--> 916 return self.apply_standard()
File ~/work/freezapy/.venv/lib/python3.13/site-packages/pandas/core/apply.py:1063, in FrameApply.apply_standard(self)
   1061 def apply_standard(self):
   1062     if self.engine == "python":
-> 1063         results, res_index = self.apply_series_generator()
   1064     else:
   1065         results, res_index = self.apply_series_numba()
File ~/work/freezapy/.venv/lib/python3.13/site-packages/pandas/core/apply.py:1081, in FrameApply.apply_series_generator(self)
   1078 with option_context("mode.chained_assignment", None):
   1079     for i, v in enumerate(series_gen):
   1080         # ignore SettingWithCopy here in case the user mutates
-> 1081         results[i] = self.func(v, *self.args, **self.kwargs)
   1082         if isinstance(results[i], ABCSeries):
   1083             # If we have a view on v, we need to make a copy because
   1084             #  series_generator will swap out the underlying data
   1085             results[i] = results[i].copy(deep=False)
File ~/work/freezapy/.venv/lib/python3.13/site-packages/plotnine/_utils/__init__.py:307, in ninteraction.<locals>.len_unique(x)
    306 def len_unique(x):
--> 307     return len(np.unique(x))
File ~/work/freezapy/.venv/lib/python3.13/site-packages/numpy/lib/_arraysetops_impl.py:294, in unique(ar, return_index, return_inverse, return_counts, axis, equal_nan, sorted)
    292 ar = np.asanyarray(ar)
    293 if axis is None:
--> 294     ret = _unique1d(ar, return_index, return_inverse, return_counts,
    295                     equal_nan=equal_nan, inverse_shape=ar.shape, axis=None,
    296                     sorted=sorted)
    297     return _unpack_tuple(ret)
    299 # axis was specified and not None
File ~/work/freezapy/.venv/lib/python3.13/site-packages/numpy/lib/_arraysetops_impl.py:382, in _unique1d(ar, return_index, return_inverse, return_counts, equal_nan, inverse_shape, axis, sorted)
    380     aux = ar[perm]
    381 else:
--> 382     ar.sort()
    383     aux = ar
    384 mask = np.empty(aux.shape, dtype=np.bool)
TypeError: '<' not supported between instances of 'pandas._libs.interval.Interval' and 'float'

Read the original on github.com ↗