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()
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'
--------------------------------------------------------------------------- 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'
