From b46dd7f55eba5304061dcb24e96c20e0f67864f5 Mon Sep 17 00:00:00 2001 From: JHM Darbyshire <24256554+attack68@users.noreply.github.com> Date: Thu, 19 Jan 2023 21:01:00 +0100 Subject: [PATCH] REF: Use `Styler` implementation for `DataFrame.to_latex` (#47970) * Base implementation * Base implementation * test fix up * test fix up * test fix up * doc change * doc change * doc change * mypy fixes * ivanov doc comment * ivanov doc comment * rhshadrach reduction * change text from 1.5.0 to 2.0.0 * remove argument col_space and add whatsnew * mroeschke requests * mroeschke requests * pylint fix * Whats new text improvements and description added * Update doc/source/whatsnew/v2.0.0.rst Co-authored-by: Matthew Roeschke <10647082+mroeschke@users.noreply.github.com> * Update doc/source/whatsnew/v2.0.0.rst * remove trailing whitespace * remove trailing whitespace * Whats new linting fixes * mroeschke requests Co-authored-by: JHM Darbyshire (iMac) Co-authored-by: Matthew Roeschke <10647082+mroeschke@users.noreply.github.com> --- doc/source/whatsnew/v2.0.0.rst | 33 ++ pandas/core/generic.py | 255 +++++++--- pandas/io/formats/style.py | 8 +- pandas/io/formats/style_render.py | 2 +- pandas/tests/frame/test_repr_info.py | 19 +- pandas/tests/io/formats/test_format.py | 11 +- pandas/tests/io/formats/test_printing.py | 6 +- pandas/tests/io/formats/test_to_latex.py | 566 +++++++++++------------ pandas/tests/io/test_common.py | 3 +- pandas/tests/series/test_repr.py | 14 +- 10 files changed, 534 insertions(+), 383 deletions(-) diff --git a/doc/source/whatsnew/v2.0.0.rst b/doc/source/whatsnew/v2.0.0.rst index 47ccdad59de3c9..7555c8b68a4f7f 100644 --- a/doc/source/whatsnew/v2.0.0.rst +++ b/doc/source/whatsnew/v2.0.0.rst @@ -454,6 +454,37 @@ 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 +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 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 + ``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 +``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: Increased minimum versions for dependencies @@ -619,6 +650,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`) @@ -793,6 +825,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 ab9b76fbdf7125..028fdacd7444e9 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 from json import loads import operator @@ -48,7 +49,6 @@ ArrayLike, Axis, AxisInt, - ColspaceArgType, CompressionOptions, Dtype, DtypeArg, @@ -182,7 +182,6 @@ Window, ) -from pandas.io.formats import format as fmt from pandas.io.formats.format import ( DataFrameFormatter, DataFrameRenderer, @@ -2101,7 +2100,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 @@ -3139,7 +3138,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 = ..., @@ -3167,7 +3165,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 = ..., @@ -3191,12 +3188,10 @@ def to_latex( ... @final - @doc(returns=fmt.return_docstring) 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", @@ -3230,14 +3225,15 @@ def to_latex( .. versionchanged:: 1.2.0 Added position argument, changed meaning of caption argument. + .. versionchanged:: 2.0.0 + Refactored to use the Styler implementation via jinja2 templating. + Parameters ---------- buf : str, Path or StringIO-like, optional, default None 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. 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. @@ -3311,7 +3307,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 -------- io.formats.style.Styler.to_latex : Render a DataFrame to LaTeX @@ -3320,30 +3321,35 @@ def to_latex( tabular output. DataFrame.to_html : Render a DataFrame as an HTML table. + Notes + ----- + 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 -------- + 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}} - """ - 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()) - + \end{tabular} + """ # Get defaults from the pandas config if self.ndim == 1: self = self.to_frame() @@ -3358,35 +3364,170 @@ 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, + 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_ = { + "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_: Callable | None = 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) + + formatters_: list | tuple | dict | Callable | None = None + if isinstance(formatters, list): + formatters_ = { + c: 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_ = formatters + float_columns = self.select_dtypes(include="float").columns + 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_] + + # Deal with hiding indexes and relabelling column names + hide_: list[dict] = [] + relabel_index_: list[dict] = [] + 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_index_.append({"labels": header, "axis": "columns"}) + format_index_ = [index_format_] # column_format is overwritten + + 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, + "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_index=relabel_index_, + format={"formatter": formatters_, **base_format_}, + format_index=format_index_, + render_kwargs=render_kwargs_, ) + def _to_latex_via_styler( + self, + buf=None, + *, + hide: 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 | None = None, + ): + """ + 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.relabel_index(**relabel_index) + 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_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 + 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. + """ + from pandas.io.formats.style import Styler + + self = cast("DataFrame", self) + styler = Styler(self, uuid="") + + for kw_name in ["hide", "relabel_index", "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 + render_kwargs = {} if render_kwargs is None else render_kwargs + 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 7f1775c53ce9ec..dd361809e197c6 100644 --- a/pandas/io/formats/style.py +++ b/pandas/io/formats/style.py @@ -237,7 +237,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, @@ -2173,13 +2173,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. @@ -2189,7 +2189,7 @@ def set_caption(self, caption: str | tuple) -> Styler: 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 c0e00d6bd30a48..5264342661b3f2 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/frame/test_repr_info.py b/pandas/tests/frame/test_repr_info.py index 702c4a505a06af..687bad07926d01 100644 --- a/pandas/tests/frame/test_repr_info.py +++ b/pandas/tests/frame/test_repr_info.py @@ -280,22 +280,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:.*DataFrame.to_latex` is expected to utilise: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 54e0feb28932b6..70a2fc7dcc9ddd 100644 --- a/pandas/tests/io/formats/test_format.py +++ b/pandas/tests/io/formats/test_format.py @@ -3430,7 +3430,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", @@ -3445,6 +3444,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( @@ -3452,10 +3453,8 @@ 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 - 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) @@ -3465,6 +3464,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 4ded7bebc431e3..3532f979665ec0 100644 --- a/pandas/tests/io/formats/test_printing.py +++ b/pandas/tests/io/formats/test_printing.py @@ -1,7 +1,6 @@ import string 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/formats/test_to_latex.py b/pandas/tests/io/formats/test_to_latex.py index d6999b32e6a814..42adf3f7b2826a 100644 --- a/pandas/tests/io/formats/test_to_latex.py +++ b/pandas/tests/io/formats/test_to_latex.py @@ -19,7 +19,7 @@ RowStringConverter, ) -pytestmark = pytest.mark.filterwarnings("ignore::FutureWarning") +pytest.importorskip("jinja2") def _dedent(string): @@ -68,10 +68,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 +85,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 +101,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 +116,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 +134,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 +151,7 @@ def test_to_latex_empty_tabular(self): r""" \begin{tabular}{l} \toprule - Empty DataFrame - Columns: RangeIndex(start=0, stop=0, step=1) - Index: RangeIndex(start=0, stop=0, step=1) \\ + \midrule \bottomrule \end{tabular} """ @@ -167,11 +165,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 +185,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 +204,17 @@ def test_to_latex_empty_longtable(self): r""" \begin{longtable}{l} \toprule - Empty DataFrame - Columns: RangeIndex(start=0, stop=0, step=1) - Index: RangeIndex(start=0, stop=0, step=1) \\ + \midrule + \endfirsthead + \toprule + \midrule + \endhead + \midrule + \multicolumn{0}{r}{Continued on next page} \\ + \midrule + \endfoot + \bottomrule + \endlastfoot \end{longtable} """ ) @@ -221,23 +227,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 +254,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 +296,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 +313,7 @@ def test_to_latex_no_header_without_index(self): r""" \begin{tabular}{rl} \toprule + \midrule 1 & b1 \\ 2 & b2 \\ \bottomrule @@ -326,10 +330,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 +350,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} """ @@ -382,10 +386,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 +406,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 +424,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 +467,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 +487,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 +507,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 +532,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 +567,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 +595,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 +606,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 +628,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 +657,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 +693,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 +732,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 +771,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 +787,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 +804,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 +823,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 +849,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 +868,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 +889,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 +938,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 +957,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 +974,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 +997,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 +1044,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 +1063,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 +1081,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 +1098,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 +1113,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 +1136,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 +1156,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 +1172,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(float_format="{:.1f}".format) 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 +1205,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 +1221,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 +1241,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 +1261,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{2}{*}{c2} & 0 & 0 & 1 & 2 & 3 & 4 \\ - & 1 & 5 & 6 & 7 & 8 & 9 \\ + \multirow[t]{2}{*}{c2} & 0 & 0 & 1 & 2 & 3 & 4 \\ + & 1 & 5 & 6 & 7 & 8 & 9 \\ + \cline{1-7} + c3 & 0 & 0 & 1 & 2 & 3 & 4 \\ \cline{1-7} - c3 & 0 & 0 & 1 & 2 & 3 & 4 \\ \bottomrule \end{tabular} """ @@ -1299,16 +1288,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,25 +1316,24 @@ 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() # pylint: disable-next=consider-using-f-string 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( @@ -1363,14 +1352,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} @@ -1385,11 +1374,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} """ @@ -1407,27 +1396,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,15 +1505,3 @@ 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): - 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() diff --git a/pandas/tests/io/test_common.py b/pandas/tests/io/test_common.py index 5ade1e09138045..b248c0c460c74a 100644 --- a/pandas/tests/io/test_common.py +++ b/pandas/tests/io/test_common.py @@ -330,7 +330,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", [ @@ -345,6 +344,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 57dcd06f8f5240..43a6c7028883b3 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_()