proplot-dev / proplot

🎨 A succinct matplotlib wrapper for making beautiful, publication-quality graphics
https://proplot.readthedocs.io
MIT License
1.11k stars 102 forks source link

pandas dataframe with date index fails in masked array conversion #320

Closed scottstanie closed 2 years ago

scottstanie commented 2 years ago

Description

Plotting a pandas dataframe using a proplot axis fails when the index is a DatetimeIndex

Steps to reproduce

import proplot as pplt
import pandas as pd
import numpy as np

fig, ax = pplt.subplots()
dates = pd.date_range(start='20200101', end='20200105')
s = pd.DataFrame(data={'col': np.arange(5)}, index=dates)
s['col'].plot(ax=ax)

Actual behavior:

...
~/miniconda3/envs/mapping/lib/python3.8/site-packages/numpy/ma/core.py in masked_invalid(a, copy)
   2367         cls = type(a)
   2368     else:
-> 2369         condition = ~(np.isfinite(a))
   2370         cls = MaskedArray
   2371     result = a.view(cls)
TypeError: ufunc 'isfinite' not supported for the input types, and the inputs could not be safely coerced to any supported types according to the casting rule ''safe''

Full traceback:

```python-traceback --------------------------------------------------------------------------- TypeError Traceback (most recent call last) in 2 dates = pd.date_range(start='20200101', end='20200105') 3 s = pd.DataFrame(data={'col': np.arange(5)}, index=dates) ----> 4 s['col'].plot(ax=ax) ~/miniconda3/envs/mapping/lib/python3.8/site-packages/pandas/plotting/_core.py in __call__(self, *args, **kwargs) 947 data.columns = label_name 948 --> 949 return plot_backend.plot(data, kind=kind, **kwargs) 950 951 __call__.__doc__ = __doc__ ~/miniconda3/envs/mapping/lib/python3.8/site-packages/pandas/plotting/_matplotlib/__init__.py in plot(data, kind, **kwargs) 59 kwargs["ax"] = getattr(ax, "left_ax", ax) 60 plot_obj = PLOT_CLASSES[kind](data, **kwargs) ---> 61 plot_obj.generate() 62 plot_obj.draw() 63 return plot_obj.result ~/miniconda3/envs/mapping/lib/python3.8/site-packages/pandas/plotting/_matplotlib/core.py in generate(self) 269 self._compute_plot_data() 270 self._setup_subplots() --> 271 self._make_plot() 272 self._add_table() 273 self._make_legend() ~/miniconda3/envs/mapping/lib/python3.8/site-packages/pandas/plotting/_matplotlib/core.py in _make_plot(self) 1116 kwds["label"] = label 1117 -> 1118 newlines = plotf( 1119 ax, 1120 x, ~/miniconda3/envs/mapping/lib/python3.8/site-packages/pandas/plotting/_matplotlib/core.py in _ts_plot(cls, ax, x, data, style, **kwds) 1169 ax._plot_data.append((data, cls._kind, kwds)) 1170 -> 1171 lines = cls._plot(ax, data.index, data.values, style=style, **kwds) 1172 # set date formatter, locators and rescale limits 1173 format_dateaxis(ax, ax.freq, data.index) ~/miniconda3/envs/mapping/lib/python3.8/site-packages/pandas/plotting/_matplotlib/core.py in _plot(cls, ax, x, y, style, column_num, stacking_id, **kwds) 1143 cls._initialize_stacker(ax, stacking_id, len(y)) 1144 y_values = cls._get_stacked_values(ax, stacking_id, y, kwds["label"]) -> 1145 lines = MPLPlot._plot(ax, x, y_values, style=style, **kwds) 1146 cls._update_stacker(ax, stacking_id, y) 1147 return lines ~/miniconda3/envs/mapping/lib/python3.8/site-packages/pandas/plotting/_matplotlib/converter.py in wrapper(*args, **kwargs) 63 def wrapper(*args, **kwargs): 64 with pandas_converters(): ---> 65 return func(*args, **kwargs) 66 67 return wrapper ~/miniconda3/envs/mapping/lib/python3.8/site-packages/pandas/plotting/_matplotlib/core.py in _plot(cls, ax, x, y, style, is_errorbar, **kwds) 666 else: 667 args = (x, y) --> 668 return ax.plot(*args, **kwds) 669 670 def _get_index_name(self): ~/miniconda3/envs/mapping/lib/python3.8/site-packages/proplot/internals/process.py in _redirect_or_standardize(self, *args, **kwargs) 282 283 # Call main function --> 284 return func(self, *args, **kwargs) # call unbound method 285 286 return _redirect_or_standardize ~/miniconda3/envs/mapping/lib/python3.8/site-packages/proplot/axes/plot.py in plot(self, *args, **kwargs) 2877 """ 2878 kwargs = _parse_vert(default_vert=True, **kwargs) -> 2879 return self._apply_plot(*args, **kwargs) 2880 2881 @process._preprocess_args('y', 'x', allow_extra=True) ~/miniconda3/envs/mapping/lib/python3.8/site-packages/proplot/axes/plot.py in _apply_plot(self, vert, *pairs, **kwargs) 2851 2852 # Add sticky edges -> 2853 self._add_sticky_edges(objs, 'x' if vert else 'y', xsides, only=mlines.Line2D) 2854 self._update_guide(objs, **guide_kw) 2855 return cbook.silent_list('Line2D', objs) # always return list ~/miniconda3/envs/mapping/lib/python3.8/site-packages/proplot/axes/plot.py in _add_sticky_edges(self, objs, axis, only, *args) 1511 if not sides.size: 1512 continue -> 1513 min_, max_ = process._safe_range(sides) 1514 if min_ is None or max_ is None: 1515 continue ~/miniconda3/envs/mapping/lib/python3.8/site-packages/proplot/internals/process.py in _safe_range(data, lo, hi) 488 """ 489 _load_objects() --> 490 data, units = _to_masked_array(data) 491 data = data.compressed() # remove all invalid values 492 min_ = max_ = None ~/miniconda3/envs/mapping/lib/python3.8/site-packages/proplot/internals/process.py in _to_masked_array(data, copy) 141 if ndarray is not Quantity and isinstance(data, Quantity): 142 data, units = data.magnitude, data.units --> 143 data = ma.masked_invalid(data, copy=copy) 144 if np.issubdtype(data.dtype, np.integer): 145 data = data.astype(np.float) ~/miniconda3/envs/mapping/lib/python3.8/site-packages/numpy/ma/core.py in masked_invalid(a, copy) 2367 cls = type(a) 2368 else: -> 2369 condition = ~(np.isfinite(a)) 2370 cls = MaskedArray 2371 result = a.view(cls) TypeError: ufunc 'isfinite' not supported for the input types, and the inputs could not be safely coerced to any supported types according to the casting rule ''safe'' ```

Possibly because it's converting to a 2D array of Periods:

TypeError: ufunc 'isfinite' not supported for the input types, and the inputs could not be safely coerced to any supported types according to the casting rule ''safe''

> /home/scott/miniconda3/envs/mapping/lib/python3.8/site-packages/numpy/ma/core.py(2369)masked_invalid()
   2367         cls = type(a)
   2368     else:
-> 2369         condition = ~(np.isfinite(a))
   2370         cls = MaskedArray
   2371     result = a.view(cls)

ipdb>  p a

array([[Period('2020-01-01', 'D'), Period('2020-01-02', 'D'),
        Period('2020-01-03', 'D'), Period('2020-01-04', 'D'),
        Period('2020-01-05', 'D')]], dtype=object)

Equivalent steps in matplotlib

Using

fig, ax = plt.subplots()
...

does work.

Proplot version

import matplotlib; print(matplotlib.__version__); import proplot; print(proplot.version)

3.3.1
0.9.5
scottstanie commented 2 years ago

Does it seem like there's another workaround for using masked_invalid? https://numpy.org/doc/stable/reference/generated/numpy.ma.masked_invalid.html this says

Only applies to arrays with a dtype where NaNs or infs make sense (i.e. floating point types), but accepts any array_like object.

This is kinda annoying behavior from numpy IMO.... I'll point out that the workaround I've got now is to do

ax.plot(s.index, s['col'])

indead of

s['col'].plot(ax=ax)

It's not a big deal for me, so if you think this would be too many annoying changes to make for proplot, feel free to close this.

lukelbd commented 2 years ago

Thanks for the report, this is now fixed (54acfb1). Also used your example to discover + fix an issue with auto-axis reversal that cropped up since version 0.9.5 (f9ac77f4).

import proplot as pplt
import pandas as pd
import numpy as np

fig, ax = pplt.subplots()
dates = pd.date_range(start='20200101', end='20200105')
s = pd.DataFrame(data={'col': np.arange(5)}, index=dates)
s['col'].plot(ax=ax)

tmp