Repository: statsmodels/statsmodels

Issue:
Displaying summary tables in LaTeX when converting Jupyter Notebooks to PDF via nbconvert
#### Is your feature request related to a problem? Please describe
There's currently no built-in method to display tables in LaTeX when converting Jupyter Notebooks to PDF via nbconvert. Output is a string in the converted PDF.

#### Describe the solution you'd like
Including a method along the lines of:
```
def _repr_latex_(self):
    return self.as_latex()
```
somewhere in the summary table scripts would mean the tables could be displayed as both HTML directly in the Jupyter notebook and LaTeX in the converted PDF.

#### Describe alternatives you have considered
I can manually patch my Jupyter notebook to produce a centred LaTeX summary table in my output PDF file:
```
def _repr_latex_(self):
    return f"\\begin{{center}}\n{self.as_latex()}\n\\end{{center}}"

statsmodels.iolib.summary.Summary._repr_latex_ = _repr_latex_
```
however, it would be nice to have this feature built directly into statsmodels. :)

If I have time I'll try making a PR myself, but I've never contributed to statsmodels before, so it might take me a bit of time to ensure that I'm doing everything correctly.


Discussion:

Resolving patch:
diff --git a/statsmodels/iolib/summary.py b/statsmodels/iolib/summary.py
--- a/statsmodels/iolib/summary.py
+++ b/statsmodels/iolib/summary.py
@@ -764,6 +764,10 @@ def _repr_html_(self):
         '''Display as HTML in IPython notebook.'''
         return self.as_html()
 
+    def _repr_latex_(self):
+        '''Display as LaTeX when converting IPython notebook to PDF.'''
+        return self.as_latex()
+
     def add_table_2cols(self, res,  title=None, gleft=None, gright=None,
                         yname=None, xname=None):
         """
diff --git a/statsmodels/iolib/summary2.py b/statsmodels/iolib/summary2.py
--- a/statsmodels/iolib/summary2.py
+++ b/statsmodels/iolib/summary2.py
@@ -30,6 +30,10 @@ def _repr_html_(self):
         """Display as HTML in IPython notebook."""
         return self.as_html()
 
+    def _repr_latex_(self):
+        '''Display as LaTeX when converting IPython notebook to PDF.'''
+        return self.as_latex()
+
     def add_df(self, df, index=True, header=True, float_format='%.4f',
                align='r'):
         """
diff --git a/statsmodels/iolib/table.py b/statsmodels/iolib/table.py
--- a/statsmodels/iolib/table.py
+++ b/statsmodels/iolib/table.py
@@ -221,6 +221,9 @@ def __repr__(self):
     def _repr_html_(self, **fmt_dict):
         return self.as_html(**fmt_dict)
 
+    def _repr_latex_(self, center=True, **fmt_dict):
+        return self.as_latex_tabular(center, **fmt_dict)
+
     def _add_headers_stubs(self, headers, stubs):
         """Return None.  Adds headers and stubs to table,
         if these were provided at initialization.


Test patch:
diff --git a/statsmodels/iolib/tests/test_summary.py b/statsmodels/iolib/tests/test_summary.py
--- a/statsmodels/iolib/tests/test_summary.py
+++ b/statsmodels/iolib/tests/test_summary.py
@@ -5,8 +5,10 @@
 
 import numpy as np  # noqa: F401
 import pytest
+from numpy.testing import assert_equal
 
 from statsmodels.datasets import macrodata
+from statsmodels.tools.tools import add_constant
 from statsmodels.regression.linear_model import OLS
 
 
@@ -31,6 +33,30 @@ def test_wrong_len_xname(reset_randomstate):
         res.summary(xname=['x1', 'x2', 'x3'])
 
 
+class TestSummaryLatex(object):
+    def test__repr_latex_(self):
+        desired = r'''
+\begin{center}
+\begin{tabular}{lcccccc}
+\toprule
+               & \textbf{coef} & \textbf{std err} & \textbf{t} & \textbf{P$> |$t$|$} & \textbf{[0.025} & \textbf{0.975]}  \\
+\midrule
+\textbf{const} &       7.2248  &        0.866     &     8.346  &         0.000        &        5.406    &        9.044     \\
+\textbf{x1}    &      -0.6609  &        0.177     &    -3.736  &         0.002        &       -1.033    &       -0.289     \\
+\bottomrule
+\end{tabular}
+\end{center}
+'''
+        x = [1, 5, 7, 3, 5, 5, 8, 3, 3, 4, 6, 4, 2, 7, 4, 2, 1, 9, 2, 6]
+        x = add_constant(x)
+        y = [6, 4, 2, 7, 4, 2, 1, 9, 2, 6, 1, 5, 7, 3, 5, 5, 8, 3, 3, 4]
+        reg = OLS(y, x).fit()
+
+        actual = reg.summary().tables[1]._repr_latex_()
+        actual = '\n%s\n' % actual
+        assert_equal(actual, desired)
+
+
 if __name__ == '__main__':
 
     from statsmodels.regression.tests.test_regression import TestOLS
diff --git a/statsmodels/iolib/tests/test_summary2.py b/statsmodels/iolib/tests/test_summary2.py
--- a/statsmodels/iolib/tests/test_summary2.py
+++ b/statsmodels/iolib/tests/test_summary2.py
@@ -115,6 +115,38 @@ def test_summary_col_ordering_preserved(self):
                 if line.startswith(variable):
                     assert line in str(actual)
 
+    def test__repr_latex_(self):
+        desired = r'''
+\begin{table}
+\caption{}
+\label{}
+\begin{center}
+\begin{tabular}{lll}
+\hline
+               & y I      & y II      \\
+\hline
+const          & 7.7500   & 12.4231   \\
+               & (1.1058) & (3.1872)  \\
+x1             & -0.7500  & -1.5769   \\
+               & (0.2368) & (0.6826)  \\
+R-squared      & 0.7697   & 0.6401    \\
+R-squared Adj. & 0.6930   & 0.5202    \\
+\hline
+\end{tabular}
+\end{center}
+\end{table}
+'''
+        x = [1, 5, 7, 3, 5]
+        x = add_constant(x)
+        y1 = [6, 4, 2, 7, 4]
+        y2 = [8, 5, 0, 12, 4]
+        reg1 = OLS(y1, x).fit()
+        reg2 = OLS(y2, x).fit()
+
+        actual = summary_col([reg1, reg2])._repr_latex_()
+        actual = '\n%s\n' % actual
+        assert_equal(actual, desired)
+
     def test_OLSsummary(self):
         # Test that latex output of regular OLS output still contains
         # multiple tables
diff --git a/statsmodels/iolib/tests/test_table.py b/statsmodels/iolib/tests/test_table.py
--- a/statsmodels/iolib/tests/test_table.py
+++ b/statsmodels/iolib/tests/test_table.py
@@ -241,3 +241,24 @@ def test_default_alignment(self):
                              txt_fmt=default_txt_fmt)
         actual = '\n%s\n' % actual.as_text()
         assert_equal(desired, str(actual))
+
+    def test__repr_latex(self):
+        desired = r"""
+\begin{center}
+\begin{tabular}{lcc}
+\toprule
+               & \textbf{header1} & \textbf{header2}  \\
+\midrule
+\textbf{stub1} &      5.394       &       29.3        \\
+\textbf{stub2} &       343        &       34.2        \\
+\bottomrule
+\end{tabular}
+\end{center}
+"""
+        testdata = [[5.394, 29.3], [343, 34.2]]
+        teststubs = ('stub1', 'stub2')
+        testheader = ('header1', 'header2')
+        tbl = SimpleTable(testdata, testheader, teststubs,
+                          txt_fmt=default_txt_fmt)
+        actual = '\n%s\n' % tbl._repr_latex_()
+        assert_equal(actual, desired)