From b3c55af66702f11f2a097ae4511f80f159c653b9 Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Thu, 6 Jan 2022 20:26:03 +0100 Subject: [PATCH 01/46] setup new kwarg --- pandas/io/formats/style_render.py | 39 +++++++++++++++++++++++++++---- 1 file changed, 34 insertions(+), 5 deletions(-) diff --git a/pandas/io/formats/style_render.py b/pandas/io/formats/style_render.py index 2ff0a994ebb01..dd85611122fae 100644 --- a/pandas/io/formats/style_render.py +++ b/pandas/io/formats/style_render.py @@ -1044,6 +1044,7 @@ def format_index( thousands: str | None = None, escape: str | None = None, hyperlinks: str | None = None, + aliases: list[str] | None = None, ) -> StylerRenderer: r""" Format the text display value of index labels or column headers. @@ -1055,9 +1056,10 @@ 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 ``aliases`` 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 ``aliases``. na_rep : str, optional Representation for missing values. If ``na_rep`` is None, no special formatting is applied. @@ -1079,6 +1081,14 @@ def format_index( Convert string patterns containing https://, http://, ftp:// or www. to HTML tags as clickable URL hyperlinks if "html", or LaTeX \href commands if "latex". + aliases : list of strings + This list will replace the existing index or column headers. It will also + collapse a MultiIndex to a single level displaying the alias, + which is specified by the ``level`` argument. + Cannot be used simultaneously with ``formatter`` and the associated + arguments; ``thousands``, ``decimal``, ``escape``, ``hyperlinks``, + ``na_rep`` and ``precision``. + Must be of length equal to the number of visible columns, see examples. Returns ------- @@ -1163,6 +1173,11 @@ def format_index( {} & {\textbf{123}} & {\textbf{\textasciitilde }} & {\textbf{\$\%\#}} \\ 0 & 1 & 2 & 3 \\ \end{tabular} + + Using ``aliases`` to overwrite column names. + >>> df = pd.DataFrame([[1, 2, 3]], columns=[1, 2, 3]) + >>> df.style.format_index(axis=1, aliases=["A", "B", "C"]) + """ axis = self.data._get_axis_number(axis) if axis == 0: @@ -1171,10 +1186,9 @@ def format_index( display_funcs_, obj = self._display_funcs_columns, self.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, @@ -1182,10 +1196,25 @@ def format_index( escape is None, hyperlinks is None, ) - ): + ) + + aliases_unset = aliases is None + + if formatting_args_unset and level is None and aliases_unset: display_funcs_.clear() return self # clear the formatter / revert to default and avoid looping + if not aliases_unset: + if not formatting_args_unset: + raise ValueError( + "``aliases`` cannot be supplied together with any of " + "``formatter``, ``precision``, ``decimal``, ``na_rep``, " + "``escape``, or ``hyperlinks``." + ) + else: + pass + # do the alias formatting + if not isinstance(formatter, dict): formatter = {level: formatter for level in levels_} else: From c68944a19c784aeefc5349f4e4d658c22ba3f142 Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Fri, 7 Jan 2022 18:07:32 +0100 Subject: [PATCH 02/46] method and raise on error --- pandas/io/formats/style_render.py | 76 +++++++++++++++++++++---------- 1 file changed, 53 insertions(+), 23 deletions(-) diff --git a/pandas/io/formats/style_render.py b/pandas/io/formats/style_render.py index dd85611122fae..a877fa487d32a 100644 --- a/pandas/io/formats/style_render.py +++ b/pandas/io/formats/style_render.py @@ -1182,8 +1182,10 @@ def format_index( axis = self.data._get_axis_number(axis) if axis == 0: display_funcs_, obj = self._display_funcs_index, self.index + hidden_lvls, hidden_labels = self.hide_index_, self.hidden_rows else: display_funcs_, obj = self._display_funcs_columns, self.columns + hidden_lvls, hidden_labels = self.hide_columns_, self.hidden_columns levels_ = refactor_levels(level, obj) formatting_args_unset = all( @@ -1201,10 +1203,10 @@ def format_index( aliases_unset = aliases is None if formatting_args_unset and level is None and aliases_unset: + # 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 aliases_unset: + elif not aliases_unset: # then apply a formatting function from arg: aliases if not formatting_args_unset: raise ValueError( "``aliases`` cannot be supplied together with any of " @@ -1212,30 +1214,58 @@ def format_index( "``escape``, or ``hyperlinks``." ) else: - pass - # do the alias formatting + if level is None: + level = obj.nlevels - 1 # default to last level - 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() - } + if not isinstance(level, (str, int)): + raise ValueError("``level`` must identify only a single level") + else: + if len(aliases) != len(obj) - len(set(hidden_labels)): + raise ValueError( + "``aliases`` must have length equal to the " + "number of visible labels along ``axis``" + ) - 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 + + level = obj._get_level_number(level) + for lvl in range(obj.nlevels): + if lvl != level: # hide unidentified levels using + hidden_lvls[lvl] = True # alias: works on Index and MultiI + for ai, idx in enumerate( + [ + (i, level) if axis == 0 else (level, i) + for i in range(len(obj)) + if i not in hidden_labels + ] + ): + display_funcs_[idx] = partial(alias, value=aliases[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 From fcac539c23fe7ec44753ed61c02f84b431d390e0 Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Sun, 9 Jan 2022 11:50:24 +0100 Subject: [PATCH 03/46] add basic tests --- pandas/tests/io/formats/style/test_format.py | 34 ++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/pandas/tests/io/formats/style/test_format.py b/pandas/tests/io/formats/style/test_format.py index 5207be992d606..38de3632eb960 100644 --- a/pandas/tests/io/formats/style/test_format.py +++ b/pandas/tests/io/formats/style/test_format.py @@ -434,3 +434,37 @@ 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, aliases=["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, aliases=["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_collapse_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, aliases=["alias1", "alias2"]) + ctx = styler._translate(True, True) + print(ctx["head"]) + assert len(ctx["head"]) == 1 # MultiIndex collapsed to one level + + level = 1 if level is None else level # defaults to last + assert f"level{level}" in ctx["head"][0][1]["class"] + assert ctx["head"][0][1]["display_value"] == "alias1" + assert ctx["head"][0][2]["display_value"] == "alias2" From 55010b5811ba80496c4d635586909451881747da Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Sun, 9 Jan 2022 19:11:39 +0100 Subject: [PATCH 04/46] add basic tests --- pandas/io/formats/style_render.py | 71 +++++++++++++------- pandas/tests/io/formats/style/test_format.py | 56 +++++++++++++-- 2 files changed, 97 insertions(+), 30 deletions(-) diff --git a/pandas/io/formats/style_render.py b/pandas/io/formats/style_render.py index a877fa487d32a..c50fde124e13e 100644 --- a/pandas/io/formats/style_render.py +++ b/pandas/io/formats/style_render.py @@ -1060,6 +1060,8 @@ def format_index( headers. level : int, str, list The level(s) over which to apply the generic ``formatter``, or ``aliases``. + In the case of ``aliases`` 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. @@ -1081,14 +1083,15 @@ def format_index( Convert string patterns containing https://, http://, ftp:// or www. to HTML tags as clickable URL hyperlinks if "html", or LaTeX \href commands if "latex". - aliases : list of strings - This list will replace the existing index or column headers. It will also - collapse a MultiIndex to a single level displaying the alias, - which is specified by the ``level`` argument. + aliases : 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``. - Must be of length equal to the number of visible columns, see examples. + This list (or each sub-list) must be of length equal to the number of + visible columns, see examples. Returns ------- @@ -1176,16 +1179,31 @@ def format_index( Using ``aliases`` to overwrite column names. >>> df = pd.DataFrame([[1, 2, 3]], columns=[1, 2, 3]) - >>> df.style.format_index(axis=1, aliases=["A", "B", "C"]) + >>> df.style.format_index(axis=1, aliases=["A", "B", "C"]) # doctest: +SKIP + A B C + 0 1 2 3 + Using ``aliases`` 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 + 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, aliases=["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_lvls, hidden_labels = self.hide_index_, self.hidden_rows + hidden_labels = self.hidden_rows else: display_funcs_, obj = self._display_funcs_columns, self.columns - hidden_lvls, hidden_labels = self.hide_columns_, self.hidden_columns + hidden_labels = self.hidden_columns levels_ = refactor_levels(level, obj) formatting_args_unset = all( @@ -1214,33 +1232,36 @@ def format_index( "``escape``, or ``hyperlinks``." ) else: + visible_len = len(obj) - len(set(hidden_labels)) if level is None: - level = obj.nlevels - 1 # default to last level + levels_ = [obj.nlevels - 1] # default to last level + elif len(levels_) > 1 and len(aliases) != len(levels_): + raise ValueError( + f"``level`` specifies {len(levels_)} levels but the length of " + f"``aliases``, {len(aliases)}, does not match." + ) - if not isinstance(level, (str, int)): - raise ValueError("``level`` must identify only a single level") - else: - if len(aliases) != len(obj) - len(set(hidden_labels)): + def alias(x, value): + return value + + for i, lvl in enumerate(levels_): + level_aliases = aliases[i] if len(levels_) > 1 else aliases + if len(level_aliases) != visible_len: raise ValueError( - "``aliases`` must have length equal to the " - "number of visible labels along ``axis``" + "``aliases`` must be of length equal to the number of " + "visible labels along ``axis``. If ``level`` is given and " + "contains more than one level ``aliases`` should be a " + "list of lists with each sub-list having length equal to" + "the number of visible labels along ``axis``." ) - - def alias(x, value): - return value - - level = obj._get_level_number(level) - for lvl in range(obj.nlevels): - if lvl != level: # hide unidentified levels using - hidden_lvls[lvl] = True # alias: works on Index and MultiI for ai, idx in enumerate( [ - (i, level) if axis == 0 else (level, i) + (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=aliases[ai]) + display_funcs_[idx] = partial(alias, value=level_aliases[ai]) else: # then apply a formatting function from arg: formatter if not isinstance(formatter, dict): diff --git a/pandas/tests/io/formats/style/test_format.py b/pandas/tests/io/formats/style/test_format.py index 38de3632eb960..667e18d2d3701 100644 --- a/pandas/tests/io/formats/style/test_format.py +++ b/pandas/tests/io/formats/style/test_format.py @@ -456,15 +456,61 @@ def test_basic_alias_hidden_column(styler): @pytest.mark.parametrize("level", [None, 0, 1]) -def test_alias_collapse_levels(df, level): +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, aliases=["alias1", "alias2"]) ctx = styler._translate(True, True) print(ctx["head"]) - assert len(ctx["head"]) == 1 # MultiIndex collapsed to one level + 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"][0][1]["class"] - assert ctx["head"][0][1]["display_value"] == "alias1" - assert ctx["head"][0][2]["display_value"] == "alias2" + 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, aliases=[["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, aliases", + [ + ([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, aliases): + df.columns = MultiIndex.from_tuples([("X", "A"), ("Y", "B")]) + styler = Styler(df, cell_ids=False, uuid_len=0) + msg = "``aliases`` must be of length equal to" + with pytest.raises(ValueError, match=msg): + styler.format_index(axis=1, level=level, aliases=aliases) + + +@pytest.mark.parametrize( + "level, aliases", + [ + ([0, 1], [["a1", "a2"]]), # too few sublists + ([0, 1], [["a1", "a2"], ["a1", "a2"], ["a1", "a2"]]), # too many sublists + ], +) +def test_alias_warning2(df, level, aliases): + 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, aliases=aliases) From 9500c06c08815c91665013f6486e1bf02ef68015 Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Sun, 9 Jan 2022 19:19:11 +0100 Subject: [PATCH 05/46] extend docs --- pandas/io/formats/style_render.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/pandas/io/formats/style_render.py b/pandas/io/formats/style_render.py index c50fde124e13e..069908161214b 100644 --- a/pandas/io/formats/style_render.py +++ b/pandas/io/formats/style_render.py @@ -1093,6 +1093,8 @@ def format_index( This list (or each sub-list) must be of length equal to the number of visible columns, see examples. + .. versionadded:: 1.5.0 + Returns ------- self : Styler @@ -1123,6 +1125,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 ``aliases`` provides the + ability to automate this, across individual index levels if necessary. + Examples -------- Using ``na_rep`` and ``precision`` with the default ``formatter`` From 38ea09c7f44c2ac5db76d4021ecbd29cef95d1a2 Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Sun, 9 Jan 2022 19:25:13 +0100 Subject: [PATCH 06/46] mypy fix --- pandas/io/formats/style_render.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/pandas/io/formats/style_render.py b/pandas/io/formats/style_render.py index 069908161214b..b2c771bcc64c4 100644 --- a/pandas/io/formats/style_render.py +++ b/pandas/io/formats/style_render.py @@ -1044,7 +1044,7 @@ def format_index( thousands: str | None = None, escape: str | None = None, hyperlinks: str | None = None, - aliases: list[str] | None = None, + aliases: list[str] | list[list[str]] | None = None, ) -> StylerRenderer: r""" Format the text display value of index labels or column headers. @@ -1224,13 +1224,11 @@ def format_index( ) ) - aliases_unset = aliases is None - - if formatting_args_unset and level is None and aliases_unset: + if formatting_args_unset and level is None and aliases is None: # clear the formatter / revert to default and avoid looping display_funcs_.clear() - elif not aliases_unset: # then apply a formatting function from arg: aliases + elif aliases is not None: # then apply a formatting function from arg: aliases if not formatting_args_unset: raise ValueError( "``aliases`` cannot be supplied together with any of " From f48b43c2517e23a2845f995ade4c8cba2d028c8a Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Sun, 9 Jan 2022 21:19:32 +0100 Subject: [PATCH 07/46] whats new --- doc/source/whatsnew/v1.5.0.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/doc/source/whatsnew/v1.5.0.rst b/doc/source/whatsnew/v1.5.0.rst index e723918ad8b4b..a46852f75fe18 100644 --- a/doc/source/whatsnew/v1.5.0.rst +++ b/doc/source/whatsnew/v1.5.0.rst @@ -20,6 +20,7 @@ Styler ^^^^^^ - New method :meth:`.Styler.to_string` for alternative customisable output methods (:issue:`44502`) + - New keyword argument ``aliases`` added to :meth:`.Styler.format_index` to allow simple label string replacement (:issue:`45288`) .. _whatsnew_150.enhancements.enhancement2: From 9bcc32095f1ad17694945fd8c42acce065d0ff32 Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Mon, 10 Jan 2022 06:53:11 +0100 Subject: [PATCH 08/46] doctest skip --- pandas/io/formats/style_render.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pandas/io/formats/style_render.py b/pandas/io/formats/style_render.py index 67c793081804e..74cec3be7bd6d 100644 --- a/pandas/io/formats/style_render.py +++ b/pandas/io/formats/style_render.py @@ -1211,7 +1211,7 @@ def format_index( Using ``aliases`` 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 + >>> styler = df.style # doctest: +SKIP 1 2 3 X X X 0 1 2 3 From a4b5c9aadb55f20d4b83c5530525416cd98edaad Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Mon, 10 Jan 2022 21:44:56 +0100 Subject: [PATCH 09/46] fix tests --- pandas/core/generic.py | 354 +++++++++++++++++++---- pandas/io/formats/style.py | 2 + pandas/tests/io/formats/test_to_latex.py | 96 +++--- 3 files changed, 353 insertions(+), 99 deletions(-) diff --git a/pandas/core/generic.py b/pandas/core/generic.py index 58e967e4c7899..ca7bdf2a932b6 100644 --- a/pandas/core/generic.py +++ b/pandas/core/generic.py @@ -3161,25 +3161,30 @@ def to_latex( buf=None, columns=None, col_space=None, - header=True, - index=True, - na_rep="NaN", - formatters=None, - float_format=None, - sparsify=None, - index_names=True, - bold_rows=False, - column_format=None, - longtable=None, - escape=None, - encoding=None, - decimal=".", - multicolumn=None, - multicolumn_format=None, - multirow=None, - caption=None, - label=None, - position=None, + header=None, + index=None, + na_rep=None, # merge to format: use Styler + formatters=None, # fully deprecated: fallback + float_format=None, # fully deprecated: fallback + sparsify=None, # merge to render_kwargs: use Styler + index_names=None, + bold_rows=None, + column_format=None, # merge to render_kwargs: use Styler + longtable=None, # merge to render_kwargs: use Styler + escape=None, # merge to render_kwargs: use Styler + encoding=None, # merge to render_kwargs: use Styler + decimal=".", # merge to format: use Styler + multicolumn=None, # merge to render_kwargs: use Styler + multicolumn_format=None, # merge to render_kwargs: use Styler + multirow=None, # merge to render_kwargs: use Styler + caption=None, # merge to render_kwargs: use Styler + label=None, # merge to render_kwargs: use Styler + position=None, # merge to render_kwargs: use Styler + *, + hide: dict | list[dict] = None, + format: dict | list[dict] = None, + format_index: dict | list[dict] = None, + render_kwargs: dict | None = None, ): r""" Render object to a LaTeX tabular, longtable, or nested table. @@ -3298,14 +3303,16 @@ def to_latex( \bottomrule \end{{tabular}} """ - msg = ( - "In future versions `DataFrame.to_latex` is expected to utilise the base " - "implementation of `Styler.to_latex` for formatting and rendering. " - "The arguments signature may therefore change. It is recommended instead " - "to use `DataFrame.style.to_latex` which also contains additional " - "functionality." + fallback_arg_used = any( + [formatters is not None, float_format is not None, col_space is not None] ) - warnings.warn(msg, FutureWarning, stacklevel=find_stack_level()) + + # reset defaults + index = True if index is None else index + na_rep = "NaN" if na_rep is None else na_rep + header = True if header is None else header + bold_rows = True if bold_rows is None else bold_rows + index_names = True if index_names is None else index_names # Get defaults from the pandas config if self.ndim == 1: @@ -3322,34 +3329,279 @@ def to_latex( multirow = config.get_option("display.latex.multirow") self = cast("DataFrame", self) - formatter = DataFrameFormatter( + if fallback_arg_used: + msg = "USING A DEPRECATED FALLBACK ARG" + warnings.warn(msg, FutureWarning, stacklevel=find_stack_level()) + # use original DataFrame Latex Renderer + formatter = DataFrameFormatter( + self, + columns=columns, + col_space=col_space, + na_rep=na_rep, + header=header, + index=index, + formatters=formatters, + float_format=float_format, + bold_rows=bold_rows, + sparsify=sparsify, + index_names=index_names, + escape=escape, + decimal=decimal, + ) + return DataFrameRenderer(formatter).to_latex( + buf=buf, + column_format=column_format, + longtable=longtable, + encoding=encoding, + multicolumn=multicolumn, + multicolumn_format=multicolumn_format, + multirow=multirow, + caption=caption, + label=label, + position=position, + ) + else: + # use styler implementation refactoring original kwargs + if hide is None: + hide = [] + elif isinstance(hide, dict): + hide = [hide] + + if columns: + hide.append( + { + "subset": [c for c in self.columns if c not in columns], + "axis": "columns", + } + ) + if header is False: + hide.append({"names": False, "axis": "columns"}) + if index is False: + hide.append({"names": False, "axis": "index"}) + + format_ = { + "na_rep": na_rep, + "escape": "latex" if escape else None, + "decimal": decimal, + } + if format is None: + format = [format_] + elif isinstance(format, dict): + format = [format_, format] + else: + format = [format_].extend(format) + + render_kwargs = { + "hrules": True, + "sparse_index": sparsify, + "sparse_columns": sparsify, + "environment": "longtable" if longtable else None, + "column_format": column_format, + "multicol_align": multicolumn_format + if multicolumn + else f"naive-{multicolumn_format}", + "multirow_align": "t" if multirow else "naive", + "encoding": encoding, + "caption": caption, + "label": label, + "position": position, + "column_format": column_format, + } + + return self._to_latex_via_styler2( + buf, hide=hide, format=format, render_kwargs=render_kwargs + ) + + def _to_latex_via_styler2( + self, + buf=None, + *, + hide: dict | list[dict] = None, + format: dict | list[dict] = None, + format_index: dict | list[dict] = None, + render_kwargs: dict | None = None, + ): + from pandas.io.formats.style import Styler + + styler = Styler( self, - columns=columns, - col_space=col_space, - na_rep=na_rep, - header=header, - index=index, - formatters=formatters, - float_format=float_format, - bold_rows=bold_rows, - sparsify=sparsify, - index_names=index_names, - escape=escape, - decimal=decimal, - ) - return DataFrameRenderer(formatter).to_latex( - buf=buf, - column_format=column_format, - longtable=longtable, - encoding=encoding, - multicolumn=multicolumn, - multicolumn_format=multicolumn_format, - multirow=multirow, - caption=caption, - label=label, - position=position, + uuid="", ) + for kw_name in ["hide", "format", "format_index"]: + kw = vars()[kw_name] + if isinstance(kw, dict): + getattr(styler, kw_name)(**kw) + elif isinstance(kw, list): + for sub_kw in kw: + getattr(styler, kw_name)(**sub_kw) + + return styler.to_latex(**{"buf": buf, **render_kwargs}) + + def _to_latex_via_styler( + self, + buf=None, + *, + hrules=True, + columns=None, + column_format=None, + position=None, + position_float=None, + caption=None, + label=None, + index=True, + header=True, + index_names="all", + sparse_index=None, + sparse_columns=None, + multirow_align=None, + multicol_align=None, + siunitx=False, + environment=None, + formatter=None, + precision=None, + na_rep=None, + decimal=None, + thousands=None, + escape=None, + bold_header="none", + encoding=None, + ): + r""" + Render object to a LaTeX tabular, longtable, or nested table/tabular. + + Parameters + ---------- + buf : str, Path or StringIO-like, optional + Buffer to write to. If `None`, the output is returned as a string. + hrules : bool + Set to `False` to exclude `\\toprule`, `\\midrule` and `\\bottomrule` + from the `booktabs` LaTeX package. + columns : list of label, optional + The subset of columns to write. Writes all columns by default. + column_format : str, optional + The LaTeX column specification placed in location: + `\\begin{{tabular}}{{}}` + Defaults to 'l' for index and + non-numeric data columns, and, for numeric data columns, + to 'r' by default, or 'S' if ``siunitx`` is `True`. + position : str, optional + The LaTeX positional argument (e.g. 'h!') for tables, placed in location: + `\\begin{{table}}[]`. + position_float : {{"centering", "raggedleft", "raggedright"}}, optional + The LaTeX float command placed in location: + `\\begin{{table}}[]` + `\\` + Cannot be used if ``environment`` is "longtable". + caption : str, tuple, optional + If string, then table caption included as: `\\caption{{}}`. + If tuple, i.e ("full caption", "short caption"), the caption included + as: + `\\caption[]{{}}`. + label : str, optional + The LaTeX label included as: `\\label{{