From 22653c64569699da1225e26ac4f4026e52770b93 Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Tue, 16 Feb 2021 22:07:51 +0100 Subject: [PATCH 01/45] merged --- pandas/io/formats/style.py | 152 +++++++++++++++++++++++++++++++++++++ 1 file changed, 152 insertions(+) diff --git a/pandas/io/formats/style.py b/pandas/io/formats/style.py index 96043355e24d1..5a0141e5c1204 100644 --- a/pandas/io/formats/style.py +++ b/pandas/io/formats/style.py @@ -1711,6 +1711,158 @@ def f(data: FrameOrSeries, props: str) -> np.ndarray: f, axis=axis, subset=subset, props=f"background-color: {color};" ) + def highlight_range( + self, + subset: Optional[IndexLabel] = None, + color: str = "yellow", + start: Optional[Any] = None, + stop: Optional[Any] = None, + props: Optional[str] = None, + ) -> Styler: + """ + Highlight a defined range by shading the background, or otherwise. + + Parameters + ---------- + subset : IndexSlice, default None + A valid slice for ``data`` to limit the style application to. + color : str, default 'yellow' + Background color added to CSS style. + start : scalar or datetime-like, default None + Left bound for defining the range (inclusive). + stop : scalar or datetime-like, default None + Right bound for defining the range (inclusive) + 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_max: + Styler.highlight_min: + Styler.highlight_quantile: + + Notes + ----- + If ``start`` is ``None`` only the right bound is applied. + If ``stop`` is ``None`` only the left bound is applied. If both are ``None`` + all values are highlighted. + + This function only works with compatible ``dtypes``. For example a datetime-like + region can only use equivalent datetime-like ``start`` and ``stop`` arguments. + Use ``subset`` to control regions which have multiple ``dtypes``. + + Examples + -------- + Using datetimes + + >>> df = pd.DataFrame({'dates': pd.date_range(start='2021-01-01', periods=10)}) + >>> df.style.highlight_range(start=pd.to_datetime('2021-01-05')) + + Using ``props`` instead of default background shading + + >>> df = pd.DataFrame([[1,2], [3,4]]) + >>> df.style.highlight_range(start=2, stop=3, props='font-weight:bold;') + """ + + def f( + data: DataFrame, + props: str, + d: Optional[Scalar] = None, + u: Optional[Scalar] = None, + ) -> np.ndarray: + ge_d = data >= d if d is not None else np.full_like(data, True, dtype=bool) + le_u = data <= u if u is not None else np.full_like(data, True, dtype=bool) + return np.where(ge_d & le_u, props, "") + + if props is None: + props = f"background-color: {color};" + return self.apply(f, axis=None, subset=subset, props=props, d=start, u=stop) + + def highlight_quantile( + self, + subset: Optional[IndexLabel] = None, + color: str = "yellow", + q_low: float = 0.0, + q_high: float = 1.0, + axis: Optional[Axis] = 0, + props: Optional[str] = None, + ) -> Styler: + """ + Highlight values defined by inclusion in given quantile by shading the + background, or otherwise. + + Parameters + ---------- + subset : IndexSlice, default None + A valid slice for ``data`` to limit the style application to. + color : str, default 'yellow' + Background color added to CSS style. + q_low : float, default 0 + Left bound for the target quantile range (exclusive if not 0). + q_high : float, default 1 + Right bound for the target quantile range (inclusive) + axis : {0 or 'index', 1 or 'columns', None}, default 0 + Apply to each column (``axis=0`` or ``'index'``), to each row + (``axis=1`` or ``'columns'``), or to the entire DataFrame at once + with ``axis=None``. + 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_max: + Styler.highlight_min: + Styler.highlight_range: + + Notes + ----- + This function only works with consistent ``dtypes`` within the ``subset``. + For example a mixture of datetime-like and float items will raise errors. + + This method uses ``pandas.qcut`` to implement the quantile labelling of data + values. + """ + + def f( + data: FrameOrSeries, + props: str, + q_low: float = 0, + q_high: float = 1, + axis_: Optional[Axis] = 0, + ): + if q_low > 0: + q, tgt_label = [0, q_low, q_high], 1 + else: + q, tgt_label = [0, q_high], 0 + if axis_ is None: + shape = data.values.shape + labels = pd.qcut(data.values.ravel(), q=q, labels=False).reshape(shape) + else: + labels = pd.qcut(data, q=q, labels=False) + return np.where(labels == tgt_label, props, "") + + if props is None: + props = f"background-color: {color};" + return self.apply( + f, + axis=axis, + subset=subset, + props=props, + q_low=q_low, + q_high=q_high, + axis_=axis, + ) + @classmethod def from_custom_template(cls, searchpath, name): """ From 6c73f62841af40f42a8ae650103e9346d38fc47f Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Tue, 16 Feb 2021 22:09:11 +0100 Subject: [PATCH 02/45] merged --- pandas/io/formats/style.py | 45 +++++++++++++++++++------------------- 1 file changed, 23 insertions(+), 22 deletions(-) diff --git a/pandas/io/formats/style.py b/pandas/io/formats/style.py index 5a0141e5c1204..3bbee97d8ea8a 100644 --- a/pandas/io/formats/style.py +++ b/pandas/io/formats/style.py @@ -32,6 +32,7 @@ FrameOrSeries, FrameOrSeriesUnion, IndexLabel, + Scalar, ) from pandas.compat._optional import import_optional_dependency from pandas.util._decorators import doc @@ -1712,12 +1713,12 @@ def f(data: FrameOrSeries, props: str) -> np.ndarray: ) def highlight_range( - self, - subset: Optional[IndexLabel] = None, - color: str = "yellow", - start: Optional[Any] = None, - stop: Optional[Any] = None, - props: Optional[str] = None, + self, + subset: Optional[IndexLabel] = None, + color: str = "yellow", + start: Optional[Any] = None, + stop: Optional[Any] = None, + props: Optional[str] = None, ) -> Styler: """ Highlight a defined range by shading the background, or otherwise. @@ -1770,10 +1771,10 @@ def highlight_range( """ def f( - data: DataFrame, - props: str, - d: Optional[Scalar] = None, - u: Optional[Scalar] = None, + data: DataFrame, + props: str, + d: Optional[Scalar] = None, + u: Optional[Scalar] = None, ) -> np.ndarray: ge_d = data >= d if d is not None else np.full_like(data, True, dtype=bool) le_u = data <= u if u is not None else np.full_like(data, True, dtype=bool) @@ -1784,13 +1785,13 @@ def f( return self.apply(f, axis=None, subset=subset, props=props, d=start, u=stop) def highlight_quantile( - self, - subset: Optional[IndexLabel] = None, - color: str = "yellow", - q_low: float = 0.0, - q_high: float = 1.0, - axis: Optional[Axis] = 0, - props: Optional[str] = None, + self, + subset: Optional[IndexLabel] = None, + color: str = "yellow", + q_low: float = 0.0, + q_high: float = 1.0, + axis: Optional[Axis] = 0, + props: Optional[str] = None, ) -> Styler: """ Highlight values defined by inclusion in given quantile by shading the @@ -1834,11 +1835,11 @@ def highlight_quantile( """ def f( - data: FrameOrSeries, - props: str, - q_low: float = 0, - q_high: float = 1, - axis_: Optional[Axis] = 0, + data: FrameOrSeries, + props: str, + q_low: float = 0, + q_high: float = 1, + axis_: Optional[Axis] = 0, ): if q_low > 0: q, tgt_label = [0, q_low, q_high], 1 From fe0839882c279bca41ffa821a6aa7a25176897d9 Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Wed, 17 Feb 2021 09:06:21 +0100 Subject: [PATCH 03/45] upodate docs and consistency --- pandas/io/formats/style.py | 95 ++++++++++++++++++++++++++++++-------- 1 file changed, 75 insertions(+), 20 deletions(-) diff --git a/pandas/io/formats/style.py b/pandas/io/formats/style.py index 3bbee97d8ea8a..bd543c8c4b058 100644 --- a/pandas/io/formats/style.py +++ b/pandas/io/formats/style.py @@ -1626,9 +1626,10 @@ def highlight_null( self, null_color: str = "red", subset: Optional[IndexLabel] = None, + props: Optional[str] = None, ) -> Styler: """ - Shade the background ``null_color`` for missing values. + Highlight missing values with a style. Parameters ---------- @@ -1638,79 +1639,128 @@ def highlight_null( .. versionadded:: 1.1.0 + props : str, default None + CSS properties to use for highlighting. If ``props`` is given, ``color`` + is not used. + + .. versionadded:: 1.3.0 + + 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_quantile: Highlight values defined by a quantile with a style + Styler.highlight_range: Highlight a defined range with a style + + Notes + ----- + Uses ``pandas.isna()`` to detect the missing values. """ def f(data: DataFrame, props: str) -> np.ndarray: return np.where(pd.isna(data).values, props, "") - return self.apply( - f, axis=None, subset=subset, props=f"background-color: {null_color};" - ) + if props is None: + props = f"background-color: {null_color};" + return self.apply(f, axis=None, subset=subset, props=props) def highlight_max( self, subset: Optional[IndexLabel] = None, color: str = "yellow", axis: Optional[Axis] = 0, + props: Optional[str] = None, ) -> Styler: """ - Highlight the maximum by shading the background. + Highlight the maximum with a style. 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 Apply to each column (``axis=0`` or ``'index'``), to each row (``axis=1`` or ``'columns'``), or to the entire DataFrame at once with ``axis=None``. + props : str, default None + CSS properties to use for highlighting. If ``props`` is given, ``color`` + is not used. + + .. versionadded:: 1.3.0 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_quantile: Highlight values defined by a quantile with a style + Styler.highlight_range: Highlight a defined range with a style """ def f(data: FrameOrSeries, props: str) -> np.ndarray: return np.where(data == np.nanmax(data.values), props, "") - return self.apply( - f, axis=axis, subset=subset, props=f"background-color: {color};" - ) + if props is None: + props = f"background-color: {color};" + return self.apply(f, axis=axis, subset=subset, props=props) def highlight_min( self, subset: Optional[IndexLabel] = None, color: str = "yellow", axis: Optional[Axis] = 0, + props: Optional[str] = None, ) -> Styler: """ - Highlight the minimum by shading the background. + Highlight the minimum with a style. 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 Apply to each column (``axis=0`` or ``'index'``), to each row (``axis=1`` or ``'columns'``), or to the entire DataFrame at once with ``axis=None``. + props : str, default None + CSS properties to use for highlighting. If ``props`` is given, ``color`` + is not used. + + .. versionadded:: 1.3.0 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_quantile: Highlight values defined by a quantile with a style + Styler.highlight_range: Highlight a defined range with a style """ def f(data: FrameOrSeries, props: str) -> np.ndarray: return np.where(data == np.nanmin(data.values), props, "") - return self.apply( - f, axis=axis, subset=subset, props=f"background-color: {color};" - ) + if props is None: + props = f"background-color: {color};" + return self.apply(f, axis=axis, subset=subset, props=props) def highlight_range( self, @@ -1721,14 +1771,16 @@ def highlight_range( props: Optional[str] = None, ) -> Styler: """ - Highlight a defined range by shading the background, or otherwise. + Highlight a defined range with a style + + .. versionadded:: 1.3.0 Parameters ---------- subset : IndexSlice, default None A valid slice for ``data`` to limit the style application to. color : str, default 'yellow' - Background color added to CSS style. + Background color to use for highlighting. start : scalar or datetime-like, default None Left bound for defining the range (inclusive). stop : scalar or datetime-like, default None @@ -1743,9 +1795,11 @@ def highlight_range( See Also -------- - Styler.highlight_max: - Styler.highlight_min: - Styler.highlight_quantile: + 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 + Styler.highlight_range: Highlight a defined range with a style Notes ----- @@ -1794,15 +1848,16 @@ def highlight_quantile( props: Optional[str] = None, ) -> Styler: """ - Highlight values defined by inclusion in given quantile by shading the - background, or otherwise. + Highlight values defined by a quantile with a style + + .. versionadded:: 1.3.0 Parameters ---------- subset : IndexSlice, default None A valid slice for ``data`` to limit the style application to. color : str, default 'yellow' - Background color added to CSS style. + Background color to use for highlighting q_low : float, default 0 Left bound for the target quantile range (exclusive if not 0). q_high : float, default 1 From aaa15bc26774af856916a75c7f8d14443be2bdf5 Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Wed, 17 Feb 2021 09:17:51 +0100 Subject: [PATCH 04/45] upodate docs and consistency --- pandas/io/formats/style.py | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/pandas/io/formats/style.py b/pandas/io/formats/style.py index bd543c8c4b058..bda892256a8ad 100644 --- a/pandas/io/formats/style.py +++ b/pandas/io/formats/style.py @@ -1859,9 +1859,10 @@ def highlight_quantile( color : str, default 'yellow' Background color to use for highlighting q_low : float, default 0 - Left bound for the target quantile range (exclusive if not 0). + Left bound, in [0, q_high), for the target quantile range (exclusive if not + 0). q_high : float, default 1 - Right bound for the target quantile range (inclusive) + Right bound, in (q_low, 1], for the target quantile range (inclusive) axis : {0 or 'index', 1 or 'columns', None}, default 0 Apply to each column (``axis=0`` or ``'index'``), to each row (``axis=1`` or ``'columns'``), or to the entire DataFrame at once @@ -1876,14 +1877,17 @@ def highlight_quantile( See Also -------- - Styler.highlight_max: - Styler.highlight_min: - Styler.highlight_range: + 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 + Styler.highlight_range: Highlight a defined range with a style Notes ----- - This function only works with consistent ``dtypes`` within the ``subset``. - For example a mixture of datetime-like and float items will raise errors. + This function only works with consistent ``dtypes`` within the ``subset`` or + ``axis``. For example a mixture of datetime-like and float items will raise + errors. This method uses ``pandas.qcut`` to implement the quantile labelling of data values. From a362fbfae88967c9ef8d6edc2b506df30028fc35 Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Wed, 17 Feb 2021 22:09:32 +0100 Subject: [PATCH 05/45] add and update tests --- pandas/tests/io/formats/test_style.py | 135 +++++++++++++++++++------- 1 file changed, 98 insertions(+), 37 deletions(-) diff --git a/pandas/tests/io/formats/test_style.py b/pandas/tests/io/formats/test_style.py index a154e51f68dba..9b96a21efcab3 100644 --- a/pandas/tests/io/formats/test_style.py +++ b/pandas/tests/io/formats/test_style.py @@ -23,7 +23,7 @@ class TestStyler: def setup_method(self, method): np.random.seed(24) self.s = DataFrame({"A": np.random.permutation(range(6))}) - self.df = DataFrame({"A": [0, 1], "B": np.random.randn(2)}) + self.df = DataFrame({"A": [0, 1], "B": np.random.randn(2)}) # [-0.61 -1.23] self.f = lambda x: x self.g = lambda x: x @@ -1237,45 +1237,106 @@ def test_trim(self): result = self.df.style.highlight_max().render() assert result.count("#") == len(self.df.columns) - def test_highlight_max(self): - df = DataFrame([[1, 2], [3, 4]], columns=["A", "B"]) - # max(df) = min(-df) - for max_ in [True, False]: - if max_: - attr = "highlight_max" - else: - df = -df - attr = "highlight_min" - result = getattr(df.style, attr)()._compute().ctx - assert result[(1, 1)] == ["background-color: yellow"] - - result = getattr(df.style, attr)(color="green")._compute().ctx - assert result[(1, 1)] == ["background-color: green"] - - result = getattr(df.style, attr)(subset="A")._compute().ctx - assert result[(1, 0)] == ["background-color: yellow"] - - result = getattr(df.style, attr)(axis=0)._compute().ctx - expected = { - (1, 0): ["background-color: yellow"], - (1, 1): ["background-color: yellow"], - } - assert result == expected + @pytest.mark.parametrize( + "f", + [ + {"f": "highlight_min", "kw": {"axis": 1, "subset": pd.IndexSlice[1, :]}}, + {"f": "highlight_max", "kw": {"axis": 0, "subset": [0]}}, + { + "f": "highlight_quantile", + "kw": {"axis": None, "q_low": 0.6, "q_high": 0.8}, + }, + {"f": "highlight_range", "kw": {"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(self, f, df): + func = f["f"] + if func == "highlight_quantile" and isinstance(df.iloc[0, 0], str): + return None # quantile incompatible with str + elif func == "highlight_range": + f["kw"]["start"] = df.iloc[1, 0] # set the range low for testing + + expected = {(1, 0): ["background-color: yellow"]} + result = getattr(df.style, func)(**f["kw"])._compute().ctx + assert result == expected - result = getattr(df.style, attr)(axis=1)._compute().ctx - expected = { - (0, 1): ["background-color: yellow"], - (1, 1): ["background-color: yellow"], - } - assert result == expected + @pytest.mark.parametrize("f", ["highlight_min", "highlight_max"]) + def test_highlight_minmax_basic(self, f): + expected = { + (0, 0): ["background-color: red"], + (1, 0): ["background-color: red"], + } + if f == "highlight_min": + df = -self.df + else: + df = self.df + result = getattr(df.style, f)(axis=1, color="red")._compute().ctx + assert result == expected - # separate since we can't negate the strs - df["C"] = ["a", "b"] - result = df.style.highlight_max()._compute().ctx - expected = {(1, 1): ["background-color: yellow"]} + @pytest.mark.parametrize("f", ["highlight_min", "highlight_max"]) + @pytest.mark.parametrize( + "kwargs", + [ + {"axis": None, "color": "red"}, # test axis + {"axis": 0, "subset": ["A"], "color": "red"}, # test subset + {"axis": None, "props": "background-color: red"}, # test props + ], + ) + def test_highlight_minmax_ext(self, f, kwargs): + expected = {(1, 0): ["background-color: red"]} + if f == "highlight_min": + df = -self.df + else: + df = self.df + result = getattr(df.style, f)(**kwargs)._compute().ctx + assert result == expected + + @pytest.mark.parametrize( + "kwargs", + [ + {"start": 0, "stop": 1}, # test basic range + {"start": 0, "stop": 1, "props": "background-color: yellow"}, # test props + {"start": -9, "stop": 9, "subset": ["A"]}, # test subset effective + {"start": 0}, # test no stop + {"stop": 1, "subset": ["A"]}, # test no start + ], + ) + def test_highlight_range(self, kwargs): + expected = { + (0, 0): ["background-color: yellow"], + (1, 0): ["background-color: yellow"], + } + result = self.df.style.highlight_range(**kwargs)._compute().ctx + assert result == expected - result = df.style.highlight_min()._compute().ctx - expected = {(0, 0): ["background-color: yellow"]} + @pytest.mark.parametrize( + "kwargs", + [ + {"q_low": 0.5, "q_high": 1, "axis": 1}, # test basic range + {"q_low": 0.5, "q_high": 1, "axis": None}, # test axis + {"q_low": 0, "q_high": 1, "subset": ["A"]}, # test subset + {"q_low": 0.5, "axis": 1}, # test no high + {"q_high": 1, "subset": ["A"], "axis": 0}, # test no low + {"q_low": 0.5, "axis": 1, "props": "background-color: yellow"}, # tst props + ], + ) + def test_highlight_quantile(self, kwargs): + expected = { + (0, 0): ["background-color: yellow"], + (1, 0): ["background-color: yellow"], + } + result = self.df.style.highlight_quantile(**kwargs)._compute().ctx + assert result == expected def test_export(self): f = lambda x: "color: red" if x > 0 else "color: blue" From 6ab6dcadaf3ccde301b5f23d3f2354450438dd9c Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Thu, 18 Feb 2021 08:59:55 +0100 Subject: [PATCH 06/45] highlight range now accepts sequence --- pandas/io/formats/style.py | 47 ++++++++++++++++++++++----- pandas/tests/io/formats/test_style.py | 3 ++ 2 files changed, 41 insertions(+), 9 deletions(-) diff --git a/pandas/io/formats/style.py b/pandas/io/formats/style.py index bda892256a8ad..e99bb90ba1aa0 100644 --- a/pandas/io/formats/style.py +++ b/pandas/io/formats/style.py @@ -1766,8 +1766,9 @@ def highlight_range( self, subset: Optional[IndexLabel] = None, color: str = "yellow", - start: Optional[Any] = None, - stop: Optional[Any] = None, + axis: Optional[Axis] = 0, + start: Optional[Union[Scalar, Sequence]] = None, + stop: Optional[Union[Scalar, Sequence]] = None, props: Optional[str] = None, ) -> Styler: """ @@ -1781,9 +1782,13 @@ def highlight_range( A valid slice for ``data`` to limit the style application to. color : str, default 'yellow' Background color to use for highlighting. - start : scalar or datetime-like, default None + axis : {0 or 'index', 1 or 'columns', None}, default 0 + Apply to each column (``axis=0`` or ``'index'``), to each row + (``axis=1`` or ``'columns'``), or to the entire DataFrame at once + with ``axis=None``. + start : scalar or datetime-like, or sequence or array-like, default None Left bound for defining the range (inclusive). - stop : scalar or datetime-like, default None + stop : scalar or datetime-like, or sequence or array-like, default None Right bound for defining the range (inclusive) props : str, default None CSS properties to use for highlighting. If ``props`` is given, ``color`` @@ -1807,6 +1812,10 @@ def highlight_range( If ``stop`` is ``None`` only the left bound is applied. If both are ``None`` all values are highlighted. + ``axis`` is only needed if ``start`` or ``stop`` are provide as a sequence or + an array-like object for aligning the shapes. If ``start`` and ``stop`` are + both scalars then all ``axis`` inputs will give the same result. + This function only works with compatible ``dtypes``. For example a datetime-like region can only use equivalent datetime-like ``start`` and ``stop`` arguments. Use ``subset`` to control regions which have multiple ``dtypes``. @@ -1818,25 +1827,45 @@ def highlight_range( >>> df = pd.DataFrame({'dates': pd.date_range(start='2021-01-01', periods=10)}) >>> df.style.highlight_range(start=pd.to_datetime('2021-01-05')) - Using ``props`` instead of default background shading + Basic usage >>> df = pd.DataFrame([[1,2], [3,4]]) - >>> df.style.highlight_range(start=2, stop=3, props='font-weight:bold;') + >>> df.style.highlight_range(start=1, stop=3) + + Using ``props`` instead of default background coloring + + >>> df.style.highlight_range(start=1, stop=3, props='font-weight:bold;') + + Using ``start`` and ``stop`` sequences or array-like objects with ``axis`` + + >>> df.style.highlight_range(start=[0.9, 2.9], stop=[1.1, 3.1], axis=0) + >>> df.style.highlight_range(start=[0.9, 2.9], stop=[1.1, 3.1], axis=1) + >>> df.style.highlight_range(start=pd.DataFrame([[1,2],[10,4]]), axis=None) """ def f( data: DataFrame, props: str, - d: Optional[Scalar] = None, - u: Optional[Scalar] = None, + d: Optional[Union[Scalar, Sequence]] = None, + u: Optional[Union[Scalar, Sequence]] = None, ) -> np.ndarray: + d = ( + np.asarray(d).reshape(data.shape) + if (np.iterable(d) and not isinstance(d, str)) + else d + ) + u = ( + np.asarray(u).reshape(data.shape) + if (np.iterable(u) and not isinstance(u, str)) + else u + ) ge_d = data >= d if d is not None else np.full_like(data, True, dtype=bool) le_u = data <= u if u is not None else np.full_like(data, True, dtype=bool) return np.where(ge_d & le_u, props, "") if props is None: props = f"background-color: {color};" - return self.apply(f, axis=None, subset=subset, props=props, d=start, u=stop) + return self.apply(f, axis=axis, subset=subset, props=props, d=start, u=stop) def highlight_quantile( self, diff --git a/pandas/tests/io/formats/test_style.py b/pandas/tests/io/formats/test_style.py index 9b96a21efcab3..c2797d397780b 100644 --- a/pandas/tests/io/formats/test_style.py +++ b/pandas/tests/io/formats/test_style.py @@ -1309,6 +1309,9 @@ def test_highlight_minmax_ext(self, f, kwargs): {"start": -9, "stop": 9, "subset": ["A"]}, # test subset effective {"start": 0}, # test no stop {"stop": 1, "subset": ["A"]}, # test no start + {"start": [0, 1], "axis": 0}, # test start as sequence + {"start": DataFrame([[0, 1], [1, 1]]), "axis": None}, # test axis with seq + {"start": 0, "stop": [0, 1], "axis": 0}, # test sequence stop ], ) def test_highlight_range(self, kwargs): From 411ef6be1dd0c5d92a3d84cde7403a603246e262 Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Thu, 18 Feb 2021 09:17:59 +0100 Subject: [PATCH 07/45] highlight range now accepts sequence --- pandas/io/formats/style.py | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/pandas/io/formats/style.py b/pandas/io/formats/style.py index e99bb90ba1aa0..3600026de10d9 100644 --- a/pandas/io/formats/style.py +++ b/pandas/io/formats/style.py @@ -1849,16 +1849,12 @@ def f( d: Optional[Union[Scalar, Sequence]] = None, u: Optional[Union[Scalar, Sequence]] = None, ) -> np.ndarray: - d = ( - np.asarray(d).reshape(data.shape) - if (np.iterable(d) and not isinstance(d, str)) - else d - ) - u = ( - np.asarray(u).reshape(data.shape) - if (np.iterable(u) and not isinstance(u, str)) - else u - ) + def realign(x): + if np.iterable(x) and not isinstance(x, str): + return np.asarray(x).reshape(data.shape) + return x + + d, u = realign(d), realign(u) ge_d = data >= d if d is not None else np.full_like(data, True, dtype=bool) le_u = data <= u if u is not None else np.full_like(data, True, dtype=bool) return np.where(ge_d & le_u, props, "") From 85e5241980e38f67db6526cc5ffdbd822679eeec Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Thu, 18 Feb 2021 09:25:55 +0100 Subject: [PATCH 08/45] highlight range now accepts sequence --- pandas/io/formats/style.py | 1 - 1 file changed, 1 deletion(-) diff --git a/pandas/io/formats/style.py b/pandas/io/formats/style.py index 3600026de10d9..82cf0e7963221 100644 --- a/pandas/io/formats/style.py +++ b/pandas/io/formats/style.py @@ -1645,7 +1645,6 @@ def highlight_null( .. versionadded:: 1.3.0 - Returns ------- self : Styler From 4f105d6e88512553a1f045c95a0a9f9d09373a4b Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Thu, 18 Feb 2021 10:05:46 +0100 Subject: [PATCH 09/45] examples --- pandas/io/formats/style.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/pandas/io/formats/style.py b/pandas/io/formats/style.py index 82cf0e7963221..bc3c5bb2a586f 100644 --- a/pandas/io/formats/style.py +++ b/pandas/io/formats/style.py @@ -1915,6 +1915,14 @@ def highlight_quantile( This method uses ``pandas.qcut`` to implement the quantile labelling of data values. + + Examples + -------- + >>> df = pd.DataFrame(np.arange(1,100).reshape(10,10)+1) + >>> df.style.highlight_quantile(q_low=0.5, q_high=0.7, axis=None) + >>> df.style.highlight_quantile(q_low=0.5, q_high=0.7, axis=0) + >>> df.style.highlight_quantile(q_low=0.5, q_high=0.7, axis=1) + >>> df.style.highlight_quantile(q_low=0.5, props='font-weight:bold;color:red') """ def f( From 26f6d12338056b30fdac0aa0c09d7beed708e461 Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Thu, 18 Feb 2021 10:13:17 +0100 Subject: [PATCH 10/45] reorder --- pandas/tests/io/formats/test_style.py | 66 +++++++++++++-------------- 1 file changed, 33 insertions(+), 33 deletions(-) diff --git a/pandas/tests/io/formats/test_style.py b/pandas/tests/io/formats/test_style.py index c2797d397780b..0a605ef0668f4 100644 --- a/pandas/tests/io/formats/test_style.py +++ b/pandas/tests/io/formats/test_style.py @@ -1237,39 +1237,6 @@ def test_trim(self): result = self.df.style.highlight_max().render() assert result.count("#") == len(self.df.columns) - @pytest.mark.parametrize( - "f", - [ - {"f": "highlight_min", "kw": {"axis": 1, "subset": pd.IndexSlice[1, :]}}, - {"f": "highlight_max", "kw": {"axis": 0, "subset": [0]}}, - { - "f": "highlight_quantile", - "kw": {"axis": None, "q_low": 0.6, "q_high": 0.8}, - }, - {"f": "highlight_range", "kw": {"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(self, f, df): - func = f["f"] - if func == "highlight_quantile" and isinstance(df.iloc[0, 0], str): - return None # quantile incompatible with str - elif func == "highlight_range": - f["kw"]["start"] = df.iloc[1, 0] # set the range low for testing - - expected = {(1, 0): ["background-color: yellow"]} - result = getattr(df.style, func)(**f["kw"])._compute().ctx - assert result == expected - @pytest.mark.parametrize("f", ["highlight_min", "highlight_max"]) def test_highlight_minmax_basic(self, f): expected = { @@ -1341,6 +1308,39 @@ def test_highlight_quantile(self, kwargs): result = self.df.style.highlight_quantile(**kwargs)._compute().ctx assert result == expected + @pytest.mark.parametrize( + "f", + [ + {"f": "highlight_min", "kw": {"axis": 1, "subset": pd.IndexSlice[1, :]}}, + {"f": "highlight_max", "kw": {"axis": 0, "subset": [0]}}, + { + "f": "highlight_quantile", + "kw": {"axis": None, "q_low": 0.6, "q_high": 0.8}, + }, + {"f": "highlight_range", "kw": {"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(self, f, df): + func = f["f"] + if func == "highlight_quantile" and isinstance(df.iloc[0, 0], str): + return None # quantile incompatible with str + elif func == "highlight_range": + f["kw"]["start"] = df.iloc[1, 0] # set the range low for testing + + expected = {(1, 0): ["background-color: yellow"]} + result = getattr(df.style, func)(**f["kw"])._compute().ctx + assert result == expected + def test_export(self): f = lambda x: "color: red" if x > 0 else "color: blue" g = lambda x, z: f"color: {z}" if x > 0 else f"color: {z}" From 92eb5fd5c4cf4c8cd7870991b96003d9657a5f7b Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Fri, 19 Feb 2021 07:39:23 +0100 Subject: [PATCH 11/45] adj tests --- pandas/tests/io/formats/test_style.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/pandas/tests/io/formats/test_style.py b/pandas/tests/io/formats/test_style.py index e187ae5fce46a..9521bbe983cc2 100644 --- a/pandas/tests/io/formats/test_style.py +++ b/pandas/tests/io/formats/test_style.py @@ -1101,8 +1101,8 @@ def test_trim(self): @pytest.mark.parametrize("f", ["highlight_min", "highlight_max"]) def test_highlight_minmax_basic(self, f): expected = { - (0, 0): ["background-color: red"], - (1, 0): ["background-color: red"], + (0, 0): [("background-color", "red")], + (1, 0): [("background-color", "red")], } if f == "highlight_min": df = -self.df @@ -1121,7 +1121,7 @@ def test_highlight_minmax_basic(self, f): ], ) def test_highlight_minmax_ext(self, f, kwargs): - expected = {(1, 0): ["background-color: red"]} + expected = {(1, 0): [("background-color", "red")]} if f == "highlight_min": df = -self.df else: @@ -1144,8 +1144,8 @@ def test_highlight_minmax_ext(self, f, kwargs): ) def test_highlight_range(self, kwargs): expected = { - (0, 0): ["background-color: yellow"], - (1, 0): ["background-color: yellow"], + (0, 0): [("background-color", "yellow")], + (1, 0): [("background-color", "yellow")], } result = self.df.style.highlight_range(**kwargs)._compute().ctx assert result == expected @@ -1163,8 +1163,8 @@ def test_highlight_range(self, kwargs): ) def test_highlight_quantile(self, kwargs): expected = { - (0, 0): ["background-color: yellow"], - (1, 0): ["background-color: yellow"], + (0, 0): [("background-color", "yellow")], + (1, 0): [("background-color", "yellow")], } result = self.df.style.highlight_quantile(**kwargs)._compute().ctx assert result == expected @@ -1198,7 +1198,7 @@ def test_all_highlight_dtypes(self, f, df): elif func == "highlight_range": f["kw"]["start"] = df.iloc[1, 0] # set the range low for testing - expected = {(1, 0): ["background-color: yellow"]} + expected = {(1, 0): [("background-color", "yellow")]} result = getattr(df.style, func)(**f["kw"])._compute().ctx assert result == expected From cbea15a31e7eab3e632192fef404f7ded9aef87c Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Fri, 19 Feb 2021 08:54:00 +0100 Subject: [PATCH 12/45] adj tests --- pandas/tests/io/formats/test_style.py | 24 ++++++++++-------------- 1 file changed, 10 insertions(+), 14 deletions(-) diff --git a/pandas/tests/io/formats/test_style.py b/pandas/tests/io/formats/test_style.py index 9521bbe983cc2..b1a4ad1797beb 100644 --- a/pandas/tests/io/formats/test_style.py +++ b/pandas/tests/io/formats/test_style.py @@ -1170,15 +1170,12 @@ def test_highlight_quantile(self, kwargs): assert result == expected @pytest.mark.parametrize( - "f", + "f,kwargs", [ - {"f": "highlight_min", "kw": {"axis": 1, "subset": pd.IndexSlice[1, :]}}, - {"f": "highlight_max", "kw": {"axis": 0, "subset": [0]}}, - { - "f": "highlight_quantile", - "kw": {"axis": None, "q_low": 0.6, "q_high": 0.8}, - }, - {"f": "highlight_range", "kw": {"subset": [0]}}, + ("highlight_min", {"axis": 1, "subset": pd.IndexSlice[1, :]}), + ("highlight_max", {"axis": 0, "subset": [0]}), + ("highlight_quantile", {"axis": None, "q_low": 0.6, "q_high": 0.8}), + ("highlight_range", {"subset": [0]}), ], ) @pytest.mark.parametrize( @@ -1191,15 +1188,14 @@ def test_highlight_quantile(self, kwargs): DataFrame([[0, 1], [2, 3]], dtype="timedelta64[ns]"), ], ) - def test_all_highlight_dtypes(self, f, df): - func = f["f"] - if func == "highlight_quantile" and isinstance(df.iloc[0, 0], str): + def test_all_highlight_dtypes(self, f, kwargs, df): + if f == "highlight_quantile" and isinstance(df.iloc[0, 0], str): return None # quantile incompatible with str - elif func == "highlight_range": - f["kw"]["start"] = df.iloc[1, 0] # set the range low for testing + elif f == "highlight_range": + kwargs["start"] = df.iloc[1, 0] # set the range low for testing expected = {(1, 0): [("background-color", "yellow")]} - result = getattr(df.style, func)(**f["kw"])._compute().ctx + result = getattr(df.style, f)(**kwargs)._compute().ctx assert result == expected def test_export(self): From caef3a51344f3d31ca41bfb7827f6eeb39a74cc6 Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Fri, 19 Feb 2021 08:56:51 +0100 Subject: [PATCH 13/45] whats new --- doc/source/whatsnew/v1.3.0.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/doc/source/whatsnew/v1.3.0.rst b/doc/source/whatsnew/v1.3.0.rst index 388c5dbf6a7ee..2eda5ed0aa70d 100644 --- a/doc/source/whatsnew/v1.3.0.rst +++ b/doc/source/whatsnew/v1.3.0.rst @@ -67,6 +67,7 @@ Other enhancements - :meth:`.Styler.set_tooltips_class` and :meth:`.Styler.set_table_styles` amended to optionally allow certain css-string input arguments (:issue:`39564`) - :meth:`.Styler.apply` now more consistently accepts ndarray function returns, i.e. in all cases for ``axis`` is ``0, 1 or None`` (:issue:`39359`) - :meth:`.Styler.apply` and :meth:`.Styler.applymap` now raise errors if wrong format CSS is passed on render (:issue:`39660`) +- :meth:`.Styler.highlight_range` and :meth:`.Styler.highlight_quantile` added to list of builtin styling methods (:issue:`39821`) - :meth:`Series.loc.__getitem__` and :meth:`Series.loc.__setitem__` with :class:`MultiIndex` now raising helpful error message when indexer has too many dimensions (:issue:`35349`) - :meth:`pandas.read_stata` and :class:`StataReader` support reading data from compressed files. - Add support for parsing ``ISO 8601``-like timestamps with negative signs to :meth:`pandas.Timedelta` (:issue:`37172`) From dc12b0c90696eac9d1f49176592a08eafe0d9e30 Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Fri, 19 Feb 2021 09:00:53 +0100 Subject: [PATCH 14/45] to_numpy() --- pandas/io/formats/style.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/pandas/io/formats/style.py b/pandas/io/formats/style.py index 9b3276bd2585a..8a9f15ea7df44 100644 --- a/pandas/io/formats/style.py +++ b/pandas/io/formats/style.py @@ -925,7 +925,7 @@ def apply( Examples -------- >>> def highlight_max(x, color): - ... return np.where(x == np.nanmax(x.values), f"color: {color};", None) + ... return np.where(x == np.nanmax(x.to_numpy()), f"color: {color};", None) >>> df = pd.DataFrame(np.random.randn(5, 2)) >>> df.style.apply(highlight_max, color='red') >>> df.style.apply(highlight_max, color='blue', axis=1) @@ -1657,7 +1657,7 @@ def highlight_null( """ def f(data: DataFrame, props: str) -> np.ndarray: - return np.where(pd.isna(data).values, props, "") + return np.where(pd.isna(data).to_numpy(), props, "") if props is None: props = f"background-color: {null_color};" @@ -1703,7 +1703,7 @@ def highlight_max( """ def f(data: FrameOrSeries, props: str) -> np.ndarray: - return np.where(data == np.nanmax(data.values), props, "") + return np.where(data == np.nanmax(data.to_numpy()), props, "") if props is None: props = f"background-color: {color};" @@ -1749,7 +1749,7 @@ def highlight_min( """ def f(data: FrameOrSeries, props: str) -> np.ndarray: - return np.where(data == np.nanmin(data.values), props, "") + return np.where(data == np.nanmin(data.to_numpy()), props, "") if props is None: props = f"background-color: {color};" @@ -1931,8 +1931,9 @@ def f( else: q, tgt_label = [0, q_high], 0 if axis_ is None: - shape = data.values.shape - labels = pd.qcut(data.values.ravel(), q=q, labels=False).reshape(shape) + labels = pd.qcut(data.to_numpy().ravel(), q=q, labels=False).reshape( + data.to_numpy().shape + ) else: labels = pd.qcut(data, q=q, labels=False) return np.where(labels == tgt_label, props, "") From 95c1a68c215edf2e98baeb49bc264872161205c8 Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Fri, 19 Feb 2021 10:02:55 +0100 Subject: [PATCH 15/45] docstring fix --- pandas/io/formats/style.py | 54 +++++++++++++++++++------------------- 1 file changed, 27 insertions(+), 27 deletions(-) diff --git a/pandas/io/formats/style.py b/pandas/io/formats/style.py index 8a9f15ea7df44..df3c919c2371a 100644 --- a/pandas/io/formats/style.py +++ b/pandas/io/formats/style.py @@ -1645,11 +1645,11 @@ def highlight_null( 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_quantile: Highlight values defined by a quantile with a style - Styler.highlight_range: Highlight a defined range with a style + 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. + Styler.highlight_range: Highlight a defined range with a style. Notes ----- @@ -1695,11 +1695,11 @@ def highlight_max( 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_quantile: Highlight values defined by a quantile with a style - Styler.highlight_range: Highlight a defined range with a style + 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. + Styler.highlight_range: Highlight a defined range with a style. """ def f(data: FrameOrSeries, props: str) -> np.ndarray: @@ -1741,11 +1741,11 @@ def highlight_min( 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_quantile: Highlight values defined by a quantile with a style - Styler.highlight_range: Highlight a defined range with a style + 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. + Styler.highlight_range: Highlight a defined range with a style. """ def f(data: FrameOrSeries, props: str) -> np.ndarray: @@ -1765,7 +1765,7 @@ def highlight_range( props: Optional[str] = None, ) -> Styler: """ - Highlight a defined range with a style + Highlight a defined range with a style. .. versionadded:: 1.3.0 @@ -1793,11 +1793,11 @@ def highlight_range( 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_quantile: Highlight values defined by a quantile with a style - Styler.highlight_range: Highlight a defined range with a style + 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. + Styler.highlight_range: Highlight a defined range with a style. Notes ----- @@ -1866,7 +1866,7 @@ def highlight_quantile( props: Optional[str] = None, ) -> Styler: """ - Highlight values defined by a quantile with a style + Highlight values defined by a quantile with a style. .. versionadded:: 1.3.0 @@ -1895,11 +1895,11 @@ def highlight_quantile( 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_quantile: Highlight values defined by a quantile with a style - Styler.highlight_range: Highlight a defined range with a style + 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. + Styler.highlight_range: Highlight a defined range with a style. Notes ----- From d9656353bf9ac172ab94f6d451a11a08120a0a5d Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Fri, 19 Feb 2021 11:09:09 +0100 Subject: [PATCH 16/45] test fix --- pandas/tests/io/formats/test_style.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pandas/tests/io/formats/test_style.py b/pandas/tests/io/formats/test_style.py index b1a4ad1797beb..ba31d49f5d52f 100644 --- a/pandas/tests/io/formats/test_style.py +++ b/pandas/tests/io/formats/test_style.py @@ -1169,6 +1169,9 @@ def test_highlight_quantile(self, kwargs): result = self.df.style.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", [ From b30b099964f111d4bda126e5a3bb2ef3ce2d3233 Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Fri, 19 Feb 2021 12:35:13 +0100 Subject: [PATCH 17/45] improve coding remove conditional --- pandas/io/formats/style.py | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/pandas/io/formats/style.py b/pandas/io/formats/style.py index df3c919c2371a..8b1f85bac204c 100644 --- a/pandas/io/formats/style.py +++ b/pandas/io/formats/style.py @@ -1926,17 +1926,13 @@ def f( q_high: float = 1, axis_: Optional[Axis] = 0, ): - if q_low > 0: - q, tgt_label = [0, q_low, q_high], 1 - else: - q, tgt_label = [0, q_high], 0 if axis_ is None: - labels = pd.qcut(data.to_numpy().ravel(), q=q, labels=False).reshape( - data.to_numpy().shape - ) + labels = pd.qcut( + data.to_numpy().ravel(), q=[q_low, q_high], labels=False + ).reshape(data.to_numpy().shape) else: - labels = pd.qcut(data, q=q, labels=False) - return np.where(labels == tgt_label, props, "") + labels = pd.qcut(data, q=[q_low, q_high], labels=False) + return np.where(labels == 0, props, "") if props is None: props = f"background-color: {color};" From d947c004c326216479b44eb9ac6ed12c2b1c701d Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Sat, 20 Feb 2021 14:15:52 +0100 Subject: [PATCH 18/45] docs links --- doc/source/reference/style.rst | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/doc/source/reference/style.rst b/doc/source/reference/style.rst index 3a8d912fa6ffe..41bd1f0692ef5 100644 --- a/doc/source/reference/style.rst +++ b/doc/source/reference/style.rst @@ -53,9 +53,11 @@ Builtin styles .. autosummary:: :toctree: api/ + Styler.highlight_null Styler.highlight_max Styler.highlight_min - Styler.highlight_null + Styler.highlight_range + Styler.highlight_quantile Styler.background_gradient Styler.bar From 5ffe49f672c5fea5f8cf698c2db6be5bc158af5f Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Tue, 23 Feb 2021 07:55:03 +0100 Subject: [PATCH 19/45] add tests to new modules. --- .../tests/io/formats/style/test_highlight.py | 142 +++++++++++++----- 1 file changed, 106 insertions(+), 36 deletions(-) diff --git a/pandas/tests/io/formats/style/test_highlight.py b/pandas/tests/io/formats/style/test_highlight.py index e02e1f012c662..ad677ddeb2024 100644 --- a/pandas/tests/io/formats/style/test_highlight.py +++ b/pandas/tests/io/formats/style/test_highlight.py @@ -1,12 +1,20 @@ import numpy as np import pytest -from pandas import DataFrame +from pandas import ( + DataFrame, + IndexSlice, +) pytest.importorskip("jinja2") class TestStylerHighlight: + def setup_method(self, method): + np.random.seed(24) + self.s = DataFrame({"A": np.random.permutation(range(6))}) + self.df = DataFrame({"A": [0, 1], "B": np.random.randn(2)}) + def test_highlight_null(self): df = DataFrame({"A": [0, np.nan]}) result = df.style.highlight_null()._compute().ctx @@ -28,43 +36,105 @@ def test_highlight_null_subset(self): } assert result == expected - def test_highlight_max(self): - df = DataFrame([[1, 2], [3, 4]], columns=["A", "B"]) - css_seq = [("background-color", "yellow")] - # max(df) = min(-df) - for max_ in [True, False]: - if max_: - attr = "highlight_max" - else: - df = -df - attr = "highlight_min" - result = getattr(df.style, attr)()._compute().ctx - assert result[(1, 1)] == css_seq - - result = getattr(df.style, attr)(color="green")._compute().ctx - assert result[(1, 1)] == [("background-color", "green")] + @pytest.mark.parametrize("f", ["highlight_min", "highlight_max"]) + def test_highlight_minmax_basic(self, f): + expected = { + (0, 0): [("background-color", "red")], + (1, 0): [("background-color", "red")], + } + if f == "highlight_min": + df = -self.df + else: + df = self.df + result = getattr(df.style, f)(axis=1, color="red")._compute().ctx + assert result == expected - result = getattr(df.style, attr)(subset="A")._compute().ctx - assert result[(1, 0)] == css_seq + @pytest.mark.parametrize("f", ["highlight_min", "highlight_max"]) + @pytest.mark.parametrize( + "kwargs", + [ + {"axis": None, "color": "red"}, # test axis + {"axis": 0, "subset": ["A"], "color": "red"}, # test subset + {"axis": None, "props": "background-color: red"}, # test props + ], + ) + def test_highlight_minmax_ext(self, f, kwargs): + expected = {(1, 0): [("background-color", "red")]} + if f == "highlight_min": + df = -self.df + else: + df = self.df + result = getattr(df.style, f)(**kwargs)._compute().ctx + assert result == expected - result = getattr(df.style, attr)(axis=0)._compute().ctx - expected = { - (1, 0): css_seq, - (1, 1): css_seq, - } - assert result == expected + @pytest.mark.parametrize( + "kwargs", + [ + {"start": 0, "stop": 1}, # test basic range + {"start": 0, "stop": 1, "props": "background-color: yellow"}, # test props + {"start": -9, "stop": 9, "subset": ["A"]}, # test subset effective + {"start": 0}, # test no stop + {"stop": 1, "subset": ["A"]}, # test no start + {"start": [0, 1], "axis": 0}, # test start as sequence + {"start": DataFrame([[0, 1], [1, 1]]), "axis": None}, # test axis with seq + {"start": 0, "stop": [0, 1], "axis": 0}, # test sequence stop + ], + ) + def test_highlight_range(self, kwargs): + expected = { + (0, 0): [("background-color", "yellow")], + (1, 0): [("background-color", "yellow")], + } + result = self.df.style.highlight_range(**kwargs)._compute().ctx + assert result == expected - result = getattr(df.style, attr)(axis=1)._compute().ctx - expected = { - (0, 1): css_seq, - (1, 1): css_seq, - } - assert result == expected + @pytest.mark.parametrize( + "kwargs", + [ + {"q_low": 0.5, "q_high": 1, "axis": 1}, # test basic range + {"q_low": 0.5, "q_high": 1, "axis": None}, # test axis + {"q_low": 0, "q_high": 1, "subset": ["A"]}, # test subset + {"q_low": 0.5, "axis": 1}, # test no high + {"q_high": 1, "subset": ["A"], "axis": 0}, # test no low + {"q_low": 0.5, "axis": 1, "props": "background-color: yellow"}, # tst props + ], + ) + def test_highlight_quantile(self, kwargs): + expected = { + (0, 0): [("background-color", "yellow")], + (1, 0): [("background-color", "yellow")], + } + result = self.df.style.highlight_quantile(**kwargs)._compute().ctx + assert result == expected - # separate since we can't negate the strs - df["C"] = ["a", "b"] - result = df.style.highlight_max()._compute().ctx - expected = {(1, 1): css_seq} + @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_low": 0.6, "q_high": 0.8}), + ("highlight_range", {"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(self, f, kwargs, df): + if f == "highlight_quantile" and isinstance(df.iloc[0, 0], str): + return None # quantile incompatible with str + elif f == "highlight_range": + kwargs["start"] = df.iloc[1, 0] # set the range low for testing - result = df.style.highlight_min()._compute().ctx - expected = {(0, 0): css_seq} + expected = {(1, 0): [("background-color", "yellow")]} + result = getattr(df.style, f)(**kwargs)._compute().ctx + assert result == expected From 6b5bc67716c43a4f40a598c9c522214ae33e09af Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Tue, 23 Feb 2021 09:16:16 +0100 Subject: [PATCH 20/45] add examples and input validation --- doc/source/_static/style/hr_axNone.png | Bin 0 -> 7309 bytes doc/source/_static/style/hr_basic.png | Bin 0 -> 7504 bytes doc/source/_static/style/hr_props.png | Bin 0 -> 7776 bytes doc/source/_static/style/hr_seq.png | Bin 0 -> 7275 bytes pandas/io/formats/style.py | 52 +++++++++++++----- .../tests/io/formats/style/test_highlight.py | 17 ++++++ 6 files changed, 54 insertions(+), 15 deletions(-) create mode 100644 doc/source/_static/style/hr_axNone.png create mode 100644 doc/source/_static/style/hr_basic.png create mode 100644 doc/source/_static/style/hr_props.png create mode 100644 doc/source/_static/style/hr_seq.png diff --git a/doc/source/_static/style/hr_axNone.png b/doc/source/_static/style/hr_axNone.png new file mode 100644 index 0000000000000000000000000000000000000000..2918131b40bde1ccfb65010bc29fe83319a5e070 GIT binary patch literal 7309 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 literal 0 HcmV?d00001 diff --git a/doc/source/_static/style/hr_props.png b/doc/source/_static/style/hr_props.png new file mode 100644 index 0000000000000000000000000000000000000000..56bbe8479d564d34bbe896855f16f6d04aa4432d GIT binary patch literal 7776 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 R>p$PwWF?g(s>D78{x6GavOE9) literal 0 HcmV?d00001 diff --git a/doc/source/_static/style/hr_seq.png b/doc/source/_static/style/hr_seq.png new file mode 100644 index 0000000000000000000000000000000000000000..0fc3108a7968c04a13f858d246a1aa87a03f68eb GIT binary patch literal 7275 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 df = pd.DataFrame({'dates': pd.date_range(start='2021-01-01', periods=10)}) - >>> df.style.highlight_range(start=pd.to_datetime('2021-01-05')) + >>> df = pd.DataFrame({ + ... 'One': [1.2, 1.6, 1.5], + ... 'Two': [2.9, 2.1, 2.5], + ... 'Three': [3.1, 3.2, 3.8], + ... }) + >>> df.style.highlight_range(start=2.1, stop=2.9) - Basic usage + .. figure:: ../../_static/style/hr_basic.png - >>> df = pd.DataFrame([[1,2], [3,4]]) - >>> df.style.highlight_range(start=1, stop=3) + Using a range input sequnce along an ``axis``, in this case setting a ``start`` + and ``stop`` for each column individually - Using ``props`` instead of default background coloring + >>> df.style.highlight_range(start=[1.4, 2.4, 3.4], stop=[1.6, 2.6, 3.6], + ... axis=1, color="#fffd75") + + .. figure:: ../../_static/style/hr_seq.png + + Using ``axis=None`` and providing the ``start`` argument as an array that + matches the input DataFrame, with a constant ``stop`` - >>> df.style.highlight_range(start=1, stop=3, props='font-weight:bold;') + >>> df.style.highlight_range(start=[[2,2,3],[2,2,3],[3,3,3]], stop=3.5, + ... axis=None, color="#fffd75") - Using ``start`` and ``stop`` sequences or array-like objects with ``axis`` + .. figure:: ../../_static/style/hr_axNone.png - >>> df.style.highlight_range(start=[0.9, 2.9], stop=[1.1, 3.1], axis=0) - >>> df.style.highlight_range(start=[0.9, 2.9], stop=[1.1, 3.1], axis=1) - >>> df.style.highlight_range(start=pd.DataFrame([[1,2],[10,4]]), axis=None) + Using ``props`` instead of default background coloring + + >>> df.style.highlight_range(start=1.5, stop=3.5, + ... props='font-weight:bold;color:#e83e8c') + + .. figure:: ../../_static/style/hr_props.png """ def f( @@ -1843,12 +1857,20 @@ def f( d: Optional[Union[Scalar, Sequence]] = None, u: Optional[Union[Scalar, Sequence]] = None, ) -> np.ndarray: - def realign(x): + def realign(x, arg): if np.iterable(x) and not isinstance(x, str): - return np.asarray(x).reshape(data.shape) + try: + return np.asarray(x).reshape(data.shape) + except ValueError: + raise ValueError( + f"supplied '{arg}' is not right shape for " + "data over selected 'axis': got " + f"{np.asarray(x).shape}, expected " + f"{data.shape}" + ) return x - d, u = realign(d), realign(u) + d, u = realign(d, "start"), realign(u, "stop") ge_d = data >= d if d is not None else np.full_like(data, True, dtype=bool) le_u = data <= u if u is not None else np.full_like(data, True, dtype=bool) return np.where(ge_d & le_u, props, "") diff --git a/pandas/tests/io/formats/style/test_highlight.py b/pandas/tests/io/formats/style/test_highlight.py index ad677ddeb2024..91cc0b011b380 100644 --- a/pandas/tests/io/formats/style/test_highlight.py +++ b/pandas/tests/io/formats/style/test_highlight.py @@ -88,6 +88,23 @@ def test_highlight_range(self, kwargs): result = self.df.style.highlight_range(**kwargs)._compute().ctx assert result == expected + @pytest.mark.parametrize( + "arg, map, axis", + [ + ("start", [1, 2, 3], 0), + ("start", [1, 2], 1), + ("start", np.array([[1, 2], [1, 2]]), None), + ("stop", [1, 2, 3], 0), + ("stop", [1, 2], 1), + ("stop", np.array([[1, 2], [1, 2]]), None), + ], + ) + def test_highlight_range_raises(self, arg, map, axis): + df = DataFrame([[1, 2, 3], [1, 2, 3]]) + msg = f"supplied '{arg}' is not right shape" + with pytest.raises(ValueError, match=msg): + df.style.highlight_range(**{arg: map, "axis": axis})._compute() + @pytest.mark.parametrize( "kwargs", [ From c9d70ed044049c18958cdabae2ab250a3e1f084d Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Tue, 23 Feb 2021 09:54:07 +0100 Subject: [PATCH 21/45] add examples and input validation --- 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 pandas/io/formats/style.py | 24 +++++++++++++++++++----- 4 files changed, 19 insertions(+), 5 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 diff --git a/pandas/io/formats/style.py b/pandas/io/formats/style.py index 5e21797e88102..f0bd64205d177 100644 --- a/pandas/io/formats/style.py +++ b/pandas/io/formats/style.py @@ -1935,11 +1935,25 @@ def highlight_quantile( Examples -------- - >>> df = pd.DataFrame(np.arange(1,100).reshape(10,10)+1) - >>> df.style.highlight_quantile(q_low=0.5, q_high=0.7, axis=None) - >>> df.style.highlight_quantile(q_low=0.5, q_high=0.7, axis=0) - >>> df.style.highlight_quantile(q_low=0.5, q_high=0.7, axis=1) - >>> df.style.highlight_quantile(q_low=0.5, props='font-weight:bold;color:red') + 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_low=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_low=0.8, color="#fffd75") + + .. figure:: ../../_static/style/hq_ax1.png + + Use ``props`` instead of default background coloring + + >>> df.style.highlight_quantile(axis=None, q_low=0.2, q_high=0.8, + ... props='font-weight:bold;color:#e83e8c') + + .. figure:: ../../_static/style/hq_props.png """ def f( From 1aefc6beddf9d9d12505d52ac76abfb8402251b6 Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (MBP)" Date: Thu, 4 Mar 2021 19:35:16 +0100 Subject: [PATCH 22/45] name change: highlight_range to highlight_between --- doc/source/reference/style.rst | 2 +- doc/source/whatsnew/v1.3.0.rst | 2 +- pandas/io/formats/style.py | 111 ++++++++++++------ .../tests/io/formats/style/test_highlight.py | 65 ++++++---- 4 files changed, 119 insertions(+), 61 deletions(-) diff --git a/doc/source/reference/style.rst b/doc/source/reference/style.rst index 41bd1f0692ef5..5c81b68e87a8b 100644 --- a/doc/source/reference/style.rst +++ b/doc/source/reference/style.rst @@ -56,7 +56,7 @@ Builtin styles Styler.highlight_null Styler.highlight_max Styler.highlight_min - Styler.highlight_range + Styler.highlight_between Styler.highlight_quantile Styler.background_gradient Styler.bar diff --git a/doc/source/whatsnew/v1.3.0.rst b/doc/source/whatsnew/v1.3.0.rst index f90596d107e2d..51c71f5dce519 100644 --- a/doc/source/whatsnew/v1.3.0.rst +++ b/doc/source/whatsnew/v1.3.0.rst @@ -134,7 +134,7 @@ Other enhancements - :meth:`.Styler.set_tooltips_class` and :meth:`.Styler.set_table_styles` amended to optionally allow certain css-string input arguments (:issue:`39564`) - :meth:`.Styler.apply` now more consistently accepts ndarray function returns, i.e. in all cases for ``axis`` is ``0, 1 or None`` (:issue:`39359`) - :meth:`.Styler.apply` and :meth:`.Styler.applymap` now raise errors if wrong format CSS is passed on render (:issue:`39660`) -- :meth:`.Styler.highlight_range` and :meth:`.Styler.highlight_quantile` added to list of builtin styling methods (:issue:`39821`) +- :meth:`.Styler.highlight_between` and :meth:`.Styler.highlight_quantile` added to list of builtin styling methods (:issue:`39821`) - :meth:`Series.loc.__getitem__` and :meth:`Series.loc.__setitem__` with :class:`MultiIndex` now raising helpful error message when indexer has too many dimensions (:issue:`35349`) - :meth:`pandas.read_stata` and :class:`StataReader` support reading data from compressed files. - Add support for parsing ``ISO 8601``-like timestamps with negative signs to :meth:`pandas.Timedelta` (:issue:`37172`) diff --git a/pandas/io/formats/style.py b/pandas/io/formats/style.py index 88bc46016174e..41f69138efa3d 100644 --- a/pandas/io/formats/style.py +++ b/pandas/io/formats/style.py @@ -1649,11 +1649,10 @@ def highlight_null( 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_quantile: Highlight values defined by a quantile with a style. - Styler.highlight_range: Highlight a defined range with a style. + Styler.highlight_between: Highlight a defined range with a style. Notes ----- @@ -1700,10 +1699,9 @@ def highlight_max( 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_quantile: Highlight values defined by a quantile with a style. - Styler.highlight_range: Highlight a defined range with a style. + Styler.highlight_between: Highlight a defined range with a style. """ def f(data: FrameOrSeries, props: str) -> np.ndarray: @@ -1747,9 +1745,8 @@ def highlight_min( -------- 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. - Styler.highlight_range: Highlight a defined range with a style. + Styler.highlight_between: Highlight a defined range with a style. """ def f(data: FrameOrSeries, props: str) -> np.ndarray: @@ -1759,13 +1756,14 @@ def f(data: FrameOrSeries, props: str) -> np.ndarray: props = f"background-color: {color};" return self.apply(f, axis=axis, subset=subset, props=props) - def highlight_range( + def highlight_between( self, subset: Optional[IndexLabel] = None, color: str = "yellow", axis: Optional[Axis] = 0, - start: Optional[Union[Scalar, Sequence]] = None, - stop: Optional[Union[Scalar, Sequence]] = None, + left: Optional[Union[Scalar, Sequence]] = None, + right: Optional[Union[Scalar, Sequence]] = None, + inclusive: Union[bool, str] = True, props: Optional[str] = None, ) -> Styler: """ @@ -1783,10 +1781,13 @@ def highlight_range( Apply to each column (``axis=0`` or ``'index'``), to each row (``axis=1`` or ``'columns'``), or to the entire DataFrame at once with ``axis=None``. - start : scalar or datetime-like, or sequence or array-like, default None - Left bound for defining the range (inclusive). - stop : scalar or datetime-like, or sequence or array-like, default None - Right bound for defining the range (inclusive) + left : scalar or datetime-like, or sequence or array-like, default None + Left bound for defining the range. + right : scalar or datetime-like, or sequence or array-like, default None + Right bound for defining the range. + inclusive : str or bool, default True + Indicate which bounds to include, values allowed: True, False, 'right', + 'left' props : str, default None CSS properties to use for highlighting. If ``props`` is given, ``color`` is not used. @@ -1801,20 +1802,19 @@ def highlight_range( 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. - Styler.highlight_range: Highlight a defined range with a style. Notes ----- - If ``start`` is ``None`` only the right bound is applied. - If ``stop`` is ``None`` only the left bound is applied. If both are ``None`` + If ``left`` is ``None`` only the right bound is applied. + If ``right`` is ``None`` only the left bound is applied. If both are ``None`` all values are highlighted. - ``axis`` is only needed if ``start`` or ``stop`` are provide as a sequence or - an array-like object for aligning the shapes. If ``start`` and ``stop`` are + ``axis`` is only needed if ``left`` or ``right`` are provided as a sequence or + an array-like object for aligning the shapes. If ``left`` and ``right`` are both scalars then all ``axis`` inputs will give the same result. This function only works with compatible ``dtypes``. For example a datetime-like - region can only use equivalent datetime-like ``start`` and ``stop`` arguments. + region can only use equivalent datetime-like ``left`` and ``right`` arguments. Use ``subset`` to control regions which have multiple ``dtypes``. Examples @@ -1826,29 +1826,29 @@ def highlight_range( ... 'Two': [2.9, 2.1, 2.5], ... 'Three': [3.1, 3.2, 3.8], ... }) - >>> df.style.highlight_range(start=2.1, stop=2.9) + >>> df.style.highlight_between(left=2.1, right=2.9) .. figure:: ../../_static/style/hr_basic.png - Using a range input sequnce along an ``axis``, in this case setting a ``start`` - and ``stop`` for each column individually + Using a range input sequnce along an ``axis``, in this case setting a ``left`` + and ``right`` for each column individually - >>> df.style.highlight_range(start=[1.4, 2.4, 3.4], stop=[1.6, 2.6, 3.6], + >>> df.style.highlight_between(left=[1.4, 2.4, 3.4], right=[1.6, 2.6, 3.6], ... axis=1, color="#fffd75") .. figure:: ../../_static/style/hr_seq.png - Using ``axis=None`` and providing the ``start`` argument as an array that - matches the input DataFrame, with a constant ``stop`` + Using ``axis=None`` and providing the ``left`` argument as an array that + matches the input DataFrame, with a constant ``right`` - >>> df.style.highlight_range(start=[[2,2,3],[2,2,3],[3,3,3]], stop=3.5, + >>> df.style.highlight_between(left=[[2,2,3],[2,2,3],[3,3,3]], right=3.5, ... axis=None, color="#fffd75") .. figure:: ../../_static/style/hr_axNone.png Using ``props`` instead of default background coloring - >>> df.style.highlight_range(start=1.5, stop=3.5, + >>> df.style.highlight_between(left=1.5, right=3.5, ... props='font-weight:bold;color:#e83e8c') .. figure:: ../../_static/style/hr_props.png @@ -1857,8 +1857,9 @@ def highlight_range( def f( data: DataFrame, props: str, - d: Optional[Union[Scalar, Sequence]] = None, - u: Optional[Union[Scalar, Sequence]] = None, + left: Optional[Union[Scalar, Sequence]] = None, + right: Optional[Union[Scalar, Sequence]] = None, + inclusive: Union[bool, str] = True, ) -> np.ndarray: def realign(x, arg): if np.iterable(x) and not isinstance(x, str): @@ -1866,21 +1867,58 @@ def realign(x, arg): return np.asarray(x).reshape(data.shape) except ValueError: raise ValueError( - f"supplied '{arg}' is not right shape for " + f"supplied '{arg}' is not correct shape for " "data over selected 'axis': got " f"{np.asarray(x).shape}, expected " f"{data.shape}" ) return x - d, u = realign(d, "start"), realign(u, "stop") - ge_d = data >= d if d is not None else np.full_like(data, True, dtype=bool) - le_u = data <= u if u is not None else np.full_like(data, True, dtype=bool) - return np.where(ge_d & le_u, props, "") + left, right = realign(left, "left"), realign(right, "right") + + # get ops with correct boundary attribution + if isinstance(inclusive, str): + if inclusive == "left": + ops = ("__ge__", "__lt__") + elif inclusive == "right": + ops = ("__gt__", "__le__") + else: + raise ValueError( + f"'inclusive' as string must be 'left' or 'right', got " + f"{inclusive}" + ) + elif inclusive is True: + ops = ("__ge__", "__le__") + elif inclusive is False: + ops = ("__gt__", "__lt__") + else: + raise ValueError( + f"'inclusive' must be boolean or string, got {type(inclusive)}" + ) + + g_left = ( + getattr(data, ops[0])(left) + if left is not None + else np.full_like(data, True, dtype=bool) + ) + l_right = ( + getattr(data, ops[1])(right) + if right is not None + else np.full_like(data, True, dtype=bool) + ) + return np.where(g_left & l_right, props, "") if props is None: props = f"background-color: {color};" - return self.apply(f, axis=axis, subset=subset, props=props, d=start, u=stop) + return self.apply( + f, + axis=axis, + subset=subset, + props=props, + left=left, + right=right, + inclusive=inclusive, + ) def highlight_quantile( self, @@ -1924,8 +1962,7 @@ def highlight_quantile( 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. - Styler.highlight_range: Highlight a defined range with a style. + Styler.highlight_between: Highlight a defined range with a style. Notes ----- diff --git a/pandas/tests/io/formats/style/test_highlight.py b/pandas/tests/io/formats/style/test_highlight.py index 91cc0b011b380..86910bbf2bf56 100644 --- a/pandas/tests/io/formats/style/test_highlight.py +++ b/pandas/tests/io/formats/style/test_highlight.py @@ -70,40 +70,61 @@ def test_highlight_minmax_ext(self, f, kwargs): @pytest.mark.parametrize( "kwargs", [ - {"start": 0, "stop": 1}, # test basic range - {"start": 0, "stop": 1, "props": "background-color: yellow"}, # test props - {"start": -9, "stop": 9, "subset": ["A"]}, # test subset effective - {"start": 0}, # test no stop - {"stop": 1, "subset": ["A"]}, # test no start - {"start": [0, 1], "axis": 0}, # test start as sequence - {"start": DataFrame([[0, 1], [1, 1]]), "axis": None}, # test axis with seq - {"start": 0, "stop": [0, 1], "axis": 0}, # test sequence stop + {"left": 0, "right": 1}, # test basic range + {"left": 0, "right": 1, "props": "background-color: yellow"}, # test props + {"left": -9, "right": 9, "subset": ["A"]}, # test subset effective + {"left": 0}, # test no right + {"right": 1, "subset": ["A"]}, # test no left + {"left": [0, 1], "axis": 0}, # test left as sequence + {"left": DataFrame([[0, 1], [1, 1]]), "axis": None}, # test axis with seq + {"left": 0, "right": [0, 1], "axis": 0}, # test sequence right ], ) - def test_highlight_range(self, kwargs): + def test_highlight_between(self, kwargs): expected = { (0, 0): [("background-color", "yellow")], (1, 0): [("background-color", "yellow")], } - result = self.df.style.highlight_range(**kwargs)._compute().ctx + result = self.df.style.highlight_between(**kwargs)._compute().ctx assert result == expected @pytest.mark.parametrize( "arg, map, axis", [ - ("start", [1, 2, 3], 0), - ("start", [1, 2], 1), - ("start", np.array([[1, 2], [1, 2]]), None), - ("stop", [1, 2, 3], 0), - ("stop", [1, 2], 1), - ("stop", np.array([[1, 2], [1, 2]]), None), + ("left", [1, 2, 3], 0), + ("left", [1, 2], 1), + ("left", np.array([[1, 2], [1, 2]]), None), + ("right", [1, 2, 3], 0), + ("right", [1, 2], 1), + ("right", np.array([[1, 2], [1, 2]]), None), ], ) - def test_highlight_range_raises(self, arg, map, axis): + def test_highlight_between_raises(self, arg, map, axis): df = DataFrame([[1, 2, 3], [1, 2, 3]]) - msg = f"supplied '{arg}' is not right shape" + msg = f"supplied '{arg}' is not correct shape" with pytest.raises(ValueError, match=msg): - df.style.highlight_range(**{arg: map, "axis": axis})._compute() + df.style.highlight_between(**{arg: map, "axis": axis})._compute() + + def test_highlight_between_raises2(self): + with pytest.raises(ValueError, match="as string must be 'left' or 'right'"): + self.df.style.highlight_between(inclusive="badstring")._compute() + + with pytest.raises(ValueError, match="'inclusive' must be boolean or string"): + self.df.style.highlight_between(inclusive=1)._compute() + + def test_highlight_between_inclusive(self): + kwargs = {"left": 0, "right": 1, "subset": ["A"]} + result = self.df.style.highlight_between(**kwargs, inclusive=True)._compute() + assert result.ctx == { + (0, 0): [("background-color", "yellow")], + (1, 0): [("background-color", "yellow")], + } + result = self.df.style.highlight_between(**kwargs, inclusive=False)._compute() + assert result.ctx == {} + result = self.df.style.highlight_between(**kwargs, inclusive="left")._compute() + assert result.ctx == {(0, 0): [("background-color", "yellow")]} + result = self.df.style.highlight_between(**kwargs, inclusive="right")._compute() + assert result.ctx == {(1, 0): [("background-color", "yellow")]} @pytest.mark.parametrize( "kwargs", @@ -133,7 +154,7 @@ def test_highlight_quantile(self, kwargs): ("highlight_min", {"axis": 1, "subset": IndexSlice[1, :]}), ("highlight_max", {"axis": 0, "subset": [0]}), ("highlight_quantile", {"axis": None, "q_low": 0.6, "q_high": 0.8}), - ("highlight_range", {"subset": [0]}), + ("highlight_between", {"subset": [0]}), ], ) @pytest.mark.parametrize( @@ -149,8 +170,8 @@ def test_highlight_quantile(self, kwargs): def test_all_highlight_dtypes(self, f, kwargs, df): if f == "highlight_quantile" and isinstance(df.iloc[0, 0], str): return None # quantile incompatible with str - elif f == "highlight_range": - kwargs["start"] = df.iloc[1, 0] # set the range low for testing + 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 From 7f8d335290badb83149be8932f0048621932cfd8 Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (MBP)" Date: Fri, 5 Mar 2021 08:33:53 +0100 Subject: [PATCH 23/45] split PR --- doc/source/_static/style/hq_ax1.png | Bin 6092 -> 0 bytes doc/source/_static/style/hq_axNone.png | Bin 6102 -> 0 bytes doc/source/_static/style/hq_props.png | Bin 6241 -> 0 bytes pandas/io/formats/style.py | 103 ------------------ .../tests/io/formats/style/test_highlight.py | 56 +--------- 5 files changed, 1 insertion(+), 158 deletions(-) delete mode 100644 doc/source/_static/style/hq_ax1.png delete mode 100644 doc/source/_static/style/hq_axNone.png delete 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 deleted file mode 100644 index 95d840b7c8f99beca00098cc27e2b30760478884..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6092 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 diff --git a/doc/source/_static/style/hq_axNone.png b/doc/source/_static/style/hq_axNone.png deleted file mode 100644 index 40a33b194e640de84ed8cdf6e54e039c4cbf7cc3..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6102 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 aUQ3Cv6jm7Udw2YGx>1mO1F4XK1^pk84RLD# diff --git a/doc/source/_static/style/hq_props.png b/doc/source/_static/style/hq_props.png deleted file mode 100644 index 1f117490966907b6f8c7ecd215a3453fb09a5496..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6241 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 X*r<(xYQDg~H(H9aYS1!i)8PLDRAaj^ diff --git a/pandas/io/formats/style.py b/pandas/io/formats/style.py index 41f69138efa3d..9482e9fc9ccae 100644 --- a/pandas/io/formats/style.py +++ b/pandas/io/formats/style.py @@ -1920,109 +1920,6 @@ def realign(x, arg): inclusive=inclusive, ) - def highlight_quantile( - self, - subset: Optional[IndexLabel] = None, - color: str = "yellow", - q_low: float = 0.0, - q_high: float = 1.0, - axis: Optional[Axis] = 0, - props: Optional[str] = None, - ) -> Styler: - """ - Highlight values defined by a quantile with a style. - - .. versionadded:: 1.3.0 - - 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 - q_low : float, default 0 - Left bound, in [0, q_high), for the target quantile range (exclusive if not - 0). - q_high : float, default 1 - Right bound, in (q_low, 1], for the target quantile range (inclusive) - axis : {0 or 'index', 1 or 'columns', None}, default 0 - Apply to each column (``axis=0`` or ``'index'``), to each row - (``axis=1`` or ``'columns'``), or to the entire DataFrame at once - with ``axis=None``. - 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 only works with consistent ``dtypes`` within the ``subset`` or - ``axis``. For example a mixture of datetime-like and float items will raise - errors. - - This method uses ``pandas.qcut`` to implement the quantile labelling of data - values. - - 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_low=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_low=0.8, color="#fffd75") - - .. figure:: ../../_static/style/hq_ax1.png - - Use ``props`` instead of default background coloring - - >>> df.style.highlight_quantile(axis=None, q_low=0.2, q_high=0.8, - ... props='font-weight:bold;color:#e83e8c') - - .. figure:: ../../_static/style/hq_props.png - """ - - def f( - data: FrameOrSeries, - props: str, - q_low: float = 0, - q_high: float = 1, - axis_: Optional[Axis] = 0, - ): - if axis_ is None: - labels = pd.qcut( - data.to_numpy().ravel(), q=[q_low, q_high], labels=False - ).reshape(data.to_numpy().shape) - else: - labels = pd.qcut(data, q=[q_low, q_high], labels=False) - return np.where(labels == 0, props, "") - - if props is None: - props = f"background-color: {color};" - return self.apply( - f, - axis=axis, - subset=subset, - props=props, - q_low=q_low, - q_high=q_high, - axis_=axis, - ) - @classmethod def from_custom_template(cls, searchpath, name): """ diff --git a/pandas/tests/io/formats/style/test_highlight.py b/pandas/tests/io/formats/style/test_highlight.py index 86910bbf2bf56..c2e34e359240c 100644 --- a/pandas/tests/io/formats/style/test_highlight.py +++ b/pandas/tests/io/formats/style/test_highlight.py @@ -1,10 +1,7 @@ import numpy as np import pytest -from pandas import ( - DataFrame, - IndexSlice, -) +from pandas import DataFrame pytest.importorskip("jinja2") @@ -125,54 +122,3 @@ def test_highlight_between_inclusive(self): assert result.ctx == {(0, 0): [("background-color", "yellow")]} result = self.df.style.highlight_between(**kwargs, inclusive="right")._compute() assert result.ctx == {(1, 0): [("background-color", "yellow")]} - - @pytest.mark.parametrize( - "kwargs", - [ - {"q_low": 0.5, "q_high": 1, "axis": 1}, # test basic range - {"q_low": 0.5, "q_high": 1, "axis": None}, # test axis - {"q_low": 0, "q_high": 1, "subset": ["A"]}, # test subset - {"q_low": 0.5, "axis": 1}, # test no high - {"q_high": 1, "subset": ["A"], "axis": 0}, # test no low - {"q_low": 0.5, "axis": 1, "props": "background-color: yellow"}, # tst props - ], - ) - def test_highlight_quantile(self, kwargs): - expected = { - (0, 0): [("background-color", "yellow")], - (1, 0): [("background-color", "yellow")], - } - result = self.df.style.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_low": 0.6, "q_high": 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(self, f, kwargs, df): - if f == "highlight_quantile" and isinstance(df.iloc[0, 0], str): - return None # quantile incompatible with str - 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 e4abcbe6fd7fe207f6520d0eb59be165968a42cf Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (MBP)" Date: Fri, 5 Mar 2021 08:35:43 +0100 Subject: [PATCH 24/45] split PR --- .../tests/io/formats/style/test_highlight.py | 68 +++++++++++-------- 1 file changed, 39 insertions(+), 29 deletions(-) diff --git a/pandas/tests/io/formats/style/test_highlight.py b/pandas/tests/io/formats/style/test_highlight.py index c2e34e359240c..1222ffe293088 100644 --- a/pandas/tests/io/formats/style/test_highlight.py +++ b/pandas/tests/io/formats/style/test_highlight.py @@ -33,36 +33,46 @@ def test_highlight_null_subset(self): } assert result == expected - @pytest.mark.parametrize("f", ["highlight_min", "highlight_max"]) - def test_highlight_minmax_basic(self, f): - expected = { - (0, 0): [("background-color", "red")], - (1, 0): [("background-color", "red")], - } - if f == "highlight_min": - df = -self.df - else: - df = self.df - result = getattr(df.style, f)(axis=1, color="red")._compute().ctx - assert result == expected + def test_highlight_max(self): + df = DataFrame([[1, 2], [3, 4]], columns=["A", "B"]) + css_seq = [("background-color", "yellow")] + # max(df) = min(-df) + for max_ in [True, False]: + if max_: + attr = "highlight_max" + else: + df = -df + attr = "highlight_min" + result = getattr(df.style, attr)()._compute().ctx + assert result[(1, 1)] == css_seq - @pytest.mark.parametrize("f", ["highlight_min", "highlight_max"]) - @pytest.mark.parametrize( - "kwargs", - [ - {"axis": None, "color": "red"}, # test axis - {"axis": 0, "subset": ["A"], "color": "red"}, # test subset - {"axis": None, "props": "background-color: red"}, # test props - ], - ) - def test_highlight_minmax_ext(self, f, kwargs): - expected = {(1, 0): [("background-color", "red")]} - if f == "highlight_min": - df = -self.df - else: - df = self.df - result = getattr(df.style, f)(**kwargs)._compute().ctx - assert result == expected + result = getattr(df.style, attr)(color="green")._compute().ctx + assert result[(1, 1)] == [("background-color", "green")] + + result = getattr(df.style, attr)(subset="A")._compute().ctx + assert result[(1, 0)] == css_seq + + result = getattr(df.style, attr)(axis=0)._compute().ctx + expected = { + (1, 0): css_seq, + (1, 1): css_seq, + } + assert result == expected + + result = getattr(df.style, attr)(axis=1)._compute().ctx + expected = { + (0, 1): css_seq, + (1, 1): css_seq, + } + assert result == expected + + # separate since we can't negate the strs + df["C"] = ["a", "b"] + result = df.style.highlight_max()._compute().ctx + expected = {(1, 1): css_seq} + + result = df.style.highlight_min()._compute().ctx + expected = {(0, 0): css_seq} @pytest.mark.parametrize( "kwargs", From 352216067a35e22ee2840e8dce4446f9cb62be8c Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (MBP)" Date: Fri, 5 Mar 2021 08:37:04 +0100 Subject: [PATCH 25/45] split PR --- doc/source/reference/style.rst | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/doc/source/reference/style.rst b/doc/source/reference/style.rst index 5c81b68e87a8b..a54e024ae0cdf 100644 --- a/doc/source/reference/style.rst +++ b/doc/source/reference/style.rst @@ -53,11 +53,10 @@ Builtin styles .. autosummary:: :toctree: api/ - Styler.highlight_null Styler.highlight_max Styler.highlight_min + Styler.highlight_null Styler.highlight_between - Styler.highlight_quantile Styler.background_gradient Styler.bar From ed4a7584df65a0f8de14ea9b5f59a2ae1f87055d Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (MBP)" Date: Fri, 5 Mar 2021 08:41:00 +0100 Subject: [PATCH 26/45] split PR --- pandas/io/formats/style.py | 78 ++++++++------------------------------ 1 file changed, 16 insertions(+), 62 deletions(-) diff --git a/pandas/io/formats/style.py b/pandas/io/formats/style.py index 9482e9fc9ccae..ac10c176c0a5c 100644 --- a/pandas/io/formats/style.py +++ b/pandas/io/formats/style.py @@ -929,7 +929,7 @@ def apply( Examples -------- >>> def highlight_max(x, color): - ... return np.where(x == np.nanmax(x.to_numpy()), f"color: {color};", None) + ... return np.where(x == np.nanmax(x.values), f"color: {color};", None) >>> df = pd.DataFrame(np.random.randn(5, 2)) >>> df.style.apply(highlight_max, color='red') >>> df.style.apply(highlight_max, color='blue', axis=1) @@ -1624,10 +1624,9 @@ def highlight_null( self, null_color: str = "red", subset: Optional[IndexLabel] = None, - props: Optional[str] = None, ) -> Styler: """ - Highlight missing values with a style. + Shade the background ``null_color`` for missing values. Parameters ---------- @@ -1637,124 +1636,79 @@ def highlight_null( .. versionadded:: 1.1.0 - props : str, default None - CSS properties to use for highlighting. If ``props`` is given, ``color`` - is not used. - - .. versionadded:: 1.3.0 - Returns ------- self : Styler - - See Also - -------- - 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. - Styler.highlight_between: Highlight a defined range with a style. - - Notes - ----- - Uses ``pandas.isna()`` to detect the missing values. """ def f(data: DataFrame, props: str) -> np.ndarray: - return np.where(pd.isna(data).to_numpy(), props, "") + return np.where(pd.isna(data).values, props, "") - if props is None: - props = f"background-color: {null_color};" - return self.apply(f, axis=None, subset=subset, props=props) + return self.apply( + f, axis=None, subset=subset, props=f"background-color: {null_color};" + ) def highlight_max( self, subset: Optional[IndexLabel] = None, color: str = "yellow", axis: Optional[Axis] = 0, - props: Optional[str] = None, ) -> Styler: """ - Highlight the maximum with a style. + Highlight the maximum by shading the background. 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 Apply to each column (``axis=0`` or ``'index'``), to each row (``axis=1`` or ``'columns'``), or to the entire DataFrame at once with ``axis=None``. - props : str, default None - CSS properties to use for highlighting. If ``props`` is given, ``color`` - is not used. - - .. versionadded:: 1.3.0 Returns ------- self : Styler - - See Also - -------- - Styler.highlight_null: Highlight missing values with a style. - Styler.highlight_min: Highlight the minimum with a style. - Styler.highlight_quantile: Highlight values defined by a quantile with a style. - Styler.highlight_between: Highlight a defined range with a style. """ def f(data: FrameOrSeries, props: str) -> np.ndarray: - return np.where(data == np.nanmax(data.to_numpy()), props, "") + return np.where(data == np.nanmax(data.values), props, "") - if props is None: - props = f"background-color: {color};" - return self.apply(f, axis=axis, subset=subset, props=props) + return self.apply( + f, axis=axis, subset=subset, props=f"background-color: {color};" + ) def highlight_min( self, subset: Optional[IndexLabel] = None, color: str = "yellow", axis: Optional[Axis] = 0, - props: Optional[str] = None, ) -> Styler: """ - Highlight the minimum with a style. + Highlight the minimum by shading the background. 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 Apply to each column (``axis=0`` or ``'index'``), to each row (``axis=1`` or ``'columns'``), or to the entire DataFrame at once with ``axis=None``. - props : str, default None - CSS properties to use for highlighting. If ``props`` is given, ``color`` - is not used. - - .. versionadded:: 1.3.0 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_quantile: Highlight values defined by a quantile with a style. - Styler.highlight_between: Highlight a defined range with a style. """ def f(data: FrameOrSeries, props: str) -> np.ndarray: - return np.where(data == np.nanmin(data.to_numpy()), props, "") + return np.where(data == np.nanmin(data.values), props, "") - if props is None: - props = f"background-color: {color};" - return self.apply(f, axis=axis, subset=subset, props=props) + return self.apply( + f, axis=axis, subset=subset, props=f"background-color: {color};" + ) def highlight_between( self, From 5e982c728d64d3acbb851e62c79a5bc576db6500 Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (MBP)" Date: Fri, 5 Mar 2021 08:42:09 +0100 Subject: [PATCH 27/45] split PR --- doc/source/whatsnew/v1.3.0.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/source/whatsnew/v1.3.0.rst b/doc/source/whatsnew/v1.3.0.rst index 51c71f5dce519..601a3e57edcff 100644 --- a/doc/source/whatsnew/v1.3.0.rst +++ b/doc/source/whatsnew/v1.3.0.rst @@ -134,7 +134,7 @@ Other enhancements - :meth:`.Styler.set_tooltips_class` and :meth:`.Styler.set_table_styles` amended to optionally allow certain css-string input arguments (:issue:`39564`) - :meth:`.Styler.apply` now more consistently accepts ndarray function returns, i.e. in all cases for ``axis`` is ``0, 1 or None`` (:issue:`39359`) - :meth:`.Styler.apply` and :meth:`.Styler.applymap` now raise errors if wrong format CSS is passed on render (:issue:`39660`) -- :meth:`.Styler.highlight_between` and :meth:`.Styler.highlight_quantile` added to list of builtin styling methods (:issue:`39821`) +- :meth:`.Styler.highlight_between` added to list of builtin styling methods (:issue:`39821`) - :meth:`Series.loc.__getitem__` and :meth:`Series.loc.__setitem__` with :class:`MultiIndex` now raising helpful error message when indexer has too many dimensions (:issue:`35349`) - :meth:`pandas.read_stata` and :class:`StataReader` support reading data from compressed files. - Add support for parsing ``ISO 8601``-like timestamps with negative signs to :meth:`pandas.Timedelta` (:issue:`37172`) From cc4148b1cacf4615526758a24f2f340787f01be4 Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (MBP)" Date: Fri, 5 Mar 2021 08:59:56 +0100 Subject: [PATCH 28/45] split PR --- pandas/io/formats/style.py | 30 ++++++++----------- .../tests/io/formats/style/test_highlight.py | 14 +++++++-- 2 files changed, 25 insertions(+), 19 deletions(-) diff --git a/pandas/io/formats/style.py b/pandas/io/formats/style.py index ac10c176c0a5c..15c0761166641 100644 --- a/pandas/io/formats/style.py +++ b/pandas/io/formats/style.py @@ -1740,8 +1740,8 @@ def highlight_between( right : scalar or datetime-like, or sequence or array-like, default None Right bound for defining the range. inclusive : str or bool, default True - Indicate which bounds to include, values allowed: True, False, 'right', - 'left' + Indicate which bounds to include, values allowed: True or 'both', False or + 'neither', 'right' or 'left'. props : str, default None CSS properties to use for highlighting. If ``props`` is given, ``color`` is not used. @@ -1815,7 +1815,8 @@ def f( right: Optional[Union[Scalar, Sequence]] = None, inclusive: Union[bool, str] = True, ) -> np.ndarray: - def realign(x, arg): + def reshape(x, arg): + """if x is sequence check it fits axis, otherwise raise""" if np.iterable(x) and not isinstance(x, str): try: return np.asarray(x).reshape(data.shape) @@ -1828,26 +1829,21 @@ def realign(x, arg): ) return x - left, right = realign(left, "left"), realign(right, "right") + left, right = reshape(left, "left"), reshape(right, "right") # get ops with correct boundary attribution - if isinstance(inclusive, str): - if inclusive == "left": - ops = ("__ge__", "__lt__") - elif inclusive == "right": - ops = ("__gt__", "__le__") - else: - raise ValueError( - f"'inclusive' as string must be 'left' or 'right', got " - f"{inclusive}" - ) - elif inclusive is True: + if inclusive == "both" or inclusive is True: ops = ("__ge__", "__le__") - elif inclusive is False: + elif inclusive == "neither" or inclusive is False: ops = ("__gt__", "__lt__") + elif inclusive == "left": + ops = ("__ge__", "__lt__") + elif inclusive == "right": + ops = ("__gt__", "__le__") else: raise ValueError( - f"'inclusive' must be boolean or string, got {type(inclusive)}" + f"'inclusive' values can be 'both', 'left', 'right', 'neither' " + f"or bool, got {inclusive}" ) g_left = ( diff --git a/pandas/tests/io/formats/style/test_highlight.py b/pandas/tests/io/formats/style/test_highlight.py index 1222ffe293088..298e579f162e2 100644 --- a/pandas/tests/io/formats/style/test_highlight.py +++ b/pandas/tests/io/formats/style/test_highlight.py @@ -113,10 +113,11 @@ def test_highlight_between_raises(self, arg, map, axis): df.style.highlight_between(**{arg: map, "axis": axis})._compute() def test_highlight_between_raises2(self): - with pytest.raises(ValueError, match="as string must be 'left' or 'right'"): + msg = "values can be 'both', 'left', 'right', 'neither' or bool" + with pytest.raises(ValueError, match=msg): self.df.style.highlight_between(inclusive="badstring")._compute() - with pytest.raises(ValueError, match="'inclusive' must be boolean or string"): + with pytest.raises(ValueError, match=msg): self.df.style.highlight_between(inclusive=1)._compute() def test_highlight_between_inclusive(self): @@ -128,6 +129,15 @@ def test_highlight_between_inclusive(self): } result = self.df.style.highlight_between(**kwargs, inclusive=False)._compute() assert result.ctx == {} + result = self.df.style.highlight_between(**kwargs, inclusive="both")._compute() + assert result.ctx == { + (0, 0): [("background-color", "yellow")], + (1, 0): [("background-color", "yellow")], + } + result = self.df.style.highlight_between( + **kwargs, inclusive="neither" + )._compute() + assert result.ctx == {} result = self.df.style.highlight_between(**kwargs, inclusive="left")._compute() assert result.ctx == {(0, 0): [("background-color", "yellow")]} result = self.df.style.highlight_between(**kwargs, inclusive="right")._compute() From 41c1c3855888df88ad711acd915461f9b90a105e Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (MBP)" Date: Fri, 5 Mar 2021 09:03:25 +0100 Subject: [PATCH 29/45] split PR --- pandas/tests/io/formats/style/test_highlight.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/pandas/tests/io/formats/style/test_highlight.py b/pandas/tests/io/formats/style/test_highlight.py index 298e579f162e2..f53b587ba4a96 100644 --- a/pandas/tests/io/formats/style/test_highlight.py +++ b/pandas/tests/io/formats/style/test_highlight.py @@ -98,12 +98,12 @@ def test_highlight_between(self, kwargs): @pytest.mark.parametrize( "arg, map, axis", [ - ("left", [1, 2, 3], 0), - ("left", [1, 2], 1), - ("left", np.array([[1, 2], [1, 2]]), None), - ("right", [1, 2, 3], 0), - ("right", [1, 2], 1), - ("right", np.array([[1, 2], [1, 2]]), None), + ("left", [1, 2, 3], 0), # 0 axis has 2 elements not 3 + ("left", [1, 2], 1), # 1 axis has 3 elements not 2 + ("left", np.array([[1, 2], [1, 2]]), None), # df is (2,3) not (2,2) + ("right", [1, 2, 3], 0), # same tests as above for 'right' not 'left' + ("right", [1, 2], 1), # .. + ("right", np.array([[1, 2], [1, 2]]), None), # .. ], ) def test_highlight_between_raises(self, arg, map, axis): From 0bd73237bb2e241da64f6262640d243b28282f1b Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (MBP)" Date: Fri, 5 Mar 2021 20:42:49 +0100 Subject: [PATCH 30/45] remove reference to highlight_quantile --- pandas/io/formats/style.py | 1 - 1 file changed, 1 deletion(-) diff --git a/pandas/io/formats/style.py b/pandas/io/formats/style.py index f8ab1264b9a15..7a44e17f291aa 100644 --- a/pandas/io/formats/style.py +++ b/pandas/io/formats/style.py @@ -1768,7 +1768,6 @@ 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 7c7a3373ac58ea63edd4ba4a23c94b73c1403a21 Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (MBP)" Date: Fri, 5 Mar 2021 23:55:17 +0100 Subject: [PATCH 31/45] link see alsos --- pandas/io/formats/style.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pandas/io/formats/style.py b/pandas/io/formats/style.py index a733ae4664644..40340e23d551d 100644 --- a/pandas/io/formats/style.py +++ b/pandas/io/formats/style.py @@ -1726,6 +1726,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. """ def f(data: DataFrame, props: str) -> np.ndarray: @@ -1769,6 +1770,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. """ def f(data: FrameOrSeries, props: str) -> np.ndarray: @@ -1812,6 +1814,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. """ def f(data: FrameOrSeries, props: str) -> np.ndarray: From f80d01223be58c8060bc613b2b6a829fc3da0f40 Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (MBP)" Date: Sat, 6 Mar 2021 07:41:52 +0100 Subject: [PATCH 32/45] doc edit --- pandas/io/formats/style.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/pandas/io/formats/style.py b/pandas/io/formats/style.py index 40340e23d551d..1edce3bc7de70 100644 --- a/pandas/io/formats/style.py +++ b/pandas/io/formats/style.py @@ -1846,9 +1846,8 @@ def highlight_between( color : str, default 'yellow' Background color to use for highlighting. axis : {0 or 'index', 1 or 'columns', None}, default 0 - Apply to each column (``axis=0`` or ``'index'``), to each row - (``axis=1`` or ``'columns'``), or to the entire DataFrame at once - with ``axis=None``. + If ``left`` or ``right`` given as sequence axis along which to apply those + boundaries. See examples. left : scalar or datetime-like, or sequence or array-like, default None Left bound for defining the range. right : scalar or datetime-like, or sequence or array-like, default None From 0228ab77f8c2fd0047cdad7e9665d94ec8dbf365 Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (MBP)" Date: Sat, 6 Mar 2021 13:34:46 +0100 Subject: [PATCH 33/45] doc edit --- pandas/io/formats/style.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/pandas/io/formats/style.py b/pandas/io/formats/style.py index 1edce3bc7de70..3c91028f9451a 100644 --- a/pandas/io/formats/style.py +++ b/pandas/io/formats/style.py @@ -1846,15 +1846,14 @@ def highlight_between( color : str, default 'yellow' Background color to use for highlighting. axis : {0 or 'index', 1 or 'columns', None}, default 0 - If ``left`` or ``right`` given as sequence axis along which to apply those + If ``left`` or ``right`` given as sequence, axis along which to apply those boundaries. See examples. left : scalar or datetime-like, or sequence or array-like, default None Left bound for defining the range. right : scalar or datetime-like, or sequence or array-like, default None Right bound for defining the range. - inclusive : str or bool, default True - Indicate which bounds to include, values allowed: True or 'both', False or - 'neither', 'right' or 'left'. + inclusive : {'both', 'neither', 'left', 'right'} or bool, default True + Identify whether bounds are closed or open. props : str, default None CSS properties to use for highlighting. If ``props`` is given, ``color`` is not used. From 472d32109702b40402b5b38027ed5be63ced2578 Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Wed, 10 Mar 2021 07:44:01 +0100 Subject: [PATCH 34/45] remove bool option from inclusive --- pandas/io/formats/style.py | 12 ++++++------ pandas/tests/io/formats/style/test_highlight.py | 9 +-------- 2 files changed, 7 insertions(+), 14 deletions(-) diff --git a/pandas/io/formats/style.py b/pandas/io/formats/style.py index 85afc24cb0a19..7858e9352d2f9 100644 --- a/pandas/io/formats/style.py +++ b/pandas/io/formats/style.py @@ -1807,7 +1807,7 @@ def highlight_between( axis: Optional[Axis] = 0, left: Optional[Union[Scalar, Sequence]] = None, right: Optional[Union[Scalar, Sequence]] = None, - inclusive: Union[bool, str] = True, + inclusive: Union[bool, str] = "both", props: Optional[str] = None, ) -> Styler: """ @@ -1828,7 +1828,7 @@ def highlight_between( Left bound for defining the range. right : scalar or datetime-like, or sequence or array-like, default None Right bound for defining the range. - inclusive : {'both', 'neither', 'left', 'right'} or bool, default True + inclusive : {'both', 'neither', 'left', 'right'} Identify whether bounds are closed or open. props : str, default None CSS properties to use for highlighting. If ``props`` is given, ``color`` @@ -1919,9 +1919,9 @@ def reshape(x, arg): left, right = reshape(left, "left"), reshape(right, "right") # get ops with correct boundary attribution - if inclusive == "both" or inclusive is True: + if inclusive == "both": ops = ("__ge__", "__le__") - elif inclusive == "neither" or inclusive is False: + elif inclusive == "neither": ops = ("__gt__", "__lt__") elif inclusive == "left": ops = ("__ge__", "__lt__") @@ -1929,8 +1929,8 @@ def reshape(x, arg): ops = ("__gt__", "__le__") else: raise ValueError( - f"'inclusive' values can be 'both', 'left', 'right', 'neither' " - f"or bool, got {inclusive}" + f"'inclusive' values can be 'both', 'left', 'right', or 'neither' " + f"got {inclusive}" ) g_left = ( diff --git a/pandas/tests/io/formats/style/test_highlight.py b/pandas/tests/io/formats/style/test_highlight.py index e26040e14663a..83c0cc1e48946 100644 --- a/pandas/tests/io/formats/style/test_highlight.py +++ b/pandas/tests/io/formats/style/test_highlight.py @@ -103,7 +103,7 @@ def test_highlight_between_raises(self, arg, map, axis): df.style.highlight_between(**{arg: map, "axis": axis})._compute() def test_highlight_between_raises2(self): - msg = "values can be 'both', 'left', 'right', 'neither' or bool" + msg = "values can be 'both', 'left', 'right', or 'neither'" with pytest.raises(ValueError, match=msg): self.df.style.highlight_between(inclusive="badstring")._compute() @@ -112,13 +112,6 @@ def test_highlight_between_raises2(self): def test_highlight_between_inclusive(self): kwargs = {"left": 0, "right": 1, "subset": ["A"]} - result = self.df.style.highlight_between(**kwargs, inclusive=True)._compute() - assert result.ctx == { - (0, 0): [("background-color", "yellow")], - (1, 0): [("background-color", "yellow")], - } - result = self.df.style.highlight_between(**kwargs, inclusive=False)._compute() - assert result.ctx == {} result = self.df.style.highlight_between(**kwargs, inclusive="both")._compute() assert result.ctx == { (0, 0): [("background-color", "yellow")], From 37b131f33c9f020e30bfe32708ada7724a68232f Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Wed, 10 Mar 2021 16:44:12 +0100 Subject: [PATCH 35/45] use operator --- pandas/io/formats/style.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/pandas/io/formats/style.py b/pandas/io/formats/style.py index ffdbf5ef58d51..dace13fe27ad5 100644 --- a/pandas/io/formats/style.py +++ b/pandas/io/formats/style.py @@ -7,6 +7,7 @@ from contextlib import contextmanager import copy from functools import partial +import operator from typing import ( Any, Callable, @@ -1920,13 +1921,13 @@ def reshape(x, arg): # get ops with correct boundary attribution if inclusive == "both": - ops = ("__ge__", "__le__") + ops = (operator.ge, operator.le) elif inclusive == "neither": - ops = ("__gt__", "__lt__") + ops = (operator.gt, operator.lt) elif inclusive == "left": - ops = ("__ge__", "__lt__") + ops = (operator.ge, operator.lt) elif inclusive == "right": - ops = ("__gt__", "__le__") + ops = (operator.gt, operator.le) else: raise ValueError( f"'inclusive' values can be 'both', 'left', 'right', or 'neither' " @@ -1934,12 +1935,12 @@ def reshape(x, arg): ) g_left = ( - getattr(data, ops[0])(left) + ops[0](data, left) if left is not None else np.full_like(data, True, dtype=bool) ) l_right = ( - getattr(data, ops[1])(right) + ops[1](data, right) if right is not None else np.full_like(data, True, dtype=bool) ) From 353376305bc77d17e9d7d08769e5b0d1cb9a059c Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Thu, 11 Mar 2021 15:07:40 +0100 Subject: [PATCH 36/45] remove misleading random note from test fixtures --- pandas/tests/io/formats/style/test_style.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/pandas/tests/io/formats/style/test_style.py b/pandas/tests/io/formats/style/test_style.py index ca453b55eae2e..ebe5c3e0c1879 100644 --- a/pandas/tests/io/formats/style/test_style.py +++ b/pandas/tests/io/formats/style/test_style.py @@ -20,9 +20,8 @@ class TestStyler: def setup_method(self, method): - np.random.seed(24) - self.s = DataFrame({"A": np.random.permutation(range(6))}) - self.df = DataFrame({"A": [0, 1], "B": np.random.randn(2)}) + self.s = DataFrame({"A": [4, 5, 1, 0, 3, 2]}) + self.df = DataFrame({"A": [0, 1], "B": [-0.609, -1.228]}) self.f = lambda x: x self.g = lambda x: x From c69f29dba50655914fca0633385889fac054843e Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Thu, 11 Mar 2021 15:10:27 +0100 Subject: [PATCH 37/45] remove misleading random note from test fixtures --- pandas/tests/io/formats/style/test_highlight.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/pandas/tests/io/formats/style/test_highlight.py b/pandas/tests/io/formats/style/test_highlight.py index 83c0cc1e48946..08d1fa03b029d 100644 --- a/pandas/tests/io/formats/style/test_highlight.py +++ b/pandas/tests/io/formats/style/test_highlight.py @@ -8,9 +8,8 @@ class TestStylerHighlight: def setup_method(self, method): - np.random.seed(24) - self.s = DataFrame({"A": np.random.permutation(range(6))}) - self.df = DataFrame({"A": [0, 1], "B": np.random.randn(2)}) + self.s = DataFrame({"A": [4, 5, 1, 0, 3, 2]}) + self.df = DataFrame({"A": [0, 1], "B": [-0.609, -1.228]}) def test_highlight_null(self): df = DataFrame({"A": [0, np.nan]}) From 9168be75a40f0a3c01dfe9ba103aeeb3e370c51f Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Mon, 15 Mar 2021 00:03:10 +0100 Subject: [PATCH 38/45] mypy fix --- pandas/io/formats/style.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pandas/io/formats/style.py b/pandas/io/formats/style.py index f7991e5bda057..48a3cf7fb21d6 100644 --- a/pandas/io/formats/style.py +++ b/pandas/io/formats/style.py @@ -1909,7 +1909,7 @@ def highlight_between( """ def f( - data: DataFrame, + data: FrameOrSeries, props: str, left: Optional[Union[Scalar, Sequence]] = None, right: Optional[Union[Scalar, Sequence]] = None, @@ -1949,19 +1949,19 @@ def reshape(x, arg): g_left = ( ops[0](data, left) if left is not None - else np.full_like(data, True, dtype=bool) + else np.full(data.shape, True, dtype=bool) ) l_right = ( ops[1](data, right) if right is not None - else np.full_like(data, True, dtype=bool) + else np.full(data.shape, True, dtype=bool) ) return np.where(g_left & l_right, props, "") if props is None: props = f"background-color: {color};" return self.apply( - f, + f, # type: ignore[arg-type] axis=axis, subset=subset, props=props, From 86fb87c12e88a8eb3dbc5b04f02b797e551ecc51 Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Mon, 15 Mar 2021 08:01:24 +0100 Subject: [PATCH 39/45] filesnames --- .../style/{hr_axNone.png => hbetw_axNone.png} | Bin .../_static/style/{hr_basic.png => hbetw_basic.png} | Bin .../_static/style/{hr_props.png => hbetw_props.png} | Bin .../_static/style/{hr_seq.png => hbetw_seq.png} | Bin pandas/io/formats/style.py | 8 ++++---- 5 files changed, 4 insertions(+), 4 deletions(-) rename doc/source/_static/style/{hr_axNone.png => hbetw_axNone.png} (100%) rename doc/source/_static/style/{hr_basic.png => hbetw_basic.png} (100%) rename doc/source/_static/style/{hr_props.png => hbetw_props.png} (100%) rename doc/source/_static/style/{hr_seq.png => hbetw_seq.png} (100%) diff --git a/doc/source/_static/style/hr_axNone.png b/doc/source/_static/style/hbetw_axNone.png similarity index 100% rename from doc/source/_static/style/hr_axNone.png rename to doc/source/_static/style/hbetw_axNone.png diff --git a/doc/source/_static/style/hr_basic.png b/doc/source/_static/style/hbetw_basic.png similarity index 100% rename from doc/source/_static/style/hr_basic.png rename to doc/source/_static/style/hbetw_basic.png diff --git a/doc/source/_static/style/hr_props.png b/doc/source/_static/style/hbetw_props.png similarity index 100% rename from doc/source/_static/style/hr_props.png rename to doc/source/_static/style/hbetw_props.png diff --git a/doc/source/_static/style/hr_seq.png b/doc/source/_static/style/hbetw_seq.png similarity index 100% rename from doc/source/_static/style/hr_seq.png rename to doc/source/_static/style/hbetw_seq.png diff --git a/pandas/io/formats/style.py b/pandas/io/formats/style.py index 2639e67de18d6..cb3cbe157f062 100644 --- a/pandas/io/formats/style.py +++ b/pandas/io/formats/style.py @@ -1884,7 +1884,7 @@ def highlight_between( ... }) >>> df.style.highlight_between(left=2.1, right=2.9) - .. figure:: ../../_static/style/hr_basic.png + .. figure:: ../../_static/style/hbetw_basic.png Using a range input sequnce along an ``axis``, in this case setting a ``left`` and ``right`` for each column individually @@ -1892,7 +1892,7 @@ def highlight_between( >>> df.style.highlight_between(left=[1.4, 2.4, 3.4], right=[1.6, 2.6, 3.6], ... axis=1, color="#fffd75") - .. figure:: ../../_static/style/hr_seq.png + .. figure:: ../../_static/style/hbetw_seq.png Using ``axis=None`` and providing the ``left`` argument as an array that matches the input DataFrame, with a constant ``right`` @@ -1900,14 +1900,14 @@ def highlight_between( >>> df.style.highlight_between(left=[[2,2,3],[2,2,3],[3,3,3]], right=3.5, ... axis=None, color="#fffd75") - .. figure:: ../../_static/style/hr_axNone.png + .. figure:: ../../_static/style/hbetw_axNone.png Using ``props`` instead of default background coloring >>> df.style.highlight_between(left=1.5, right=3.5, ... props='font-weight:bold;color:#e83e8c') - .. figure:: ../../_static/style/hr_props.png + .. figure:: ../../_static/style/hbetw_props.png """ def f( From bbb63d425f066be140e80c1e23be2cf3ee80118a Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Wed, 17 Mar 2021 16:03:53 +0100 Subject: [PATCH 40/45] add arg shape validation function --- pandas/io/formats/style.py | 81 +++++++++++++++---- .../tests/io/formats/style/test_highlight.py | 2 +- 2 files changed, 67 insertions(+), 16 deletions(-) diff --git a/pandas/io/formats/style.py b/pandas/io/formats/style.py index cb3cbe157f062..0f73d57b46836 100644 --- a/pandas/io/formats/style.py +++ b/pandas/io/formats/style.py @@ -45,7 +45,10 @@ from pandas.api.types import is_list_like from pandas.core import generic import pandas.core.common as com -from pandas.core.frame import DataFrame +from pandas.core.frame import ( + DataFrame, + Series, +) from pandas.core.generic import NDFrame from pandas.core.indexes.api import Index @@ -1916,22 +1919,13 @@ def f( left: Optional[Union[Scalar, Sequence]] = None, right: Optional[Union[Scalar, Sequence]] = None, inclusive: Union[bool, str] = True, + axis_: Optional[Axis] = None, ) -> np.ndarray: - def reshape(x, arg): - """if x is sequence check it fits axis, otherwise raise""" - if np.iterable(x) and not isinstance(x, str): - try: - return np.asarray(x).reshape(data.shape) - except ValueError: - raise ValueError( - f"supplied '{arg}' is not correct shape for " - "data over selected 'axis': got " - f"{np.asarray(x).shape}, expected " - f"{data.shape}" - ) - return x + if np.iterable(left) and not isinstance(left, str): + left = _validate_apply_axis_arg(left, "left", None, axis_, data) - left, right = reshape(left, "left"), reshape(right, "right") + if np.iterable(right) and not isinstance(right, str): + right = _validate_apply_axis_arg(right, "right", None, axis_, data) # get ops with correct boundary attribution if inclusive == "both": @@ -1970,6 +1964,7 @@ def reshape(x, arg): left=left, right=right, inclusive=inclusive, + axis_=axis, ) @classmethod @@ -2419,3 +2414,59 @@ def pred(part) -> bool: else: slice_ = [part if pred(part) else [part] for part in slice_] return tuple(slice_) + + +def _validate_apply_axis_arg( + arg: Union[FrameOrSeries, Sequence], + arg_name: str, + dtype: Optional[Any], + axis: Optional[Axis], + data: FrameOrSeries, +) -> np.ndarray: + """ + For the apply-type methods, ``axis=None`` creates ``data`` as DataFrame, and for + ``axis=[1,0]`` it creates a Series. Where ``arg`` is expected as an element + of some operator with ``data`` we must make sure that the two are compatible shapes, + or raise. + + Parameters + ---------- + arg : sequence, Series or DataFrame + the user input arg + arg_name : string + name of the arg for use in error messages + dtype : numpy dtype, optional + forced numpy dtype if given + axis : {0,1, None} + axis over which apply-type method is used + data : Series or DataFrame + underling subset of Styler data on which operations are performed + + Returns + ------- + ndarray + """ + dtype = {"dtype": dtype} if dtype else {} + # raise if input is wrong for axis: + if isinstance(arg, Series) and axis is None: + raise ValueError( + f"'{arg_name}' is a Series but underlying data for operations " + f"is a DataFrame since 'axis=None'" + ) + elif isinstance(arg, DataFrame) and axis in [0, 1]: + raise ValueError( + f"'{arg_name}' is a DataFrame but underlying data for " + f"operations is a Series with 'axis={axis}'" + ) + elif isinstance(arg, (Series, DataFrame)): # align indx / cols to data + arg = arg.reindex_like(data, method=None).to_numpy(**dtype) + else: + arg = np.asarray(arg, **dtype) + assert isinstance(arg, np.ndarray) # mypy requirement + if arg.shape != data.shape: # check valid input + raise ValueError( + f"supplied '{arg_name}' is not correct shape for data over " + f"selected 'axis': got {arg.shape}, " + f"expected {data.shape}" + ) + return arg diff --git a/pandas/tests/io/formats/style/test_highlight.py b/pandas/tests/io/formats/style/test_highlight.py index 08d1fa03b029d..595a8a7a462cf 100644 --- a/pandas/tests/io/formats/style/test_highlight.py +++ b/pandas/tests/io/formats/style/test_highlight.py @@ -72,7 +72,7 @@ def test_highlight_minmax_ext(self, f, kwargs): {"left": 0}, # test no right {"right": 1, "subset": ["A"]}, # test no left {"left": [0, 1], "axis": 0}, # test left as sequence - {"left": DataFrame([[0, 1], [1, 1]]), "axis": None}, # test axis with seq + {"left": DataFrame({"A": [0, 1], "B": [1, 1]}), "axis": None}, # test axis {"left": 0, "right": [0, 1], "axis": 0}, # test sequence right ], ) From 6dd81376526f91d6a1e0be6c05d59fa148926a0b Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Wed, 17 Mar 2021 20:12:21 +0100 Subject: [PATCH 41/45] mypy updates --- pandas/io/formats/style.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/pandas/io/formats/style.py b/pandas/io/formats/style.py index 0f73d57b46836..59bcd43177af0 100644 --- a/pandas/io/formats/style.py +++ b/pandas/io/formats/style.py @@ -1916,16 +1916,20 @@ def highlight_between( def f( data: FrameOrSeries, props: str, - left: Optional[Union[Scalar, Sequence]] = None, - right: Optional[Union[Scalar, Sequence]] = None, + left: Optional[Union[Scalar, Sequence, np.ndarray, FrameOrSeries]] = None, + right: Optional[Union[Scalar, Sequence, np.ndarray, FrameOrSeries]] = None, inclusive: Union[bool, str] = True, axis_: Optional[Axis] = None, ) -> np.ndarray: if np.iterable(left) and not isinstance(left, str): - left = _validate_apply_axis_arg(left, "left", None, axis_, data) + left = _validate_apply_axis_arg( + left, "left", None, axis_, data # type: ignore[arg-type] + ) if np.iterable(right) and not isinstance(right, str): - right = _validate_apply_axis_arg(right, "right", None, axis_, data) + right = _validate_apply_axis_arg( + right, "right", None, axis_, data # type: ignore[arg-type] + ) # get ops with correct boundary attribution if inclusive == "both": @@ -2417,7 +2421,7 @@ def pred(part) -> bool: def _validate_apply_axis_arg( - arg: Union[FrameOrSeries, Sequence], + arg: Union[FrameOrSeries, Sequence, np.ndarray], arg_name: str, dtype: Optional[Any], axis: Optional[Axis], From 45be30a7ec87046a6542ae69ab634b5d842feb2a Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Tue, 23 Mar 2021 18:31:48 +0100 Subject: [PATCH 42/45] integrate into new test structure --- .../tests/io/formats/style/test_highlight.py | 134 ++++++++++-------- 1 file changed, 72 insertions(+), 62 deletions(-) diff --git a/pandas/tests/io/formats/style/test_highlight.py b/pandas/tests/io/formats/style/test_highlight.py index aab86482c60f4..b8c194f8955ab 100644 --- a/pandas/tests/io/formats/style/test_highlight.py +++ b/pandas/tests/io/formats/style/test_highlight.py @@ -1,7 +1,10 @@ import numpy as np import pytest -from pandas import DataFrame +from pandas import ( + DataFrame, + IndexSlice, +) pytest.importorskip("jinja2") @@ -71,64 +74,71 @@ def test_highlight_minmax_ext(df, f, kwargs): result = getattr(df.style, f)(**kwargs)._compute().ctx assert result == expected - @pytest.mark.parametrize( - "kwargs", - [ - {"left": 0, "right": 1}, # test basic range - {"left": 0, "right": 1, "props": "background-color: yellow"}, # test props - {"left": -9, "right": 9, "subset": ["A"]}, # test subset effective - {"left": 0}, # test no right - {"right": 1, "subset": ["A"]}, # test no left - {"left": [0, 1], "axis": 0}, # test left as sequence - {"left": DataFrame({"A": [0, 1], "B": [1, 1]}), "axis": None}, # test axis - {"left": 0, "right": [0, 1], "axis": 0}, # test sequence right - ], - ) - def test_highlight_between(self, kwargs): - expected = { - (0, 0): [("background-color", "yellow")], - (1, 0): [("background-color", "yellow")], - } - result = self.df.style.highlight_between(**kwargs)._compute().ctx - assert result == expected - - @pytest.mark.parametrize( - "arg, map, axis", - [ - ("left", [1, 2, 3], 0), # 0 axis has 2 elements not 3 - ("left", [1, 2], 1), # 1 axis has 3 elements not 2 - ("left", np.array([[1, 2], [1, 2]]), None), # df is (2,3) not (2,2) - ("right", [1, 2, 3], 0), # same tests as above for 'right' not 'left' - ("right", [1, 2], 1), # .. - ("right", np.array([[1, 2], [1, 2]]), None), # .. - ], - ) - def test_highlight_between_raises(self, arg, map, axis): - df = DataFrame([[1, 2, 3], [1, 2, 3]]) - msg = f"supplied '{arg}' is not correct shape" - with pytest.raises(ValueError, match=msg): - df.style.highlight_between(**{arg: map, "axis": axis})._compute() - - def test_highlight_between_raises2(self): - msg = "values can be 'both', 'left', 'right', or 'neither'" - with pytest.raises(ValueError, match=msg): - self.df.style.highlight_between(inclusive="badstring")._compute() - - with pytest.raises(ValueError, match=msg): - self.df.style.highlight_between(inclusive=1)._compute() - - def test_highlight_between_inclusive(self): - kwargs = {"left": 0, "right": 1, "subset": ["A"]} - result = self.df.style.highlight_between(**kwargs, inclusive="both")._compute() - assert result.ctx == { - (0, 0): [("background-color", "yellow")], - (1, 0): [("background-color", "yellow")], - } - result = self.df.style.highlight_between( - **kwargs, inclusive="neither" - )._compute() - assert result.ctx == {} - result = self.df.style.highlight_between(**kwargs, inclusive="left")._compute() - assert result.ctx == {(0, 0): [("background-color", "yellow")]} - result = self.df.style.highlight_between(**kwargs, inclusive="right")._compute() - assert result.ctx == {(1, 0): [("background-color", "yellow")]} + +@pytest.mark.parametrize( + "kwargs", + [ + {"left": 0, "right": 1}, # test basic range + {"left": 0, "right": 1, "props": "background-color: yellow"}, # test props + {"left": -100, "right": 100, "subset": IndexSlice[[0, 1], :]}, # test subset + {"left": 0, "subset": IndexSlice[[0, 1], :]}, # test no right + {"right": 1}, # test no left + {"left": [0, 0, 11], "axis": 0}, # test left as sequence + {"left": DataFrame({"A": [0, 0, 11], "B": [1, 1, 11]}), "axis": None}, # axis + {"left": 0, "right": [0, 1], "axis": 1}, # test sequence right + ], +) +def test_highlight_between(styler, kwargs): + expected = { + (0, 0): [("background-color", "yellow")], + (0, 1): [("background-color", "yellow")], + } + result = styler.highlight_between(**kwargs)._compute().ctx + assert result == expected + + +@pytest.mark.parametrize( + "arg, map, axis", + [ + ("left", [1, 2], 0), # 0 axis has 3 elements not 2 + ("left", [1, 2, 3], 1), # 1 axis has 2 elements not 3 + ("left", np.array([[1, 2], [1, 2]]), None), # df is (2,3) not (2,2) + ("right", [1, 2], 0), # same tests as above for 'right' not 'left' + ("right", [1, 2, 3], 1), # .. + ("right", np.array([[1, 2], [1, 2]]), None), # .. + ], +) +def test_highlight_between_raises(arg, styler, map, axis): + msg = f"supplied '{arg}' is not correct shape" + with pytest.raises(ValueError, match=msg): + styler.highlight_between(**{arg: map, "axis": axis})._compute() + + +def test_highlight_between_raises2(styler): + msg = "values can be 'both', 'left', 'right', or 'neither'" + with pytest.raises(ValueError, match=msg): + styler.highlight_between(inclusive="badstring")._compute() + + with pytest.raises(ValueError, match=msg): + styler.highlight_between(inclusive=1)._compute() + + +@pytest.mark.parametrize( + "inclusive, expected", + [ + ( + "both", + { + (0, 0): [("background-color", "yellow")], + (0, 1): [("background-color", "yellow")], + }, + ), + ("neither", {}), + ("left", {(0, 0): [("background-color", "yellow")]}), + ("right", {(0, 1): [("background-color", "yellow")]}), + ], +) +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 From c7799619c79238112197c8d31e1bcacabafcf1b3 Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Mon, 29 Mar 2021 16:43:13 +0200 Subject: [PATCH 43/45] remove axis from validate arg func --- pandas/io/formats/style.py | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/pandas/io/formats/style.py b/pandas/io/formats/style.py index 13b1d642ea258..194175b1613de 100644 --- a/pandas/io/formats/style.py +++ b/pandas/io/formats/style.py @@ -1991,16 +1991,15 @@ def f( left: Optional[Union[Scalar, Sequence, np.ndarray, FrameOrSeries]] = None, right: Optional[Union[Scalar, Sequence, np.ndarray, FrameOrSeries]] = None, inclusive: Union[bool, str] = True, - axis_: Optional[Axis] = None, ) -> np.ndarray: if np.iterable(left) and not isinstance(left, str): left = _validate_apply_axis_arg( - left, "left", None, axis_, data # type: ignore[arg-type] + 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, axis_, data # type: ignore[arg-type] + right, "right", None, data # type: ignore[arg-type] ) # get ops with correct boundary attribution @@ -2040,7 +2039,6 @@ def f( left=left, right=right, inclusive=inclusive, - axis_=axis, ) @classmethod @@ -2506,7 +2504,6 @@ def _validate_apply_axis_arg( arg: Union[FrameOrSeries, Sequence, np.ndarray], arg_name: str, dtype: Optional[Any], - axis: Optional[Axis], data: FrameOrSeries, ) -> np.ndarray: """ @@ -2523,8 +2520,6 @@ def _validate_apply_axis_arg( name of the arg for use in error messages dtype : numpy dtype, optional forced numpy dtype if given - axis : {0,1, None} - axis over which apply-type method is used data : Series or DataFrame underling subset of Styler data on which operations are performed @@ -2534,15 +2529,15 @@ def _validate_apply_axis_arg( """ dtype = {"dtype": dtype} if dtype else {} # raise if input is wrong for axis: - if isinstance(arg, Series) and axis is None: + if isinstance(arg, Series) and isinstance(data, DataFrame) is None: raise ValueError( f"'{arg_name}' is a Series but underlying data for operations " f"is a DataFrame since 'axis=None'" ) - elif isinstance(arg, DataFrame) and axis in [0, 1]: + elif isinstance(arg, DataFrame) and isinstance(data, Series): raise ValueError( f"'{arg_name}' is a DataFrame but underlying data for " - f"operations is a Series with 'axis={axis}'" + f"operations is a Series with 'axis in [0,1]'" ) elif isinstance(arg, (Series, DataFrame)): # align indx / cols to data arg = arg.reindex_like(data, method=None).to_numpy(**dtype) From 210a00dc0f2c756d72ccb97d7862a3a4208400ca Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Fri, 9 Apr 2021 07:37:54 +0200 Subject: [PATCH 44/45] no bool in inclusive --- 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 024402e019150..6383e112da0b6 100644 --- a/pandas/io/formats/style.py +++ b/pandas/io/formats/style.py @@ -1908,7 +1908,7 @@ def highlight_between( axis: Axis | None = 0, left: Scalar | Sequence | None = None, right: Scalar | Sequence | None = None, - inclusive: bool | str = "both", + inclusive: str = "both", props: str | None = None, ) -> Styler: """ From 73861f601284778b189b90401ff7847968ca5955 Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (MBP)" Date: Sat, 10 Apr 2021 07:27:13 +0200 Subject: [PATCH 45/45] revert random explicit --- pandas/tests/io/formats/style/test_style.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/pandas/tests/io/formats/style/test_style.py b/pandas/tests/io/formats/style/test_style.py index 4c874533fca22..3422eb9dc64b7 100644 --- a/pandas/tests/io/formats/style/test_style.py +++ b/pandas/tests/io/formats/style/test_style.py @@ -20,8 +20,9 @@ class TestStyler: def setup_method(self, method): - self.s = DataFrame({"A": [4, 5, 1, 0, 3, 2]}) - self.df = DataFrame({"A": [0, 1], "B": [-0.609, -1.228]}) + np.random.seed(24) + self.s = DataFrame({"A": np.random.permutation(range(6))}) + self.df = DataFrame({"A": [0, 1], "B": np.random.randn(2)}) self.f = lambda x: x self.g = lambda x: x