Skip to content

ENH: add kwargs rename to Styler.format_index for overwriting index labels #45288

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 38 commits into from
Closed
Show file tree
Hide file tree
Changes from 22 commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
b3c55af
setup new kwarg
attack68 Jan 6, 2022
c68944a
method and raise on error
attack68 Jan 7, 2022
929df89
Merge remote-tracking branch 'upstream/master' into styler_format_ali…
attack68 Jan 8, 2022
fcac539
add basic tests
attack68 Jan 9, 2022
55010b5
add basic tests
attack68 Jan 9, 2022
9500c06
extend docs
attack68 Jan 9, 2022
38ea09c
mypy fix
attack68 Jan 9, 2022
86287a4
Merge remote-tracking branch 'upstream/master' into styler_format_ali…
attack68 Jan 9, 2022
f48b43c
whats new
attack68 Jan 9, 2022
9bcc320
doctest skip
attack68 Jan 10, 2022
526cc31
Merge remote-tracking branch 'upstream/main' into styler_format_alias…
attack68 Jan 16, 2022
a6952bb
doc string fixes
attack68 Jan 16, 2022
2a18d60
Merge remote-tracking branch 'upstream/main' into styler_format_alias…
attack68 Jan 20, 2022
150ee1d
Merge remote-tracking branch 'upstream/main' into styler_format_alias…
attack68 Jan 21, 2022
0c4bc3d
Merge remote-tracking branch 'upstream/main' into styler_format_alias…
attack68 Jan 25, 2022
06c68d2
Merge remote-tracking branch 'upstream/main' into styler_format_alias…
attack68 Jan 25, 2022
53cc5e2
Merge remote-tracking branch 'upstream/main' into styler_format_alias…
attack68 Jan 27, 2022
6820423
Merge remote-tracking branch 'upstream/main' into styler_format_alias…
attack68 Feb 2, 2022
48b2202
refactor
attack68 Feb 2, 2022
85b7f6d
whats new update
attack68 Feb 2, 2022
bd1aa9a
Merge remote-tracking branch 'upstream/main' into styler_format_alias…
attack68 Feb 7, 2022
599c4b5
Merge remote-tracking branch 'upstream/main' into styler_format_alias…
attack68 Feb 8, 2022
a459fa3
Merge remote-tracking branch 'upstream/main' into styler_format_alias…
attack68 Feb 11, 2022
e6ef392
Merge remote-tracking branch 'upstream/main' into styler_format_alias…
attack68 Feb 13, 2022
db82122
Merge remote-tracking branch 'upstream/main' into styler_format_alias…
attack68 Feb 16, 2022
bf13d8e
Merge remote-tracking branch 'upstream/main' into styler_format_alias…
attack68 Feb 21, 2022
695df51
Merge branch 'main' into styler_format_alias_multi
attack68 Mar 2, 2022
ece8b10
change arg `alias` to `rename`
attack68 Mar 2, 2022
33d89b5
change arg `alias` to `rename`
attack68 Mar 2, 2022
6eabbc3
change arg `alias` to `rename`
attack68 Mar 2, 2022
92c114e
Merge remote-tracking branch 'upstream/main' into styler_format_alias…
attack68 Mar 4, 2022
f38c315
Merge remote-tracking branch 'upstream/main' into styler_format_alias…
attack68 Mar 6, 2022
cfdd639
Merge remote-tracking branch 'upstream/main' into styler_format_alias…
attack68 Mar 6, 2022
76fc4ee
Merge remote-tracking branch 'upstream/main' into styler_format_alias…
attack68 Mar 7, 2022
6b93ff8
Merge remote-tracking branch 'upstream/main' into styler_format_alias…
attack68 Mar 18, 2022
2b2406c
Merge remote-tracking branch 'upstream/main' into styler_format_alias…
attack68 Mar 28, 2022
c48e659
Merge remote-tracking branch 'upstream/main' into styler_format_alias…
attack68 Apr 5, 2022
3439c50
Merge remote-tracking branch 'upstream/main' into styler_format_alias…
attack68 Apr 29, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions doc/source/whatsnew/v1.5.0.rst
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ Styler
^^^^^^

- New method :meth:`.Styler.to_string` for alternative customisable output methods (:issue:`44502`)
- New keyword argument ``alias`` added to :meth:`.Styler.format_index` to allow simple label string replacement (:issue:`45288`)
- Various bug fixes, see below.

.. _whatsnew_150.enhancements.enhancement2:
Expand Down
136 changes: 111 additions & 25 deletions pandas/io/formats/style_render.py
Original file line number Diff line number Diff line change
Expand Up @@ -1089,6 +1089,7 @@ def format_index(
thousands: str | None = None,
escape: str | None = None,
hyperlinks: str | None = None,
alias: list[str] | list[list[str]] | None = None,
) -> StylerRenderer:
r"""
Format the text display value of index labels or column headers.
Expand All @@ -1100,9 +1101,12 @@ def format_index(
formatter : str, callable, dict or None
Object to define how values are displayed. See notes.
axis : {0, "index", 1, "columns"}
Whether to apply the formatter to the index or column headers.
Whether to apply the ``formatter`` or ``alias`` to the index or column
headers.
level : int, str, list
The level(s) over which to apply the generic formatter.
The level(s) over which to apply the generic ``formatter``, or ``alias``.
In the case of ``alias`` defaults to the last level of a MultiIndex,
for the reason that the last level is never sparsified.
na_rep : str, optional
Representation for missing values.
If ``na_rep`` is None, no special formatting is applied.
Expand All @@ -1124,6 +1128,17 @@ def format_index(
Convert string patterns containing https://, http://, ftp:// or www. to
HTML <a> tags as clickable URL hyperlinks if "html", or LaTeX \href
commands if "latex".
alias : list of str, list of list of str
Values to replace the existing index or column headers. If specifying
more than one ``level`` then this should be a list containing sub-lists for
each identified level, in the respective order.
Cannot be used simultaneously with ``formatter`` and the associated
arguments; ``thousands``, ``decimal``, ``escape``, ``hyperlinks``,
``na_rep`` and ``precision``.
This list (or each sub-list) must be of length equal to the number of
visible columns, see examples.

.. versionadded:: 1.5.0

Returns
-------
Expand Down Expand Up @@ -1155,6 +1170,10 @@ def format_index(
When using a ``formatter`` string the dtypes must be compatible, otherwise a
`ValueError` will be raised.

Since it is not possible to apply a generic function which will return an
arbitrary set of column aliases, the argument ``alias`` provides the
ability to automate this, across individual index levels if necessary.

Examples
--------
Using ``na_rep`` and ``precision`` with the default ``formatter``
Expand Down Expand Up @@ -1208,50 +1227,117 @@ def format_index(
{} & {\textbf{123}} & {\textbf{\textasciitilde }} & {\textbf{\$\%\#}} \\
0 & 1 & 2 & 3 \\
\end{tabular}

Using ``alias`` to overwrite column names.

>>> df = pd.DataFrame([[1, 2, 3]], columns=[1, 2, 3])
>>> df.style.format_index(axis=1, alias=["A", "B", "C"]) # doctest: +SKIP
A B C
0 1 2 3

Using ``alias`` to overwrite column names of remaining **visible** items.

>>> df = pd.DataFrame([[1, 2, 3]],
... columns=pd.MultiIndex.from_product([[1, 2, 3], ["X"]]))
>>> styler = df.style # doctest: +SKIP
1 2 3
X X X
0 1 2 3

>>> styler.hide([2], axis=1) # hides a column as a `subset` hide
... .hide(level=1, axis=1) # hides the entire axis level
... .format_index(axis=1, alias=["A", "C"], level=0) # doctest: +SKIP
A C
0 1 3
"""
axis = self.data._get_axis_number(axis)
if axis == 0:
display_funcs_, obj = self._display_funcs_index, self.index
hidden_labels = self.hidden_rows
else:
display_funcs_, obj = self._display_funcs_columns, self.columns
hidden_labels = self.hidden_columns
levels_ = refactor_levels(level, obj)

if all(
formatting_args_unset = all(
(
formatter is None,
level is None,
precision is None,
decimal == ".",
thousands is None,
na_rep is None,
escape is None,
hyperlinks is None,
)
):
)

if formatting_args_unset and level is None and alias is None:
# clear the formatter / revert to default and avoid looping
display_funcs_.clear()
return self # clear the formatter / revert to default and avoid looping

if not isinstance(formatter, dict):
formatter = {level: formatter for level in levels_}
else:
formatter = {
obj._get_level_number(level): formatter_
for level, formatter_ in formatter.items()
}
elif alias is not None: # then apply a formatting function from arg: alias
if not formatting_args_unset:
raise ValueError(
"``alias`` cannot be supplied together with any of "
"``formatter``, ``precision``, ``decimal``, ``na_rep``, "
"``escape``, or ``hyperlinks``."
)
else:
visible_len = len(obj) - len(set(hidden_labels))
if level is None:
levels_ = [obj.nlevels - 1] # default to last level
elif len(levels_) > 1 and len(alias) != len(levels_):
raise ValueError(
f"``level`` specifies {len(levels_)} levels but the length of "
f"``alias``, {len(alias)}, does not match."
)

for lvl in levels_:
format_func = _maybe_wrap_formatter(
formatter.get(lvl),
na_rep=na_rep,
precision=precision,
decimal=decimal,
thousands=thousands,
escape=escape,
hyperlinks=hyperlinks,
)
def alias_(x, value):
return value

for i, lvl in enumerate(levels_):
level_alias = alias[i] if len(levels_) > 1 else alias
if len(level_alias) != visible_len:
raise ValueError(
"``alias`` must be of length equal to the number of "
"visible labels along ``axis``. If ``level`` is given and "
"contains more than one level ``alias`` should be a "
"list of lists with each sub-list having length equal to"
"the number of visible labels along ``axis``."
)
for ai, idx in enumerate(
[
(i, lvl) if axis == 0 else (lvl, i)
for i in range(len(obj))
if i not in hidden_labels
]
):
display_funcs_[idx] = partial(alias_, value=level_alias[ai])

else: # then apply a formatting function from arg: formatter
if not isinstance(formatter, dict):
formatter = {level: formatter for level in levels_}
else:
formatter = {
obj._get_level_number(level): formatter_
for level, formatter_ in formatter.items()
}

for lvl in levels_:
format_func = _maybe_wrap_formatter(
formatter.get(lvl),
na_rep=na_rep,
precision=precision,
decimal=decimal,
thousands=thousands,
escape=escape,
hyperlinks=hyperlinks,
)

for idx in [(i, lvl) if axis == 0 else (lvl, i) for i in range(len(obj))]:
display_funcs_[idx] = format_func
for idx in [
(i, lvl) if axis == 0 else (lvl, i) for i in range(len(obj))
]:
display_funcs_[idx] = format_func

return self

Expand Down
80 changes: 80 additions & 0 deletions pandas/tests/io/formats/style/test_format.py
Original file line number Diff line number Diff line change
Expand Up @@ -434,3 +434,83 @@ def test_1level_multiindex():
assert ctx["body"][0][0]["is_visible"] is True
assert ctx["body"][1][0]["display_value"] == "2"
assert ctx["body"][1][0]["is_visible"] is True


def test_basic_alias(styler):
styler.format_index(axis=1, alias=["alias1", "alias2"])
ctx = styler._translate(True, True)
assert ctx["head"][0][1]["value"] == "A"
assert ctx["head"][0][1]["display_value"] == "alias1" # alias
assert ctx["head"][0][2]["value"] == "B"
assert ctx["head"][0][2]["display_value"] == "alias2" # alias


def test_basic_alias_hidden_column(styler):
styler.hide(subset="A", axis=1)
styler.format_index(axis=1, alias=["alias2"])
ctx = styler._translate(True, True)
assert ctx["head"][0][1]["value"] == "A"
assert ctx["head"][0][1]["display_value"] == "A" # no alias for hidden
assert ctx["head"][0][2]["value"] == "B"
assert ctx["head"][0][2]["display_value"] == "alias2" # alias


@pytest.mark.parametrize("level", [None, 0, 1])
def test_alias_single_levels(df, level):
df.columns = MultiIndex.from_tuples([("X", "A"), ("Y", "B")])
styler = Styler(df, cell_ids=False, uuid_len=0)
styler.format_index(axis=1, level=level, alias=["alias1", "alias2"])
ctx = styler._translate(True, True)
print(ctx["head"])
assert len(ctx["head"]) == 2 # MultiIndex levels

level = 1 if level is None else level # defaults to last
assert f"level{level}" in ctx["head"][level][1]["class"]
assert ctx["head"][level][1]["display_value"] == "alias1"
assert ctx["head"][level][2]["display_value"] == "alias2"


@pytest.mark.parametrize("level", [[0, 1], [1, 0]])
def test_alias_multi_levels_order(df, level):
df.columns = MultiIndex.from_tuples([("X", "A"), ("Y", "B")])
styler = Styler(df, cell_ids=False, uuid_len=0)
styler.format_index(axis=1, level=level, alias=[["a1", "a2"], ["b1", "b2"]])
ctx = styler._translate(True, True)

assert ctx["head"][1 - level[1]][1]["display_value"] == "a1"
assert ctx["head"][1 - level[1]][2]["display_value"] == "a2"
assert ctx["head"][1 - level[0]][1]["display_value"] == "b1"
assert ctx["head"][1 - level[0]][2]["display_value"] == "b2"


@pytest.mark.parametrize(
"level, alias",
[
([0, 1], ["alias1", "alias2"]), # no sublists
([0], ["alias1"]), # too short
(None, ["alias1", "alias2", "alias3"]), # too long
([0, 1], [["alias1", "alias2"], ["alias1"]]), # sublist too short
([0, 1], [["a1", "a2"], ["a1", "a2", "a3"]]), # sublist too long
],
)
def test_alias_warning(df, level, alias):
df.columns = MultiIndex.from_tuples([("X", "A"), ("Y", "B")])
styler = Styler(df, cell_ids=False, uuid_len=0)
msg = "``alias`` must be of length equal to"
with pytest.raises(ValueError, match=msg):
styler.format_index(axis=1, level=level, alias=alias)


@pytest.mark.parametrize(
"level, alias",
[
([0, 1], [["a1", "a2"]]), # too few sublists
([0, 1], [["a1", "a2"], ["a1", "a2"], ["a1", "a2"]]), # too many sublists
],
)
def test_alias_warning2(df, level, alias):
df.columns = MultiIndex.from_tuples([("X", "A"), ("Y", "B")])
styler = Styler(df, cell_ids=False, uuid_len=0)
msg = "``level`` specifies 2 levels but the length of"
with pytest.raises(ValueError, match=msg):
styler.format_index(axis=1, level=level, alias=alias)