Skip to content

Commit afca08b

Browse files
authoredAug 2, 2022
ENH: Styler.relabel_index for directly specifying display of index labels (pandas-dev#47864)
1 parent cde13db commit afca08b

File tree

4 files changed

+223
-1
lines changed

4 files changed

+223
-1
lines changed
 

‎doc/source/reference/style.rst

+1
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ Style application
4141
Styler.applymap_index
4242
Styler.format
4343
Styler.format_index
44+
Styler.relabel_index
4445
Styler.hide
4546
Styler.concat
4647
Styler.set_td_classes

‎doc/source/whatsnew/v1.5.0.rst

+3
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,9 @@ e.g. totals and counts etc. (:issue:`43875`, :issue:`46186`)
4444
Additionally there is an alternative output method :meth:`.Styler.to_string`,
4545
which allows using the Styler's formatting methods to create, for example, CSVs (:issue:`44502`).
4646

47+
A new feature :meth:`.Styler.relabel_index` is also made available to provide full customisation of the display of
48+
index or column headers (:issue:`47864`)
49+
4750
Minor feature improvements are:
4851

4952
- Adding the ability to render ``border`` and ``border-{side}`` CSS properties in Excel (:issue:`42276`)

‎pandas/io/formats/style_render.py

+162-1
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,10 @@
2222
from pandas._config import get_option
2323

2424
from pandas._libs import lib
25-
from pandas._typing import Level
25+
from pandas._typing import (
26+
Axis,
27+
Level,
28+
)
2629
from pandas.compat._optional import import_optional_dependency
2730

2831
from pandas.core.dtypes.common import (
@@ -1339,6 +1342,164 @@ def format_index(
13391342

13401343
return self
13411344

1345+
def relabel_index(
1346+
self,
1347+
labels: Sequence | Index,
1348+
axis: Axis = 0,
1349+
level: Level | list[Level] | None = None,
1350+
) -> StylerRenderer:
1351+
r"""
1352+
Relabel the index, or column header, keys to display a set of specified values.
1353+
1354+
.. versionadded:: 1.5.0
1355+
1356+
Parameters
1357+
----------
1358+
labels : list-like or Index
1359+
New labels to display. Must have same length as the underlying values not
1360+
hidden.
1361+
axis : {"index", 0, "columns", 1}
1362+
Apply to the index or columns.
1363+
level : int, str, list, optional
1364+
The level(s) over which to apply the new labels. If `None` will apply
1365+
to all levels of an Index or MultiIndex which are not hidden.
1366+
1367+
Returns
1368+
-------
1369+
self : Styler
1370+
1371+
See Also
1372+
--------
1373+
Styler.format_index: Format the text display value of index or column headers.
1374+
Styler.hide: Hide the index, column headers, or specified data from display.
1375+
1376+
Notes
1377+
-----
1378+
As part of Styler, this method allows the display of an index to be
1379+
completely user-specified without affecting the underlying DataFrame data,
1380+
index, or column headers. This means that the flexibility of indexing is
1381+
maintained whilst the final display is customisable.
1382+
1383+
Since Styler is designed to be progressively constructed with method chaining,
1384+
this method is adapted to react to the **currently specified hidden elements**.
1385+
This is useful because it means one does not have to specify all the new
1386+
labels if the majority of an index, or column headers, have already been hidden.
1387+
The following produce equivalent display (note the length of ``labels`` in
1388+
each case).
1389+
1390+
.. code-block:: python
1391+
1392+
# relabel first, then hide
1393+
df = pd.DataFrame({"col": ["a", "b", "c"]})
1394+
df.style.relabel_index(["A", "B", "C"]).hide([0,1])
1395+
# hide first, then relabel
1396+
df = pd.DataFrame({"col": ["a", "b", "c"]})
1397+
df.style.hide([0,1]).relabel_index(["C"])
1398+
1399+
This method should be used, rather than :meth:`Styler.format_index`, in one of
1400+
the following cases (see examples):
1401+
1402+
- A specified set of labels are required which are not a function of the
1403+
underlying index keys.
1404+
- The function of the underlying index keys requires a counter variable,
1405+
such as those available upon enumeration.
1406+
1407+
Examples
1408+
--------
1409+
Basic use
1410+
1411+
>>> df = pd.DataFrame({"col": ["a", "b", "c"]})
1412+
>>> df.style.relabel_index(["A", "B", "C"]) # doctest: +SKIP
1413+
col
1414+
A a
1415+
B b
1416+
C c
1417+
1418+
Chaining with pre-hidden elements
1419+
1420+
>>> df.style.hide([0,1]).relabel_index(["C"]) # doctest: +SKIP
1421+
col
1422+
C c
1423+
1424+
Using a MultiIndex
1425+
1426+
>>> midx = pd.MultiIndex.from_product([[0, 1], [0, 1], [0, 1]])
1427+
>>> df = pd.DataFrame({"col": list(range(8))}, index=midx)
1428+
>>> styler = df.style # doctest: +SKIP
1429+
col
1430+
0 0 0 0
1431+
1 1
1432+
1 0 2
1433+
1 3
1434+
1 0 0 4
1435+
1 5
1436+
1 0 6
1437+
1 7
1438+
>>> styler.hide((midx.get_level_values(0)==0)|(midx.get_level_values(1)==0))
1439+
... # doctest: +SKIP
1440+
>>> styler.hide(level=[0,1]) # doctest: +SKIP
1441+
>>> styler.relabel_index(["binary6", "binary7"]) # doctest: +SKIP
1442+
col
1443+
binary6 6
1444+
binary7 7
1445+
1446+
We can also achieve the above by indexing first and then re-labeling
1447+
1448+
>>> styler = df.loc[[(1,1,0), (1,1,1)]].style
1449+
>>> styler.hide(level=[0,1]).relabel_index(["binary6", "binary7"])
1450+
... # doctest: +SKIP
1451+
col
1452+
binary6 6
1453+
binary7 7
1454+
1455+
Defining a formatting function which uses an enumeration counter. Also note
1456+
that the value of the index key is passed in the case of string labels so it
1457+
can also be inserted into the label, using curly brackets (or double curly
1458+
brackets if the string if pre-formatted),
1459+
1460+
>>> df = pd.DataFrame({"samples": np.random.rand(10)})
1461+
>>> styler = df.loc[np.random.randint(0,10,3)].style
1462+
>>> styler.relabel_index([f"sample{i+1} ({{}})" for i in range(3)])
1463+
... # doctest: +SKIP
1464+
samples
1465+
sample1 (5) 0.315811
1466+
sample2 (0) 0.495941
1467+
sample3 (2) 0.067946
1468+
"""
1469+
axis = self.data._get_axis_number(axis)
1470+
if axis == 0:
1471+
display_funcs_, obj = self._display_funcs_index, self.index
1472+
hidden_labels, hidden_lvls = self.hidden_rows, self.hide_index_
1473+
else:
1474+
display_funcs_, obj = self._display_funcs_columns, self.columns
1475+
hidden_labels, hidden_lvls = self.hidden_columns, self.hide_columns_
1476+
visible_len = len(obj) - len(set(hidden_labels))
1477+
if len(labels) != visible_len:
1478+
raise ValueError(
1479+
"``labels`` must be of length equal to the number of "
1480+
f"visible labels along ``axis`` ({visible_len})."
1481+
)
1482+
1483+
if level is None:
1484+
level = [i for i in range(obj.nlevels) if not hidden_lvls[i]]
1485+
levels_ = refactor_levels(level, obj)
1486+
1487+
def alias_(x, value):
1488+
if isinstance(value, str):
1489+
return value.format(x)
1490+
return value
1491+
1492+
for ai, i in enumerate([i for i in range(len(obj)) if i not in hidden_labels]):
1493+
if len(levels_) == 1:
1494+
idx = (i, levels_[0]) if axis == 0 else (levels_[0], i)
1495+
display_funcs_[idx] = partial(alias_, value=labels[ai])
1496+
else:
1497+
for aj, lvl in enumerate(levels_):
1498+
idx = (i, lvl) if axis == 0 else (lvl, i)
1499+
display_funcs_[idx] = partial(alias_, value=labels[ai][aj])
1500+
1501+
return self
1502+
13421503

13431504
def _element(
13441505
html_element: str,

‎pandas/tests/io/formats/style/test_format.py

+57
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,20 @@ def styler(df):
3030
return Styler(df, uuid_len=0)
3131

3232

33+
@pytest.fixture
34+
def df_multi():
35+
return DataFrame(
36+
data=np.arange(16).reshape(4, 4),
37+
columns=MultiIndex.from_product([["A", "B"], ["a", "b"]]),
38+
index=MultiIndex.from_product([["X", "Y"], ["x", "y"]]),
39+
)
40+
41+
42+
@pytest.fixture
43+
def styler_multi(df_multi):
44+
return Styler(df_multi, uuid_len=0)
45+
46+
3347
def test_display_format(styler):
3448
ctx = styler.format("{:0.1f}")._translate(True, True)
3549
assert all(["display_value" in c for c in row] for row in ctx["body"])
@@ -442,3 +456,46 @@ def test_boolean_format():
442456
ctx = df.style._translate(True, True)
443457
assert ctx["body"][0][1]["display_value"] is True
444458
assert ctx["body"][0][2]["display_value"] is False
459+
460+
461+
@pytest.mark.parametrize(
462+
"hide, labels",
463+
[
464+
(False, [1, 2]),
465+
(True, [1, 2, 3, 4]),
466+
],
467+
)
468+
def test_relabel_raise_length(styler_multi, hide, labels):
469+
if hide:
470+
styler_multi.hide(axis=0, subset=[("X", "x"), ("Y", "y")])
471+
with pytest.raises(ValueError, match="``labels`` must be of length equal"):
472+
styler_multi.relabel_index(labels=labels)
473+
474+
475+
def test_relabel_index(styler_multi):
476+
labels = [(1, 2), (3, 4)]
477+
styler_multi.hide(axis=0, subset=[("X", "x"), ("Y", "y")])
478+
styler_multi.relabel_index(labels=labels)
479+
ctx = styler_multi._translate(True, True)
480+
assert {"value": "X", "display_value": 1}.items() <= ctx["body"][0][0].items()
481+
assert {"value": "y", "display_value": 2}.items() <= ctx["body"][0][1].items()
482+
assert {"value": "Y", "display_value": 3}.items() <= ctx["body"][1][0].items()
483+
assert {"value": "x", "display_value": 4}.items() <= ctx["body"][1][1].items()
484+
485+
486+
def test_relabel_columns(styler_multi):
487+
labels = [(1, 2), (3, 4)]
488+
styler_multi.hide(axis=1, subset=[("A", "a"), ("B", "b")])
489+
styler_multi.relabel_index(axis=1, labels=labels)
490+
ctx = styler_multi._translate(True, True)
491+
assert {"value": "A", "display_value": 1}.items() <= ctx["head"][0][3].items()
492+
assert {"value": "B", "display_value": 3}.items() <= ctx["head"][0][4].items()
493+
assert {"value": "b", "display_value": 2}.items() <= ctx["head"][1][3].items()
494+
assert {"value": "a", "display_value": 4}.items() <= ctx["head"][1][4].items()
495+
496+
497+
def test_relabel_roundtrip(styler):
498+
styler.relabel_index(["{}", "{}"])
499+
ctx = styler._translate(True, True)
500+
assert {"value": "x", "display_value": "x"}.items() <= ctx["body"][0][0].items()
501+
assert {"value": "y", "display_value": "y"}.items() <= ctx["body"][1][0].items()

0 commit comments

Comments
 (0)
Please sign in to comment.