has2k1 / plotnine

A Grammar of Graphics for Python
https://plotnine.org
MIT License
4.01k stars 217 forks source link

Computing histogram with datetime axis throws TypeError: can't compare offset-naive and offset-aware datetimes #879

Open chrisjcameron opened 6 days ago

chrisjcameron commented 6 days ago

This is a minimal example that produces the error. Leaving out any single row in the data table will result in the expected plot without throwing the error. Similarly, specifying + gg.scale_x_datetime(breaks='12 hours', date_labels='%m-%d %H:%M') will produce a plot without errors.

import pandas as pd
import plotnine as gg

Registration_Time = '''
2022-10-19 04:42:04
2022-10-19 10:16:29
2022-10-19 13:27:09
2022-10-19 14:40:24
2022-10-19 15:20:31
2022-10-19 15:47:18
2022-10-19 17:00:23
2022-10-19 22:45:01
'''.strip().splitlines()

org = '''
other
wcm
wcm
cu
wcm
wcm
cu
cu
'''.strip().splitlines()

df = pd.DataFrame({'Registration_Time':Registration_Time, 'org':org})
df['Registration_Time'] = pd.to_datetime(df['Registration_Time']) 
df['org'] = pd.Categorical(df.org, categories=['wcm', 'cu', 'other'])

plot_df = (
    df
    .sort_values("Registration_Time")
)#.iloc[:-1]

# plotnine to plot (using grammar of graphics like ggplot2)
plot = (
    gg.ggplot(plot_df, gg.aes(x='Registration_Time', fill='org', group='org'))
    + gg.geom_histogram(binwidth=4/24, position='stack', color='black', show_legend=False)
)
plot.show()
chrisjcameron commented 6 days ago

Full traceback:

---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
Cell In[126], line 19
      6 # plotnine to plot (using grammar of graphics like ggplot2)
      7 plot = (
      8     gg.ggplot(plot_df, gg.aes(x='Registration_Time', fill='org', group='org'))
      9     + gg.geom_histogram(binwidth=4[/24](http://localhost:51124/24), position='stack', color='black', show_legend=False)
   (...)
     17 #    + gg.ggtitle("Registrations for R workshop in first 48 hours")
     18 )
---> 19 plot.show()

File [~/miniforge3/envs/cforge/lib/python3.11/site-packages/plotnine/ggplot.py:150](http://localhost:51124/lab/tree/~/miniforge3/envs/cforge/lib/python3.11/site-packages/plotnine/ggplot.py#line=149), in ggplot.show(self)
    143 def show(self):
    144     """
    145     Show plot using the matplotlib backend set by the user
    146 
    147     Users should prefer this method instead of printing or repring
    148     the object.
    149     """
--> 150     self._display() if is_inline_backend() else self.draw(show=True)

File [~/miniforge3/envs/cforge/lib/python3.11/site-packages/plotnine/ggplot.py:175](http://localhost:51124/lab/tree/~/miniforge3/envs/cforge/lib/python3.11/site-packages/plotnine/ggplot.py#line=174), in ggplot._display(self)
    172     save_format = "png"
    174 buf = BytesIO()
--> 175 self.save(buf, format=save_format, verbose=False)
    176 display_func = get_display_function(format)
    177 display_func(buf.getvalue())

File [~/miniforge3/envs/cforge/lib/python3.11/site-packages/plotnine/ggplot.py:663](http://localhost:51124/lab/tree/~/miniforge3/envs/cforge/lib/python3.11/site-packages/plotnine/ggplot.py#line=662), in ggplot.save(self, filename, format, path, width, height, units, dpi, limitsize, verbose, **kwargs)
    615 def save(
    616     self,
    617     filename: Optional[str | Path | BytesIO] = None,
   (...)
    626     **kwargs: Any,
    627 ):
    628     """
    629     Save a ggplot object as an image file
    630 
   (...)
    661         Additional arguments to pass to matplotlib `savefig()`.
    662     """
--> 663     sv = self.save_helper(
    664         filename=filename,
    665         format=format,
    666         path=path,
    667         width=width,
    668         height=height,
    669         units=units,
    670         dpi=dpi,
    671         limitsize=limitsize,
    672         verbose=verbose,
    673         **kwargs,
    674     )
    676     with plot_context(self).rc_context:
    677         sv.figure.savefig(**sv.kwargs)

File [~/miniforge3/envs/cforge/lib/python3.11/site-packages/plotnine/ggplot.py:612](http://localhost:51124/lab/tree/~/miniforge3/envs/cforge/lib/python3.11/site-packages/plotnine/ggplot.py#line=611), in ggplot.save_helper(self, filename, format, path, width, height, units, dpi, limitsize, verbose, **kwargs)
    609 if dpi is not None:
    610     self.theme = self.theme + theme(dpi=dpi)
--> 612 figure = self.draw(show=False)
    613 return mpl_save_view(figure, fig_kwargs)

File [~/miniforge3/envs/cforge/lib/python3.11/site-packages/plotnine/ggplot.py:272](http://localhost:51124/lab/tree/~/miniforge3/envs/cforge/lib/python3.11/site-packages/plotnine/ggplot.py#line=271), in ggplot.draw(self, show)
    270 self = deepcopy(self)
    271 with plot_context(self, show=show):
--> 272     self._build()
    274     # setup
    275     self.figure, self.axs = self.facet.setup(self)

File [~/miniforge3/envs/cforge/lib/python3.11/site-packages/plotnine/ggplot.py:400](http://localhost:51124/lab/tree/~/miniforge3/envs/cforge/lib/python3.11/site-packages/plotnine/ggplot.py#line=399), in ggplot._build(self)
    397     layers.map(npscales)
    399 # Train coordinate system
--> 400 layout.setup_panel_params(self.coordinates)
    402 # fill in the defaults
    403 layers.use_defaults()

File [~/miniforge3/envs/cforge/lib/python3.11/site-packages/plotnine/facets/layout.py:198](http://localhost:51124/lab/tree/~/miniforge3/envs/cforge/lib/python3.11/site-packages/plotnine/facets/layout.py#line=197), in Layout.setup_panel_params(self, coord)
    196 for i, j in self.layout[cols].itertuples(index=False):
    197     i, j = i - 1, j - 1
--> 198     params = coord.setup_panel_params(
    199         self.panel_scales_x[i], self.panel_scales_y[j]
    200     )
    201     self.panel_params.append(params)

File [~/miniforge3/envs/cforge/lib/python3.11/site-packages/plotnine/coords/coord_cartesian.py:80](http://localhost:51124/lab/tree/~/miniforge3/envs/cforge/lib/python3.11/site-packages/plotnine/coords/coord_cartesian.py#line=79), in coord_cartesian.setup_panel_params(self, scale_x, scale_y)
     76     sv = scale.view(limits=coord_limits, range=ranges.range)
     77     return sv
     79 out = panel_view(
---> 80     x=get_scale_view(scale_x, self.limits.x),
     81     y=get_scale_view(scale_y, self.limits.y),
     82 )
     83 return out

File [~/miniforge3/envs/cforge/lib/python3.11/site-packages/plotnine/coords/coord_cartesian.py:76](http://localhost:51124/lab/tree/~/miniforge3/envs/cforge/lib/python3.11/site-packages/plotnine/coords/coord_cartesian.py#line=75), in coord_cartesian.setup_panel_params.<locals>.get_scale_view(scale, coord_limits)
     72 expansion = scale.default_expansion(expand=self.expand)
     73 ranges = scale.expand_limits(
     74     scale.limits, expansion, coord_limits, identity_trans
     75 )
---> 76 sv = scale.view(limits=coord_limits, range=ranges.range)
     77 return sv

File [~/miniforge3/envs/cforge/lib/python3.11/site-packages/plotnine/scales/scale_continuous.py:302](http://localhost:51124/lab/tree/~/miniforge3/envs/cforge/lib/python3.11/site-packages/plotnine/scales/scale_continuous.py#line=301), in scale_continuous.view(self, limits, range)
    299 if range is None:
    300     range = self.dimension(limits=limits)
--> 302 breaks = self.get_bounded_breaks(range)
    303 labels = self.get_labels(breaks)
    305 ubreaks = self.get_breaks(range)

File [~/miniforge3/envs/cforge/lib/python3.11/site-packages/plotnine/scales/scale_continuous.py:403](http://localhost:51124/lab/tree/~/miniforge3/envs/cforge/lib/python3.11/site-packages/plotnine/scales/scale_continuous.py#line=402), in scale_continuous.get_bounded_breaks(self, limits)
    401 if limits is None:
    402     limits = self.limits
--> 403 breaks = self.get_breaks(limits)
    404 strict_breaks = [b for b in breaks if limits[0] <= b <= limits[1]]
    405 return strict_breaks

File [~/miniforge3/envs/cforge/lib/python3.11/site-packages/plotnine/scales/scale_continuous.py:383](http://localhost:51124/lab/tree/~/miniforge3/envs/cforge/lib/python3.11/site-packages/plotnine/scales/scale_continuous.py#line=382), in scale_continuous.get_breaks(self, limits)
    379     breaks = []
    380 elif self.breaks is True:
    381     # TODO: Fix this type mismatch in mizani with
    382     # a typevar so that type-in = type-out
--> 383     _tlimits = self.trans.breaks(_limits)
    384     breaks: ScaleContinuousBreaks = _tlimits  # pyright: ignore
    385 elif zero_range(_limits):

File [~/miniforge3/envs/cforge/lib/python3.11/site-packages/mizani/transforms.py:224](http://localhost:51124/lab/tree/~/miniforge3/envs/cforge/lib/python3.11/site-packages/mizani/transforms.py#line=223), in trans.breaks(self, limits)
    218 # clip the breaks to the domain,
    219 # e.g. probabilities will be in [0, 1] domain
    220 limits = (
    221     max(self.domain[0], limits[0]),
    222     min(self.domain[1], limits[1]),
    223 )
--> 224 breaks = np.asarray(self.breaks_(limits))
    226 # Some methods (e.g. breaks_extended) that
    227 # calculate breaks take the limits as guide posts and
    228 # not hard limits.
    229 breaks = breaks.compress(
    230     (breaks >= self.domain[0]) & (breaks <= self.domain[1])
    231 )

File [~/miniforge3/envs/cforge/lib/python3.11/site-packages/mizani/breaks.py:481](http://localhost:51124/lab/tree/~/miniforge3/envs/cforge/lib/python3.11/site-packages/mizani/breaks.py#line=480), in breaks_date.__call__(self, limits)
    477     return calculate_date_breaks_byunits(
    478         limits, self.units, self.width
    479     )
    480 else:
--> 481     return calculate_date_breaks_auto(limits, self.n)

File [~/miniforge3/envs/cforge/lib/python3.11/site-packages/mizani/_core/dates.py:280](http://localhost:51124/lab/tree/~/miniforge3/envs/cforge/lib/python3.11/site-packages/mizani/_core/dates.py#line=279), in calculate_date_breaks_auto(limits, n)
    276 def calculate_date_breaks_auto(limits, n: int = 5) -> Sequence[datetime]:
    277     """
    278     Calcuate date breaks using appropriate units
    279     """
--> 280     info = calculate_date_breaks_info(limits, n=n)
    281     lookup = {
    282         DF.YEARLY: yearly_breaks,
    283         DF.MONTHLY: monthly_breaks,
   (...)
    288         DF.MICROSECONDLY: microsecondly_breaks,
    289     }
    290     return lookup[info.frequency](info)

File [~/miniforge3/envs/cforge/lib/python3.11/site-packages/mizani/_core/dates.py:232](http://localhost:51124/lab/tree/~/miniforge3/envs/cforge/lib/python3.11/site-packages/mizani/_core/dates.py#line=231), in calculate_date_breaks_info(limits, n)
    225 # Widen the duration at each granularity
    226 itv = Interval(*limits)
    227 unit_durations = (
    228     itv.y_wide,
    229     itv.M_wide,
    230     itv.d_wide,
    231     itv.h_wide,
--> 232     itv.m_wide,
    233     itv.s,
    234     itv.u,
    235 )
    236 # Search frequencies from longest (yearly) to the smallest
    237 # for one that would a good width between the breaks
    238 
    239 # Defaults
    240 freq = DF.YEARLY  # makes pyright happy

File [~/miniforge3/envs/cforge/lib/python3.11/site-packages/mizani/_core/date_utils.py:131](http://localhost:51124/lab/tree/~/miniforge3/envs/cforge/lib/python3.11/site-packages/mizani/_core/date_utils.py#line=130), in Interval.m_wide(self)
    126 @property
    127 def m_wide(self) -> int:
    128     """
    129     Minutes (enclosing the original)
    130     """
--> 131     return Interval(*self.limits_minute()).m

File <string>:5, in __init__(self, start, end)

File [~/miniforge3/envs/cforge/lib/python3.11/site-packages/mizani/_core/date_utils.py:46](http://localhost:51124/lab/tree/~/miniforge3/envs/cforge/lib/python3.11/site-packages/mizani/_core/date_utils.py#line=45), in Interval.__post_init__(self)
     43 if isinstance(self.end, date):
     44     self.end = datetime.fromisoformat(self.end.isoformat())
---> 46 self._delta = relativedelta(self.end, self.start)
     47 self._tdelta = self.end - self.start

File [~/miniforge3/envs/cforge/lib/python3.11/site-packages/dateutil/relativedelta.py:154](http://localhost:51124/lab/tree/~/miniforge3/envs/cforge/lib/python3.11/site-packages/dateutil/relativedelta.py#line=153), in relativedelta.__init__(self, dt1, dt2, years, months, days, leapdays, weeks, hours, minutes, seconds, microseconds, year, month, day, weekday, yearday, nlyearday, hour, minute, second, microsecond)
    151 dtm = self.__radd__(dt2)
    153 # If we've overshot our target, make an adjustment
--> 154 if dt1 < dt2:
    155     compare = operator.gt
    156     increment = 1

TypeError: can't compare offset-naive and offset-aware datetimes
has2k1 commented 5 days ago

What versions of plotnine and mizani do you have installed? Try upgrading to the latest versions.

chrisjcameron commented 5 days ago

Originally:

python                    3.11.9          h932a869_0_cpython    conda-forge
mizani                    0.11.2             pyhd8ed1ab_0    conda-forge
plotnine                  0.13.5             pyhd8ed1ab_0    conda-forge

conda update plotnine mizani updated mizani and downgraded plotnine:

mizani                    0.12.2             pyhd8ed1ab_0    conda-forge
plotnine                  0.12.2             pyhd8ed1ab_0    conda-forge

With these versions, plotnine complained about not having a "show()" method.

pip install -U mizani plotnine installed:

mizani-0.11.4
plotnine-0.13.6

For this set, I got the same TypeError

has2k1 commented 5 days ago

Okay got it. This is solved in the current mizani-0.12.2 which can only be installed by the next version of plotnine; vO.14.0 will be out in a couple of days.

If you really need it fixed, you can install the development version (main branch) of plotnine.