Skip to content

Commit 5bd9bac

Browse files
authored
ENH: Styler.apply_index and Styler.applymap_index for conditional formatting of column/index headers (#41893)
1 parent 2fad5d7 commit 5bd9bac

File tree

11 files changed

+417
-181
lines changed

11 files changed

+417
-181
lines changed
4.91 KB
Loading
7.46 KB
Loading

doc/source/reference/style.rst

+2
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@ Style application
3636

3737
Styler.apply
3838
Styler.applymap
39+
Styler.apply_index
40+
Styler.applymap_index
3941
Styler.format
4042
Styler.hide_index
4143
Styler.hide_columns

doc/source/user_guide/style.ipynb

+45-1
Original file line numberDiff line numberDiff line change
@@ -225,13 +225,15 @@
225225
"\n",
226226
"- Using [.set_table_styles()][table] to control broader areas of the table with specified internal CSS. Although table styles allow the flexibility to add CSS selectors and properties controlling all individual parts of the table, they are unwieldy for individual cell specifications. Also, note that table styles cannot be exported to Excel. \n",
227227
"- Using [.set_td_classes()][td_class] to directly link either external CSS classes to your data cells or link the internal CSS classes created by [.set_table_styles()][table]. See [here](#Setting-Classes-and-Linking-to-External-CSS). These cannot be used on column header rows or indexes, and also won't export to Excel. \n",
228-
"- Using the [.apply()][apply] and [.applymap()][applymap] functions to add direct internal CSS to specific data cells. See [here](#Styler-Functions). These cannot be used on column header rows or indexes, but only these methods add styles that will export to Excel. These methods work in a similar way to [DataFrame.apply()][dfapply] and [DataFrame.applymap()][dfapplymap].\n",
228+
"- Using the [.apply()][apply] and [.applymap()][applymap] functions to add direct internal CSS to specific data cells. See [here](#Styler-Functions). As of v1.4.0 there are also methods that work directly on column header rows or indexes; [.apply_index()][applyindex] and [.applymap_index()][applymapindex]. Note that only these methods add styles that will export to Excel. These methods work in a similar way to [DataFrame.apply()][dfapply] and [DataFrame.applymap()][dfapplymap].\n",
229229
"\n",
230230
"[table]: ../reference/api/pandas.io.formats.style.Styler.set_table_styles.rst\n",
231231
"[styler]: ../reference/api/pandas.io.formats.style.Styler.rst\n",
232232
"[td_class]: ../reference/api/pandas.io.formats.style.Styler.set_td_classes.rst\n",
233233
"[apply]: ../reference/api/pandas.io.formats.style.Styler.apply.rst\n",
234234
"[applymap]: ../reference/api/pandas.io.formats.style.Styler.applymap.rst\n",
235+
"[applyindex]: ../reference/api/pandas.io.formats.style.Styler.apply_index.rst\n",
236+
"[applymapindex]: ../reference/api/pandas.io.formats.style.Styler.applymap_index.rst\n",
235237
"[dfapply]: ../reference/api/pandas.DataFrame.apply.rst\n",
236238
"[dfapplymap]: ../reference/api/pandas.DataFrame.applymap.rst"
237239
]
@@ -432,6 +434,8 @@
432434
"source": [
433435
"## Styler Functions\n",
434436
"\n",
437+
"### Acting on Data\n",
438+
"\n",
435439
"We use the following methods to pass your style functions. Both of those methods take a function (and some other keyword arguments) and apply it to the DataFrame in a certain way, rendering CSS styles.\n",
436440
"\n",
437441
"- [.applymap()][applymap] (elementwise): accepts a function that takes a single value and returns a string with the CSS attribute-value pair.\n",
@@ -533,6 +537,18 @@
533537
" .apply(highlight_max, props='color:white;background-color:purple', axis=None)"
534538
]
535539
},
540+
{
541+
"cell_type": "code",
542+
"execution_count": null,
543+
"metadata": {
544+
"nbsphinx": "hidden"
545+
},
546+
"outputs": [],
547+
"source": [
548+
"# Hidden cell to avoid CSS clashes and latter code upcoding previous formatting \n",
549+
"s2.set_uuid('after_apply_again')"
550+
]
551+
},
536552
{
537553
"cell_type": "markdown",
538554
"metadata": {},
@@ -548,6 +564,33 @@
548564
"</div>"
549565
]
550566
},
567+
{
568+
"cell_type": "markdown",
569+
"metadata": {},
570+
"source": [
571+
"### Acting on the Index and Column Headers\n",
572+
"\n",
573+
"Similar application is acheived for headers by using:\n",
574+
" \n",
575+
"- [.applymap_index()][applymapindex] (elementwise): accepts a function that takes a single value and returns a string with the CSS attribute-value pair.\n",
576+
"- [.apply_index()][applyindex] (level-wise): accepts a function that takes a Series and returns a Series, or numpy array with an identical shape where each element is a string with a CSS attribute-value pair. This method passes each level of your Index one-at-a-time. To style the index use `axis=0` and to style the column headers use `axis=1`.\n",
577+
"\n",
578+
"You can select a `level` of a `MultiIndex` but currently no similar `subset` application is available for these methods.\n",
579+
"\n",
580+
"[applyindex]: ../reference/api/pandas.io.formats.style.Styler.apply_index.rst\n",
581+
"[applymapindex]: ../reference/api/pandas.io.formats.style.Styler.applymap_index.rst"
582+
]
583+
},
584+
{
585+
"cell_type": "code",
586+
"execution_count": null,
587+
"metadata": {},
588+
"outputs": [],
589+
"source": [
590+
"s2.applymap_index(lambda v: \"color:pink;\" if v>4 else \"color:darkblue;\", axis=0)\n",
591+
"s2.apply_index(lambda s: np.where(s.isin([\"A\", \"B\"]), \"color:pink;\", \"color:darkblue;\"), axis=1)"
592+
]
593+
},
551594
{
552595
"cell_type": "markdown",
553596
"metadata": {},
@@ -1931,6 +1974,7 @@
19311974
}
19321975
],
19331976
"metadata": {
1977+
"celltoolbar": "Edit Metadata",
19341978
"kernelspec": {
19351979
"display_name": "Python 3",
19361980
"language": "python",

doc/source/whatsnew/v1.4.0.rst

+1
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ Other enhancements
3838
- :meth:`Series.ewm`, :meth:`DataFrame.ewm`, now support a ``method`` argument with a ``'table'`` option that performs the windowing operation over an entire :class:`DataFrame`. See :ref:`Window Overview <window.overview>` for performance and functional benefits (:issue:`42273`)
3939
- Added ``sparse_index`` and ``sparse_columns`` keyword arguments to :meth:`.Styler.to_html` (:issue:`41946`)
4040
- Added keyword argument ``environment`` to :meth:`.Styler.to_latex` also allowing a specific "longtable" entry with a separate jinja2 template (:issue:`41866`)
41+
- :meth:`.Styler.apply_index` and :meth:`.Styler.applymap_index` added to allow conditional styling of index and column header values (:issue:`41893`)
4142
- :meth:`.GroupBy.cummin` and :meth:`.GroupBy.cummax` now support the argument ``skipna`` (:issue:`34047`)
4243
-
4344

pandas/io/formats/style.py

+176-1
Original file line numberDiff line numberDiff line change
@@ -1012,6 +1012,32 @@ def _update_ctx(self, attrs: DataFrame) -> None:
10121012
i, j = self.index.get_loc(rn), self.columns.get_loc(cn)
10131013
self.ctx[(i, j)].extend(css_list)
10141014

1015+
def _update_ctx_header(self, attrs: DataFrame, axis: str) -> None:
1016+
"""
1017+
Update the state of the ``Styler`` for header cells.
1018+
1019+
Collects a mapping of {index_label: [('<property>', '<value>'), ..]}.
1020+
1021+
Parameters
1022+
----------
1023+
attrs : Series
1024+
Should contain strings of '<property>: <value>;<prop2>: <val2>', and an
1025+
integer index.
1026+
Whitespace shouldn't matter and the final trailing ';' shouldn't
1027+
matter.
1028+
axis : str
1029+
Identifies whether the ctx object being updated is the index or columns
1030+
"""
1031+
for j in attrs.columns:
1032+
for i, c in attrs[[j]].itertuples():
1033+
if not c:
1034+
continue
1035+
css_list = maybe_convert_css_to_tuples(c)
1036+
if axis == "index":
1037+
self.ctx_index[(i, j)].extend(css_list)
1038+
else:
1039+
self.ctx_columns[(j, i)].extend(css_list)
1040+
10151041
def _copy(self, deepcopy: bool = False) -> Styler:
10161042
"""
10171043
Copies a Styler, allowing for deepcopy or shallow copy
@@ -1051,6 +1077,8 @@ def _copy(self, deepcopy: bool = False) -> Styler:
10511077
"hidden_rows",
10521078
"hidden_columns",
10531079
"ctx",
1080+
"ctx_index",
1081+
"ctx_columns",
10541082
"cell_context",
10551083
"_todo",
10561084
"table_styles",
@@ -1172,6 +1200,8 @@ def apply(
11721200
11731201
See Also
11741202
--------
1203+
Styler.applymap_index: Apply a CSS-styling function to headers elementwise.
1204+
Styler.apply_index: Apply a CSS-styling function to headers level-wise.
11751205
Styler.applymap: Apply a CSS-styling function elementwise.
11761206
11771207
Notes
@@ -1215,6 +1245,149 @@ def apply(
12151245
)
12161246
return self
12171247

1248+
def _apply_index(
1249+
self,
1250+
func: Callable[..., Styler],
1251+
axis: int | str = 0,
1252+
level: Level | list[Level] | None = None,
1253+
method: str = "apply",
1254+
**kwargs,
1255+
) -> Styler:
1256+
if axis in [0, "index"]:
1257+
obj, axis = self.index, "index"
1258+
elif axis in [1, "columns"]:
1259+
obj, axis = self.columns, "columns"
1260+
else:
1261+
raise ValueError(
1262+
f"`axis` must be one of 0, 1, 'index', 'columns', got {axis}"
1263+
)
1264+
1265+
levels_ = _refactor_levels(level, obj)
1266+
data = DataFrame(obj.to_list()).loc[:, levels_]
1267+
1268+
if method == "apply":
1269+
result = data.apply(func, axis=0, **kwargs)
1270+
elif method == "applymap":
1271+
result = data.applymap(func, **kwargs)
1272+
1273+
self._update_ctx_header(result, axis)
1274+
return self
1275+
1276+
@doc(
1277+
this="apply",
1278+
wise="level-wise",
1279+
alt="applymap",
1280+
altwise="elementwise",
1281+
func="take a Series and return a string array of the same length",
1282+
axis='{0, 1, "index", "columns"}',
1283+
input_note="the index as a Series, if an Index, or a level of a MultiIndex",
1284+
output_note="an identically sized array of CSS styles as strings",
1285+
var="s",
1286+
ret='np.where(s == "B", "background-color: yellow;", "")',
1287+
ret2='["background-color: yellow;" if "x" in v else "" for v in s]',
1288+
)
1289+
def apply_index(
1290+
self,
1291+
func: Callable[..., Styler],
1292+
axis: int | str = 0,
1293+
level: Level | list[Level] | None = None,
1294+
**kwargs,
1295+
) -> Styler:
1296+
"""
1297+
Apply a CSS-styling function to the index or column headers, {wise}.
1298+
1299+
Updates the HTML representation with the result.
1300+
1301+
.. versionadded:: 1.4.0
1302+
1303+
Parameters
1304+
----------
1305+
func : function
1306+
``func`` should {func}.
1307+
axis : {axis}
1308+
The headers over which to apply the function.
1309+
level : int, str, list, optional
1310+
If index is MultiIndex the level(s) over which to apply the function.
1311+
**kwargs : dict
1312+
Pass along to ``func``.
1313+
1314+
Returns
1315+
-------
1316+
self : Styler
1317+
1318+
See Also
1319+
--------
1320+
Styler.{alt}_index: Apply a CSS-styling function to headers {altwise}.
1321+
Styler.apply: Apply a CSS-styling function column-wise, row-wise, or table-wise.
1322+
Styler.applymap: Apply a CSS-styling function elementwise.
1323+
1324+
Notes
1325+
-----
1326+
Each input to ``func`` will be {input_note}. The output of ``func`` should be
1327+
{output_note}, in the format 'attribute: value; attribute2: value2; ...'
1328+
or, if nothing is to be applied to that element, an empty string or ``None``.
1329+
1330+
Examples
1331+
--------
1332+
Basic usage to conditionally highlight values in the index.
1333+
1334+
>>> df = pd.DataFrame([[1,2], [3,4]], index=["A", "B"])
1335+
>>> def color_b(s):
1336+
... return {ret}
1337+
>>> df.style.{this}_index(color_b) # doctest: +SKIP
1338+
1339+
.. figure:: ../../_static/style/appmaphead1.png
1340+
1341+
Selectively applying to specific levels of MultiIndex columns.
1342+
1343+
>>> midx = pd.MultiIndex.from_product([['ix', 'jy'], [0, 1], ['x3', 'z4']])
1344+
>>> df = pd.DataFrame([np.arange(8)], columns=midx)
1345+
>>> def highlight_x({var}):
1346+
... return {ret2}
1347+
>>> df.style.{this}_index(highlight_x, axis="columns", level=[0, 2])
1348+
... # doctest: +SKIP
1349+
1350+
.. figure:: ../../_static/style/appmaphead2.png
1351+
"""
1352+
self._todo.append(
1353+
(
1354+
lambda instance: getattr(instance, "_apply_index"),
1355+
(func, axis, level, "apply"),
1356+
kwargs,
1357+
)
1358+
)
1359+
return self
1360+
1361+
@doc(
1362+
apply_index,
1363+
this="applymap",
1364+
wise="elementwise",
1365+
alt="apply",
1366+
altwise="level-wise",
1367+
func="take a scalar and return a string",
1368+
axis='{0, 1, "index", "columns"}',
1369+
input_note="an index value, if an Index, or a level value of a MultiIndex",
1370+
output_note="CSS styles as a string",
1371+
var="v",
1372+
ret='"background-color: yellow;" if v == "B" else None',
1373+
ret2='"background-color: yellow;" if "x" in v else None',
1374+
)
1375+
def applymap_index(
1376+
self,
1377+
func: Callable[..., Styler],
1378+
axis: int | str = 0,
1379+
level: Level | list[Level] | None = None,
1380+
**kwargs,
1381+
) -> Styler:
1382+
self._todo.append(
1383+
(
1384+
lambda instance: getattr(instance, "_apply_index"),
1385+
(func, axis, level, "applymap"),
1386+
kwargs,
1387+
)
1388+
)
1389+
return self
1390+
12181391
def _applymap(
12191392
self, func: Callable, subset: Subset | None = None, **kwargs
12201393
) -> Styler:
@@ -1237,7 +1410,7 @@ def applymap(
12371410
Parameters
12381411
----------
12391412
func : function
1240-
``func`` should take a scalar and return a scalar.
1413+
``func`` should take a scalar and return a string.
12411414
subset : label, array-like, IndexSlice, optional
12421415
A valid 2d input to `DataFrame.loc[<subset>]`, or, in the case of a 1d input
12431416
or single key, to `DataFrame.loc[:, <subset>]` where the columns are
@@ -1251,6 +1424,8 @@ def applymap(
12511424
12521425
See Also
12531426
--------
1427+
Styler.applymap_index: Apply a CSS-styling function to headers elementwise.
1428+
Styler.apply_index: Apply a CSS-styling function to headers level-wise.
12541429
Styler.apply: Apply a CSS-styling function column-wise, row-wise, or table-wise.
12551430
12561431
Notes

0 commit comments

Comments
 (0)