Skip to content
5 changes: 5 additions & 0 deletions pandas/core/generic.py
Original file line number Diff line number Diff line change
Expand Up @@ -2840,6 +2840,7 @@ def to_latex(
multirow=None,
caption=None,
label=None,
position=None,
):
r"""
Render object to a LaTeX tabular, longtable, or nested table/tabular.
Expand Down Expand Up @@ -2925,6 +2926,9 @@ def to_latex(
This is used with ``\ref{}`` in the main ``.tex`` file.

.. versionadded:: 1.0.0
position : str, optional
The LaTeX positional argument for tables, to be placed after
``\begin{}`` in the output.
%(returns)s
See Also
--------
Expand Down Expand Up @@ -2986,6 +2990,7 @@ def to_latex(
multirow=multirow,
caption=caption,
label=label,
position=position,
)

def to_csv(
Expand Down
2 changes: 2 additions & 0 deletions pandas/io/formats/format.py
Original file line number Diff line number Diff line change
Expand Up @@ -931,6 +931,7 @@ def to_latex(
multirow: bool = False,
caption: Optional[str] = None,
label: Optional[str] = None,
position: Optional[str] = None,
) -> Optional[str]:
"""
Render a DataFrame to a LaTeX tabular/longtable environment output.
Expand All @@ -946,6 +947,7 @@ def to_latex(
multirow=multirow,
caption=caption,
label=label,
position=position,
).get_result(buf=buf, encoding=encoding)

def _format_col(self, i: int) -> List[str]:
Expand Down
28 changes: 24 additions & 4 deletions pandas/io/formats/latex.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ def __init__(
multirow: bool = False,
caption: Optional[str] = None,
label: Optional[str] = None,
position: Optional[str] = None,
):
self.fmt = formatter
self.frame = self.fmt.frame
Expand All @@ -50,6 +51,7 @@ def __init__(
self.caption = caption
self.label = label
self.escape = self.fmt.escape
self.position = position

def write_result(self, buf: IO[str]) -> None:
"""
Expand Down Expand Up @@ -284,8 +286,13 @@ def _write_tabular_begin(self, buf, column_format: str):
<https://en.wikibooks.org/wiki/LaTeX/Tables>`__ e.g 'rcl'
for 3 columns
"""
if self.caption is not None or self.label is not None:
if (
self.caption is not None
or self.label is not None
or self.position is not None
):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As this test grows, it would be better to have self._table_float = any([p is not None for p in (caption, label, position)]) inside __init__.py, to be used both here and in _write_tabular_end.

# then write output in a nested table/tabular environment
buf.write(f"\\begin{{table}}")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any obvious reason why this was moved up here? I would find buf.write(f"\\begin{{table}}{position_}") more readable that the write below.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The reason I did this was only because the line was getting too long, but I can write it as it was for sure.
Maybe we should unify the way it's done in _write_tabular_begin and _write_longtable_begin ? But it may be nitpicking

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The reason I did this was only because the line was getting too long, but I can write it as it was for sure.

I do find it more readable - but if you want to break the line, you could do so at \n, so after the position.

Maybe we should unify the way it's done in _write_tabular_begin and _write_longtable_begin ?

Wow... indeed the difference between _write_tabular_begin and _write_longtable_begin seems to amount to the environment name: unless I'm mistaken, the merge is definitely worth doing (same for _end). By the way, if the function are unified feel free to move back the any([p is not None... down there.

if self.caption is None:
caption_ = ""
else:
Expand All @@ -296,7 +303,12 @@ def _write_tabular_begin(self, buf, column_format: str):
else:
label_ = f"\n\\label{{{self.label}}}"

buf.write(f"\\begin{{table}}\n\\centering{caption_}{label_}\n")
if self.position is None:
position_ = ""
else:
position_ = f"[{self.position}]"

buf.write(f"{position_}\n\\centering{caption_}{label_}\n")
else:
# then write output only in a tabular environment
pass
Expand All @@ -317,7 +329,11 @@ def _write_tabular_end(self, buf):
"""
buf.write("\\bottomrule\n")
buf.write("\\end{tabular}\n")
if self.caption is not None or self.label is not None:
if (
self.caption is not None
or self.label is not None
or self.position is not None
):
buf.write("\\end{table}\n")
else:
pass
Expand All @@ -337,7 +353,11 @@ def _write_longtable_begin(self, buf, column_format: str):
<https://en.wikibooks.org/wiki/LaTeX/Tables>`__ e.g 'rcl'
for 3 columns
"""
buf.write(f"\\begin{{longtable}}{{{column_format}}}\n")
if self.position is None:
position_ = ""
else:
position_ = f"[{self.position}]"
buf.write(f"\\begin{{longtable}}{position_}{{{column_format}}}\n")

if self.caption is not None or self.label is not None:
if self.caption is None:
Expand Down
48 changes: 48 additions & 0 deletions pandas/tests/io/formats/test_to_latex.py
Original file line number Diff line number Diff line change
Expand Up @@ -573,6 +573,54 @@ def test_to_latex_longtable_caption_label(self):
"""
assert result_cl == expected_cl

def test_to_latex_position(self):
the_position = "h"

df = DataFrame({"a": [1, 2], "b": ["b1", "b2"]})

# test when only the position is provided
result_p = df.to_latex(position=the_position)

expected_p = r"""\begin{table}[h]
\centering
\begin{tabular}{lrl}
\toprule
{} & a & b \\
\midrule
0 & 1 & b1 \\
1 & 2 & b2 \\
\bottomrule
\end{tabular}
\end{table}
"""
assert result_p == expected_p

def test_to_latex_longtable_position(self):
the_position = "t"

df = DataFrame({"a": [1, 2], "b": ["b1", "b2"]})

# test when only the position is provided
result_p = df.to_latex(longtable=True, position=the_position)

expected_p = r"""\begin{longtable}[t]{lrl}
\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_p == expected_p

def test_to_latex_escape_special_chars(self):
special_characters = ["&", "%", "$", "#", "_", "{", "}", "~", "^", "\\"]
df = DataFrame(data=special_characters)
Expand Down