Skip to content

Commit 14dfbdb

Browse files
attack68JulianWgs
authored andcommitted
ENH: Styler.highlight_quantile method (pandas-dev#40926)
1 parent 026fdf7 commit 14dfbdb

File tree

6 files changed

+162
-1
lines changed

6 files changed

+162
-1
lines changed

doc/source/_static/style/hq_ax1.png

5.95 KB
Loading
5.96 KB
Loading

doc/source/_static/style/hq_props.png

6.09 KB
Loading

doc/source/whatsnew/v1.3.0.rst

+3-1
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,9 @@ to accept more universal CSS language for arguments, such as ``'color:red;'`` in
119119
to allow custom CSS highlighting instead of default background coloring (:issue:`40242`).
120120
Enhancements to other built-in methods include extending the :meth:`.Styler.background_gradient`
121121
method to shade elements based on a given gradient map and not be restricted only to
122-
values in the DataFrame (:issue:`39930` :issue:`22727` :issue:`28901`).
122+
values in the DataFrame (:issue:`39930` :issue:`22727` :issue:`28901`). Additional
123+
built-in methods such as :meth:`.Styler.highlight_between` and :meth:`.Styler.highlight_quantile`
124+
have been added (:issue:`39821` and :issue:`40926`).
123125

124126
The :meth:`.Styler.apply` now consistently allows functions with ``ndarray`` output to
125127
allow more flexible development of UDFs when ``axis`` is ``None`` ``0`` or ``1`` (:issue:`39393`).

pandas/io/formats/style.py

+108
Original file line numberDiff line numberDiff line change
@@ -1374,6 +1374,7 @@ def highlight_null(
13741374
Styler.highlight_max: Highlight the maximum with a style.
13751375
Styler.highlight_min: Highlight the minimum with a style.
13761376
Styler.highlight_between: Highlight a defined range with a style.
1377+
Styler.highlight_quantile: Highlight values defined by a quantile with a style.
13771378
"""
13781379

13791380
def f(data: DataFrame, props: str) -> np.ndarray:
@@ -1422,6 +1423,7 @@ def highlight_max(
14221423
Styler.highlight_null: Highlight missing values with a style.
14231424
Styler.highlight_min: Highlight the minimum with a style.
14241425
Styler.highlight_between: Highlight a defined range with a style.
1426+
Styler.highlight_quantile: Highlight values defined by a quantile with a style.
14251427
"""
14261428

14271429
def f(data: FrameOrSeries, props: str) -> np.ndarray:
@@ -1470,6 +1472,7 @@ def highlight_min(
14701472
Styler.highlight_null: Highlight missing values with a style.
14711473
Styler.highlight_max: Highlight the maximum with a style.
14721474
Styler.highlight_between: Highlight a defined range with a style.
1475+
Styler.highlight_quantile: Highlight values defined by a quantile with a style.
14731476
"""
14741477

14751478
def f(data: FrameOrSeries, props: str) -> np.ndarray:
@@ -1526,6 +1529,7 @@ def highlight_between(
15261529
Styler.highlight_null: Highlight missing values with a style.
15271530
Styler.highlight_max: Highlight the maximum with a style.
15281531
Styler.highlight_min: Highlight the minimum with a style.
1532+
Styler.highlight_quantile: Highlight values defined by a quantile with a style.
15291533
15301534
Notes
15311535
-----
@@ -1589,6 +1593,110 @@ def highlight_between(
15891593
inclusive=inclusive,
15901594
)
15911595

1596+
def highlight_quantile(
1597+
self,
1598+
subset: IndexLabel | None = None,
1599+
color: str = "yellow",
1600+
axis: Axis | None = 0,
1601+
q_left: float = 0.0,
1602+
q_right: float = 1.0,
1603+
interpolation: str = "linear",
1604+
inclusive: str = "both",
1605+
props: str | None = None,
1606+
) -> Styler:
1607+
"""
1608+
Highlight values defined by a quantile with a style.
1609+
1610+
.. versionadded:: 1.3.0
1611+
1612+
Parameters
1613+
----------
1614+
subset : IndexSlice, default None
1615+
A valid slice for ``data`` to limit the style application to.
1616+
color : str, default 'yellow'
1617+
Background color to use for highlighting
1618+
axis : {0 or 'index', 1 or 'columns', None}, default 0
1619+
Axis along which to determine and highlight quantiles. If ``None`` quantiles
1620+
are measured over the entire DataFrame. See examples.
1621+
q_left : float, default 0
1622+
Left bound, in [0, q_right), for the target quantile range.
1623+
q_right : float, default 1
1624+
Right bound, in (q_left, 1], for the target quantile range.
1625+
interpolation : {‘linear’, ‘lower’, ‘higher’, ‘midpoint’, ‘nearest’}
1626+
Argument passed to ``Series.quantile`` or ``DataFrame.quantile`` for
1627+
quantile estimation.
1628+
inclusive : {'both', 'neither', 'left', 'right'}
1629+
Identify whether quantile bounds are closed or open.
1630+
props : str, default None
1631+
CSS properties to use for highlighting. If ``props`` is given, ``color``
1632+
is not used.
1633+
1634+
Returns
1635+
-------
1636+
self : Styler
1637+
1638+
See Also
1639+
--------
1640+
Styler.highlight_null: Highlight missing values with a style.
1641+
Styler.highlight_max: Highlight the maximum with a style.
1642+
Styler.highlight_min: Highlight the minimum with a style.
1643+
Styler.highlight_between: Highlight a defined range with a style.
1644+
1645+
Notes
1646+
-----
1647+
This function does not work with ``str`` dtypes.
1648+
1649+
Examples
1650+
--------
1651+
Using ``axis=None`` and apply a quantile to all collective data
1652+
1653+
>>> df = pd.DataFrame(np.arange(10).reshape(2,5) + 1)
1654+
>>> df.style.highlight_quantile(axis=None, q_left=0.8, color="#fffd75")
1655+
1656+
.. figure:: ../../_static/style/hq_axNone.png
1657+
1658+
Or highlight quantiles row-wise or column-wise, in this case by row-wise
1659+
1660+
>>> df.style.highlight_quantile(axis=1, q_left=0.8, color="#fffd75")
1661+
1662+
.. figure:: ../../_static/style/hq_ax1.png
1663+
1664+
Use ``props`` instead of default background coloring
1665+
1666+
>>> df.style.highlight_quantile(axis=None, q_left=0.2, q_right=0.8,
1667+
... props='font-weight:bold;color:#e83e8c')
1668+
1669+
.. figure:: ../../_static/style/hq_props.png
1670+
"""
1671+
subset_ = slice(None) if subset is None else subset
1672+
subset_ = non_reducing_slice(subset_)
1673+
data = self.data.loc[subset_]
1674+
1675+
# after quantile is found along axis, e.g. along rows,
1676+
# applying the calculated quantile to alternate axis, e.g. to each column
1677+
kwargs = {"q": [q_left, q_right], "interpolation": interpolation}
1678+
if axis in [0, "index"]:
1679+
q = data.quantile(axis=axis, numeric_only=False, **kwargs)
1680+
axis_apply: int | None = 1
1681+
elif axis in [1, "columns"]:
1682+
q = data.quantile(axis=axis, numeric_only=False, **kwargs)
1683+
axis_apply = 0
1684+
else: # axis is None
1685+
q = Series(data.to_numpy().ravel()).quantile(**kwargs)
1686+
axis_apply = None
1687+
1688+
if props is None:
1689+
props = f"background-color: {color};"
1690+
return self.apply(
1691+
_highlight_between, # type: ignore[arg-type]
1692+
axis=axis_apply,
1693+
subset=subset,
1694+
props=props,
1695+
left=q.iloc[0],
1696+
right=q.iloc[1],
1697+
inclusive=inclusive,
1698+
)
1699+
15921700
@classmethod
15931701
def from_custom_template(cls, searchpath, name):
15941702
"""

pandas/tests/io/formats/style/test_highlight.py

+51
Original file line numberDiff line numberDiff line change
@@ -142,3 +142,54 @@ def test_highlight_between_inclusive(styler, inclusive, expected):
142142
kwargs = {"left": 0, "right": 1, "subset": IndexSlice[[0, 1], :]}
143143
result = styler.highlight_between(**kwargs, inclusive=inclusive)._compute()
144144
assert result.ctx == expected
145+
146+
147+
@pytest.mark.parametrize(
148+
"kwargs",
149+
[
150+
{"q_left": 0.5, "q_right": 1, "axis": 0}, # base case
151+
{"q_left": 0.5, "q_right": 1, "axis": None}, # test axis
152+
{"q_left": 0, "q_right": 1, "subset": IndexSlice[2, :]}, # test subset
153+
{"q_left": 0.5, "axis": 0}, # test no high
154+
{"q_right": 1, "subset": IndexSlice[2, :], "axis": 1}, # test no low
155+
{"q_left": 0.5, "axis": 0, "props": "background-color: yellow"}, # tst prop
156+
],
157+
)
158+
def test_highlight_quantile(styler, kwargs):
159+
expected = {
160+
(2, 0): [("background-color", "yellow")],
161+
(2, 1): [("background-color", "yellow")],
162+
}
163+
result = styler.highlight_quantile(**kwargs)._compute().ctx
164+
assert result == expected
165+
166+
167+
@pytest.mark.skipif(np.__version__[:4] in ["1.16", "1.17"], reason="Numpy Issue #14831")
168+
@pytest.mark.parametrize(
169+
"f,kwargs",
170+
[
171+
("highlight_min", {"axis": 1, "subset": IndexSlice[1, :]}),
172+
("highlight_max", {"axis": 0, "subset": [0]}),
173+
("highlight_quantile", {"axis": None, "q_left": 0.6, "q_right": 0.8}),
174+
("highlight_between", {"subset": [0]}),
175+
],
176+
)
177+
@pytest.mark.parametrize(
178+
"df",
179+
[
180+
DataFrame([[0, 10], [20, 30]], dtype=int),
181+
DataFrame([[0, 10], [20, 30]], dtype=float),
182+
DataFrame([[0, 10], [20, 30]], dtype="datetime64[ns]"),
183+
DataFrame([[0, 10], [20, 30]], dtype=str),
184+
DataFrame([[0, 10], [20, 30]], dtype="timedelta64[ns]"),
185+
],
186+
)
187+
def test_all_highlight_dtypes(f, kwargs, df):
188+
if f == "highlight_quantile" and isinstance(df.iloc[0, 0], (str)):
189+
return None # quantile incompatible with str
190+
if f == "highlight_between":
191+
kwargs["left"] = df.iloc[1, 0] # set the range low for testing
192+
193+
expected = {(1, 0): [("background-color", "yellow")]}
194+
result = getattr(df.style, f)(**kwargs)._compute().ctx
195+
assert result == expected

0 commit comments

Comments
 (0)