Repository: pandas-dev/pandas
Pull request: ENH: Enable short_caption in to_latex
- [x] closes #36267
- [x] tests added / passed
- [x] passes black pandas
- [x] passes git diff upstream/master -u -- "*.py" | flake8 --diff
- [x] whatsnew entry


Enable short_caption for ``DataFrame.to_latex`` by expanding the meaning of kwarg ``caption``.
Optionally ``caption`` can be a ``Tuple[str, str] = full_caption, short_caption``.

The final caption macro would look like this:
```
\caption[short_caption]{full_caption}
```


Problem statement:
ENH: short caption for LaTeX tables
#### Is your feature request related to a problem?

``DataFrame.to_latex()`` does not allow one to specify short caption.
Short caption is useful for the creation of neat-looking list of tables, where the caption is short, fitting into one line.

#### Describe the solution you'd like

Either add kwarg ``short_caption`` to ``to_latex`` method.
Alternatively expand the meaning of kwarg ``caption``, so that it can optionally be unpacked into ``full_caption, short_caption``.

#### API breaking implications

No.

#### Describe alternatives you've considered

No.

#### Additional context

Example of LaTeX table with short caption.

```
\begin{table}
  \centering
  \caption[Short caption]{Long-long full caption}
  \label{tab:label}
  \begin{tabular}{lr}
    \toprule
    Col1 & Col2 \\
    \midrule
    1 & 3 \\
    2 & 4 \\
    \bottomrule
  \end{tabular}
\end{table}
```

Possible code snippet:

```
df = pd.DataFrame({'a': [1, 2], 'b': [3, 4]})
df.to_latex(caption=('long caption', 'short caption'))
```

Hints from issue discussion:

Commit message: DOC: add parameters to LatexFormatter docstring

Patch to review:
diff --git a/pandas/core/generic.py b/pandas/core/generic.py
index 93c945638a174..27e34608a310b 100644
--- a/pandas/core/generic.py
+++ b/pandas/core/generic.py
@@ -3074,11 +3074,12 @@ def to_latex(
             centered labels (instead of top-aligned) across the contained
             rows, separating groups via clines. The default will be read
             from the pandas config module.
-        caption : str, optional
-            The LaTeX caption to be placed inside ``\caption{}`` in the output.
+        caption : str, tuple, optional
+            Tuple (short_caption, full_caption),
+            which results in \caption[short_caption]{caption};
+            if a single string is passed, no short caption will be set.
 
             .. versionadded:: 1.0.0
-
         label : str, optional
             The LaTeX label to be placed inside ``\label{}`` in the output.
             This is used with ``\ref{}`` in the main ``.tex`` file.
@@ -3123,6 +3124,18 @@ def to_latex(
         if multirow is None:
             multirow = config.get_option("display.latex.multirow")
 
+        if caption:
+            if isinstance(caption, str):
+                short_caption = ""
+            else:
+                try:
+                    caption, short_caption = caption
+                except ValueError as err:
+                    msg = "caption must be either str or tuple of two strings"
+                    raise ValueError(msg) from err
+        else:
+            short_caption = None
+
         formatter = DataFrameFormatter(
             self,
             columns=columns,
@@ -3147,6 +3160,7 @@ def to_latex(
             multicolumn_format=multicolumn_format,
             multirow=multirow,
             caption=caption,
+            short_caption=short_caption,
             label=label,
             position=position,
         )
diff --git a/pandas/io/formats/format.py b/pandas/io/formats/format.py
index 623dc6e6bad91..1e1393de6f306 100644
--- a/pandas/io/formats/format.py
+++ b/pandas/io/formats/format.py
@@ -931,6 +931,7 @@ def to_latex(
         multicolumn_format: Optional[str] = None,
         multirow: bool = False,
         caption: Optional[str] = None,
+        short_caption: Optional[str] = None,
         label: Optional[str] = None,
         position: Optional[str] = None,
     ) -> Optional[str]:
@@ -947,6 +948,7 @@ def to_latex(
             multicolumn_format=multicolumn_format,
             multirow=multirow,
             caption=caption,
+            short_caption=short_caption,
             label=label,
             position=position,
         )
diff --git a/pandas/io/formats/latex.py b/pandas/io/formats/latex.py
index 8080d953da308..fd7952b28f92d 100644
--- a/pandas/io/formats/latex.py
+++ b/pandas/io/formats/latex.py
@@ -275,6 +275,8 @@ class TableBuilderAbstract(ABC):
         Use multirow to enhance MultiIndex rows.
     caption: str, optional
         Table caption.
+    short_caption: str, optional
+        Table short caption.
     label: str, optional
         LaTeX label.
     position: str, optional
@@ -289,6 +291,7 @@ def __init__(
         multicolumn_format: Optional[str] = None,
         multirow: bool = False,
         caption: Optional[str] = None,
+        short_caption: Optional[str] = None,
         label: Optional[str] = None,
         position: Optional[str] = None,
     ):
@@ -298,6 +301,7 @@ def __init__(
         self.multicolumn_format = multicolumn_format
         self.multirow = multirow
         self.caption = caption
+        self.short_caption = short_caption
         self.label = label
         self.position = position
 
@@ -384,8 +388,23 @@ def _position_macro(self) -> str:
 
     @property
     def _caption_macro(self) -> str:
-        r"""Caption macro, extracted from self.caption, like \caption{cap}."""
-        return f"\\caption{{{self.caption}}}" if self.caption else ""
+        r"""Caption macro, extracted from self.caption.
+
+        With short caption:
+            \caption[short_caption]{caption_string}.
+
+        Without short caption:
+            \caption{caption_string}.
+        """
+        if self.caption:
+            return f"\\caption{self._short_caption_macro}{{{self.caption}}}"
+        return ""
+
+    @property
+    def _short_caption_macro(self) -> str:
+        if self.short_caption:
+            return f"[{self.short_caption}]"
+        return ""
 
     @property
     def _label_macro(self) -> str:
@@ -582,15 +601,34 @@ def env_end(self) -> str:
 
 
 class LatexFormatter(TableFormatter):
-    """
+    r"""
     Used to render a DataFrame to a LaTeX tabular/longtable environment output.
 
     Parameters
     ----------
     formatter : `DataFrameFormatter`
+    longtable : bool, default False
+        Use longtable environment.
     column_format : str, default None
         The columns format as specified in `LaTeX table format
         <https://en.wikibooks.org/wiki/LaTeX/Tables>`__ e.g 'rcl' for 3 columns
+    multicolumn : bool, default False
+        Use \multicolumn to enhance MultiIndex columns.
+    multicolumn_format : str, default 'l'
+        The alignment for multicolumns, similar to `column_format`
+    multirow : bool, default False
+        Use \multirow to enhance MultiIndex rows.
+    caption : str, optional
+        Full caption.
+        Caption macro is to be rendered as ``\caption{caption}``.
+    short_caption : str, optional
+        Short caption.
+        Caption macro is to be rendered as ``\caption[short_caption]{caption}``.
+    label : str, optional
+        The LaTeX label to be placed inside ``\label{}`` in the output.
+    position : str, optional
+        The LaTeX positional argument for tables, to be placed after
+        ``\begin{}`` in the output.
 
     See Also
     --------
@@ -606,6 +644,7 @@ def __init__(
         multicolumn_format: Optional[str] = None,
         multirow: bool = False,
         caption: Optional[str] = None,
+        short_caption: Optional[str] = None,
         label: Optional[str] = None,
         position: Optional[str] = None,
     ):
@@ -617,6 +656,7 @@ def __init__(
         self.multicolumn_format = multicolumn_format
         self.multirow = multirow
         self.caption = caption
+        self.short_caption = short_caption
         self.label = label
         self.position = position
 
@@ -644,6 +684,7 @@ def builder(self) -> TableBuilderAbstract:
             multicolumn_format=self.multicolumn_format,
             multirow=self.multirow,
             caption=self.caption,
+            short_caption=self.short_caption,
             label=self.label,
             position=self.position,
         )
diff --git a/pandas/tests/io/formats/test_to_latex.py b/pandas/tests/io/formats/test_to_latex.py
index a98644250b328..b4f4da547788c 100644
--- a/pandas/tests/io/formats/test_to_latex.py
+++ b/pandas/tests/io/formats/test_to_latex.py
@@ -459,6 +459,7 @@ def test_to_latex_longtable(self):
     def test_to_latex_caption_label(self):
         # GH 25436
         the_caption = "a table in a \\texttt{table/tabular} environment"
+        the_short_caption = "a table"
         the_label = "tab:table_tabular"
 
         df = DataFrame({"a": [1, 2], "b": ["b1", "b2"]})
@@ -515,12 +516,73 @@ def test_to_latex_caption_label(self):
 \bottomrule
 \end{tabular}
 \end{table}
+"""
+        assert result_cl == expected_cl
+
+        # test when the short_caption is provided alongside caption
+        result_cl = df.to_latex(caption=(the_caption, the_short_caption))
+
+        expected_cl = r"""\begin{table}
+\centering
+\caption[a table]{a table in a \texttt{table/tabular} environment}
+\begin{tabular}{lrl}
+\toprule
+{} &  a &   b \\
+\midrule
+0 &  1 &  b1 \\
+1 &  2 &  b2 \\
+\bottomrule
+\end{tabular}
+\end{table}
+"""
+        assert result_cl == expected_cl
+
+        # test when the short_caption is provided alongside caption and label
+        result_cl = df.to_latex(
+            caption=(the_caption, the_short_caption), label=the_label,
+        )
+
+        expected_cl = 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 \\
+\midrule
+0 &  1 &  b1 \\
+1 &  2 &  b2 \\
+\bottomrule
+\end{tabular}
+\end{table}
+"""
+        assert result_cl == expected_cl
+
+        # test that wrong number of params is raised
+        with pytest.raises(ValueError):
+            df.to_latex(caption=(the_caption, the_short_caption, "extra_string"))
+
+        # test that two chars caption is handled correctly
+        result_cl = df.to_latex(caption="xy")
+        expected_cl = r"""\begin{table}
+\centering
+\caption{xy}
+\begin{tabular}{lrl}
+\toprule
+{} &  a &   b \\
+\midrule
+0 &  1 &  b1 \\
+1 &  2 &  b2 \\
+\bottomrule
+\end{tabular}
+\end{table}
 """
         assert result_cl == expected_cl
 
     def test_to_latex_longtable_caption_label(self):
         # GH 25436
         the_caption = "a table in a \\texttt{longtable} environment"
+        the_short_caption = "a table"
         the_label = "tab:longtable"
 
         df = DataFrame({"a": [1, 2], "b": ["b1", "b2"]})
@@ -584,6 +646,31 @@ def test_to_latex_longtable_caption_label(self):
 \midrule
 \endfoot
 
+\bottomrule
+\endlastfoot
+0 &  1 &  b1 \\
+1 &  2 &  b2 \\
+\end{longtable}
+"""
+        assert result_cl == expected_cl
+
+        # test when the caption, the short_caption and the label are provided
+        result_cl = df.to_latex(
+            longtable=True, caption=(the_caption, the_short_caption), label=the_label,
+        )
+
+        expected_cl = r"""\begin{longtable}{lrl}
+\caption[a table]{a table in a \texttt{longtable} environment}
+\label{tab:longtable}\\
+\toprule
+{} &  a &   b \\
+\midrule
+\endhead
+\midrule
+\multicolumn{3}{r}{{Continued on next page}} \\
+\midrule
+\endfoot
+
 \bottomrule
 \endlastfoot
 0 &  1 &  b1 \\


Code review comments:
diff_hunk: @@ -3123,6 +3124,18 @@ def to_latex(
         if multirow is None:
             multirow = config.get_option("display.latex.multirow")
 
+        if caption:
path: pandas/core/generic.py
text: @user1:
instead of doing this here, can you do it in the formatter itself (in the constructor is ok)

@author:
Done.

diff_hunk: @@ -3074,11 +3074,12 @@ def to_latex(
             centered labels (instead of top-aligned) across the contained
             rows, separating groups via clines. The default will be read
             from the pandas config module.
-        caption : str, optional
-            The LaTeX caption to be placed inside ``\caption{}`` in the output.
+        caption : str, tuple, optional
path: pandas/core/generic.py
text: @user1:
str or tuple, optional (change the signature as well)
Optional[Union[str, Tuple[str, str]]]

@author:
Done. I updated typing on caption in DataFrameFormatter.to_latex() and LatexFormatter.