diff --git a/doc/source/api.rst b/doc/source/api.rst index 81bb420c47a99..415a8dfa029c4 100644 --- a/doc/source/api.rst +++ b/doc/source/api.rst @@ -2482,6 +2482,7 @@ Style Application Styler.set_properties Styler.set_uuid Styler.clear + Styler.pipe Builtin Styles ~~~~~~~~~~~~~~ diff --git a/doc/source/whatsnew/v0.24.0.rst b/doc/source/whatsnew/v0.24.0.rst index d0dddb19f4c93..1a7f96bf7ae41 100644 --- a/doc/source/whatsnew/v0.24.0.rst +++ b/doc/source/whatsnew/v0.24.0.rst @@ -184,6 +184,30 @@ array, but rather an ``ExtensionArray``: This is the same behavior as ``Series.values`` for categorical data. See :ref:`whatsnew_0240.api_breaking.interval_values` for more. + +.. _whatsnew_0240.enhancements.styler_pipe: + +New ``Styler.pipe()`` method +^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +The :class:`~pandas.io.formats.style.Styler` class has gained a +:meth:`~pandas.io.formats.style.Styler.pipe` method (:issue:`23229`). This provides a +convenient way to apply users' predefined styling functions, and can help reduce +"boilerplate" when using DataFrame styling functionality repeatedly within a notebook. + +.. ipython:: python + + df = pandas.DataFrame({'N': [1250, 1500, 1750], 'X': [0.25, 0.35, 0.50]}) + + def format_and_align(styler): + return (styler.format({'N': '{:,}', 'X': '{:.1%}'}) + .set_properties(**{'text-align': 'right'})) + + df.style.pipe(format_and_align).set_caption('Summary of results.') + +Similar methods already exist for other classes in pandas, including :meth:`DataFrame.pipe`, +:meth:`Groupby.pipe`, and :meth:`Resampler.pipe`. + + .. _whatsnew_0240.enhancements.join_with_two_multiindexes: Joining with two multi-indexes @@ -225,6 +249,7 @@ For earlier versions this can be done using the following. pd.merge(left.reset_index(), right.reset_index(), on=['key'], how='inner').set_index(['key', 'X', 'Y']) + .. _whatsnew_0240.enhancements.rename_axis: Renaming names in a MultiIndex @@ -248,6 +273,7 @@ Example: See the :ref:`advanced docs on renaming` for more details. + .. _whatsnew_0240.enhancements.other: Other Enhancements diff --git a/pandas/io/formats/style.py b/pandas/io/formats/style.py index dda50b6a0e7f8..8ee9ea5b3d980 100644 --- a/pandas/io/formats/style.py +++ b/pandas/io/formats/style.py @@ -1235,6 +1235,75 @@ class MyStyler(cls): return MyStyler + def pipe(self, func, *args, **kwargs): + """ + Apply ``func(self, *args, **kwargs)``, and return the result. + + .. versionadded:: 0.24.0 + + Parameters + ---------- + func : function + Function to apply to the Styler. Alternatively, a + ``(callable, keyword)`` tuple where ``keyword`` is a string + indicating the keyword of ``callable`` that expects the Styler. + *args, **kwargs : + Arguments passed to `func`. + + Returns + ------- + object : + The value returned by ``func``. + + See Also + -------- + DataFrame.pipe : Analogous method for DataFrame. + Styler.apply : Apply a function row-wise, column-wise, or table-wise to + modify the dataframe's styling. + + Notes + ----- + Like :meth:`DataFrame.pipe`, this method can simplify the + application of several user-defined functions to a styler. Instead + of writing: + + .. code-block:: python + + f(g(df.style.set_precision(3), arg1=a), arg2=b, arg3=c) + + users can write: + + .. code-block:: python + + (df.style.set_precision(3) + .pipe(g, arg1=a) + .pipe(f, arg2=b, arg3=c)) + + In particular, this allows users to define functions that take a + styler object, along with other parameters, and return the styler after + making styling changes (such as calling :meth:`Styler.apply` or + :meth:`Styler.set_properties`). Using ``.pipe``, these user-defined + style "transformations" can be interleaved with calls to the built-in + Styler interface. + + Examples + -------- + >>> def format_conversion(styler): + ... return (styler.set_properties(**{'text-align': 'right'}) + ... .format({'conversion': '{:.1%}'})) + + The user-defined ``format_conversion`` function above can be called + within a sequence of other style modifications: + + >>> df = pd.DataFrame({'trial': list(range(5)), + ... 'conversion': [0.75, 0.85, np.nan, 0.7, 0.72]}) + >>> (df.style + ... .highlight_min(subset=['conversion'], color='yellow') + ... .pipe(format_conversion) + ... .set_caption("Results with minimum conversion highlighted.")) + """ + return com._pipe(self, func, *args, **kwargs) + def _is_visible(idx_row, idx_col, lengths): """ diff --git a/pandas/tests/io/formats/test_style.py b/pandas/tests/io/formats/test_style.py index 6027fc08624df..fa8bd91dce939 100644 --- a/pandas/tests/io/formats/test_style.py +++ b/pandas/tests/io/formats/test_style.py @@ -1173,6 +1173,22 @@ def test_hide_columns_mult_levels(self): assert ctx['body'][1][2]['is_visible'] assert ctx['body'][1][2]['display_value'] == 3 + def test_pipe(self): + def set_caption_from_template(styler, a, b): + return styler.set_caption( + 'Dataframe with a = {a} and b = {b}'.format(a=a, b=b)) + + styler = self.df.style.pipe(set_caption_from_template, 'A', b='B') + assert 'Dataframe with a = A and b = B' in styler.render() + + # Test with an argument that is a (callable, keyword_name) pair. + def f(a, b, styler): + return (a, b, styler) + + styler = self.df.style + result = styler.pipe((f, 'styler'), a=1, b=2) + assert result == (1, 2, styler) + @td.skip_if_no_mpl class TestStylerMatplotlibDep(object):