From d7779b600733a6eb8b47a7ed031035abaca64adc Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Thu, 2 Sep 2021 23:40:00 +0200 Subject: [PATCH 1/7] multirow naive implementation --- pandas/core/config_init.py | 2 +- pandas/io/formats/style.py | 7 ++++--- pandas/io/formats/style_render.py | 2 ++ .../tests/io/formats/style/test_to_latex.py | 19 +++++++++++++++++++ 4 files changed, 26 insertions(+), 4 deletions(-) diff --git a/pandas/core/config_init.py b/pandas/core/config_init.py index f594232f5fb6d..de4efb9ca70ae 100644 --- a/pandas/core/config_init.py +++ b/pandas/core/config_init.py @@ -888,7 +888,7 @@ def register_converter_cb(key): "latex.multirow_align", "c", styler_multirow_align, - validator=is_one_of_factory(["c", "t", "b"]), + validator=is_one_of_factory(["c", "t", "b", "naive"]), ) cf.register_option( diff --git a/pandas/io/formats/style.py b/pandas/io/formats/style.py index e6f9503a1e6f2..fa013a591a446 100644 --- a/pandas/io/formats/style.py +++ b/pandas/io/formats/style.py @@ -520,10 +520,11 @@ def to_latex( Whether to sparsify the display of a hierarchical index. Setting to False will display each explicit level element in a hierarchical key for each column. Defaults to ``pandas.options.styler.sparse.columns`` value. - multirow_align : {"c", "t", "b"}, optional + multirow_align : {"c", "t", "b", "naive"}, optional If sparsifying hierarchical MultiIndexes whether to align text centrally, - at the top or bottom. If not given defaults to - ``pandas.options.styler.latex.multirow_align`` + at the top or bottom using the multirow package. If not given defaults to + ``pandas.options.styler.latex.multirow_align``. If "naive" is given renders + without multirow. multicol_align : {"r", "c", "l"}, optional If sparsifying hierarchical MultiIndex columns whether to align text at the left, centrally, or at the right. If not given defaults to diff --git a/pandas/io/formats/style_render.py b/pandas/io/formats/style_render.py index 49952adf4fb4c..45105116d6cd7 100644 --- a/pandas/io/formats/style_render.py +++ b/pandas/io/formats/style_render.py @@ -1413,6 +1413,8 @@ def _parse_latex_header_span( colspan = int(colspan[: colspan.find('"')]) return f"\\multicolumn{{{colspan}}}{{{multicol_align}}}{{{display_val}}}" elif 'rowspan="' in attrs: + if multirow_align == "naive": + return display_val rowspan = attrs[attrs.find('rowspan="') + 9 :] rowspan = int(rowspan[: rowspan.find('"')]) return f"\\multirow[{multirow_align}]{{{rowspan}}}{{*}}{{{display_val}}}" diff --git a/pandas/tests/io/formats/style/test_to_latex.py b/pandas/tests/io/formats/style/test_to_latex.py index 671219872bb34..6b5b28014426f 100644 --- a/pandas/tests/io/formats/style/test_to_latex.py +++ b/pandas/tests/io/formats/style/test_to_latex.py @@ -241,6 +241,25 @@ def test_multiindex_row(df): assert expected == s.to_latex(sparse_index=False) +def test_multirow_naive(df): + ridx = MultiIndex.from_tuples([("A", "a"), ("A", "b"), ("B", "c")]) + df.loc[2, :] = [2, -2.22, "de"] + df = df.astype({"A": int}) + df.index = ridx + expected = dedent( + """\ + \\begin{tabular}{llrrl} + {} & {} & {A} & {B} & {C} \\\\ + A & a & 0 & -0.61 & ab \\\\ + & b & 1 & -1.22 & cd \\\\ + B & c & 2 & -2.22 & de \\\\ + \\end{tabular} + """ + ) + s = df.style.format(precision=2) + assert expected == s.to_latex(multirow_align="naive") + + def test_multiindex_row_and_col(df): cidx = MultiIndex.from_tuples([("Z", "a"), ("Z", "b"), ("Y", "c")]) ridx = MultiIndex.from_tuples([("A", "a"), ("A", "b"), ("B", "c")]) From 11ac603aca3136b0caf417998d866ead7f30dea9 Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Thu, 2 Sep 2021 23:43:58 +0200 Subject: [PATCH 2/7] multirow naive implementation --- doc/source/whatsnew/v1.4.0.rst | 1 + pandas/io/formats/style.py | 2 ++ 2 files changed, 3 insertions(+) diff --git a/doc/source/whatsnew/v1.4.0.rst b/doc/source/whatsnew/v1.4.0.rst index 2f8cb346935a9..5c2fb611aaf30 100644 --- a/doc/source/whatsnew/v1.4.0.rst +++ b/doc/source/whatsnew/v1.4.0.rst @@ -78,6 +78,7 @@ Styler - :meth:`.Styler.to_html` introduces keyword arguments ``sparse_index``, ``sparse_columns``, ``bold_headers``, ``caption`` (:issue:`41946`, :issue:`43149`). - Keyword argument ``level`` is added to :meth:`.Styler.hide_index` and :meth:`.Styler.hide_columns` for optionally controlling hidden levels in a MultiIndex (:issue:`25475`) - Global options have been extended to configure default ``Styler`` properties including formatting and encoding and mathjax options and LaTeX (:issue:`41395`) + - Naive sparsification is now possible for LaTeX without the multirow package (:issue:`43369`) Formerly Styler relied on ``display.html.use_mathjax``, which has now been replaced by ``styler.html.mathjax``. diff --git a/pandas/io/formats/style.py b/pandas/io/formats/style.py index fa013a591a446..fdac0637df31d 100644 --- a/pandas/io/formats/style.py +++ b/pandas/io/formats/style.py @@ -525,6 +525,8 @@ def to_latex( at the top or bottom using the multirow package. If not given defaults to ``pandas.options.styler.latex.multirow_align``. If "naive" is given renders without multirow. + + .. versionchanged:: 1.4.0 multicol_align : {"r", "c", "l"}, optional If sparsifying hierarchical MultiIndex columns whether to align text at the left, centrally, or at the right. If not given defaults to From 20f9b316b007afaf02fcf0f000efea55fd22f327 Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (MBP)" Date: Sat, 4 Sep 2021 21:19:50 +0200 Subject: [PATCH 3/7] fix tests --- pandas/tests/io/formats/style/test_to_latex.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pandas/tests/io/formats/style/test_to_latex.py b/pandas/tests/io/formats/style/test_to_latex.py index 7620cd1caec24..7ec9f7219c856 100644 --- a/pandas/tests/io/formats/style/test_to_latex.py +++ b/pandas/tests/io/formats/style/test_to_latex.py @@ -250,7 +250,7 @@ def test_multirow_naive(df): expected = dedent( """\ \\begin{tabular}{llrrl} - {} & {} & {A} & {B} & {C} \\\\ + & & A & B & C \\\\ A & a & 0 & -0.61 & ab \\\\ & b & 1 & -1.22 & cd \\\\ B & c & 2 & -2.22 & de \\\\ From 1732772924c2db5341c2176939b7d80d5325e45e Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Mon, 6 Sep 2021 19:36:23 +0200 Subject: [PATCH 4/7] fixture --- .../tests/io/formats/style/test_to_latex.py | 24 ++++++++++++------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/pandas/tests/io/formats/style/test_to_latex.py b/pandas/tests/io/formats/style/test_to_latex.py index 7ec9f7219c856..cc8ca912f2631 100644 --- a/pandas/tests/io/formats/style/test_to_latex.py +++ b/pandas/tests/io/formats/style/test_to_latex.py @@ -24,6 +24,14 @@ def df(): return DataFrame({"A": [0, 1], "B": [-0.61, -1.22], "C": ["ab", "cd"]}) +@pytest.fixture +def df2(df): + df2 = df.copy() + df2.loc[2, :] = [2, -2.22, "de"] + df2 = df2.astype({"A": int}) + return df2 + + @pytest.fixture def styler(df): return Styler(df, uuid_len=0, precision=2) @@ -210,11 +218,9 @@ def test_multiindex_columns(df): assert expected == s.to_latex(sparse_columns=False) -def test_multiindex_row(df): +def test_multiindex_row(df2): ridx = MultiIndex.from_tuples([("A", "a"), ("A", "b"), ("B", "c")]) - df.loc[2, :] = [2, -2.22, "de"] - df = df.astype({"A": int}) - df.index = ridx + df2.index = ridx expected = dedent( """\ \\begin{tabular}{llrrl} @@ -225,8 +231,9 @@ def test_multiindex_row(df): \\end{tabular} """ ) - s = df.style.format(precision=2) - assert expected == s.to_latex() + styler = df2.style.format(precision=2) + result = styler.to_latex() + assert expected == result # non-sparse expected = dedent( @@ -239,7 +246,8 @@ def test_multiindex_row(df): \\end{tabular} """ ) - assert expected == s.to_latex(sparse_index=False) + result = styler.to_latex(sparse_index=False) + assert expected == result def test_multirow_naive(df): @@ -709,8 +717,6 @@ def test_longtable_caption_label(styler, caption, cap_exp, label, lab_exp): def test_apply_map_header_render_mi(df, index, columns, siunitx): cidx = MultiIndex.from_tuples([("Z", "a"), ("Z", "b"), ("Y", "c")]) ridx = MultiIndex.from_tuples([("A", "a"), ("A", "b"), ("B", "c")]) - df.loc[2, :] = [2, -2.22, "de"] - df = df.astype({"A": int}) df.index, df.columns = ridx, cidx styler = df.style From 318bc7bee18bfaf7a9f204300ece16d24a84573c Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Mon, 6 Sep 2021 19:49:32 +0200 Subject: [PATCH 5/7] clean up tests with fixture --- .../tests/io/formats/style/test_to_latex.py | 114 ++++++++---------- 1 file changed, 53 insertions(+), 61 deletions(-) diff --git a/pandas/tests/io/formats/style/test_to_latex.py b/pandas/tests/io/formats/style/test_to_latex.py index cc8ca912f2631..f203e64860c77 100644 --- a/pandas/tests/io/formats/style/test_to_latex.py +++ b/pandas/tests/io/formats/style/test_to_latex.py @@ -25,11 +25,11 @@ def df(): @pytest.fixture -def df2(df): - df2 = df.copy() - df2.loc[2, :] = [2, -2.22, "de"] - df2 = df2.astype({"A": int}) - return df2 +def df_ext(df): + df = df.copy() + df.loc[2, :] = [2, -2.22, "de"] + df = df.astype({"A": int}) + return df @pytest.fixture @@ -218,9 +218,9 @@ def test_multiindex_columns(df): assert expected == s.to_latex(sparse_columns=False) -def test_multiindex_row(df2): +def test_multiindex_row(df_ext): ridx = MultiIndex.from_tuples([("A", "a"), ("A", "b"), ("B", "c")]) - df2.index = ridx + df_ext.index = ridx expected = dedent( """\ \\begin{tabular}{llrrl} @@ -231,7 +231,7 @@ def test_multiindex_row(df2): \\end{tabular} """ ) - styler = df2.style.format(precision=2) + styler = df_ext.style.format(precision=2) result = styler.to_latex() assert expected == result @@ -250,11 +250,9 @@ def test_multiindex_row(df2): assert expected == result -def test_multirow_naive(df): +def test_multirow_naive(df_ext): ridx = MultiIndex.from_tuples([("A", "a"), ("A", "b"), ("B", "c")]) - df.loc[2, :] = [2, -2.22, "de"] - df = df.astype({"A": int}) - df.index = ridx + df_ext.index = ridx expected = dedent( """\ \\begin{tabular}{llrrl} @@ -265,16 +263,15 @@ def test_multirow_naive(df): \\end{tabular} """ ) - s = df.style.format(precision=2) - assert expected == s.to_latex(multirow_align="naive") + styler = df_ext.style.format(precision=2) + result = styler.to_latex(multirow_align="naive") + assert expected == result -def test_multiindex_row_and_col(df): +def test_multiindex_row_and_col(df_ext): cidx = MultiIndex.from_tuples([("Z", "a"), ("Z", "b"), ("Y", "c")]) ridx = MultiIndex.from_tuples([("A", "a"), ("A", "b"), ("B", "c")]) - df.loc[2, :] = [2, -2.22, "de"] - df = df.astype({"A": int}) - df.index, df.columns = ridx, cidx + df_ext.index, df_ext.columns = ridx, cidx expected = dedent( """\ \\begin{tabular}{llrrl} @@ -286,8 +283,9 @@ def test_multiindex_row_and_col(df): \\end{tabular} """ ) - s = df.style.format(precision=2) - assert s.to_latex(multirow_align="b", multicol_align="l") == expected + styler = df_ext.style.format(precision=2) + result = styler.to_latex(multirow_align="b", multicol_align="l") + assert result == expected # non-sparse expected = dedent( @@ -301,16 +299,15 @@ def test_multiindex_row_and_col(df): \\end{tabular} """ ) - assert s.to_latex(sparse_index=False, sparse_columns=False) == expected + result = styler.to_latex(sparse_index=False, sparse_columns=False) + assert result == expected -def test_multi_options(df): +def test_multi_options(df_ext): cidx = MultiIndex.from_tuples([("Z", "a"), ("Z", "b"), ("Y", "c")]) ridx = MultiIndex.from_tuples([("A", "a"), ("A", "b"), ("B", "c")]) - df.loc[2, :] = [2, -2.22, "de"] - df = df.astype({"A": int}) - df.index, df.columns = ridx, cidx - styler = df.style.format(precision=2) + df_ext.index, df_ext.columns = ridx, cidx + styler = df_ext.style.format(precision=2) expected = dedent( """\ @@ -319,7 +316,8 @@ def test_multi_options(df): \\multirow[c]{2}{*}{A} & a & 0 & -0.61 & ab \\\\ """ ) - assert expected in styler.to_latex() + result = styler.to_latex() + assert expected in result with option_context("styler.latex.multicol_align", "l"): assert " & & \\multicolumn{2}{l}{Z} & Y \\\\" in styler.to_latex() @@ -338,30 +336,25 @@ def test_multiindex_columns_hidden(): assert "{tabular}{lrrr}" in s.to_latex() -def test_sparse_options(df): +@pytest.mark.parametrize( + "option, value", + [ + ("styler.sparse.index", True), + ("styler.sparse.index", False), + ("styler.sparse.columns", True), + ("styler.sparse.columns", False), + ], +) +def test_sparse_options(df_ext, option, value): cidx = MultiIndex.from_tuples([("Z", "a"), ("Z", "b"), ("Y", "c")]) ridx = MultiIndex.from_tuples([("A", "a"), ("A", "b"), ("B", "c")]) - df.loc[2, :] = [2, -2.22, "de"] - df.index, df.columns = ridx, cidx - s = df.style - - latex1 = s.to_latex() + df_ext.index, df_ext.columns = ridx, cidx + styler = df_ext.style - with option_context("styler.sparse.index", True): - latex2 = s.to_latex() - assert latex1 == latex2 - - with option_context("styler.sparse.index", False): - latex2 = s.to_latex() - assert latex1 != latex2 - - with option_context("styler.sparse.columns", True): - latex2 = s.to_latex() - assert latex1 == latex2 - - with option_context("styler.sparse.columns", False): - latex2 = s.to_latex() - assert latex1 != latex2 + latex1 = styler.to_latex() + with option_context(option, value): + latex2 = styler.to_latex() + assert (latex1 == latex2) is value def test_hidden_index(styler): @@ -379,16 +372,14 @@ def test_hidden_index(styler): @pytest.mark.parametrize("environment", ["table", "figure*", None]) -def test_comprehensive(df, environment): +def test_comprehensive(df_ext, environment): # test as many low level features simultaneously as possible cidx = MultiIndex.from_tuples([("Z", "a"), ("Z", "b"), ("Y", "c")]) ridx = MultiIndex.from_tuples([("A", "a"), ("A", "b"), ("B", "c")]) - df.loc[2, :] = [2, -2.22, "de"] - df = df.astype({"A": int}) - df.index, df.columns = ridx, cidx - s = df.style - s.set_caption("mycap") - s.set_table_styles( + df_ext.index, df_ext.columns = ridx, cidx + stlr = df_ext.style + stlr.set_caption("mycap") + stlr.set_table_styles( [ {"selector": "label", "props": ":{fig§item}"}, {"selector": "position", "props": ":h!"}, @@ -400,8 +391,8 @@ def test_comprehensive(df, environment): {"selector": "rowcolors", "props": ":{3}{pink}{}"}, # custom command ] ) - s.highlight_max(axis=0, props="textbf:--rwrap;cellcolor:[rgb]{1,1,0.6}--rwrap") - s.highlight_max(axis=None, props="Huge:--wrap;", subset=[("Z", "a"), ("Z", "b")]) + stlr.highlight_max(axis=0, props="textbf:--rwrap;cellcolor:[rgb]{1,1,0.6}--rwrap") + stlr.highlight_max(axis=None, props="Huge:--wrap;", subset=[("Z", "a"), ("Z", "b")]) expected = ( """\ @@ -425,7 +416,8 @@ def test_comprehensive(df, environment): \\end{table} """ ).replace("table", environment if environment else "table") - assert s.format(precision=2).to_latex(environment=environment) == expected + result = stlr.format(precision=2).to_latex(environment=environment) + assert result == expected def test_environment_option(styler): @@ -714,11 +706,11 @@ def test_longtable_caption_label(styler, caption, cap_exp, label, lab_exp): (False, False), ], ) -def test_apply_map_header_render_mi(df, index, columns, siunitx): +def test_apply_map_header_render_mi(df_ext, index, columns, siunitx): cidx = MultiIndex.from_tuples([("Z", "a"), ("Z", "b"), ("Y", "c")]) ridx = MultiIndex.from_tuples([("A", "a"), ("A", "b"), ("B", "c")]) - df.index, df.columns = ridx, cidx - styler = df.style + df_ext.index, df_ext.columns = ridx, cidx + styler = df_ext.style func = lambda v: "bfseries: --rwrap" if "A" in v or "Z" in v or "c" in v else None From 6fa0b356e5b3e74f7bca30d1ea436863cba436df Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Mon, 6 Sep 2021 19:52:42 +0200 Subject: [PATCH 6/7] (ivanomg req) ref tests --- pandas/tests/io/formats/style/test_to_latex.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pandas/tests/io/formats/style/test_to_latex.py b/pandas/tests/io/formats/style/test_to_latex.py index f203e64860c77..2813cbce1a67f 100644 --- a/pandas/tests/io/formats/style/test_to_latex.py +++ b/pandas/tests/io/formats/style/test_to_latex.py @@ -251,15 +251,15 @@ def test_multiindex_row(df_ext): def test_multirow_naive(df_ext): - ridx = MultiIndex.from_tuples([("A", "a"), ("A", "b"), ("B", "c")]) + ridx = MultiIndex.from_tuples([("X", "x"), ("X", "x"), ("Y", "y")]) df_ext.index = ridx expected = dedent( """\ \\begin{tabular}{llrrl} & & A & B & C \\\\ - A & a & 0 & -0.61 & ab \\\\ - & b & 1 & -1.22 & cd \\\\ - B & c & 2 & -2.22 & de \\\\ + X & x & 0 & -0.61 & ab \\\\ + & x & 1 & -1.22 & cd \\\\ + Y & y & 2 & -2.22 & de \\\\ \\end{tabular} """ ) From 237bee24f0ed5faaa01c2aba50cb17308e406ab2 Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Mon, 6 Sep 2021 23:46:03 +0200 Subject: [PATCH 7/7] ivoanovmg req --- pandas/tests/io/formats/style/test_to_latex.py | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/pandas/tests/io/formats/style/test_to_latex.py b/pandas/tests/io/formats/style/test_to_latex.py index 2813cbce1a67f..3df94c4eb0a02 100644 --- a/pandas/tests/io/formats/style/test_to_latex.py +++ b/pandas/tests/io/formats/style/test_to_latex.py @@ -25,11 +25,10 @@ def df(): @pytest.fixture -def df_ext(df): - df = df.copy() - df.loc[2, :] = [2, -2.22, "de"] - df = df.astype({"A": int}) - return df +def df_ext(): + return DataFrame( + {"A": [0, 1, 2], "B": [-0.61, -1.22, -2.22], "C": ["ab", "cd", "de"]} + ) @pytest.fixture @@ -251,15 +250,15 @@ def test_multiindex_row(df_ext): def test_multirow_naive(df_ext): - ridx = MultiIndex.from_tuples([("X", "x"), ("X", "x"), ("Y", "y")]) + ridx = MultiIndex.from_tuples([("X", "x"), ("X", "y"), ("Y", "z")]) df_ext.index = ridx expected = dedent( """\ \\begin{tabular}{llrrl} & & A & B & C \\\\ X & x & 0 & -0.61 & ab \\\\ - & x & 1 & -1.22 & cd \\\\ - Y & y & 2 & -2.22 & de \\\\ + & y & 1 & -1.22 & cd \\\\ + Y & z & 2 & -2.22 & de \\\\ \\end{tabular} """ )