Skip to content

ENH: Allow plotting backend to be an option #28622

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 12 commits into from
Nov 5, 2019
1 change: 1 addition & 0 deletions doc/source/whatsnew/v1.0.0.rst
Original file line number Diff line number Diff line change
Expand Up @@ -398,6 +398,7 @@ Plotting
- Bug where :meth:`DataFrame.boxplot` would not accept a `color` parameter like `DataFrame.plot.box` (:issue:`26214`)
- Bug in the ``xticks`` argument being ignored for :meth:`DataFrame.plot.bar` (:issue:`14119`)
- :func:`set_option` now validates that the plot backend provided to ``'plotting.backend'`` implements the backend when the option is set, rather than when a plot is created (:issue:`28163`)
- :meth:`DataFrame.plot` now allow a ``backend`` keyword arugment to allow changing between backends in one session (:issue:`28619`).

Groupby/resample/rolling
^^^^^^^^^^^^^^^^^^^^^^^^
Expand Down
204 changes: 196 additions & 8 deletions pandas/plotting/_core.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,6 @@

from pandas._config import get_option

from pandas.util._decorators import Appender

from pandas.core.dtypes.common import is_integer, is_list_like
from pandas.core.dtypes.generic import ABCDataFrame, ABCSeries

Expand All @@ -22,6 +20,7 @@ def hist_series(
yrot=None,
figsize=None,
bins=10,
backend=None,
**kwargs
):
"""
Expand Down Expand Up @@ -50,6 +49,11 @@ def hist_series(
bin edges are calculated and returned. If bins is a sequence, gives
bin edges, including left edge of first bin and right edge of last
bin. In this case, bins is returned unmodified.
backend : str, default None
Backend to use instead of the backend specified in the option
``plotting.backend``. For instance, 'matplotlib'. Alternatively, to
specify the ``plotting.backend`` for the whole session, set
``pd.options.plotting.backend``.
**kwargs
To be passed to the actual plotting function.

Expand All @@ -62,7 +66,7 @@ def hist_series(
--------
matplotlib.axes.Axes.hist : Plot a histogram using matplotlib.
"""
plot_backend = _get_plot_backend()
plot_backend = _get_plot_backend(backend)
return plot_backend.hist_series(
self,
by=by,
Expand Down Expand Up @@ -93,6 +97,7 @@ def hist_frame(
figsize=None,
layout=None,
bins=10,
backend=None,
**kwargs
):
"""
Expand Down Expand Up @@ -145,6 +150,11 @@ def hist_frame(
bin edges are calculated and returned. If bins is a sequence, gives
bin edges, including left edge of first bin and right edge of last
bin. In this case, bins is returned unmodified.
backend : str, default None
Backend to use instead of the backend specified in the option
``plotting.backend``. For instance, 'matplotlib'. Alternatively, to
specify the ``plotting.backend`` for the whole session, set
``pd.options.plotting.backend``.
**kwargs
All other plotting keyword arguments to be passed to
:meth:`matplotlib.pyplot.hist`.
Expand Down Expand Up @@ -172,7 +182,7 @@ def hist_frame(
... }, index=['pig', 'rabbit', 'duck', 'chicken', 'horse'])
>>> hist = df.hist(bins=3)
"""
plot_backend = _get_plot_backend()
plot_backend = _get_plot_backend(backend)
return plot_backend.hist_frame(
data,
column=column,
Expand Down Expand Up @@ -383,7 +393,6 @@ def boxplot(
)


@Appender(boxplot.__doc__)
def boxplot_frame(
self,
column=None,
Expand All @@ -395,9 +404,177 @@ def boxplot_frame(
figsize=None,
layout=None,
return_type=None,
backend=None,
**kwargs
):
plot_backend = _get_plot_backend()
"""
Make a box plot from DataFrame columns.

Make a box-and-whisker plot from DataFrame columns, optionally grouped
by some other columns. A box plot is a method for graphically depicting
groups of numerical data through their quartiles.
The box extends from the Q1 to Q3 quartile values of the data,
with a line at the median (Q2). The whiskers extend from the edges
of box to show the range of the data. The position of the whiskers
is set by default to `1.5 * IQR (IQR = Q3 - Q1)` from the edges of the box.
Outlier points are those past the end of the whiskers.

For further details see
Wikipedia's entry for `boxplot <https://en.wikipedia.org/wiki/Box_plot>`_.

Parameters
----------
column : str or list of str, optional
Column name or list of names, or vector.
Can be any valid input to :meth:`pandas.DataFrame.groupby`.
by : str or array-like, optional
Column in the DataFrame to :meth:`pandas.DataFrame.groupby`.
One box-plot will be done per value of columns in `by`.
ax : object of class matplotlib.axes.Axes, optional
The matplotlib axes to be used by boxplot.
fontsize : float or str
Tick label font size in points or as a string (e.g., `large`).
rot : int or float, default 0
The rotation angle of labels (in degrees)
with respect to the screen coordinate system.
grid : bool, default True
Setting this to True will show the grid.
figsize : A tuple (width, height) in inches
The size of the figure to create in matplotlib.
layout : tuple (rows, columns), optional
For example, (3, 5) will display the subplots
using 3 columns and 5 rows, starting from the top-left.
return_type : {'axes', 'dict', 'both'} or None, default 'axes'
The kind of object to return. The default is ``axes``.

* 'axes' returns the matplotlib axes the boxplot is drawn on.
* 'dict' returns a dictionary whose values are the matplotlib
Lines of the boxplot.
* 'both' returns a namedtuple with the axes and dict.
* when grouping with ``by``, a Series mapping columns to
``return_type`` is returned.

If ``return_type`` is `None`, a NumPy array
of axes with the same shape as ``layout`` is returned.
backend : str, default None
Backend to use instead of the backend specified in the option
``plotting.backend``. For instance, 'matplotlib'. Alternatively, to
specify the ``plotting.backend`` for the whole session, set
``pd.options.plotting.backend``.
**kwds
All other plotting keyword arguments to be passed to
:func:`matplotlib.pyplot.boxplot`.

Returns
-------
result
See Notes.

See Also
--------
Series.plot.hist: Make a histogram.
matplotlib.pyplot.boxplot : Matplotlib equivalent plot.

Notes
-----
The return type depends on the `return_type` parameter:

* 'axes' : object of class matplotlib.axes.Axes
* 'dict' : dict of matplotlib.lines.Line2D objects
* 'both' : a namedtuple with structure (ax, lines)

For data grouped with ``by``, return a Series of the above or a numpy
array:

* :class:`~pandas.Series`
* :class:`~numpy.array` (for ``return_type = None``)

Use ``return_type='dict'`` when you want to tweak the appearance
of the lines after plotting. In this case a dict containing the Lines
making up the boxes, caps, fliers, medians, and whiskers is returned.

Examples
--------

Boxplots can be created for every column in the dataframe
by ``df.boxplot()`` or indicating the columns to be used:

.. plot::
:context: close-figs

>>> np.random.seed(1234)
>>> df = pd.DataFrame(np.random.randn(10,4),
... columns=['Col1', 'Col2', 'Col3', 'Col4'])
>>> boxplot = df.boxplot(column=['Col1', 'Col2', 'Col3'])

Boxplots of variables distributions grouped by the values of a third
variable can be created using the option ``by``. For instance:

.. plot::
:context: close-figs

>>> df = pd.DataFrame(np.random.randn(10, 2),
... columns=['Col1', 'Col2'])
>>> df['X'] = pd.Series(['A', 'A', 'A', 'A', 'A',
... 'B', 'B', 'B', 'B', 'B'])
>>> boxplot = df.boxplot(by='X')

A list of strings (i.e. ``['X', 'Y']``) can be passed to boxplot
in order to group the data by combination of the variables in the x-axis:

.. plot::
:context: close-figs

>>> df = pd.DataFrame(np.random.randn(10,3),
... columns=['Col1', 'Col2', 'Col3'])
>>> df['X'] = pd.Series(['A', 'A', 'A', 'A', 'A',
... 'B', 'B', 'B', 'B', 'B'])
>>> df['Y'] = pd.Series(['A', 'B', 'A', 'B', 'A',
... 'B', 'A', 'B', 'A', 'B'])
>>> boxplot = df.boxplot(column=['Col1', 'Col2'], by=['X', 'Y'])

The layout of boxplot can be adjusted giving a tuple to ``layout``:

.. plot::
:context: close-figs

>>> boxplot = df.boxplot(column=['Col1', 'Col2'], by='X',
... layout=(2, 1))

Additional formatting can be done to the boxplot, like suppressing the grid
(``grid=False``), rotating the labels in the x-axis (i.e. ``rot=45``)
or changing the fontsize (i.e. ``fontsize=15``):

.. plot::
:context: close-figs

>>> boxplot = df.boxplot(grid=False, rot=45, fontsize=15)

The parameter ``return_type`` can be used to select the type of element
returned by `boxplot`. When ``return_type='axes'`` is selected,
the matplotlib axes on which the boxplot is drawn are returned:

>>> boxplot = df.boxplot(column=['Col1','Col2'], return_type='axes')
>>> type(boxplot)
<class 'matplotlib.axes._subplots.AxesSubplot'>

When grouping with ``by``, a Series mapping columns to ``return_type``
is returned:

>>> boxplot = df.boxplot(column=['Col1', 'Col2'], by='X',
... return_type='axes')
>>> type(boxplot)
<class 'pandas.core.series.Series'>

If ``return_type`` is `None`, a NumPy array of axes with the same shape
as ``layout`` is returned:

>>> boxplot = df.boxplot(column=['Col1', 'Col2'], by='X',
... return_type=None)
>>> type(boxplot)
<class 'numpy.ndarray'>
"""
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd like not to duplicate this, but I couldn't figure out a good way to inject the backend argument doc string.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just to clarify: this is because boxplot doesn't accept a backend argument right?

I may have a fix, will push it up if that works.

plot_backend = _get_plot_backend(backend)
return plot_backend.boxplot_frame(
self,
column=column,
Expand Down Expand Up @@ -425,6 +602,7 @@ def boxplot_frame_groupby(
layout=None,
sharex=False,
sharey=True,
backend=None,
**kwargs
):
"""
Expand Down Expand Up @@ -454,6 +632,11 @@ def boxplot_frame_groupby(
Whether y-axes will be shared among subplots.

.. versionadded:: 0.23.1
backend : str, default None
Backend to use instead of the backend specified in the option
``plotting.backend``. For instance, 'matplotlib'. Alternatively, to
specify the ``plotting.backend`` for the whole session, set
``pd.options.plotting.backend``.
**kwargs
All other plotting keyword arguments to be passed to
matplotlib's boxplot function.
Expand All @@ -477,7 +660,7 @@ def boxplot_frame_groupby(
>>> grouped = df.unstack(level='lvl1').groupby(level=0, axis=1)
>>> boxplot_frame_groupby(grouped, subplots=False)
"""
plot_backend = _get_plot_backend()
plot_backend = _get_plot_backend(backend)
return plot_backend.boxplot_frame_groupby(
grouped,
subplots=subplots,
Expand Down Expand Up @@ -584,6 +767,11 @@ class PlotAccessor(PandasObject):
labels with "(right)" in the legend.
include_bool : bool, default is False
If True, boolean values can be plotted.
backend : str, default None
Backend to use instead of the backend specified in the option
``plotting.backend``. For instance, 'matplotlib'. Alternatively, to
specify the ``plotting.backend`` for the whole session, set
``pd.options.plotting.backend``.
**kwargs
Options to pass to matplotlib plotting method.

Expand Down Expand Up @@ -713,7 +901,7 @@ def _get_call_args(backend_name, data, args, kwargs):
return x, y, kind, kwargs

def __call__(self, *args, **kwargs):
plot_backend = _get_plot_backend()
plot_backend = _get_plot_backend(kwargs.pop("backend", None))

x, y, kind, kwargs = self._get_call_args(
plot_backend.__name__, self._parent, args, kwargs
Expand Down
10 changes: 9 additions & 1 deletion pandas/tests/plotting/test_backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
import pandas

dummy_backend = types.ModuleType("pandas_dummy_backend")
setattr(dummy_backend, "plot", lambda *args, **kwargs: None)
setattr(dummy_backend, "plot", lambda *args, **kwargs: "used_dummy")


@pytest.fixture
Expand Down Expand Up @@ -38,6 +38,14 @@ def test_backend_is_correct(monkeypatch, restore_backend):
)


def test_backend_can_be_set_in_plot_call(monkeypatch, restore_backend):
monkeypatch.setitem(sys.modules, "pandas_dummy_backend", dummy_backend)
df = pandas.DataFrame([1, 2, 3])

assert pandas.get_option("plotting.backend") == "matplotlib"
assert df.plot(backend="pandas_dummy_backend") == "used_dummy"


@td.skip_if_no_mpl
def test_register_entrypoint(restore_backend):

Expand Down