From 02d7d0c8e309b840532193c53734d84cb91ee53c Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Wed, 3 Aug 2022 20:58:08 +0200 Subject: [PATCH 01/24] Base implementation --- pandas/core/generic.py | 212 +++++++-- pandas/io/formats/style.py | 8 +- pandas/io/formats/style_render.py | 2 +- pandas/tests/io/formats/test_to_latex.py | 580 +++++++++++------------ 4 files changed, 465 insertions(+), 337 deletions(-) diff --git a/pandas/core/generic.py b/pandas/core/generic.py index caad4b45216ed..e40e872af0a75 100644 --- a/pandas/core/generic.py +++ b/pandas/core/generic.py @@ -171,7 +171,6 @@ Window, ) -from pandas.io.formats import format as fmt from pandas.io.formats.format import ( DataFrameFormatter, DataFrameRenderer, @@ -2121,7 +2120,7 @@ def _repr_latex_(self): Returns a LaTeX representation for a particular object. Mainly for use with nbconvert (jupyter notebook conversion to pdf). """ - if config.get_option("display.latex.repr"): + if config.get_option("styler.render.repr") == "latex": return self.to_latex() else: return None @@ -3225,7 +3224,6 @@ def to_latex( ... @final - @doc(returns=fmt.return_docstring) def to_latex( self, buf: FilePath | WriteBuffer[str] | None = None, @@ -3368,15 +3366,6 @@ 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." - ) - warnings.warn(msg, FutureWarning, stacklevel=find_stack_level()) - # Get defaults from the pandas config if self.ndim == 1: self = self.to_frame() @@ -3391,35 +3380,180 @@ def to_latex( if multirow is None: multirow = config.get_option("display.latex.multirow") - self = cast("DataFrame", self) - 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, + # Refactor formatters/float_format/decimal/na_rep/escape to Styler structure + base_format_ = { + "na_rep": na_rep, + "escape": "latex" if escape else None, + "decimal": decimal, + } + index_format_: dict[str, Any] = {"axis": 0, **base_format_} + column_format_: dict[str, Any] = {"axis": 1, **base_format_} + + if isinstance(float_format, str): + float_format_ = lambda x: float_format % x + else: + float_format_ = float_format + + def _wrap(x, alt_format_): + if isinstance(x, (float, complex)) and float_format_ is not None: + return float_format_(x) + else: + return alt_format_(x) + + if isinstance(formatters, list): + formatters = { + c: functools.partial(_wrap, alt_format_=formatters[i]) + for i, c in enumerate(self.columns) + } + elif isinstance(formatters, dict): + index_formatter = formatters.pop("__index__", None) + column_formatter = formatters.pop("__columns__", None) + if index_formatter is not None: + index_format_.update({"formatter": index_formatter}) + if column_formatter is not None: + column_format_.update({"formatter": column_formatter}) + + formatters = { + k: functools.partial(_wrap, alt_format_=v) + for k, v in formatters.items() + } + elif formatters is None and float_format is not None: + formatters = functools.partial(_wrap, alt_format_=lambda v: v) + else: + formatters = None + + hide, relabel = [], [] + if columns: + hide.append( + { + "subset": [c for c in self.columns if c not in columns], + "axis": "columns", + } + ) + if header is False: + hide.append({"axis": "columns"}) + elif isinstance(header, (list, tuple)): + relabel = {"labels": header, "axis": "columns"} + + if index is False: + hide.append({"axis": "index"}) + if index_names is False: + hide.append({"names": True, "axis": "index"}) + + 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, + "clines": "skip-last;data" if multirow else None, + "bold_rows": bold_rows, + } + + return self._to_latex_via_styler( + buf, + hide=hide, + relabel=relabel, + format={"formatter": formatters, **base_format_}, + format_index=[index_format_, column_format_], + render_kwargs=render_kwargs, ) + def _to_latex_via_styler( + self, + buf=None, + *, + hide: dict | list[dict] | None = None, + relabel: dict | list[dict] | None = None, + format: dict | list[dict] | None = None, + format_index: dict | list[dict] | None = None, + render_kwargs: dict = {}, + ): + """ + Render object to a LaTeX tabular, longtable, or nested table. + Uses the ``Styler`` implementation with the following, ordered, method chaining: + .. code-block:: python + styler = Styler(DataFrame) + styler.hide(**hide) + styler.format(**format) + styler.format_index(**format_index) + styler.to_latex(buf=buf, **render_kwargs) + Parameters + ---------- + buf : str, Path or StringIO-like, optional, default None + Buffer to write to. If None, the output is returned as a string. + hide : dict, list of dict + Keyword args to pass to the method call of ``Styler.hide``. If a list will + call the method numerous times. + relabel : dict, list of dict + Keyword args to pass to the method of ``Styler.relabel_index``. If a list + will call the method numerous times. + format : dict, list of dict + Keyword args to pass to the method call of ``Styler.format``. If a list will + call the method numerous times. + format_index : dict, list of dict + Keyword args to pass to the method call of ``Styler.format_index``. If a + list will call the method numerous times. + render_kwargs : dict + Keyword args to pass to the method call of ``Styler.to_latex``. + Returns + ------- + str or None + If buf is None, returns the result as a string. Otherwise returns None. + See Also + -------- + Styler.to_latex : Render a DataFrame to LaTeX with conditional formatting. + DataFrame.to_string : Render a DataFrame to a console-friendly + tabular output. + DataFrame.to_html : Render a DataFrame as an HTML table. + Examples + -------- + Convert a general DataFrame to LaTeX with formatting: + >>> df = pd.DataFrame(dict(name=['Raphael', 'Donatello'], + ... age=[26, 45], + ... height=[181.23, 177.65])) + >>> print(df.to_latex(hide={"axis": "index"}, + ... format={"formatter": {"name": str.upper}, + ... "precision": 1}, + ... render_kwargs={"hrules": True} + ... ) # doctest: +SKIP + \begin{{tabular}}{{lrr}} + \toprule + name & age & height \\ + \\midrule + RAPHAEL & 26 & 181.2 \\ + DONATELLO & 45 & 177.7 \\ + \bottomrule + \\end{{tabular}} + """ + from pandas.io.formats.style import Styler + + self = cast("DataFrame", self) + styler = Styler(self, uuid="") + + for kw_name in ["hide", "relabel", "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) + + # bold_rows is not a direct kwarg of Styler.to_latex + if render_kwargs.pop("bold_rows"): + styler.applymap_index(lambda v: "textbf:--rwrap;") + + return styler.to_latex(buf=buf, **render_kwargs) + @overload def to_csv( self, diff --git a/pandas/io/formats/style.py b/pandas/io/formats/style.py index df246ad30d806..e3364084ea5fd 100644 --- a/pandas/io/formats/style.py +++ b/pandas/io/formats/style.py @@ -236,7 +236,7 @@ def __init__( precision: int | None = None, table_styles: CSSStyles | None = None, uuid: str | None = None, - caption: str | tuple | None = None, + caption: str | tuple | list | None = None, table_attributes: str | None = None, cell_ids: bool = True, na_rep: str | None = None, @@ -2336,13 +2336,13 @@ def set_uuid(self, uuid: str) -> Styler: self.uuid = uuid return self - def set_caption(self, caption: str | tuple) -> Styler: + def set_caption(self, caption: str | tuple | list) -> Styler: """ Set the text added to a ```` HTML element. Parameters ---------- - caption : str, tuple + caption : str, tuple, list For HTML output either the string input is used or the first element of the tuple. For LaTeX the string input provides a caption and the additional tuple input allows for full captions and short captions, in that order. @@ -2352,7 +2352,7 @@ def set_caption(self, caption: str | tuple) -> Styler: self : Styler """ msg = "`caption` must be either a string or 2-tuple of strings." - if isinstance(caption, tuple): + if isinstance(caption, (list, tuple)): if ( len(caption) != 2 or not isinstance(caption[0], str) diff --git a/pandas/io/formats/style_render.py b/pandas/io/formats/style_render.py index 7631ae2405585..cf2e0be3f3eee 100644 --- a/pandas/io/formats/style_render.py +++ b/pandas/io/formats/style_render.py @@ -85,7 +85,7 @@ def __init__( uuid_len: int = 5, table_styles: CSSStyles | None = None, table_attributes: str | None = None, - caption: str | tuple | None = None, + caption: str | tuple | list | None = None, cell_ids: bool = True, precision: int | None = None, ) -> None: diff --git a/pandas/tests/io/formats/test_to_latex.py b/pandas/tests/io/formats/test_to_latex.py index f8015851c9a83..aa0f41f4470ff 100644 --- a/pandas/tests/io/formats/test_to_latex.py +++ b/pandas/tests/io/formats/test_to_latex.py @@ -19,7 +19,10 @@ RowStringConverter, ) -pytestmark = pytest.mark.filterwarnings("ignore::FutureWarning") +pytest.importorskip("jinja2") +pytestmark = pytest.mark.filterwarnings( + "ignore:Deprecated arguments supplied:FutureWarning" +) def _dedent(string): @@ -68,10 +71,10 @@ def test_to_latex_tabular_with_index(self): r""" \begin{tabular}{lrl} \toprule - {} & a & b \\ + & a & b \\ \midrule - 0 & 1 & b1 \\ - 1 & 2 & b2 \\ + 0 & 1 & b1 \\ + 1 & 2 & b2 \\ \bottomrule \end{tabular} """ @@ -85,10 +88,10 @@ def test_to_latex_tabular_without_index(self): r""" \begin{tabular}{rl} \toprule - a & b \\ + a & b \\ \midrule - 1 & b1 \\ - 2 & b2 \\ + 1 & b1 \\ + 2 & b2 \\ \bottomrule \end{tabular} """ @@ -101,7 +104,7 @@ def test_to_latex_tabular_without_index(self): ) def test_to_latex_bad_column_format(self, bad_column_format): df = DataFrame({"a": [1, 2], "b": ["b1", "b2"]}) - msg = r"column_format must be str or unicode" + msg = r"`column_format` must be str or unicode" with pytest.raises(ValueError, match=msg): df.to_latex(column_format=bad_column_format) @@ -116,10 +119,10 @@ def test_to_latex_column_format(self): r""" \begin{tabular}{lcr} \toprule - {} & a & b \\ + & a & b \\ \midrule - 0 & 1 & b1 \\ - 1 & 2 & b2 \\ + 0 & 1 & b1 \\ + 1 & 2 & b2 \\ \bottomrule \end{tabular} """ @@ -134,10 +137,10 @@ def test_to_latex_float_format_object_col(self): r""" \begin{tabular}{ll} \toprule - {} & 0 \\ + & 0 \\ \midrule 0 & 1,000 \\ - 1 & test \\ + 1 & test \\ \bottomrule \end{tabular} """ @@ -151,9 +154,7 @@ def test_to_latex_empty_tabular(self): r""" \begin{tabular}{l} \toprule - Empty DataFrame - Columns: Index([], dtype='object') - Index: Index([], dtype='object') \\ + \midrule \bottomrule \end{tabular} """ @@ -167,11 +168,11 @@ def test_to_latex_series(self): r""" \begin{tabular}{ll} \toprule - {} & 0 \\ + & 0 \\ \midrule - 0 & a \\ - 1 & b \\ - 2 & c \\ + 0 & a \\ + 1 & b \\ + 2 & c \\ \bottomrule \end{tabular} """ @@ -187,10 +188,10 @@ def test_to_latex_midrule_location(self): r""" \begin{tabular}{lr} \toprule - {} & a \\ + & a \\ \midrule - 0 & 1 \\ - 1 & 2 \\ + 0 & 1 \\ + 1 & 2 \\ \bottomrule \end{tabular} """ @@ -206,9 +207,17 @@ def test_to_latex_empty_longtable(self): r""" \begin{longtable}{l} \toprule - Empty DataFrame - Columns: Index([], dtype='object') - Index: Index([], dtype='object') \\ + \midrule + \endfirsthead + \toprule + \midrule + \endhead + \midrule + \multicolumn{1}{r}{Continued on next page} \\ + \midrule + \endfoot + \bottomrule + \endlastfoot \end{longtable} """ ) @@ -221,23 +230,21 @@ def test_to_latex_longtable_with_index(self): r""" \begin{longtable}{lrl} \toprule - {} & a & b \\ + & a & b \\ \midrule \endfirsthead - \toprule - {} & a & b \\ + & a & b \\ \midrule \endhead \midrule - \multicolumn{3}{r}{{Continued on next page}} \\ + \multicolumn{3}{r}{Continued on next page} \\ \midrule \endfoot - \bottomrule \endlastfoot - 0 & 1 & b1 \\ - 1 & 2 & b2 \\ + 0 & 1 & b1 \\ + 1 & 2 & b2 \\ \end{longtable} """ ) @@ -250,23 +257,21 @@ def test_to_latex_longtable_without_index(self): r""" \begin{longtable}{rl} \toprule - a & b \\ + a & b \\ \midrule \endfirsthead - \toprule - a & b \\ + a & b \\ \midrule \endhead \midrule - \multicolumn{2}{r}{{Continued on next page}} \\ + \multicolumn{2}{r}{Continued on next page} \\ \midrule \endfoot - \bottomrule \endlastfoot - 1 & b1 \\ - 2 & b2 \\ + 1 & b1 \\ + 2 & b2 \\ \end{longtable} """ ) @@ -294,8 +299,9 @@ def test_to_latex_no_header_with_index(self): r""" \begin{tabular}{lrl} \toprule - 0 & 1 & b1 \\ - 1 & 2 & b2 \\ + \midrule + 0 & 1 & b1 \\ + 1 & 2 & b2 \\ \bottomrule \end{tabular} """ @@ -310,6 +316,7 @@ def test_to_latex_no_header_without_index(self): r""" \begin{tabular}{rl} \toprule + \midrule 1 & b1 \\ 2 & b2 \\ \bottomrule @@ -326,10 +333,10 @@ def test_to_latex_specified_header_with_index(self): r""" \begin{tabular}{lrl} \toprule - {} & AA & BB \\ + & AA & BB \\ \midrule - 0 & 1 & b1 \\ - 1 & 2 & b2 \\ + 0 & 1 & b1 \\ + 1 & 2 & b2 \\ \bottomrule \end{tabular} """ @@ -346,8 +353,8 @@ def test_to_latex_specified_header_without_index(self): \toprule AA & BB \\ \midrule - 1 & b1 \\ - 2 & b2 \\ + 1 & b1 \\ + 2 & b2 \\ \bottomrule \end{tabular} """ @@ -370,7 +377,7 @@ def test_to_latex_number_of_items_in_header_missmatch_raises( ): # GH 7124 df = DataFrame({"a": [1, 2], "b": ["b1", "b2"]}) - msg = f"Writing 2 cols but got {num_aliases} aliases" + msg = "``aliases`` must be of length equal to the number of visible labels" with pytest.raises(ValueError, match=msg): df.to_latex(header=header) @@ -382,10 +389,10 @@ def test_to_latex_decimal(self): r""" \begin{tabular}{lrl} \toprule - {} & a & b \\ + & a & b \\ \midrule - 0 & 1,0 & b1 \\ - 1 & 2,1 & b2 \\ + 0 & 1,000000 & b1 \\ + 1 & 2,100000 & b2 \\ \bottomrule \end{tabular} """ @@ -402,10 +409,10 @@ def test_to_latex_bold_rows(self): r""" \begin{tabular}{lrl} \toprule - {} & a & b \\ + & a & b \\ \midrule - \textbf{0} & 1 & b1 \\ - \textbf{1} & 2 & b2 \\ + \textbf{0} & 1 & b1 \\ + \textbf{1} & 2 & b2 \\ \bottomrule \end{tabular} """ @@ -420,10 +427,10 @@ def test_to_latex_no_bold_rows(self): r""" \begin{tabular}{lrl} \toprule - {} & a & b \\ + & a & b \\ \midrule - 0 & 1 & b1 \\ - 1 & 2 & b2 \\ + 0 & 1 & b1 \\ + 1 & 2 & b2 \\ \bottomrule \end{tabular} """ @@ -463,14 +470,13 @@ def test_to_latex_caption_only(self, df_short, caption_table): expected = _dedent( r""" \begin{table} - \centering \caption{a table in a \texttt{table/tabular} environment} \begin{tabular}{lrl} \toprule - {} & a & b \\ + & a & b \\ \midrule - 0 & 1 & b1 \\ - 1 & 2 & b2 \\ + 0 & 1 & b1 \\ + 1 & 2 & b2 \\ \bottomrule \end{tabular} \end{table} @@ -484,14 +490,13 @@ def test_to_latex_label_only(self, df_short, label_table): expected = _dedent( r""" \begin{table} - \centering \label{tab:table_tabular} \begin{tabular}{lrl} \toprule - {} & a & b \\ + & a & b \\ \midrule - 0 & 1 & b1 \\ - 1 & 2 & b2 \\ + 0 & 1 & b1 \\ + 1 & 2 & b2 \\ \bottomrule \end{tabular} \end{table} @@ -505,15 +510,14 @@ def test_to_latex_caption_and_label(self, df_short, caption_table, label_table): expected = _dedent( r""" \begin{table} - \centering \caption{a table in a \texttt{table/tabular} environment} \label{tab:table_tabular} \begin{tabular}{lrl} \toprule - {} & a & b \\ + & a & b \\ \midrule - 0 & 1 & b1 \\ - 1 & 2 & b2 \\ + 0 & 1 & b1 \\ + 1 & 2 & b2 \\ \bottomrule \end{tabular} \end{table} @@ -531,14 +535,13 @@ def test_to_latex_caption_and_shortcaption( expected = _dedent( r""" \begin{table} - \centering \caption[a table]{a table in a \texttt{table/tabular} environment} \begin{tabular}{lrl} \toprule - {} & a & b \\ + & a & b \\ \midrule - 0 & 1 & b1 \\ - 1 & 2 & b2 \\ + 0 & 1 & b1 \\ + 1 & 2 & b2 \\ \bottomrule \end{tabular} \end{table} @@ -567,15 +570,14 @@ def test_to_latex_caption_shortcaption_and_label( expected = _dedent( r""" \begin{table} - \centering \caption[a table]{a table in a \texttt{table/tabular} environment} \label{tab:table_tabular} \begin{tabular}{lrl} \toprule - {} & a & b \\ + & a & b \\ \midrule - 0 & 1 & b1 \\ - 1 & 2 & b2 \\ + 0 & 1 & b1 \\ + 1 & 2 & b2 \\ \bottomrule \end{tabular} \end{table} @@ -596,7 +598,7 @@ def test_to_latex_caption_shortcaption_and_label( def test_to_latex_bad_caption_raises(self, bad_caption): # test that wrong number of params is raised df = DataFrame({"a": [1]}) - msg = "caption must be either a string or a tuple of two strings" + msg = "`caption` must be either a string or 2-tuple of strings" with pytest.raises(ValueError, match=msg): df.to_latex(caption=bad_caption) @@ -607,14 +609,13 @@ def test_to_latex_two_chars_caption(self, df_short): expected = _dedent( r""" \begin{table} - \centering \caption{xy} \begin{tabular}{lrl} \toprule - {} & a & b \\ + & a & b \\ \midrule - 0 & 1 & b1 \\ - 1 & 2 & b2 \\ + 0 & 1 & b1 \\ + 1 & 2 & b2 \\ \bottomrule \end{tabular} \end{table} @@ -630,25 +631,24 @@ def test_to_latex_longtable_caption_only(self, df_short, caption_longtable): expected = _dedent( r""" \begin{longtable}{lrl} - \caption{a table in a \texttt{longtable} environment}\\ + \caption{a table in a \texttt{longtable} environment} \\ \toprule - {} & a & b \\ + & a & b \\ \midrule \endfirsthead \caption[]{a table in a \texttt{longtable} environment} \\ \toprule - {} & a & b \\ + & a & b \\ \midrule \endhead \midrule - \multicolumn{3}{r}{{Continued on next page}} \\ + \multicolumn{3}{r}{Continued on next page} \\ \midrule \endfoot - \bottomrule \endlastfoot - 0 & 1 & b1 \\ - 1 & 2 & b2 \\ + 0 & 1 & b1 \\ + 1 & 2 & b2 \\ \end{longtable} """ ) @@ -660,25 +660,23 @@ def test_to_latex_longtable_label_only(self, df_short, label_longtable): expected = _dedent( r""" \begin{longtable}{lrl} - \label{tab:longtable}\\ + \label{tab:longtable} \\ \toprule - {} & a & b \\ + & a & b \\ \midrule \endfirsthead - \toprule - {} & a & b \\ + & a & b \\ \midrule \endhead \midrule - \multicolumn{3}{r}{{Continued on next page}} \\ + \multicolumn{3}{r}{Continued on next page} \\ \midrule \endfoot - \bottomrule \endlastfoot - 0 & 1 & b1 \\ - 1 & 2 & b2 \\ + 0 & 1 & b1 \\ + 1 & 2 & b2 \\ \end{longtable} """ ) @@ -698,29 +696,27 @@ def test_to_latex_longtable_caption_and_label( ) expected = _dedent( r""" - \begin{longtable}{lrl} - \caption{a table in a \texttt{longtable} environment} - \label{tab:longtable}\\ - \toprule - {} & a & b \\ - \midrule - \endfirsthead - \caption[]{a table in a \texttt{longtable} environment} \\ - \toprule - {} & a & b \\ - \midrule - \endhead - \midrule - \multicolumn{3}{r}{{Continued on next page}} \\ - \midrule - \endfoot - - \bottomrule - \endlastfoot - 0 & 1 & b1 \\ - 1 & 2 & b2 \\ - \end{longtable} - """ + \begin{longtable}{lrl} + \caption{a table in a \texttt{longtable} environment} \label{tab:longtable} \\ + \toprule + & a & b \\ + \midrule + \endfirsthead + \caption[]{a table in a \texttt{longtable} environment} \\ + \toprule + & a & b \\ + \midrule + \endhead + \midrule + \multicolumn{3}{r}{Continued on next page} \\ + \midrule + \endfoot + \bottomrule + \endlastfoot + 0 & 1 & b1 \\ + 1 & 2 & b2 \\ + \end{longtable} + """ ) assert result == expected @@ -739,29 +735,27 @@ def test_to_latex_longtable_caption_shortcaption_and_label( ) expected = _dedent( r""" - \begin{longtable}{lrl} - \caption[a table]{a table in a \texttt{longtable} environment} - \label{tab:longtable}\\ - \toprule - {} & a & b \\ - \midrule - \endfirsthead - \caption[]{a table in a \texttt{longtable} environment} \\ - \toprule - {} & a & b \\ - \midrule - \endhead - \midrule - \multicolumn{3}{r}{{Continued on next page}} \\ - \midrule - \endfoot - - \bottomrule - \endlastfoot - 0 & 1 & b1 \\ - 1 & 2 & b2 \\ - \end{longtable} - """ +\begin{longtable}{lrl} +\caption[a table]{a table in a \texttt{longtable} environment} \label{tab:longtable} \\ +\toprule + & a & b \\ +\midrule +\endfirsthead +\caption[]{a table in a \texttt{longtable} environment} \\ +\toprule + & a & b \\ +\midrule +\endhead +\midrule +\multicolumn{3}{r}{Continued on next page} \\ +\midrule +\endfoot +\bottomrule +\endlastfoot +0 & 1 & b1 \\ +1 & 2 & b2 \\ +\end{longtable} +""" ) assert result == expected @@ -780,10 +774,10 @@ def test_to_latex_escape_false(self, df_with_symbols): r""" \begin{tabular}{lll} \toprule - {} & co$e^x$ & co^l1 \\ + & co$e^x$ & co^l1 \\ \midrule - a & a & a \\ - b & b & b \\ + a & a & a \\ + b & b & b \\ \bottomrule \end{tabular} """ @@ -796,10 +790,10 @@ def test_to_latex_escape_default(self, df_with_symbols): r""" \begin{tabular}{lll} \toprule - {} & co\$e\textasciicircum x\$ & co\textasciicircum l1 \\ + & co\$e\textasciicircum x\$ & co\textasciicircum l1 \\ \midrule - a & a & a \\ - b & b & b \\ + a & a & a \\ + b & b & b \\ \bottomrule \end{tabular} """ @@ -813,11 +807,11 @@ def test_to_latex_special_escape(self): r""" \begin{tabular}{ll} \toprule - {} & 0 \\ + & 0 \\ \midrule - 0 & a\textbackslash b\textbackslash c \\ - 1 & \textasciicircum a\textasciicircum b\textasciicircum c \\ - 2 & \textasciitilde a\textasciitilde b\textasciitilde c \\ + 0 & a\textbackslash b\textbackslash c \\ + 1 & \textasciicircum a\textasciicircum b\textasciicircum c \\ + 2 & \textasciitilde a\textasciitilde b\textasciitilde c \\ \bottomrule \end{tabular} """ @@ -832,18 +826,18 @@ def test_to_latex_escape_special_chars(self): r""" \begin{tabular}{ll} \toprule - {} & 0 \\ - \midrule - 0 & \& \\ - 1 & \% \\ - 2 & \$ \\ - 3 & \# \\ - 4 & \_ \\ - 5 & \{ \\ - 6 & \} \\ - 7 & \textasciitilde \\ - 8 & \textasciicircum \\ - 9 & \textbackslash \\ + & 0 \\ + \midrule + 0 & \& \\ + 1 & \% \\ + 2 & \$ \\ + 3 & \# \\ + 4 & \_ \\ + 5 & \{ \\ + 6 & \} \\ + 7 & \textasciitilde \\ + 8 & \textasciicircum \\ + 9 & \textbackslash \\ \bottomrule \end{tabular} """ @@ -858,10 +852,10 @@ def test_to_latex_specified_header_special_chars_without_escape(self): r""" \begin{tabular}{lrl} \toprule - {} & $A$ & $B$ \\ + & $A$ & $B$ \\ \midrule - 0 & 1 & b1 \\ - 1 & 2 & b2 \\ + 0 & 1 & b1 \\ + 1 & 2 & b2 \\ \bottomrule \end{tabular} """ @@ -877,13 +871,12 @@ def test_to_latex_position(self): expected = _dedent( r""" \begin{table}[h] - \centering \begin{tabular}{lrl} \toprule - {} & a & b \\ + & a & b \\ \midrule - 0 & 1 & b1 \\ - 1 & 2 & b2 \\ + 0 & 1 & b1 \\ + 1 & 2 & b2 \\ \bottomrule \end{tabular} \end{table} @@ -899,23 +892,21 @@ def test_to_latex_longtable_position(self): r""" \begin{longtable}[t]{lrl} \toprule - {} & a & b \\ + & a & b \\ \midrule \endfirsthead - \toprule - {} & a & b \\ + & a & b \\ \midrule \endhead \midrule - \multicolumn{3}{r}{{Continued on next page}} \\ + \multicolumn{3}{r}{Continued on next page} \\ \midrule \endfoot - \bottomrule \endlastfoot - 0 & 1 & b1 \\ - 1 & 2 & b2 \\ + 0 & 1 & b1 \\ + 1 & 2 & b2 \\ \end{longtable} """ ) @@ -950,11 +941,11 @@ def test_to_latex_with_formatters(self): r""" \begin{tabular}{llrrl} \toprule - {} & datetime64 & float & int & object \\ + & datetime64 & float & int & object \\ \midrule - index: 0 & 2016-01 & [ 1.0] & 0x1 & -(1, 2)- \\ - index: 1 & 2016-02 & [ 2.0] & 0x2 & -True- \\ - index: 2 & 2016-03 & [ 3.0] & 0x3 & -False- \\ + index: 0 & 2016-01 & [ 1.0] & 0x1 & -(1, 2)- \\ + index: 1 & 2016-02 & [ 2.0] & 0x2 & -True- \\ + index: 2 & 2016-03 & [ 3.0] & 0x3 & -False- \\ \bottomrule \end{tabular} """ @@ -969,7 +960,7 @@ def test_to_latex_float_format_no_fixed_width_3decimals(self): r""" \begin{tabular}{lr} \toprule - {} & x \\ + & x \\ \midrule 0 & 0.200 \\ \bottomrule @@ -986,7 +977,7 @@ def test_to_latex_float_format_no_fixed_width_integer(self): r""" \begin{tabular}{lr} \toprule - {} & x \\ + & x \\ \midrule 0 & 100 \\ \bottomrule @@ -1009,10 +1000,10 @@ def test_to_latex_na_rep_and_float_format(self, na_rep): rf""" \begin{{tabular}}{{llr}} \toprule - {{}} & Group & Data \\ + & Group & Data \\ \midrule - 0 & A & 1.22 \\ - 1 & A & {na_rep} \\ + 0 & A & 1.22 \\ + 1 & A & {na_rep} \\ \bottomrule \end{{tabular}} """ @@ -1056,10 +1047,10 @@ def test_to_latex_multindex_header(self): r""" \begin{tabular}{llrr} \toprule - & & r1 & r2 \\ - a & b & & \\ + & & r1 & r2 \\ + a & b & & \\ \midrule - 0 & 1 & 2 & 3 \\ + 0 & 1 & 2 & 3 \\ \bottomrule \end{tabular} """ @@ -1075,8 +1066,8 @@ def test_to_latex_multiindex_empty_name(self): r""" \begin{tabular}{lrrrr} \toprule - & 0 & 1 & 2 & 3 \\ - {} & & & & \\ + & 0 & 1 & 2 & 3 \\ + & & & & \\ \midrule 1 & -1 & -1 & -1 & -1 \\ 2 & -1 & -1 & -1 & -1 \\ @@ -1093,10 +1084,10 @@ def test_to_latex_multiindex_column_tabular(self): r""" \begin{tabular}{ll} \toprule - {} & x \\ - {} & y \\ + & x \\ + & y \\ \midrule - 0 & a \\ + 0 & a \\ \bottomrule \end{tabular} """ @@ -1110,9 +1101,9 @@ def test_to_latex_multiindex_small_tabular(self): r""" \begin{tabular}{lll} \toprule - & & 0 \\ + & & 0 \\ \midrule - x & y & a \\ + x & y & a \\ \bottomrule \end{tabular} """ @@ -1125,13 +1116,13 @@ def test_to_latex_multiindex_tabular(self, multiindex_frame): r""" \begin{tabular}{llrrrr} \toprule - & & 0 & 1 & 2 & 3 \\ + & & 0 & 1 & 2 & 3 \\ \midrule - c1 & 0 & 0 & 1 & 2 & 3 \\ - & 1 & 4 & 5 & 6 & 7 \\ - c2 & 0 & 0 & 1 & 2 & 3 \\ - & 1 & 4 & 5 & 6 & 7 \\ - c3 & 0 & 0 & 1 & 2 & 3 \\ + c1 & 0 & 0 & 1 & 2 & 3 \\ + & 1 & 4 & 5 & 6 & 7 \\ + c2 & 0 & 0 & 1 & 2 & 3 \\ + & 1 & 4 & 5 & 6 & 7 \\ + c3 & 0 & 0 & 1 & 2 & 3 \\ \bottomrule \end{tabular} """ @@ -1148,12 +1139,12 @@ def test_to_latex_multicolumn_tabular(self, multiindex_frame): \begin{tabular}{lrrrrr} \toprule a & \multicolumn{2}{l}{c1} & \multicolumn{2}{l}{c2} & c3 \\ - b & 0 & 1 & 0 & 1 & 0 \\ + b & 0 & 1 & 0 & 1 & 0 \\ \midrule - 0 & 0 & 4 & 0 & 4 & 0 \\ - 1 & 1 & 5 & 1 & 5 & 1 \\ - 2 & 2 & 6 & 2 & 6 & 2 \\ - 3 & 3 & 7 & 3 & 7 & 3 \\ + 0 & 0 & 4 & 0 & 4 & 0 \\ + 1 & 1 & 5 & 1 & 5 & 1 \\ + 2 & 2 & 6 & 2 & 6 & 2 \\ + 3 & 3 & 7 & 3 & 7 & 3 \\ \bottomrule \end{tabular} """ @@ -1168,13 +1159,13 @@ def test_to_latex_index_has_name_tabular(self): r""" \begin{tabular}{llr} \toprule - & & c \\ - a & b & \\ + & & c \\ + a & b & \\ \midrule - 0 & a & 1 \\ - & b & 2 \\ - 1 & a & 3 \\ - & b & 4 \\ + 0 & a & 1 \\ + & b & 2 \\ + 1 & a & 3 \\ + & b & 4 \\ \bottomrule \end{tabular} """ @@ -1184,17 +1175,17 @@ def test_to_latex_index_has_name_tabular(self): def test_to_latex_groupby_tabular(self): # GH 10660 df = DataFrame({"a": [0, 0, 1, 1], "b": list("abab"), "c": [1, 2, 3, 4]}) - result = df.groupby("a").describe().to_latex() + result = df.groupby("a").describe().to_latex(format={"precision": 1}) expected = _dedent( r""" \begin{tabular}{lrrrrrrrr} \toprule - {} & \multicolumn{8}{l}{c} \\ - {} & count & mean & std & min & 25\% & 50\% & 75\% & max \\ - a & & & & & & & & \\ + & \multicolumn{8}{l}{c} \\ + & count & mean & std & min & 25\% & 50\% & 75\% & max \\ + a & & & & & & & & \\ \midrule - 0 & 2.0 & 1.5 & 0.707107 & 1.0 & 1.25 & 1.5 & 1.75 & 2.0 \\ - 1 & 2.0 & 3.5 & 0.707107 & 3.0 & 3.25 & 3.5 & 3.75 & 4.0 \\ + 0 & 2.0 & 1.5 & 0.7 & 1.0 & 1.2 & 1.5 & 1.8 & 2.0 \\ + 1 & 2.0 & 3.5 & 0.7 & 3.0 & 3.2 & 3.5 & 3.8 & 4.0 \\ \bottomrule \end{tabular} """ @@ -1217,10 +1208,10 @@ def test_to_latex_multiindex_dupe_level(self): r""" \begin{tabular}{lll} \toprule - & & col \\ + & & col \\ \midrule - A & c & NaN \\ - B & c & NaN \\ + A & c & NaN \\ + B & c & NaN \\ \bottomrule \end{tabular} """ @@ -1233,14 +1224,14 @@ def test_to_latex_multicolumn_default(self, multicolumn_frame): r""" \begin{tabular}{lrrrrr} \toprule - {} & \multicolumn{2}{l}{c1} & \multicolumn{2}{l}{c2} & c3 \\ - {} & 0 & 1 & 0 & 1 & 0 \\ - \midrule - 0 & 0 & 5 & 0 & 5 & 0 \\ - 1 & 1 & 6 & 1 & 6 & 1 \\ - 2 & 2 & 7 & 2 & 7 & 2 \\ - 3 & 3 & 8 & 3 & 8 & 3 \\ - 4 & 4 & 9 & 4 & 9 & 4 \\ + & \multicolumn{2}{l}{c1} & \multicolumn{2}{l}{c2} & c3 \\ + & 0 & 1 & 0 & 1 & 0 \\ + \midrule + 0 & 0 & 5 & 0 & 5 & 0 \\ + 1 & 1 & 6 & 1 & 6 & 1 \\ + 2 & 2 & 7 & 2 & 7 & 2 \\ + 3 & 3 & 8 & 3 & 8 & 3 \\ + 4 & 4 & 9 & 4 & 9 & 4 \\ \bottomrule \end{tabular} """ @@ -1253,14 +1244,14 @@ def test_to_latex_multicolumn_false(self, multicolumn_frame): r""" \begin{tabular}{lrrrrr} \toprule - {} & c1 & & c2 & & c3 \\ - {} & 0 & 1 & 0 & 1 & 0 \\ - \midrule - 0 & 0 & 5 & 0 & 5 & 0 \\ - 1 & 1 & 6 & 1 & 6 & 1 \\ - 2 & 2 & 7 & 2 & 7 & 2 \\ - 3 & 3 & 8 & 3 & 8 & 3 \\ - 4 & 4 & 9 & 4 & 9 & 4 \\ + & c1 & & c2 & & c3 \\ + & 0 & 1 & 0 & 1 & 0 \\ + \midrule + 0 & 0 & 5 & 0 & 5 & 0 \\ + 1 & 1 & 6 & 1 & 6 & 1 \\ + 2 & 2 & 7 & 2 & 7 & 2 \\ + 3 & 3 & 8 & 3 & 8 & 3 \\ + 4 & 4 & 9 & 4 & 9 & 4 \\ \bottomrule \end{tabular} """ @@ -1273,15 +1264,16 @@ def test_to_latex_multirow_true(self, multicolumn_frame): r""" \begin{tabular}{llrrrrr} \toprule - & & 0 & 1 & 2 & 3 & 4 \\ + & & 0 & 1 & 2 & 3 & 4 \\ \midrule - \multirow{2}{*}{c1} & 0 & 0 & 1 & 2 & 3 & 4 \\ - & 1 & 5 & 6 & 7 & 8 & 9 \\ + \multirow[t]{2}{*}{c1} & 0 & 0 & 1 & 2 & 3 & 4 \\ + & 1 & 5 & 6 & 7 & 8 & 9 \\ + \cline{1-7} + \multirow[t]{2}{*}{c2} & 0 & 0 & 1 & 2 & 3 & 4 \\ + & 1 & 5 & 6 & 7 & 8 & 9 \\ \cline{1-7} - \multirow{2}{*}{c2} & 0 & 0 & 1 & 2 & 3 & 4 \\ - & 1 & 5 & 6 & 7 & 8 & 9 \\ + c3 & 0 & 0 & 1 & 2 & 3 & 4 \\ \cline{1-7} - c3 & 0 & 0 & 1 & 2 & 3 & 4 \\ \bottomrule \end{tabular} """ @@ -1299,16 +1291,17 @@ def test_to_latex_multicolumnrow_with_multicol_format(self, multicolumn_frame): r""" \begin{tabular}{llrrrrr} \toprule - & & \multicolumn{2}{c}{c1} & \multicolumn{2}{c}{c2} & c3 \\ - & & 0 & 1 & 0 & 1 & 0 \\ + & & \multicolumn{2}{c}{c1} & \multicolumn{2}{c}{c2} & c3 \\ + & & 0 & 1 & 0 & 1 & 0 \\ \midrule - \multirow{2}{*}{c1} & 0 & 0 & 1 & 2 & 3 & 4 \\ - & 1 & 5 & 6 & 7 & 8 & 9 \\ + \multirow[t]{2}{*}{c1} & 0 & 0 & 1 & 2 & 3 & 4 \\ + & 1 & 5 & 6 & 7 & 8 & 9 \\ + \cline{1-7} + \multirow[t]{2}{*}{c2} & 0 & 0 & 1 & 2 & 3 & 4 \\ + & 1 & 5 & 6 & 7 & 8 & 9 \\ \cline{1-7} - \multirow{2}{*}{c2} & 0 & 0 & 1 & 2 & 3 & 4 \\ - & 1 & 5 & 6 & 7 & 8 & 9 \\ + c3 & 0 & 0 & 1 & 2 & 3 & 4 \\ \cline{1-7} - c3 & 0 & 0 & 1 & 2 & 3 & 4 \\ \bottomrule \end{tabular} """ @@ -1326,24 +1319,23 @@ def test_to_latex_multiindex_names(self, name0, name1, axes): for idx in axes: df.axes[idx].names = names - idx_names = tuple(n or "{}" for n in names) + idx_names = tuple(n or "" for n in names) idx_names_row = ( - f"{idx_names[0]} & {idx_names[1]} & & & & \\\\\n" + f"{idx_names[0]} & {idx_names[1]} & & & & \\\\\n" if (0 in axes and any(names)) else "" ) - placeholder = "{}" if any(names) and 1 in axes else " " - col_names = [n if (bool(n) and 1 in axes) else placeholder for n in names] + col_names = [n if (bool(n) and 1 in axes) else "" for n in names] observed = df.to_latex() expected = r"""\begin{tabular}{llrrrr} \toprule - & %s & \multicolumn{2}{l}{1} & \multicolumn{2}{l}{2} \\ - & %s & 3 & 4 & 3 & 4 \\ + & %s & \multicolumn{2}{l}{1} & \multicolumn{2}{l}{2} \\ + & %s & 3 & 4 & 3 & 4 \\ %s\midrule 1 & 3 & -1 & -1 & -1 & -1 \\ - & 4 & -1 & -1 & -1 & -1 \\ + & 4 & -1 & -1 & -1 & -1 \\ 2 & 3 & -1 & -1 & -1 & -1 \\ - & 4 & -1 & -1 & -1 & -1 \\ + & 4 & -1 & -1 & -1 & -1 \\ \bottomrule \end{tabular} """ % tuple( @@ -1362,14 +1354,14 @@ def test_to_latex_multiindex_nans(self, one_row): r""" \begin{tabular}{llr} \toprule - & & c \\ - a & b & \\ + & & c \\ + a & b & \\ \midrule - NaN & 2 & 4 \\ + NaN & 2 & 4 \\ """ ) if not one_row: - expected += r"""1.0 & 3 & 5 \\ + expected += r"""1.000000 & 3 & 5 \\ """ expected += r"""\bottomrule \end{tabular} @@ -1384,11 +1376,11 @@ def test_to_latex_non_string_index(self): r""" \begin{tabular}{llr} \toprule - & & 2 \\ - 0 & 1 & \\ + & & 2 \\ + 0 & 1 & \\ \midrule - 1 & 2 & 3 \\ - & 2 & 3 \\ + 1 & 2 & 3 \\ + & 2 & 3 \\ \bottomrule \end{tabular} """ @@ -1406,27 +1398,26 @@ def test_to_latex_multiindex_multirow(self): r""" \begin{tabular}{lll} \toprule - & & \\ i & val0 & val1 \\ \midrule - \multirow{6}{*}{0.0} & \multirow{2}{*}{3.0} & 0 \\ - & & 1 \\ + \multirow[t]{6}{*}{0.000000} & \multirow[t]{2}{*}{3.000000} & 0 \\ + & & 1 \\ \cline{2-3} - & \multirow{2}{*}{2.0} & 0 \\ - & & 1 \\ + & \multirow[t]{2}{*}{2.000000} & 0 \\ + & & 1 \\ \cline{2-3} - & \multirow{2}{*}{1.0} & 0 \\ - & & 1 \\ - \cline{1-3} + & \multirow[t]{2}{*}{1.000000} & 0 \\ + & & 1 \\ + \cline{1-3} \cline{2-3} + \multirow[t]{6}{*}{1.000000} & \multirow[t]{2}{*}{3.000000} & 0 \\ + & & 1 \\ \cline{2-3} - \multirow{6}{*}{1.0} & \multirow{2}{*}{3.0} & 0 \\ - & & 1 \\ + & \multirow[t]{2}{*}{2.000000} & 0 \\ + & & 1 \\ \cline{2-3} - & \multirow{2}{*}{2.0} & 0 \\ - & & 1 \\ - \cline{2-3} - & \multirow{2}{*}{1.0} & 0 \\ - & & 1 \\ + & \multirow[t]{2}{*}{1.000000} & 0 \\ + & & 1 \\ + \cline{1-3} \cline{2-3} \bottomrule \end{tabular} """ @@ -1517,14 +1508,17 @@ def test_get_strrow_multindex_multicolumn(self, row_num, expected): assert row_string_converter.get_strrow(row_num=row_num) == expected - def test_future_warning(self): + @pytest.mark.parametrize( + "deprecated_arg, value", + [ + ("col_space", 10), + ], + ) + def test_deprecation_warning(self, deprecated_arg, value): df = DataFrame([[1]]) 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." - ) - with tm.assert_produces_warning(FutureWarning, match=msg): - df.to_latex() + "`col_space` is deprecated. Whitespace in LaTeX does not impact " + "the rendered version, and this argument is ignored." + ) + with tm.assert_produces_warning(DeprecationWarning, match=msg): + df.to_latex(**{deprecated_arg: value}) From 0c12f3ab942811acd1fb43c2e86263db0020f84b Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Wed, 3 Aug 2022 21:28:55 +0200 Subject: [PATCH 02/24] Base implementation --- pandas/core/generic.py | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/pandas/core/generic.py b/pandas/core/generic.py index e40e872af0a75..cabc6bb8441f3 100644 --- a/pandas/core/generic.py +++ b/pandas/core/generic.py @@ -3380,6 +3380,9 @@ def to_latex( if multirow is None: multirow = config.get_option("display.latex.multirow") + if column_format is not None and not isinstance(column_format, str): + raise ValueError("`column_format` must be str or unicode") + # Refactor formatters/float_format/decimal/na_rep/escape to Styler structure base_format_ = { "na_rep": na_rep, @@ -3421,8 +3424,9 @@ def _wrap(x, alt_format_): formatters = functools.partial(_wrap, alt_format_=lambda v: v) else: formatters = None + format_index_ = [index_format_, column_format_] - hide, relabel = [], [] + hide, relabel_index = [], [] if columns: hide.append( { @@ -3433,7 +3437,8 @@ def _wrap(x, alt_format_): if header is False: hide.append({"axis": "columns"}) elif isinstance(header, (list, tuple)): - relabel = {"labels": header, "axis": "columns"} + relabel_index = {"labels": header, "axis": "columns"} + format_index_ = [index_format_] # column_format is overwritten if index is False: hide.append({"axis": "index"}) @@ -3462,9 +3467,9 @@ def _wrap(x, alt_format_): return self._to_latex_via_styler( buf, hide=hide, - relabel=relabel, + relabel_index=relabel_index, format={"formatter": formatters, **base_format_}, - format_index=[index_format_, column_format_], + format_index=format_index_, render_kwargs=render_kwargs, ) @@ -3473,7 +3478,7 @@ def _to_latex_via_styler( buf=None, *, hide: dict | list[dict] | None = None, - relabel: dict | list[dict] | None = None, + relabel_index: dict | list[dict] | None = None, format: dict | list[dict] | None = None, format_index: dict | list[dict] | None = None, render_kwargs: dict = {}, @@ -3484,6 +3489,7 @@ def _to_latex_via_styler( .. code-block:: python styler = Styler(DataFrame) styler.hide(**hide) + styler.relabel_index(**relabel_index) styler.format(**format) styler.format_index(**format_index) styler.to_latex(buf=buf, **render_kwargs) @@ -3494,7 +3500,7 @@ def _to_latex_via_styler( hide : dict, list of dict Keyword args to pass to the method call of ``Styler.hide``. If a list will call the method numerous times. - relabel : dict, list of dict + relabel_index : dict, list of dict Keyword args to pass to the method of ``Styler.relabel_index``. If a list will call the method numerous times. format : dict, list of dict @@ -3540,7 +3546,7 @@ def _to_latex_via_styler( self = cast("DataFrame", self) styler = Styler(self, uuid="") - for kw_name in ["hide", "relabel", "format", "format_index"]: + for kw_name in ["hide", "relabel_index", "format", "format_index"]: kw = vars()[kw_name] if isinstance(kw, dict): getattr(styler, kw_name)(**kw) From d64406551afa9a3ba271f214f2fe186c2f5e9d6d Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Wed, 3 Aug 2022 21:50:27 +0200 Subject: [PATCH 03/24] test fix up --- pandas/core/generic.py | 14 ++++++++++++++ pandas/tests/io/formats/test_to_latex.py | 6 +++--- 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/pandas/core/generic.py b/pandas/core/generic.py index cabc6bb8441f3..5e6cab1b527cb 100644 --- a/pandas/core/generic.py +++ b/pandas/core/generic.py @@ -3227,6 +3227,7 @@ def to_latex( def to_latex( self, buf: FilePath | WriteBuffer[str] | None = None, + *, columns: Sequence[Hashable] | None = None, col_space: ColspaceArgType | None = None, header: bool_t | Sequence[str] = True, @@ -3270,6 +3271,9 @@ def to_latex( The subset of columns to write. Writes all columns by default. col_space : int, optional The minimum width of each column. + + .. deprecated:: 1.5.0 + Whitespace does not affect a rendered LaTeX file and is ignored. header : bool or list of str, default True Write out the column names. If a list of strings is given, it is assumed to be aliases for the column names. @@ -3366,6 +3370,13 @@ def to_latex( \bottomrule \end{{tabular}} """ + msg = ( + "`col_space` is deprecated. Whitespace in LaTeX does not impact " + "the rendered version, and this argument is ignored." + ) + if col_space is not None: + warnings.warn(msg, DeprecationWarning, stacklevel=find_stack_level()) + # Get defaults from the pandas config if self.ndim == 1: self = self.to_frame() @@ -3382,6 +3393,9 @@ def to_latex( if column_format is not None and not isinstance(column_format, str): raise ValueError("`column_format` must be str or unicode") + length = len(self.columns) if columns is None else len(columns) + if isinstance(header, (list, tuple)) and len(header) != length: + raise ValueError(f"Writing {length} cols but got {len(header)} aliases") # Refactor formatters/float_format/decimal/na_rep/escape to Styler structure base_format_ = { diff --git a/pandas/tests/io/formats/test_to_latex.py b/pandas/tests/io/formats/test_to_latex.py index aa0f41f4470ff..f9e53ad1b2da5 100644 --- a/pandas/tests/io/formats/test_to_latex.py +++ b/pandas/tests/io/formats/test_to_latex.py @@ -213,7 +213,7 @@ def test_to_latex_empty_longtable(self): \midrule \endhead \midrule - \multicolumn{1}{r}{Continued on next page} \\ + \multicolumn{0}{r}{Continued on next page} \\ \midrule \endfoot \bottomrule @@ -377,7 +377,7 @@ def test_to_latex_number_of_items_in_header_missmatch_raises( ): # GH 7124 df = DataFrame({"a": [1, 2], "b": ["b1", "b2"]}) - msg = "``aliases`` must be of length equal to the number of visible labels" + msg = f"Writing 2 cols but got {num_aliases} aliases" with pytest.raises(ValueError, match=msg): df.to_latex(header=header) @@ -1175,7 +1175,7 @@ def test_to_latex_index_has_name_tabular(self): def test_to_latex_groupby_tabular(self): # GH 10660 df = DataFrame({"a": [0, 0, 1, 1], "b": list("abab"), "c": [1, 2, 3, 4]}) - result = df.groupby("a").describe().to_latex(format={"precision": 1}) + result = df.groupby("a").describe().to_latex(float_format="{:.1f}".format) expected = _dedent( r""" \begin{tabular}{lrrrrrrrr} From 9dcd2547945e4fb9b182b150f1edca713bef3a1c Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Wed, 3 Aug 2022 22:00:13 +0200 Subject: [PATCH 04/24] test fix up --- pandas/core/generic.py | 1 + pandas/tests/io/formats/test_to_latex.py | 3 --- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/pandas/core/generic.py b/pandas/core/generic.py index 5e6cab1b527cb..18e8fb18659ef 100644 --- a/pandas/core/generic.py +++ b/pandas/core/generic.py @@ -3440,6 +3440,7 @@ def _wrap(x, alt_format_): formatters = None format_index_ = [index_format_, column_format_] + # Deal with hiding indexes and relabelling column names hide, relabel_index = [], [] if columns: hide.append( diff --git a/pandas/tests/io/formats/test_to_latex.py b/pandas/tests/io/formats/test_to_latex.py index f9e53ad1b2da5..10e01b0c117e1 100644 --- a/pandas/tests/io/formats/test_to_latex.py +++ b/pandas/tests/io/formats/test_to_latex.py @@ -20,9 +20,6 @@ ) pytest.importorskip("jinja2") -pytestmark = pytest.mark.filterwarnings( - "ignore:Deprecated arguments supplied:FutureWarning" -) def _dedent(string): From cd05038bc894ad43b63517ed509f48fc1dd4e224 Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Thu, 4 Aug 2022 18:12:42 +0200 Subject: [PATCH 05/24] test fix up --- pandas/tests/frame/test_repr_info.py | 17 ++++++++++------- pandas/tests/io/formats/test_format.py | 7 +++++-- pandas/tests/io/formats/test_printing.py | 6 +----- pandas/tests/io/test_common.py | 3 ++- pandas/tests/series/test_repr.py | 14 ++++++++------ 5 files changed, 26 insertions(+), 21 deletions(-) diff --git a/pandas/tests/frame/test_repr_info.py b/pandas/tests/frame/test_repr_info.py index 86c8e36cb7bd4..29e29241c4862 100644 --- a/pandas/tests/frame/test_repr_info.py +++ b/pandas/tests/frame/test_repr_info.py @@ -286,20 +286,23 @@ def test_repr_column_name_unicode_truncation_bug(self): with option_context("display.max_columns", 20): assert "StringCol" in repr(df) - @pytest.mark.filterwarnings("ignore::FutureWarning") def test_latex_repr(self): - result = r"""\begin{tabular}{llll} + pytest.importorskip("jinja2") + expected = r"""\begin{tabular}{llll} \toprule -{} & 0 & 1 & 2 \\ + & 0 & 1 & 2 \\ \midrule -0 & $\alpha$ & b & c \\ -1 & 1 & 2 & 3 \\ +0 & $\alpha$ & b & c \\ +1 & 1 & 2 & 3 \\ \bottomrule \end{tabular} """ - with option_context("display.latex.escape", False, "display.latex.repr", True): + with option_context( + "display.latex.escape", False, "styler.render.repr", "latex" + ): df = DataFrame([[r"$\alpha$", "b", "c"], [1, 2, 3]]) - assert result == df._repr_latex_() + result = df._repr_latex_() + assert result == expected # GH 12182 assert df._repr_latex_() is None diff --git a/pandas/tests/io/formats/test_format.py b/pandas/tests/io/formats/test_format.py index a7c9c86b3d9a5..f9e2e1dab19d2 100644 --- a/pandas/tests/io/formats/test_format.py +++ b/pandas/tests/io/formats/test_format.py @@ -3362,7 +3362,6 @@ def test_repr_html_ipython_config(ip): assert not result.error_in_exec -@pytest.mark.filterwarnings("ignore:In future versions `DataFrame.to_latex`") @pytest.mark.parametrize("method", ["to_string", "to_html", "to_latex"]) @pytest.mark.parametrize( "encoding, data", @@ -3377,6 +3376,8 @@ def test_filepath_or_buffer_arg( filepath_or_buffer_id, ): df = DataFrame([data]) + if method in ["to_latex"]: # uses styler implementation + pytest.importorskip("jinja2") if filepath_or_buffer_id not in ["string", "pathlike"] and encoding is not None: with pytest.raises( @@ -3384,7 +3385,7 @@ def test_filepath_or_buffer_arg( ): getattr(df, method)(buf=filepath_or_buffer, encoding=encoding) elif encoding == "foo": - expected_warning = FutureWarning if method == "to_latex" else None + expected_warning = None with tm.assert_produces_warning(expected_warning): with pytest.raises(LookupError, match="unknown encoding"): getattr(df, method)(buf=filepath_or_buffer, encoding=encoding) @@ -3397,6 +3398,8 @@ def test_filepath_or_buffer_arg( @pytest.mark.filterwarnings("ignore::FutureWarning") @pytest.mark.parametrize("method", ["to_string", "to_html", "to_latex"]) def test_filepath_or_buffer_bad_arg_raises(float_frame, method): + if method in ["to_latex"]: # uses styler implementation + pytest.importorskip("jinja2") msg = "buf is not a file name and it has no write method" with pytest.raises(TypeError, match=msg): getattr(float_frame, method)(buf=object()) diff --git a/pandas/tests/io/formats/test_printing.py b/pandas/tests/io/formats/test_printing.py index 4fc8a46bad777..74db5d9ce8697 100644 --- a/pandas/tests/io/formats/test_printing.py +++ b/pandas/tests/io/formats/test_printing.py @@ -1,5 +1,4 @@ import numpy as np -import pytest import pandas._config.config as cf @@ -120,9 +119,6 @@ def test_ambiguous_width(self): class TestTableSchemaRepr: - @pytest.mark.filterwarnings( - "ignore:.*signature may therefore change.*:FutureWarning" - ) def test_publishes(self, ip): ipython = ip.instance(config=ip.config) df = pd.DataFrame({"A": [1, 2]}) @@ -138,7 +134,7 @@ def test_publishes(self, ip): formatted = ipython.display_formatter.format(obj) assert set(formatted[0].keys()) == expected - with_latex = pd.option_context("display.latex.repr", True) + with_latex = pd.option_context("styler.render.repr", "latex") with opt, with_latex: formatted = ipython.display_formatter.format(obj) diff --git a/pandas/tests/io/test_common.py b/pandas/tests/io/test_common.py index e9e99f6dd0ad7..a17ee37fdfa95 100644 --- a/pandas/tests/io/test_common.py +++ b/pandas/tests/io/test_common.py @@ -336,7 +336,6 @@ def test_read_fspath_all(self, reader, module, path, datapath): else: tm.assert_frame_equal(result, expected) - @pytest.mark.filterwarnings("ignore:In future versions `DataFrame.to_latex`") @pytest.mark.parametrize( "writer_name, writer_kwargs, module", [ @@ -351,6 +350,8 @@ def test_read_fspath_all(self, reader, module, path, datapath): ], ) def test_write_fspath_all(self, writer_name, writer_kwargs, module): + if writer_name in ["to_latex"]: # uses Styler implementation + pytest.importorskip("jinja2") p1 = tm.ensure_clean("string") p2 = tm.ensure_clean("fspath") df = pd.DataFrame({"A": [1, 2]}) diff --git a/pandas/tests/series/test_repr.py b/pandas/tests/series/test_repr.py index a12bc1df37269..fbfa6b61432c4 100644 --- a/pandas/tests/series/test_repr.py +++ b/pandas/tests/series/test_repr.py @@ -206,19 +206,21 @@ def test_timeseries_repr_object_dtype(self): ts2 = ts.iloc[np.random.randint(0, len(ts) - 1, 400)] repr(ts2).splitlines()[-1] - @pytest.mark.filterwarnings("ignore::FutureWarning") def test_latex_repr(self): + pytest.importorskip("jinja2") # uses Styler implementation result = r"""\begin{tabular}{ll} \toprule -{} & 0 \\ + & 0 \\ \midrule -0 & $\alpha$ \\ -1 & b \\ -2 & c \\ +0 & $\alpha$ \\ +1 & b \\ +2 & c \\ \bottomrule \end{tabular} """ - with option_context("display.latex.escape", False, "display.latex.repr", True): + with option_context( + "display.latex.escape", False, "styler.render.repr", "latex" + ): s = Series([r"$\alpha$", "b", "c"]) assert result == s._repr_latex_() From c625610833744e9630f601a356a00ef25acf6ece Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Thu, 4 Aug 2022 18:27:14 +0200 Subject: [PATCH 06/24] doc change --- pandas/core/generic.py | 29 ++++------------------------- 1 file changed, 4 insertions(+), 25 deletions(-) diff --git a/pandas/core/generic.py b/pandas/core/generic.py index 18e8fb18659ef..76bb865e41714 100644 --- a/pandas/core/generic.py +++ b/pandas/core/generic.py @@ -3500,7 +3500,9 @@ def _to_latex_via_styler( ): """ Render object to a LaTeX tabular, longtable, or nested table. + Uses the ``Styler`` implementation with the following, ordered, method chaining: + .. code-block:: python styler = Styler(DataFrame) styler.hide(**hide) @@ -3508,6 +3510,7 @@ def _to_latex_via_styler( styler.format(**format) styler.format_index(**format_index) styler.to_latex(buf=buf, **render_kwargs) + Parameters ---------- buf : str, Path or StringIO-like, optional, default None @@ -3526,35 +3529,11 @@ def _to_latex_via_styler( list will call the method numerous times. render_kwargs : dict Keyword args to pass to the method call of ``Styler.to_latex``. + Returns ------- str or None If buf is None, returns the result as a string. Otherwise returns None. - See Also - -------- - Styler.to_latex : Render a DataFrame to LaTeX with conditional formatting. - DataFrame.to_string : Render a DataFrame to a console-friendly - tabular output. - DataFrame.to_html : Render a DataFrame as an HTML table. - Examples - -------- - Convert a general DataFrame to LaTeX with formatting: - >>> df = pd.DataFrame(dict(name=['Raphael', 'Donatello'], - ... age=[26, 45], - ... height=[181.23, 177.65])) - >>> print(df.to_latex(hide={"axis": "index"}, - ... format={"formatter": {"name": str.upper}, - ... "precision": 1}, - ... render_kwargs={"hrules": True} - ... ) # doctest: +SKIP - \begin{{tabular}}{{lrr}} - \toprule - name & age & height \\ - \\midrule - RAPHAEL & 26 & 181.2 \\ - DONATELLO & 45 & 177.7 \\ - \bottomrule - \\end{{tabular}} """ from pandas.io.formats.style import Styler From 493b88432dcffed14351945922e0e3564028db1a Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Thu, 4 Aug 2022 19:12:18 +0200 Subject: [PATCH 07/24] doc change --- pandas/core/generic.py | 79 ++++++++++++++++++++++++++++++++++-------- 1 file changed, 65 insertions(+), 14 deletions(-) diff --git a/pandas/core/generic.py b/pandas/core/generic.py index 76bb865e41714..fc5f84252699f 100644 --- a/pandas/core/generic.py +++ b/pandas/core/generic.py @@ -3263,6 +3263,9 @@ def to_latex( .. versionchanged:: 1.2.0 Added position argument, changed meaning of caption argument. + .. versionchanged:: 1.5.0 + Refactored to use the Styler implementation via jinja2 templating. + Parameters ---------- buf : str, Path or StringIO-like, optional, default None @@ -3347,7 +3350,12 @@ def to_latex( ``\begin{{}}`` in the output. .. versionadded:: 1.2.0 - {returns} + + Returns + ------- + str or None + If buf is None, returns the result as a string. Otherwise returns None. + See Also -------- Styler.to_latex : Render a DataFrame to LaTeX with conditional formatting. @@ -3355,18 +3363,62 @@ def to_latex( tabular output. DataFrame.to_html : Render a DataFrame as an HTML table. + Notes + ----- + + .. note:: + As of v1.5.0 this method has changed to use the Styler implementation of + ``to_latex`` and no longer uses the DataFrameRenderer. It is advised that + users switch to using Styler, since this implementation is more frequently + updated and contains much more flexibility with the output. The following + examples indicate how this method now replicates the Styler implementation + for its legacy arguments. + + .. code-block:: python + + styler = df.style + + Styler methods are designed to be chained, so we can build complex combinations + of displays. To hide ``index`` and ``columns`` headers we use, + + .. code-block:: python + + styler.hide(axis="index").hide(axis="columns") + + To use ``formatters``, ``na_rep``, ``decimal`` and ``float_format``, + ``escape`` we use, + + .. code-block:: python + + styler.format( + formatter={"name": str.upper}, na_rep="-", precision=1, escape="latex" + ) + + To control other aspects we use the ``Styler.to_latex`` arguments such as, + + .. code-block:: python + + styler.to_latex( + column_format="lrr", caption="my table", environment="longtable" + ) + Examples -------- + Convert a general DataFrame to LaTeX with formatting: + >>> df = pd.DataFrame(dict(name=['Raphael', 'Donatello'], - ... mask=['red', 'purple'], - ... weapon=['sai', 'bo staff'])) - >>> print(df.to_latex(index=False)) # doctest: +SKIP - \begin{{tabular}}{{lll}} - \toprule - name & mask & weapon \\ - \midrule - Raphael & red & sai \\ - Donatello & purple & bo staff \\ + ... age=[26, 45], + ... height=[181.23, 177.65])) + >>> print(df.to_latex(index=False, + ... formatters={"name": str.upper}, + ... float_format="{:.1f}".format, + ... ) # doctest: +SKIP + \begin{{tabular}}{{lrr}} + \toprule + name & age & height \\ + \\midrule + RAPHAEL & 26 & 181.2 \\ + DONATELLO & 45 & 177.7 \\ \bottomrule \end{{tabular}} """ @@ -3430,10 +3482,9 @@ def _wrap(x, alt_format_): if column_formatter is not None: column_format_.update({"formatter": column_formatter}) - formatters = { - k: functools.partial(_wrap, alt_format_=v) - for k, v in formatters.items() - } + float_columns = self.select_dtypes(include="float").columns + for col in [c for c in float_columns if c not in formatters.keys()]: + formatters.update({col: float_format_}) elif formatters is None and float_format is not None: formatters = functools.partial(_wrap, alt_format_=lambda v: v) else: From 41fa426aa93a7a0ae40523c63110fbb17a60b635 Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Thu, 4 Aug 2022 22:20:54 +0200 Subject: [PATCH 08/24] doc change --- pandas/core/generic.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/pandas/core/generic.py b/pandas/core/generic.py index fc5f84252699f..d72b47ffbd129 100644 --- a/pandas/core/generic.py +++ b/pandas/core/generic.py @@ -3367,7 +3367,7 @@ def to_latex( ----- .. note:: - As of v1.5.0 this method has changed to use the Styler implementation of + As of v1.5.0 this method has changed to use the ``Styler`` implementation of ``to_latex`` and no longer uses the DataFrameRenderer. It is advised that users switch to using Styler, since this implementation is more frequently updated and contains much more flexibility with the output. The following @@ -3394,7 +3394,8 @@ def to_latex( formatter={"name": str.upper}, na_rep="-", precision=1, escape="latex" ) - To control other aspects we use the ``Styler.to_latex`` arguments such as, + To control other aspects we use the ``Styler.to_latex`` arguments, as + documented, such as, .. code-block:: python @@ -3413,14 +3414,14 @@ def to_latex( ... formatters={"name": str.upper}, ... float_format="{:.1f}".format, ... ) # doctest: +SKIP - \begin{{tabular}}{{lrr}} + \begin{tabular}{lrr} \toprule name & age & height \\ - \\midrule + \midrule RAPHAEL & 26 & 181.2 \\ DONATELLO & 45 & 177.7 \\ \bottomrule - \end{{tabular}} + \end{tabular} """ msg = ( "`col_space` is deprecated. Whitespace in LaTeX does not impact " From 2d5c4192dd034455f46f946f081b639737116372 Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Fri, 5 Aug 2022 18:05:22 +0200 Subject: [PATCH 09/24] mypy fixes --- pandas/core/generic.py | 36 ++++++++++++++++++------------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/pandas/core/generic.py b/pandas/core/generic.py index d72b47ffbd129..a23ea478cfe80 100644 --- a/pandas/core/generic.py +++ b/pandas/core/generic.py @@ -3227,7 +3227,6 @@ def to_latex( def to_latex( self, buf: FilePath | WriteBuffer[str] | None = None, - *, columns: Sequence[Hashable] | None = None, col_space: ColspaceArgType | None = None, header: bool_t | Sequence[str] = True, @@ -3460,7 +3459,7 @@ def to_latex( column_format_: dict[str, Any] = {"axis": 1, **base_format_} if isinstance(float_format, str): - float_format_ = lambda x: float_format % x + float_format_: Callable | None = lambda x: float_format % x else: float_format_ = float_format @@ -3470,8 +3469,9 @@ def _wrap(x, alt_format_): else: return alt_format_(x) + formatters_: list | tuple | dict | Callable | None = None if isinstance(formatters, list): - formatters = { + formatters_ = { c: functools.partial(_wrap, alt_format_=formatters[i]) for i, c in enumerate(self.columns) } @@ -3483,36 +3483,36 @@ def _wrap(x, alt_format_): if column_formatter is not None: column_format_.update({"formatter": column_formatter}) + formatters_ = formatters float_columns = self.select_dtypes(include="float").columns for col in [c for c in float_columns if c not in formatters.keys()]: - formatters.update({col: float_format_}) + formatters_.update({col: float_format_}) elif formatters is None and float_format is not None: - formatters = functools.partial(_wrap, alt_format_=lambda v: v) - else: - formatters = None + formatters_ = functools.partial(_wrap, alt_format_=lambda v: v) format_index_ = [index_format_, column_format_] # Deal with hiding indexes and relabelling column names - hide, relabel_index = [], [] + hide_: list[dict] = [] + relabel_index_: list[dict] = [] if columns: - hide.append( + hide_.append( { "subset": [c for c in self.columns if c not in columns], "axis": "columns", } ) if header is False: - hide.append({"axis": "columns"}) + hide_.append({"axis": "columns"}) elif isinstance(header, (list, tuple)): - relabel_index = {"labels": header, "axis": "columns"} + relabel_index_.append({"labels": header, "axis": "columns"}) format_index_ = [index_format_] # column_format is overwritten if index is False: - hide.append({"axis": "index"}) + hide_.append({"axis": "index"}) if index_names is False: - hide.append({"names": True, "axis": "index"}) + hide_.append({"names": True, "axis": "index"}) - render_kwargs = { + render_kwargs_ = { "hrules": True, "sparse_index": sparsify, "sparse_columns": sparsify, @@ -3533,11 +3533,11 @@ def _wrap(x, alt_format_): return self._to_latex_via_styler( buf, - hide=hide, - relabel_index=relabel_index, - format={"formatter": formatters, **base_format_}, + hide=hide_, + relabel_index=relabel_index_, + format={"formatter": formatters_, **base_format_}, format_index=format_index_, - render_kwargs=render_kwargs, + render_kwargs=render_kwargs_, ) def _to_latex_via_styler( From c9c61c362f06d59877b3d89560ace6259ecc2d54 Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Tue, 9 Aug 2022 07:13:50 +0200 Subject: [PATCH 10/24] ivanov doc comment --- pandas/core/generic.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/pandas/core/generic.py b/pandas/core/generic.py index a23ea478cfe80..1043d0781a2ef 100644 --- a/pandas/core/generic.py +++ b/pandas/core/generic.py @@ -3367,7 +3367,7 @@ def to_latex( .. note:: As of v1.5.0 this method has changed to use the ``Styler`` implementation of - ``to_latex`` and no longer uses the DataFrameRenderer. It is advised that + ``to_latex`` via ``jinja2`` templating. It is advised that users switch to using Styler, since this implementation is more frequently updated and contains much more flexibility with the output. The following examples indicate how this method now replicates the Styler implementation @@ -3384,13 +3384,14 @@ def to_latex( styler.hide(axis="index").hide(axis="columns") - To use ``formatters``, ``na_rep``, ``decimal`` and ``float_format``, + To use ``formatters``, ``na_rep``, ``decimal``, ``float_format``, and ``escape`` we use, .. code-block:: python styler.format( - formatter={"name": str.upper}, na_rep="-", precision=1, escape="latex" + formatter={"name": str.upper}, na_rep="-", precision=1, + escape="latex", decimal="," ) To control other aspects we use the ``Styler.to_latex`` arguments, as From ab6d3ecb61eb8f8b7fb3506a1b3472819055ea5d Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Tue, 9 Aug 2022 07:21:03 +0200 Subject: [PATCH 11/24] ivanov doc comment --- pandas/tests/io/formats/test_format.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/pandas/tests/io/formats/test_format.py b/pandas/tests/io/formats/test_format.py index f9e2e1dab19d2..502270dd8395a 100644 --- a/pandas/tests/io/formats/test_format.py +++ b/pandas/tests/io/formats/test_format.py @@ -3385,10 +3385,8 @@ def test_filepath_or_buffer_arg( ): getattr(df, method)(buf=filepath_or_buffer, encoding=encoding) elif encoding == "foo": - expected_warning = None - with tm.assert_produces_warning(expected_warning): - with pytest.raises(LookupError, match="unknown encoding"): - getattr(df, method)(buf=filepath_or_buffer, encoding=encoding) + with pytest.raises(LookupError, match="unknown encoding"): + getattr(df, method)(buf=filepath_or_buffer, encoding=encoding) else: expected = getattr(df, method)() getattr(df, method)(buf=filepath_or_buffer, encoding=encoding) From b9135831ba63f75d946832cbc8ddbcdbca3a030f Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Thu, 11 Aug 2022 18:22:37 +0200 Subject: [PATCH 12/24] rhshadrach reduction --- pandas/core/generic.py | 40 ++++------------------------------------ 1 file changed, 4 insertions(+), 36 deletions(-) diff --git a/pandas/core/generic.py b/pandas/core/generic.py index 1043d0781a2ef..2948ee3d74f0a 100644 --- a/pandas/core/generic.py +++ b/pandas/core/generic.py @@ -3366,42 +3366,10 @@ def to_latex( ----- .. note:: - As of v1.5.0 this method has changed to use the ``Styler`` implementation of - ``to_latex`` via ``jinja2`` templating. It is advised that - users switch to using Styler, since this implementation is more frequently - updated and contains much more flexibility with the output. The following - examples indicate how this method now replicates the Styler implementation - for its legacy arguments. - - .. code-block:: python - - styler = df.style - - Styler methods are designed to be chained, so we can build complex combinations - of displays. To hide ``index`` and ``columns`` headers we use, - - .. code-block:: python - - styler.hide(axis="index").hide(axis="columns") - - To use ``formatters``, ``na_rep``, ``decimal``, ``float_format``, and - ``escape`` we use, - - .. code-block:: python - - styler.format( - formatter={"name": str.upper}, na_rep="-", precision=1, - escape="latex", decimal="," - ) - - To control other aspects we use the ``Styler.to_latex`` arguments, as - documented, such as, - - .. code-block:: python - - styler.to_latex( - column_format="lrr", caption="my table", environment="longtable" - ) + As of v1.5.0 this method has changed to use the Styler implementation as + part of :meth:`.Styler.to_latex` via ``jinja2`` templating. It is advised + that users switch to using Styler, since this implementation is more + frequently updated and contains much more flexibility with the output. Examples -------- From c803b73a598047385e3d0aba072057f8d160e7aa Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Fri, 18 Nov 2022 22:42:00 +0100 Subject: [PATCH 13/24] change text from 1.5.0 to 2.0.0 --- pandas/core/generic.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/pandas/core/generic.py b/pandas/core/generic.py index 179726381f6b8..e9d8f68e1ad4b 100644 --- a/pandas/core/generic.py +++ b/pandas/core/generic.py @@ -3,6 +3,7 @@ import collections import datetime as dt +from functools import partial import gc import json import operator @@ -3233,7 +3234,7 @@ def to_latex( .. versionchanged:: 1.2.0 Added position argument, changed meaning of caption argument. - .. versionchanged:: 1.5.0 + .. versionchanged:: 2.0.0 Refactored to use the Styler implementation via jinja2 templating. Parameters @@ -3338,7 +3339,7 @@ def to_latex( ----- .. note:: - As of v1.5.0 this method has changed to use the Styler implementation as + As of v2.0.0 this method has changed to use the Styler implementation as part of :meth:`.Styler.to_latex` via ``jinja2`` templating. It is advised that users switch to using Styler, since this implementation is more frequently updated and contains much more flexibility with the output. @@ -3413,7 +3414,7 @@ def _wrap(x, alt_format_): formatters_: list | tuple | dict | Callable | None = None if isinstance(formatters, list): formatters_ = { - c: functools.partial(_wrap, alt_format_=formatters[i]) + c: partial(_wrap, alt_format_=formatters[i]) for i, c in enumerate(self.columns) } elif isinstance(formatters, dict): @@ -3429,7 +3430,7 @@ def _wrap(x, alt_format_): for col in [c for c in float_columns if c not in formatters.keys()]: formatters_.update({col: float_format_}) elif formatters is None and float_format is not None: - formatters_ = functools.partial(_wrap, alt_format_=lambda v: v) + formatters_ = partial(_wrap, alt_format_=lambda v: v) format_index_ = [index_format_, column_format_] # Deal with hiding indexes and relabelling column names From df0b334d4f44c9cc388e85e799160a6a2207245e Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Fri, 18 Nov 2022 22:59:16 +0100 Subject: [PATCH 14/24] remove argument col_space and add whatsnew --- doc/source/whatsnew/v2.0.0.rst | 2 ++ pandas/core/generic.py | 12 ------------ pandas/tests/io/formats/test_to_latex.py | 15 --------------- 3 files changed, 2 insertions(+), 27 deletions(-) diff --git a/doc/source/whatsnew/v2.0.0.rst b/doc/source/whatsnew/v2.0.0.rst index dd0609d3b1f13..dfef661ff9002 100644 --- a/doc/source/whatsnew/v2.0.0.rst +++ b/doc/source/whatsnew/v2.0.0.rst @@ -387,6 +387,7 @@ Removal of prior version deprecations/changes - Removed deprecated :meth:`.Styler.set_na_rep` and :meth:`.Styler.set_precision` (:issue:`49397`) - Removed deprecated :meth:`.Styler.where` (:issue:`49397`) - Removed deprecated :meth:`.Styler.render` (:issue:`49397`) +- Removed deprecated argument ``col_space`` in :meth:`DataFrame.to_latex` (:issue:`47970`) - Removed deprecated argument ``null_color`` in :meth:`.Styler.highlight_null` (:issue:`49397`) - Removed deprecated argument ``check_less_precise`` in :meth:`.testing.assert_frame_equal`, :meth:`.testing.assert_extension_array_equal`, :meth:`.testing.assert_series_equal`, :meth:`.testing.assert_index_equal` (:issue:`30562`) - Removed deprecated ``null_counts`` argument in :meth:`DataFrame.info`. Use ``show_counts`` instead (:issue:`37999`) @@ -554,6 +555,7 @@ Removal of prior version deprecations/changes - Changed behavior of comparison of ``NaT`` with a ``datetime.date`` object; these now raise on inequality comparisons (:issue:`39196`) - Enforced deprecation of silently dropping columns that raised a ``TypeError`` in :class:`Series.transform` and :class:`DataFrame.transform` when used with a list or dictionary (:issue:`43740`) - Changed behavior of :meth:`DataFrame.apply` with list-like so that any partial failure will raise an error (:issue:`43740`) +- Changed behaviour of :meth:`DataFrame.to_latex` to now use the Styler implementation via :meth:`.Styler.to_latex` (:issue:`47970`) - Changed behavior of :meth:`Series.__setitem__` with an integer key and a :class:`Float64Index` when the key is not present in the index; previously we treated the key as positional (behaving like ``series.iloc[key] = val``), now we treat it is a label (behaving like ``series.loc[key] = val``), consistent with :meth:`Series.__getitem__`` behavior (:issue:`33469`) - Removed ``na_sentinel`` argument from :func:`factorize`, :meth:`.Index.factorize`, and :meth:`.ExtensionArray.factorize` (:issue:`47157`) - Changed behavior of :meth:`Series.diff` and :meth:`DataFrame.diff` with :class:`ExtensionDtype` dtypes whose arrays do not implement ``diff``, these now raise ``TypeError`` rather than casting to numpy (:issue:`31025`) diff --git a/pandas/core/generic.py b/pandas/core/generic.py index e9d8f68e1ad4b..7efb07917d7da 100644 --- a/pandas/core/generic.py +++ b/pandas/core/generic.py @@ -3243,11 +3243,6 @@ def to_latex( Buffer to write to. If None, the output is returned as a string. columns : list of label, optional The subset of columns to write. Writes all columns by default. - col_space : int, optional - The minimum width of each column. - - .. deprecated:: 1.5.0 - Whitespace does not affect a rendered LaTeX file and is ignored. header : bool or list of str, default True Write out the column names. If a list of strings is given, it is assumed to be aliases for the column names. @@ -3364,13 +3359,6 @@ def to_latex( \bottomrule \end{tabular} """ - msg = ( - "`col_space` is deprecated. Whitespace in LaTeX does not impact " - "the rendered version, and this argument is ignored." - ) - if col_space is not None: - warnings.warn(msg, DeprecationWarning, stacklevel=find_stack_level()) - # Get defaults from the pandas config if self.ndim == 1: self = self.to_frame() diff --git a/pandas/tests/io/formats/test_to_latex.py b/pandas/tests/io/formats/test_to_latex.py index 54ef877aeb1de..42adf3f7b2826 100644 --- a/pandas/tests/io/formats/test_to_latex.py +++ b/pandas/tests/io/formats/test_to_latex.py @@ -1505,18 +1505,3 @@ def test_get_strrow_multindex_multicolumn(self, row_num, expected): ) assert row_string_converter.get_strrow(row_num=row_num) == expected - - @pytest.mark.parametrize( - "deprecated_arg, value", - [ - ("col_space", 10), - ], - ) - def test_deprecation_warning(self, deprecated_arg, value): - df = DataFrame([[1]]) - msg = ( - "`col_space` is deprecated. Whitespace in LaTeX does not impact " - "the rendered version, and this argument is ignored." - ) - with tm.assert_produces_warning(DeprecationWarning, match=msg): - df.to_latex(**{deprecated_arg: value}) From 97743417f793e47a0d221d641ce8766e680c50f2 Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Wed, 7 Dec 2022 19:30:08 +0100 Subject: [PATCH 15/24] mroeschke requests --- pandas/core/generic.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/pandas/core/generic.py b/pandas/core/generic.py index 4bc7917d031d7..12f7ddf199f3d 100644 --- a/pandas/core/generic.py +++ b/pandas/core/generic.py @@ -3335,12 +3335,12 @@ def to_latex( Notes ----- - - .. note:: - As of v2.0.0 this method has changed to use the Styler implementation as - part of :meth:`.Styler.to_latex` via ``jinja2`` templating. It is advised - that users switch to using Styler, since this implementation is more - frequently updated and contains much more flexibility with the output. + As of v2.0.0 this method has changed to use the Styler implementation as + part of :meth:`.Styler.to_latex` via ``jinja2`` templating. This means + that ``jinja2`` is a requirement, and needs to be installed, for this method + to function. It is advised that users switch to using Styler, since that + implementation is more frequently updated and contains much more + flexibility with the output. Examples -------- From 10b5ec9c9209edfc131f93e5c673e6c6fa4284ba Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Wed, 7 Dec 2022 19:46:20 +0100 Subject: [PATCH 16/24] mroeschke requests --- doc/source/whatsnew/v2.0.0.rst | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/doc/source/whatsnew/v2.0.0.rst b/doc/source/whatsnew/v2.0.0.rst index 1fe2d145e9083..a248fd3597e12 100644 --- a/doc/source/whatsnew/v2.0.0.rst +++ b/doc/source/whatsnew/v2.0.0.rst @@ -341,6 +341,33 @@ Now, the axes return an empty :class:`RangeIndex`. pd.Series().index pd.DataFrame().axes +.. _whatsnew_200.api_breaking.to_latex: + +DataFrame to LaTeX has a new render engine +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +The existing :meth:`DataFrame.to_latex` has been restructured to utilise the +dual implementation previously available under :meth:`.Styler.to_latex`. +The arguments signature is similar, albeit ``col_space`` has been removed since +it is ignored by LaTeX engines. This also requires ``jinja2`` as a dependency +which needs to be installed. +The pandas options below are no longer used. The dual, existing, options that +effect the same functionality are indicated below: + - ``display.latex.escape``: replaced with ``styler.format.escape``, + - ``display.latex.longtable``: replaced with ``styler.latex.environment``, + - ``display.latex.multicolumn``, ``display.latex.multicolumn_format`` and + ``display.latex.multirow``: replaced with ``styler.sparse.rows``, + ``styler.sparse.columns``, ``styler.latex.multirow_align`` and + ``styler.latex.multicol_align``, + - ``display.latex.repr``: replaced with ``styler.render.repr``, + - ``display.max_rows`` and ``display.max_columns``: replace with + ``styler.render.max_rows``, ``styler.render.max_columns`` and + ``styler.render.max_elements``. +Note that the behaviour of ``_repr_latex_`` is also affected to display LaTeX +within JupyterNotebooks in operations (not just on nbconvert) when using the +replacement ``repr`` option. + + .. _whatsnew_200.api_breaking.deps: Increased minimum versions for dependencies From 2c42a4e4bb504e55044d8b7c6649d2643c1bf4bc Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Thu, 8 Dec 2022 19:31:10 +0100 Subject: [PATCH 17/24] pylint fix --- pandas/core/generic.py | 1 - 1 file changed, 1 deletion(-) diff --git a/pandas/core/generic.py b/pandas/core/generic.py index 12f7ddf199f3d..fcaef035751ef 100644 --- a/pandas/core/generic.py +++ b/pandas/core/generic.py @@ -3450,7 +3450,6 @@ def _wrap(x, alt_format_): "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}", From 0b4144aea7b874b7e4a56c4cdb83646986b40a98 Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Thu, 5 Jan 2023 19:20:06 +0100 Subject: [PATCH 18/24] Whats new text improvements and description added --- doc/source/whatsnew/v2.0.0.rst | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/doc/source/whatsnew/v2.0.0.rst b/doc/source/whatsnew/v2.0.0.rst index c0de427b39a7a..093a1ba6022e4 100644 --- a/doc/source/whatsnew/v2.0.0.rst +++ b/doc/source/whatsnew/v2.0.0.rst @@ -401,12 +401,13 @@ DataFrame to LaTeX has a new render engine ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ The existing :meth:`DataFrame.to_latex` has been restructured to utilise the -dual implementation previously available under :meth:`.Styler.to_latex`. +extended implementation previously available under :meth:`.Styler.to_latex`. The arguments signature is similar, albeit ``col_space`` has been removed since -it is ignored by LaTeX engines. This also requires ``jinja2`` as a dependency -which needs to be installed. -The pandas options below are no longer used. The dual, existing, options that -effect the same functionality are indicated below: +it is ignored by LaTeX engines. This render engine also requires ``jinja2`` as a +dependency which needs to be installed, since rendering is based upon jinja2 templates. + +The pandas options below are no longer used and will be removed in future releases. +The alternative options giving similar functionality are indicated below: - ``display.latex.escape``: replaced with ``styler.format.escape``, - ``display.latex.longtable``: replaced with ``styler.latex.environment``, - ``display.latex.multicolumn``, ``display.latex.multicolumn_format`` and @@ -417,10 +418,11 @@ effect the same functionality are indicated below: - ``display.max_rows`` and ``display.max_columns``: replace with ``styler.render.max_rows``, ``styler.render.max_columns`` and ``styler.render.max_elements``. -Note that the behaviour of ``_repr_latex_`` is also affected to display LaTeX -within JupyterNotebooks in operations (not just on nbconvert) when using the -replacement ``repr`` option. - +Note that the behaviour of ``_repr_latex_`` is also changed. Previously +setting ``display.latex.repr`` would generate LaTeX only when using nbconvert for a +JupyterNotebook, and not when the user is running the notebook. Now the +``styler.render.repr`` option allows control of the specific output +within JupyterNotebooks for operations (not just on nbconvert). See :issue:`39911`. .. _whatsnew_200.api_breaking.deps: From fae1b3f85c321a35ce51442fa880116b9571778c Mon Sep 17 00:00:00 2001 From: JHM Darbyshire <24256554+attack68@users.noreply.github.com> Date: Fri, 13 Jan 2023 07:29:01 +0100 Subject: [PATCH 19/24] Update doc/source/whatsnew/v2.0.0.rst Co-authored-by: Matthew Roeschke <10647082+mroeschke@users.noreply.github.com> --- doc/source/whatsnew/v2.0.0.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/source/whatsnew/v2.0.0.rst b/doc/source/whatsnew/v2.0.0.rst index 093a1ba6022e4..ce36ded9c246e 100644 --- a/doc/source/whatsnew/v2.0.0.rst +++ b/doc/source/whatsnew/v2.0.0.rst @@ -398,7 +398,7 @@ Now, the axes return an empty :class:`RangeIndex`. .. _whatsnew_200.api_breaking.to_latex: DataFrame to LaTeX has a new render engine -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ The existing :meth:`DataFrame.to_latex` has been restructured to utilise the extended implementation previously available under :meth:`.Styler.to_latex`. From 9c7d780ab03ca0ace983da7458a842f3e53818c2 Mon Sep 17 00:00:00 2001 From: JHM Darbyshire <24256554+attack68@users.noreply.github.com> Date: Fri, 13 Jan 2023 08:16:00 +0100 Subject: [PATCH 20/24] Update doc/source/whatsnew/v2.0.0.rst --- doc/source/whatsnew/v2.0.0.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/doc/source/whatsnew/v2.0.0.rst b/doc/source/whatsnew/v2.0.0.rst index ce36ded9c246e..243091991f573 100644 --- a/doc/source/whatsnew/v2.0.0.rst +++ b/doc/source/whatsnew/v2.0.0.rst @@ -408,6 +408,7 @@ dependency which needs to be installed, since rendering is based upon jinja2 tem The pandas options below are no longer used and will be removed in future releases. The alternative options giving similar functionality are indicated below: + - ``display.latex.escape``: replaced with ``styler.format.escape``, - ``display.latex.longtable``: replaced with ``styler.latex.environment``, - ``display.latex.multicolumn``, ``display.latex.multicolumn_format`` and @@ -418,6 +419,7 @@ The alternative options giving similar functionality are indicated below: - ``display.max_rows`` and ``display.max_columns``: replace with ``styler.render.max_rows``, ``styler.render.max_columns`` and ``styler.render.max_elements``. + Note that the behaviour of ``_repr_latex_`` is also changed. Previously setting ``display.latex.repr`` would generate LaTeX only when using nbconvert for a JupyterNotebook, and not when the user is running the notebook. Now the From c0bdc6a302b071e32042e31de456f1a4fe803b78 Mon Sep 17 00:00:00 2001 From: JHM Darbyshire <24256554+attack68@users.noreply.github.com> Date: Mon, 16 Jan 2023 12:41:27 +0100 Subject: [PATCH 21/24] remove trailing whitespace --- doc/source/whatsnew/v2.0.0.rst | 1 - 1 file changed, 1 deletion(-) diff --git a/doc/source/whatsnew/v2.0.0.rst b/doc/source/whatsnew/v2.0.0.rst index 243091991f573..a4ca50fdfc96c 100644 --- a/doc/source/whatsnew/v2.0.0.rst +++ b/doc/source/whatsnew/v2.0.0.rst @@ -408,7 +408,6 @@ dependency which needs to be installed, since rendering is based upon jinja2 tem The pandas options below are no longer used and will be removed in future releases. The alternative options giving similar functionality are indicated below: - - ``display.latex.escape``: replaced with ``styler.format.escape``, - ``display.latex.longtable``: replaced with ``styler.latex.environment``, - ``display.latex.multicolumn``, ``display.latex.multicolumn_format`` and From c740279983daef902b8dfd9e82855fa96b509bdb Mon Sep 17 00:00:00 2001 From: JHM Darbyshire <24256554+attack68@users.noreply.github.com> Date: Mon, 16 Jan 2023 12:41:43 +0100 Subject: [PATCH 22/24] remove trailing whitespace --- doc/source/whatsnew/v2.0.0.rst | 1 - 1 file changed, 1 deletion(-) diff --git a/doc/source/whatsnew/v2.0.0.rst b/doc/source/whatsnew/v2.0.0.rst index a4ca50fdfc96c..ce36ded9c246e 100644 --- a/doc/source/whatsnew/v2.0.0.rst +++ b/doc/source/whatsnew/v2.0.0.rst @@ -418,7 +418,6 @@ The alternative options giving similar functionality are indicated below: - ``display.max_rows`` and ``display.max_columns``: replace with ``styler.render.max_rows``, ``styler.render.max_columns`` and ``styler.render.max_elements``. - Note that the behaviour of ``_repr_latex_`` is also changed. Previously setting ``display.latex.repr`` would generate LaTeX only when using nbconvert for a JupyterNotebook, and not when the user is running the notebook. Now the From 6b314cd2ad3bea51cda1cdfdb625fd50405e935e Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Tue, 17 Jan 2023 20:03:02 +0100 Subject: [PATCH 23/24] Whats new linting fixes --- doc/source/whatsnew/v2.0.0.rst | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/doc/source/whatsnew/v2.0.0.rst b/doc/source/whatsnew/v2.0.0.rst index 39563f3bcaa37..e3a4c2bc0daef 100644 --- a/doc/source/whatsnew/v2.0.0.rst +++ b/doc/source/whatsnew/v2.0.0.rst @@ -465,16 +465,18 @@ dependency which needs to be installed, since rendering is based upon jinja2 tem The pandas options below are no longer used and will be removed in future releases. The alternative options giving similar functionality are indicated below: - - ``display.latex.escape``: replaced with ``styler.format.escape``, - - ``display.latex.longtable``: replaced with ``styler.latex.environment``, - - ``display.latex.multicolumn``, ``display.latex.multicolumn_format`` and - ``display.latex.multirow``: replaced with ``styler.sparse.rows``, - ``styler.sparse.columns``, ``styler.latex.multirow_align`` and - ``styler.latex.multicol_align``, - - ``display.latex.repr``: replaced with ``styler.render.repr``, - - ``display.max_rows`` and ``display.max_columns``: replace with - ``styler.render.max_rows``, ``styler.render.max_columns`` and - ``styler.render.max_elements``. + +- ``display.latex.escape``: replaced with ``styler.format.escape``, +- ``display.latex.longtable``: replaced with ``styler.latex.environment``, +- ``display.latex.multicolumn``, ``display.latex.multicolumn_format`` and + ``display.latex.multirow``: replaced with ``styler.sparse.rows``, + ``styler.sparse.columns``, ``styler.latex.multirow_align`` and + ``styler.latex.multicol_align``, +- ``display.latex.repr``: replaced with ``styler.render.repr``, +- ``display.max_rows`` and ``display.max_columns``: replace with + ``styler.render.max_rows``, ``styler.render.max_columns`` and + ``styler.render.max_elements``. + Note that the behaviour of ``_repr_latex_`` is also changed. Previously setting ``display.latex.repr`` would generate LaTeX only when using nbconvert for a JupyterNotebook, and not when the user is running the notebook. Now the From d6333cbc731e9961534f4ac99915969f37917274 Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Wed, 18 Jan 2023 19:52:14 +0100 Subject: [PATCH 24/24] mroeschke requests --- pandas/core/generic.py | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/pandas/core/generic.py b/pandas/core/generic.py index 7cd214955ca4a..6ebba70839dd9 100644 --- a/pandas/core/generic.py +++ b/pandas/core/generic.py @@ -49,7 +49,6 @@ ArrayLike, Axis, AxisInt, - ColspaceArgType, CompressionOptions, Dtype, DtypeArg, @@ -3146,7 +3145,6 @@ def to_latex( self, buf: None = ..., columns: Sequence[Hashable] | None = ..., - col_space: ColspaceArgType | None = ..., header: bool_t | Sequence[str] = ..., index: bool_t = ..., na_rep: str = ..., @@ -3174,7 +3172,6 @@ def to_latex( self, buf: FilePath | WriteBuffer[str], columns: Sequence[Hashable] | None = ..., - col_space: ColspaceArgType | None = ..., header: bool_t | Sequence[str] = ..., index: bool_t = ..., na_rep: str = ..., @@ -3202,7 +3199,6 @@ def to_latex( self, buf: FilePath | WriteBuffer[str] | None = None, columns: Sequence[Hashable] | None = None, - col_space: ColspaceArgType | None = None, header: bool_t | Sequence[str] = True, index: bool_t = True, na_rep: str = "NaN", @@ -3417,8 +3413,9 @@ def _wrap(x, alt_format_): formatters_ = formatters float_columns = self.select_dtypes(include="float").columns - for col in [c for c in float_columns if c not in formatters.keys()]: - formatters_.update({col: float_format_}) + for col in float_columns: + if col not in formatters.keys(): + formatters_.update({col: float_format_}) elif formatters is None and float_format is not None: formatters_ = partial(_wrap, alt_format_=lambda v: v) format_index_ = [index_format_, column_format_] @@ -3479,7 +3476,7 @@ def _to_latex_via_styler( relabel_index: dict | list[dict] | None = None, format: dict | list[dict] | None = None, format_index: dict | list[dict] | None = None, - render_kwargs: dict = {}, + render_kwargs: dict | None = None, ): """ Render object to a LaTeX tabular, longtable, or nested table. @@ -3532,6 +3529,7 @@ def _to_latex_via_styler( getattr(styler, kw_name)(**sub_kw) # bold_rows is not a direct kwarg of Styler.to_latex + render_kwargs = {} if render_kwargs is None else render_kwargs if render_kwargs.pop("bold_rows"): styler.applymap_index(lambda v: "textbf:--rwrap;")