diff --git a/doc/source/whatsnew/v2.0.0.rst b/doc/source/whatsnew/v2.0.0.rst
index cb51365fa27cd..e3a4c2bc0daef 100644
--- a/doc/source/whatsnew/v2.0.0.rst
+++ b/doc/source/whatsnew/v2.0.0.rst
@@ -452,6 +452,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
@@ -617,6 +648,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`)
@@ -791,6 +823,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 700601cb4f024..6ebba70839dd9 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,
@@ -2108,7 +2107,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
@@ -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 = ...,
@@ -3198,12 +3195,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",
@@ -3237,14 +3232,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.
@@ -3318,7 +3314,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
@@ -3327,30 +3328,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()
@@ -3365,35 +3371,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 7f1775c53ce9e..dd361809e197c 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 c0e00d6bd30a4..5264342661b3f 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 702c4a505a06a..687bad07926d0 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 54e0feb28932b..70a2fc7dcc9dd 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 4ded7bebc431e..3532f979665ec 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 d6999b32e6a81..42adf3f7b2826 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 5ade1e0913804..b248c0c460c74 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 57dcd06f8f524..43a6c7028883b 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_()