From e0c64f9ec42ba9a2665436c9efca411c7482fbb7 Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Tue, 13 Apr 2021 16:58:46 +0200 Subject: [PATCH 1/9] add the highlight quantile function --- pandas/io/formats/style.py | 185 ++++++++++++++++++++++++++++--------- 1 file changed, 142 insertions(+), 43 deletions(-) diff --git a/pandas/io/formats/style.py b/pandas/io/formats/style.py index 9b4673ddb7906..b0581e6501f0a 100644 --- a/pandas/io/formats/style.py +++ b/pandas/io/formats/style.py @@ -1559,59 +1559,111 @@ def highlight_between( .. figure:: ../../_static/style/hbetw_props.png """ - def f( - data: FrameOrSeries, - props: str, - left: Scalar | Sequence | np.ndarray | FrameOrSeries | None = None, - right: Scalar | Sequence | np.ndarray | FrameOrSeries | None = None, - inclusive: bool | str = True, - ) -> np.ndarray: - if np.iterable(left) and not isinstance(left, str): - left = _validate_apply_axis_arg( - left, "left", None, data # type: ignore[arg-type] - ) + if props is None: + props = f"background-color: {color};" + return self.apply( + _highlight_between, # type: ignore[arg-type] + axis=axis, + subset=subset, + props=props, + left=left, + right=right, + inclusive=inclusive, + ) - if np.iterable(right) and not isinstance(right, str): - right = _validate_apply_axis_arg( - right, "right", None, data # type: ignore[arg-type] - ) + def highlight_quantile( + self, + subset: IndexLabel | None = None, + color: str = "yellow", + axis: Axis | None = 0, + q_left: float = 0.0, + q_right: float = 1.0, + interpolation: str = "linear", + inclusive: str | bool = True, + props: str | None = None, + ) -> Styler: + """ + Highlight values defined by a quantile with a style. - # get ops with correct boundary attribution - if inclusive == "both": - ops = (operator.ge, operator.le) - elif inclusive == "neither": - ops = (operator.gt, operator.lt) - elif inclusive == "left": - ops = (operator.ge, operator.lt) - elif inclusive == "right": - ops = (operator.gt, operator.le) - else: - raise ValueError( - f"'inclusive' values can be 'both', 'left', 'right', or 'neither' " - f"got {inclusive}" - ) + .. versionadded:: 1.3.0 - g_left = ( - ops[0](data, left) - if left is not None - else np.full(data.shape, True, dtype=bool) - ) - l_right = ( - ops[1](data, right) - if right is not None - else np.full(data.shape, True, dtype=bool) - ) - return np.where(g_left & l_right, props, "") + Parameters + ---------- + subset : IndexSlice, default None + A valid slice for ``data`` to limit the style application to. + color : str, default 'yellow' + Background color to use for highlighting + axis : {0 or 'index', 1 or 'columns', None}, default 0 + Axis along which to determine and highlight quantiles. If ``None`` quantiles + are measured over the entire DataFrame. See examples. + q_left : float, default 0 + Left bound, in [0, q_right), for the target quantile range. + q_right : float, default 1 + Right bound, in (q_left, 1], for the target quantile range. + interpolation : {‘linear’, ‘lower’, ‘higher’, ‘midpoint’, ‘nearest’} + Argument passed to ``numpy.quantile`` for quantile estimation. + inclusive : {'both', 'neither', 'left', 'right'} or bool, default True + Identify whether quantile bounds are closed or open. + props : str, default None + CSS properties to use for highlighting. If ``props`` is given, ``color`` + is not used. + + Returns + ------- + self : Styler + + See Also + -------- + Styler.highlight_null: Highlight missing values with a style. + Styler.highlight_max: Highlight the maximum with a style. + Styler.highlight_min: Highlight the minimum with a style. + Styler.highlight_between: Highlight a defined range with a style. + + Notes + ----- + This function does not work with ``str``, ``Timedelta`` or ``Timestamp`` dtypes. + + Examples + -------- + Using ``axis=None`` and apply a quantile to all collective data + + >>> df = pd.DataFrame(np.arange(10).reshape(2,5) + 1) + >>> df.style.highlight_quantile(axis=None, q_left=0.8, color="#fffd75") + + .. figure:: ../../_static/style/hq_axNone.png + + Or highlight quantiles row-wise or column-wise, in this case by row-wise + + >>> df.style.highlight_quantile(axis=1, q_left=0.8, color="#fffd75") + + .. figure:: ../../_static/style/hq_ax1.png + Use ``props`` instead of default background coloring + + >>> df.style.highlight_quantile(axis=None, q_left=0.2, q_right=0.8, + ... props='font-weight:bold;color:#e83e8c') + + .. figure:: ../../_static/style/hq_props.png + """ + subset_ = slice(None) if subset is None else subset + subset_ = non_reducing_slice(subset_) + data = self.data.loc[subset_] + + q = np.quantile( + data.to_numpy(), [q_left, q_right], axis=axis, interpolation=interpolation + ) + # after quantile is found along axis, reverse axis for highlight application + if axis in [0, 1]: + axis = 1 - axis if props is None: props = f"background-color: {color};" return self.apply( - f, # type: ignore[arg-type] + _highlight_between, axis=axis, subset=subset, props=props, - left=left, - right=right, + left=q[0], + right=q[1], inclusive=inclusive, ) @@ -1831,3 +1883,50 @@ def css(rgba) -> str: index=data.index, columns=data.columns, ) + + +def _highlight_between( + data: FrameOrSeries, + props: str, + left: Scalar | Sequence | np.ndarray | FrameOrSeries | None = None, + right: Scalar | Sequence | np.ndarray | FrameOrSeries | None = None, + inclusive: bool | str = True, +) -> np.ndarray: + """ + Calculate an array with css props based on the data values and the range boundaries + """ + if np.iterable(left) and not isinstance(left, str): + left = _validate_apply_axis_arg( + left, "left", None, data # type: ignore[arg-type] + ) + if np.iterable(right) and not isinstance(right, str): + right = _validate_apply_axis_arg( + right, "right", None, data # type: ignore[arg-type] + ) + + # get ops with correct boundary attribution + if inclusive == "both": + ops = (operator.ge, operator.le) + elif inclusive == "neither": + ops = (operator.gt, operator.lt) + elif inclusive == "left": + ops = (operator.ge, operator.lt) + elif inclusive == "right": + ops = (operator.gt, operator.le) + else: + raise ValueError( + f"'inclusive' values can be 'both', 'left', 'right', or 'neither' " + f"got {inclusive}" + ) + + g_left = ( + ops[0](data, left) + if left is not None + else np.full(data.shape, True, dtype=bool) + ) + l_right = ( + ops[1](data, right) + if right is not None + else np.full(data.shape, True, dtype=bool) + ) + return np.where(g_left & l_right, props, "") From 670135c966b9b033d33dbfdb5cb9859ceb5a0572 Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Tue, 13 Apr 2021 17:02:00 +0200 Subject: [PATCH 2/9] add binary images --- doc/source/_static/style/hq_ax1.png | Bin 0 -> 6092 bytes doc/source/_static/style/hq_axNone.png | Bin 0 -> 6102 bytes doc/source/_static/style/hq_props.png | Bin 0 -> 6241 bytes 3 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 doc/source/_static/style/hq_ax1.png create mode 100644 doc/source/_static/style/hq_axNone.png create mode 100644 doc/source/_static/style/hq_props.png diff --git a/doc/source/_static/style/hq_ax1.png b/doc/source/_static/style/hq_ax1.png new file mode 100644 index 0000000000000000000000000000000000000000..95d840b7c8f99beca00098cc27e2b30760478884 GIT binary patch literal 6092 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 literal 0 HcmV?d00001 diff --git a/doc/source/_static/style/hq_axNone.png b/doc/source/_static/style/hq_axNone.png new file mode 100644 index 0000000000000000000000000000000000000000..40a33b194e640de84ed8cdf6e54e039c4cbf7cc3 GIT binary patch literal 6102 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 aUQ3Cv6jm7Udw2YGx>1mO1F4XK1^pk84RLD# literal 0 HcmV?d00001 diff --git a/doc/source/_static/style/hq_props.png b/doc/source/_static/style/hq_props.png new file mode 100644 index 0000000000000000000000000000000000000000..1f117490966907b6f8c7ecd215a3453fb09a5496 GIT binary patch literal 6241 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 X*r<(xYQDg~H(H9aYS1!i)8PLDRAaj^ literal 0 HcmV?d00001 From cd82788d46cab1b2bebcda8e88c95aecf68fa2bb Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Tue, 13 Apr 2021 17:21:06 +0200 Subject: [PATCH 3/9] add tests --- pandas/io/formats/style.py | 9 +-- .../tests/io/formats/style/test_highlight.py | 55 +++++++++++++++++++ 2 files changed, 60 insertions(+), 4 deletions(-) diff --git a/pandas/io/formats/style.py b/pandas/io/formats/style.py index b0581e6501f0a..70b5c160deccd 100644 --- a/pandas/io/formats/style.py +++ b/pandas/io/formats/style.py @@ -1579,7 +1579,7 @@ def highlight_quantile( q_left: float = 0.0, q_right: float = 1.0, interpolation: str = "linear", - inclusive: str | bool = True, + inclusive: str = "both", props: str | None = None, ) -> Styler: """ @@ -1602,7 +1602,7 @@ def highlight_quantile( Right bound, in (q_left, 1], for the target quantile range. interpolation : {‘linear’, ‘lower’, ‘higher’, ‘midpoint’, ‘nearest’} Argument passed to ``numpy.quantile`` for quantile estimation. - inclusive : {'both', 'neither', 'left', 'right'} or bool, default True + inclusive : {'both', 'neither', 'left', 'right'} Identify whether quantile bounds are closed or open. props : str, default None CSS properties to use for highlighting. If ``props`` is given, ``color`` @@ -1648,10 +1648,11 @@ def highlight_quantile( subset_ = non_reducing_slice(subset_) data = self.data.loc[subset_] - q = np.quantile( + q = np.nanquantile( data.to_numpy(), [q_left, q_right], axis=axis, interpolation=interpolation ) - # after quantile is found along axis, reverse axis for highlight application + # after quantile is found along axis, e.g. along rows, + # applying the calculated quantile to alternate axis, e.g. to each column if axis in [0, 1]: axis = 1 - axis diff --git a/pandas/tests/io/formats/style/test_highlight.py b/pandas/tests/io/formats/style/test_highlight.py index b8c194f8955ab..c20177381a520 100644 --- a/pandas/tests/io/formats/style/test_highlight.py +++ b/pandas/tests/io/formats/style/test_highlight.py @@ -4,6 +4,8 @@ from pandas import ( DataFrame, IndexSlice, + Timedelta, + Timestamp, ) pytest.importorskip("jinja2") @@ -142,3 +144,56 @@ def test_highlight_between_inclusive(styler, inclusive, expected): kwargs = {"left": 0, "right": 1, "subset": IndexSlice[[0, 1], :]} result = styler.highlight_between(**kwargs, inclusive=inclusive)._compute() assert result.ctx == expected + + +@pytest.mark.parametrize( + "kwargs", + [ + {"q_left": 0.5, "q_right": 1, "axis": 0}, # base case + {"q_left": 0.5, "q_right": 1, "axis": None}, # test axis + {"q_left": 0, "q_right": 1, "subset": IndexSlice[2, :]}, # test subset + {"q_left": 0.5, "axis": 0}, # test no high + {"q_right": 1, "subset": IndexSlice[2, :], "axis": 1}, # test no low + {"q_left": 0.5, "axis": 0, "props": "background-color: yellow"}, # tst prop + ], +) +def test_highlight_quantile(styler, kwargs): + expected = { + (2, 0): [("background-color", "yellow")], + (2, 1): [("background-color", "yellow")], + } + result = styler.highlight_quantile(**kwargs)._compute().ctx + assert result == expected + + +@pytest.mark.skipif(np.__version__[:4] in ["1.16", "1.17"], reason="Numpy Issue #14831") +@pytest.mark.parametrize( + "f,kwargs", + [ + ("highlight_min", {"axis": 1, "subset": IndexSlice[1, :]}), + ("highlight_max", {"axis": 0, "subset": [0]}), + ("highlight_quantile", {"axis": None, "q_left": 0.6, "q_right": 0.8}), + ("highlight_between", {"subset": [0]}), + ], +) +@pytest.mark.parametrize( + "df", + [ + DataFrame([[0, 1], [2, 3]], dtype=int), + DataFrame([[0, 1], [2, 3]], dtype=float), + DataFrame([[0, 1], [2, 3]], dtype="datetime64[ns]"), + DataFrame([[0, 1], [2, 3]], dtype=str), + DataFrame([[0, 1], [2, 3]], dtype="timedelta64[ns]"), + ], +) +def test_all_highlight_dtypes(f, kwargs, df): + if f == "highlight_quantile" and isinstance( + df.iloc[0, 0], (str, Timestamp, Timedelta) + ): + return None # quantile incompatible with str, datetime64, timedelta64 + elif f == "highlight_between": + kwargs["left"] = df.iloc[1, 0] # set the range low for testing + + expected = {(1, 0): [("background-color", "yellow")]} + result = getattr(df.style, f)(**kwargs)._compute().ctx + assert result == expected From b4e08f385ff28fe29aff39a523eb09ebeb0a5f8c Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Wed, 14 Apr 2021 07:29:17 +0200 Subject: [PATCH 4/9] See also links --- doc/source/whatsnew/v1.3.0.rst | 4 +++- pandas/io/formats/style.py | 4 ++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/doc/source/whatsnew/v1.3.0.rst b/doc/source/whatsnew/v1.3.0.rst index ea7e0f88ff81e..6a60e5a1b0eff 100644 --- a/doc/source/whatsnew/v1.3.0.rst +++ b/doc/source/whatsnew/v1.3.0.rst @@ -119,7 +119,9 @@ to accept more universal CSS language for arguments, such as ``'color:red;'`` in to allow custom CSS highlighting instead of default background coloring (:issue:`40242`). Enhancements to other built-in methods include extending the :meth:`.Styler.background_gradient` method to shade elements based on a given gradient map and not be restricted only to -values in the DataFrame (:issue:`39930` :issue:`22727` :issue:`28901`). +values in the DataFrame (:issue:`39930` :issue:`22727` :issue:`28901`). Additional +built-in methods such as :meth:`.Styler.highlight_between` and :meth:`.Styler.highlight_quantile` +have been added (:issue:`39821` and :issue:`40926`). The :meth:`.Styler.apply` now consistently allows functions with ``ndarray`` output to allow more flexible development of UDFs when ``axis`` is ``None`` ``0`` or ``1`` (:issue:`39393`). diff --git a/pandas/io/formats/style.py b/pandas/io/formats/style.py index 0b4a2cee5213d..3aff9c1330a99 100644 --- a/pandas/io/formats/style.py +++ b/pandas/io/formats/style.py @@ -1355,6 +1355,7 @@ def highlight_null( Styler.highlight_max: Highlight the maximum with a style. Styler.highlight_min: Highlight the minimum with a style. Styler.highlight_between: Highlight a defined range with a style. + Styler.highlight_quantile: Highlight values defined by a quantile with a style. """ def f(data: DataFrame, props: str) -> np.ndarray: @@ -1403,6 +1404,7 @@ def highlight_max( Styler.highlight_null: Highlight missing values with a style. Styler.highlight_min: Highlight the minimum with a style. Styler.highlight_between: Highlight a defined range with a style. + Styler.highlight_quantile: Highlight values defined by a quantile with a style. """ def f(data: FrameOrSeries, props: str) -> np.ndarray: @@ -1451,6 +1453,7 @@ def highlight_min( Styler.highlight_null: Highlight missing values with a style. Styler.highlight_max: Highlight the maximum with a style. Styler.highlight_between: Highlight a defined range with a style. + Styler.highlight_quantile: Highlight values defined by a quantile with a style. """ def f(data: FrameOrSeries, props: str) -> np.ndarray: @@ -1507,6 +1510,7 @@ def highlight_between( Styler.highlight_null: Highlight missing values with a style. Styler.highlight_max: Highlight the maximum with a style. Styler.highlight_min: Highlight the minimum with a style. + Styler.highlight_quantile: Highlight values defined by a quantile with a style. Notes ----- From b4d346f9493fe9ec9cca67c6d4d1aca3a47ded53 Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Wed, 14 Apr 2021 08:39:58 +0200 Subject: [PATCH 5/9] doc fix --- pandas/io/formats/style.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pandas/io/formats/style.py b/pandas/io/formats/style.py index 3aff9c1330a99..074e3bd4f55c9 100644 --- a/pandas/io/formats/style.py +++ b/pandas/io/formats/style.py @@ -1640,6 +1640,7 @@ def highlight_quantile( >>> df.style.highlight_quantile(axis=1, q_left=0.8, color="#fffd75") .. figure:: ../../_static/style/hq_ax1.png + Use ``props`` instead of default background coloring >>> df.style.highlight_quantile(axis=None, q_left=0.2, q_right=0.8, From 2487988fbae928a36c12114e0d07989bf2d65e27 Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Wed, 14 Apr 2021 10:09:45 +0200 Subject: [PATCH 6/9] mypy fix --- pandas/io/formats/style.py | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/pandas/io/formats/style.py b/pandas/io/formats/style.py index 074e3bd4f55c9..0527556c120f3 100644 --- a/pandas/io/formats/style.py +++ b/pandas/io/formats/style.py @@ -1652,19 +1652,24 @@ def highlight_quantile( subset_ = non_reducing_slice(subset_) data = self.data.loc[subset_] + # after quantile is found along axis, e.g. along rows, + # applying the calculated quantile to alternate axis, e.g. to each column + axis_apply: int | None = 1 + if axis in [0, "index"]: + axis, axis_apply = 0, 1 + elif axis in [1, "columns"]: + axis, axis_apply = 1, 0 + else: + axis, axis_apply = None, None q = np.nanquantile( data.to_numpy(), [q_left, q_right], axis=axis, interpolation=interpolation ) - # after quantile is found along axis, e.g. along rows, - # applying the calculated quantile to alternate axis, e.g. to each column - if axis in [0, 1]: - axis = 1 - axis if props is None: props = f"background-color: {color};" return self.apply( - _highlight_between, - axis=axis, + _highlight_between, # type: ignore[arg-type] + axis=axis_apply, subset=subset, props=props, left=q[0], From 73ec52d9a6a1f1cfaafe73567de183cfb58d69de Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Fri, 16 Apr 2021 08:41:11 +0200 Subject: [PATCH 7/9] switch to DataFrame.quantile --- pandas/io/formats/style.py | 25 +++++++++++-------- .../tests/io/formats/style/test_highlight.py | 20 ++++++--------- 2 files changed, 22 insertions(+), 23 deletions(-) diff --git a/pandas/io/formats/style.py b/pandas/io/formats/style.py index 0527556c120f3..36eb5a3b7af90 100644 --- a/pandas/io/formats/style.py +++ b/pandas/io/formats/style.py @@ -1604,7 +1604,8 @@ def highlight_quantile( q_right : float, default 1 Right bound, in (q_left, 1], for the target quantile range. interpolation : {‘linear’, ‘lower’, ‘higher’, ‘midpoint’, ‘nearest’} - Argument passed to ``numpy.nanquantile`` for quantile estimation. + Argument passed to ``Series.quantile`` or ``DataFrame.quantile`` for + quantile estimation. inclusive : {'both', 'neither', 'left', 'right'} Identify whether quantile bounds are closed or open. props : str, default None @@ -1654,16 +1655,18 @@ def highlight_quantile( # after quantile is found along axis, e.g. along rows, # applying the calculated quantile to alternate axis, e.g. to each column - axis_apply: int | None = 1 + kwargs = {"numeric_only": False, "interpolation": interpolation} if axis in [0, "index"]: - axis, axis_apply = 0, 1 + q = data.quantile([q_left, q_right], axis=axis, **kwargs) + axis_apply = 1 elif axis in [1, "columns"]: - axis, axis_apply = 1, 0 - else: - axis, axis_apply = None, None - q = np.nanquantile( - data.to_numpy(), [q_left, q_right], axis=axis, interpolation=interpolation - ) + q = data.quantile([q_left, q_right], axis=axis, **kwargs) + axis_apply = 0 + else: # axis is None + # TODO: this might be better with Series.quantile but no `numeric_only` kw + q = DataFrame(data.to_numpy().ravel()) + q = q.quantile([q_left, q_right], **kwargs)[0] + axis_apply = None if props is None: props = f"background-color: {color};" @@ -1672,8 +1675,8 @@ def highlight_quantile( axis=axis_apply, subset=subset, props=props, - left=q[0], - right=q[1], + left=q.iloc[0], + right=q.iloc[1], inclusive=inclusive, ) diff --git a/pandas/tests/io/formats/style/test_highlight.py b/pandas/tests/io/formats/style/test_highlight.py index c20177381a520..9e956e055d1aa 100644 --- a/pandas/tests/io/formats/style/test_highlight.py +++ b/pandas/tests/io/formats/style/test_highlight.py @@ -4,8 +4,6 @@ from pandas import ( DataFrame, IndexSlice, - Timedelta, - Timestamp, ) pytest.importorskip("jinja2") @@ -179,19 +177,17 @@ def test_highlight_quantile(styler, kwargs): @pytest.mark.parametrize( "df", [ - DataFrame([[0, 1], [2, 3]], dtype=int), - DataFrame([[0, 1], [2, 3]], dtype=float), - DataFrame([[0, 1], [2, 3]], dtype="datetime64[ns]"), - DataFrame([[0, 1], [2, 3]], dtype=str), - DataFrame([[0, 1], [2, 3]], dtype="timedelta64[ns]"), + DataFrame([[0, 10], [20, 30]], dtype=int), + DataFrame([[0, 10], [20, 30]], dtype=float), + DataFrame([[0, 10], [20, 30]], dtype="datetime64[ns]"), + DataFrame([[0, 10], [20, 30]], dtype=str), + DataFrame([[0, 10], [20, 30]], dtype="timedelta64[ns]"), ], ) def test_all_highlight_dtypes(f, kwargs, df): - if f == "highlight_quantile" and isinstance( - df.iloc[0, 0], (str, Timestamp, Timedelta) - ): - return None # quantile incompatible with str, datetime64, timedelta64 - elif f == "highlight_between": + if f == "highlight_quantile" and isinstance(df.iloc[0, 0], (str)): + return None # quantile incompatible with str + if f == "highlight_between": kwargs["left"] = df.iloc[1, 0] # set the range low for testing expected = {(1, 0): [("background-color", "yellow")]} From fc60ca647df0e0eeca13c9f4a19afd0b44897bc9 Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Fri, 16 Apr 2021 08:48:54 +0200 Subject: [PATCH 8/9] switch to DataFrame.quantile and Series.quantile --- pandas/io/formats/style.py | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/pandas/io/formats/style.py b/pandas/io/formats/style.py index 36eb5a3b7af90..956edac61ddbc 100644 --- a/pandas/io/formats/style.py +++ b/pandas/io/formats/style.py @@ -1655,17 +1655,15 @@ def highlight_quantile( # after quantile is found along axis, e.g. along rows, # applying the calculated quantile to alternate axis, e.g. to each column - kwargs = {"numeric_only": False, "interpolation": interpolation} + kwargs = {"q": [q_left, q_right], "interpolation": interpolation} if axis in [0, "index"]: - q = data.quantile([q_left, q_right], axis=axis, **kwargs) - axis_apply = 1 + q = data.quantile(axis=axis, numeric_only=False, **kwargs) + axis_apply: int | None = 1 elif axis in [1, "columns"]: - q = data.quantile([q_left, q_right], axis=axis, **kwargs) + q = data.quantile(axis=axis, numeric_only=False, **kwargs) axis_apply = 0 else: # axis is None - # TODO: this might be better with Series.quantile but no `numeric_only` kw - q = DataFrame(data.to_numpy().ravel()) - q = q.quantile([q_left, q_right], **kwargs)[0] + q = Series(data.to_numpy().ravel()).quantile(**kwargs) axis_apply = None if props is None: From 36acb98194fd8e264f3fa4cbae68d6747fe28cf5 Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Fri, 16 Apr 2021 08:51:43 +0200 Subject: [PATCH 9/9] leave only str dtype in omissions --- pandas/io/formats/style.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pandas/io/formats/style.py b/pandas/io/formats/style.py index 956edac61ddbc..7998365234682 100644 --- a/pandas/io/formats/style.py +++ b/pandas/io/formats/style.py @@ -1625,7 +1625,7 @@ def highlight_quantile( Notes ----- - This function does not work with ``str``, ``Timedelta`` or ``Timestamp`` dtypes. + This function does not work with ``str`` dtypes. Examples --------