diff --git a/doc/source/options.rst b/doc/source/options.rst
index 77cac6d495d13..10a13ed36df8d 100644
--- a/doc/source/options.rst
+++ b/doc/source/options.rst
@@ -273,151 +273,156 @@ Options are 'right', and 'left'.
Available Options
-----------------
-========================== ============ ==================================
-Option Default Function
-========================== ============ ==================================
-display.chop_threshold None If set to a float value, all float
- values smaller then the given
- threshold will be displayed as
- exactly 0 by repr and friends.
-display.colheader_justify right Controls the justification of
- column headers. used by DataFrameFormatter.
-display.column_space 12 No description available.
-display.date_dayfirst False When True, prints and parses dates
- with the day first, eg 20/01/2005
-display.date_yearfirst False When True, prints and parses dates
- with the year first, eg 2005/01/20
-display.encoding UTF-8 Defaults to the detected encoding
- of the console. Specifies the encoding
- to be used for strings returned by
- to_string, these are generally strings
- meant to be displayed on the console.
-display.expand_frame_repr True Whether to print out the full DataFrame
- repr for wide DataFrames across
- multiple lines, `max_columns` is
- still respected, but the output will
- wrap-around across multiple "pages"
- if its width exceeds `display.width`.
-display.float_format None The callable should accept a floating
- point number and return a string with
- the desired format of the number.
- This is used in some places like
- SeriesFormatter.
- See core.format.EngFormatter for an example.
-display.height 60 Deprecated. Use `display.max_rows` instead.
-display.large_repr truncate For DataFrames exceeding max_rows/max_cols,
- the repr (and HTML repr) can show
- a truncated table (the default from 0.13),
- or switch to the view from df.info()
- (the behaviour in earlier versions of pandas).
- allowable settings, ['truncate', 'info']
-display.latex.repr False Whether to produce a latex DataFrame
- representation for jupyter frontends
- that support it.
-display.latex.escape True Escapes special caracters in Dataframes, when
- using the to_latex method.
-display.latex.longtable False Specifies if the to_latex method of a Dataframe
- uses the longtable format.
-display.line_width 80 Deprecated. Use `display.width` instead.
-display.max_columns 20 max_rows and max_columns are used
- in __repr__() methods to decide if
- to_string() or info() is used to
- render an object to a string. In
- case python/IPython is running in
- a terminal this can be set to 0 and
- pandas will correctly auto-detect
- the width the terminal and swap to
- a smaller format in case all columns
- would not fit vertically. The IPython
- notebook, IPython qtconsole, or IDLE
- do not run in a terminal and hence
- it is not possible to do correct
- auto-detection. 'None' value means
- unlimited.
-display.max_colwidth 50 The maximum width in characters of
- a column in the repr of a pandas
- data structure. When the column overflows,
- a "..." placeholder is embedded in
- the output.
-display.max_info_columns 100 max_info_columns is used in DataFrame.info
- method to decide if per column information
- will be printed.
-display.max_info_rows 1690785 df.info() will usually show null-counts
- for each column. For large frames
- this can be quite slow. max_info_rows
- and max_info_cols limit this null
- check only to frames with smaller
- dimensions then specified.
-display.max_rows 60 This sets the maximum number of rows
- pandas should output when printing
- out various output. For example,
- this value determines whether the
- repr() for a dataframe prints out
- fully or just a summary repr.
- 'None' value means unlimited.
-display.max_seq_items 100 when pretty-printing a long sequence,
- no more then `max_seq_items` will
- be printed. If items are omitted,
- they will be denoted by the addition
- of "..." to the resulting string.
- If set to None, the number of items
- to be printed is unlimited.
-display.memory_usage True This specifies if the memory usage of
- a DataFrame should be displayed when the
- df.info() method is invoked.
-display.multi_sparse True "Sparsify" MultiIndex display (don't
- display repeated elements in outer
- levels within groups)
-display.notebook_repr_html True When True, IPython notebook will
- use html representation for
- pandas objects (if it is available).
-display.pprint_nest_depth 3 Controls the number of nested levels
- to process when pretty-printing
-display.precision 6 Floating point output precision in
- terms of number of places after the
- decimal, for regular formatting as well
- as scientific notation. Similar to
- numpy's ``precision`` print option
-display.show_dimensions truncate Whether to print out dimensions
- at the end of DataFrame repr.
- If 'truncate' is specified, only
- print out the dimensions if the
- frame is truncated (e.g. not display
- all rows and/or columns)
-display.width 80 Width of the display in characters.
- In case python/IPython is running in
- a terminal this can be set to None
- and pandas will correctly auto-detect
- the width. Note that the IPython notebook,
- IPython qtconsole, or IDLE do not run in a
- terminal and hence it is not possible
- to correctly detect the width.
-html.border 1 A ``border=value`` attribute is
- inserted in the ``
`` tag
- for the DataFrame HTML repr.
-io.excel.xls.writer xlwt The default Excel writer engine for
- 'xls' files.
-io.excel.xlsm.writer openpyxl The default Excel writer engine for
- 'xlsm' files. Available options:
- 'openpyxl' (the default).
-io.excel.xlsx.writer openpyxl The default Excel writer engine for
- 'xlsx' files.
-io.hdf.default_format None default format writing format, if
- None, then put will default to
- 'fixed' and append will default to
- 'table'
-io.hdf.dropna_table True drop ALL nan rows when appending
- to a table
-mode.chained_assignment warn Raise an exception, warn, or no
- action if trying to use chained
- assignment, The default is warn
-mode.sim_interactive False Whether to simulate interactive mode
- for purposes of testing
-mode.use_inf_as_null False True means treat None, NaN, -INF,
- INF as null (old way), False means
- None and NaN are null, but INF, -INF
- are not null (new way).
-========================== ============ ==================================
+=================================== ============ ==================================
+Option Default Function
+=================================== ============ ==================================
+display.chop_threshold None If set to a float value, all float
+ values smaller then the given
+ threshold will be displayed as
+ exactly 0 by repr and friends.
+display.colheader_justify right Controls the justification of
+ column headers. used by DataFrameFormatter.
+display.column_space 12 No description available.
+display.date_dayfirst False When True, prints and parses dates
+ with the day first, eg 20/01/2005
+display.date_yearfirst False When True, prints and parses dates
+ with the year first, eg 2005/01/20
+display.encoding UTF-8 Defaults to the detected encoding
+ of the console. Specifies the encoding
+ to be used for strings returned by
+ to_string, these are generally strings
+ meant to be displayed on the console.
+display.expand_frame_repr True Whether to print out the full DataFrame
+ repr for wide DataFrames across
+ multiple lines, `max_columns` is
+ still respected, but the output will
+ wrap-around across multiple "pages"
+ if its width exceeds `display.width`.
+display.float_format None The callable should accept a floating
+ point number and return a string with
+ the desired format of the number.
+ This is used in some places like
+ SeriesFormatter.
+ See core.format.EngFormatter for an example.
+display.height 60 Deprecated. Use `display.max_rows` instead.
+display.large_repr truncate For DataFrames exceeding max_rows/max_cols,
+ the repr (and HTML repr) can show
+ a truncated table (the default from 0.13),
+ or switch to the view from df.info()
+ (the behaviour in earlier versions of pandas).
+ allowable settings, ['truncate', 'info']
+display.latex.repr False Whether to produce a latex DataFrame
+ representation for jupyter frontends
+ that support it.
+display.latex.escape True Escapes special caracters in Dataframes, when
+ using the to_latex method.
+display.latex.longtable False Specifies if the to_latex method of a Dataframe
+ uses the longtable format.
+display.latex.multicolumn True Combines columns when using a MultiIndex
+display.latex.multicolumn_format 'l' Alignment of multicolumn labels
+display.latex.multirow False Combines rows when using a MultiIndex.
+ Centered instead of top-aligned,
+ separated by clines.
+display.line_width 80 Deprecated. Use `display.width` instead.
+display.max_columns 20 max_rows and max_columns are used
+ in __repr__() methods to decide if
+ to_string() or info() is used to
+ render an object to a string. In
+ case python/IPython is running in
+ a terminal this can be set to 0 and
+ pandas will correctly auto-detect
+ the width the terminal and swap to
+ a smaller format in case all columns
+ would not fit vertically. The IPython
+ notebook, IPython qtconsole, or IDLE
+ do not run in a terminal and hence
+ it is not possible to do correct
+ auto-detection. 'None' value means
+ unlimited.
+display.max_colwidth 50 The maximum width in characters of
+ a column in the repr of a pandas
+ data structure. When the column overflows,
+ a "..." placeholder is embedded in
+ the output.
+display.max_info_columns 100 max_info_columns is used in DataFrame.info
+ method to decide if per column information
+ will be printed.
+display.max_info_rows 1690785 df.info() will usually show null-counts
+ for each column. For large frames
+ this can be quite slow. max_info_rows
+ and max_info_cols limit this null
+ check only to frames with smaller
+ dimensions then specified.
+display.max_rows 60 This sets the maximum number of rows
+ pandas should output when printing
+ out various output. For example,
+ this value determines whether the
+ repr() for a dataframe prints out
+ fully or just a summary repr.
+ 'None' value means unlimited.
+display.max_seq_items 100 when pretty-printing a long sequence,
+ no more then `max_seq_items` will
+ be printed. If items are omitted,
+ they will be denoted by the addition
+ of "..." to the resulting string.
+ If set to None, the number of items
+ to be printed is unlimited.
+display.memory_usage True This specifies if the memory usage of
+ a DataFrame should be displayed when the
+ df.info() method is invoked.
+display.multi_sparse True "Sparsify" MultiIndex display (don't
+ display repeated elements in outer
+ levels within groups)
+display.notebook_repr_html True When True, IPython notebook will
+ use html representation for
+ pandas objects (if it is available).
+display.pprint_nest_depth 3 Controls the number of nested levels
+ to process when pretty-printing
+display.precision 6 Floating point output precision in
+ terms of number of places after the
+ decimal, for regular formatting as well
+ as scientific notation. Similar to
+ numpy's ``precision`` print option
+display.show_dimensions truncate Whether to print out dimensions
+ at the end of DataFrame repr.
+ If 'truncate' is specified, only
+ print out the dimensions if the
+ frame is truncated (e.g. not display
+ all rows and/or columns)
+display.width 80 Width of the display in characters.
+ In case python/IPython is running in
+ a terminal this can be set to None
+ and pandas will correctly auto-detect
+ the width. Note that the IPython notebook,
+ IPython qtconsole, or IDLE do not run in a
+ terminal and hence it is not possible
+ to correctly detect the width.
+html.border 1 A ``border=value`` attribute is
+ inserted in the ```` tag
+ for the DataFrame HTML repr.
+io.excel.xls.writer xlwt The default Excel writer engine for
+ 'xls' files.
+io.excel.xlsm.writer openpyxl The default Excel writer engine for
+ 'xlsm' files. Available options:
+ 'openpyxl' (the default).
+io.excel.xlsx.writer openpyxl The default Excel writer engine for
+ 'xlsx' files.
+io.hdf.default_format None default format writing format, if
+ None, then put will default to
+ 'fixed' and append will default to
+ 'table'
+io.hdf.dropna_table True drop ALL nan rows when appending
+ to a table
+mode.chained_assignment warn Raise an exception, warn, or no
+ action if trying to use chained
+ assignment, The default is warn
+mode.sim_interactive False Whether to simulate interactive mode
+ for purposes of testing
+mode.use_inf_as_null False True means treat None, NaN, -INF,
+ INF as null (old way), False means
+ None and NaN are null, but INF, -INF
+ are not null (new way).
+=================================== ============ ==================================
.. _basics.console_output:
diff --git a/doc/source/whatsnew/v0.20.0.txt b/doc/source/whatsnew/v0.20.0.txt
index dca4f890e496b..0991f3873b06f 100644
--- a/doc/source/whatsnew/v0.20.0.txt
+++ b/doc/source/whatsnew/v0.20.0.txt
@@ -182,6 +182,7 @@ Other enhancements
- ``Timedelta.isoformat`` method added for formatting Timedeltas as an `ISO 8601 duration`_. See the :ref:`Timedelta docs ` (:issue:`15136`)
- ``pandas.io.json.json_normalize()`` gained the option ``errors='ignore'|'raise'``; the default is ``errors='raise'`` which is backward compatible. (:issue:`14583`)
- ``.select_dtypes()`` now allows the string 'datetimetz' to generically select datetimes with tz (:issue:`14910`)
+- The ``.to_latex()`` method will now accept ``multicolumn`` and ``multirow`` arguments to use the accompanying LaTeX enhancements
- ``pd.merge_asof()`` gained the option ``direction='backward'|'forward'|'nearest'`` (:issue:`14887`)
- ``Series/DataFrame.asfreq()`` have gained a ``fill_value`` parameter, to fill missing values (:issue:`3715`).
- ``Series/DataFrame.resample.asfreq`` have gained a ``fill_value`` parameter, to fill missing values during resampling (:issue:`3715`).
diff --git a/pandas/core/config_init.py b/pandas/core/config_init.py
index d3db633f3aa04..89616890e1de1 100644
--- a/pandas/core/config_init.py
+++ b/pandas/core/config_init.py
@@ -239,14 +239,35 @@
: bool
This specifies if the to_latex method of a Dataframe uses escapes special
characters.
- method. Valid values: False,True
+ Valid values: False,True
"""
pc_latex_longtable = """
:bool
This specifies if the to_latex method of a Dataframe uses the longtable
format.
- method. Valid values: False,True
+ Valid values: False,True
+"""
+
+pc_latex_multicolumn = """
+: bool
+ This specifies if the to_latex method of a Dataframe uses multicolumns
+ to pretty-print MultiIndex columns.
+ Valid values: False,True
+"""
+
+pc_latex_multicolumn_format = """
+: string
+ This specifies the format for multicolumn headers.
+ Can be surrounded with '|'.
+ Valid values: 'l', 'c', 'r', 'p{}'
+"""
+
+pc_latex_multirow = """
+: bool
+ This specifies if the to_latex method of a Dataframe uses multirows
+ to pretty-print MultiIndex rows.
+ Valid values: False,True
"""
style_backup = dict()
@@ -339,6 +360,12 @@ def mpl_style_cb(key):
validator=is_bool)
cf.register_option('latex.longtable', False, pc_latex_longtable,
validator=is_bool)
+ cf.register_option('latex.multicolumn', True, pc_latex_multicolumn,
+ validator=is_bool)
+ cf.register_option('latex.multicolumn_format', 'l', pc_latex_multicolumn,
+ validator=is_text)
+ cf.register_option('latex.multirow', False, pc_latex_multirow,
+ validator=is_bool)
cf.deprecate_option('display.line_width',
msg=pc_line_width_deprecation_warning,
diff --git a/pandas/core/frame.py b/pandas/core/frame.py
index 26a0a91094e7d..b3e43edc3eb55 100644
--- a/pandas/core/frame.py
+++ b/pandas/core/frame.py
@@ -1614,10 +1614,11 @@ def to_latex(self, buf=None, columns=None, col_space=None, header=True,
index=True, na_rep='NaN', formatters=None, float_format=None,
sparsify=None, index_names=True, bold_rows=True,
column_format=None, longtable=None, escape=None,
- encoding=None, decimal='.'):
- """
+ encoding=None, decimal='.', multicolumn=None,
+ multicolumn_format=None, multirow=None):
+ r"""
Render a DataFrame to a tabular environment table. You can splice
- this into a LaTeX document. Requires \\usepackage{booktabs}.
+ this into a LaTeX document. Requires \usepackage{booktabs}.
`to_latex`-specific options:
@@ -1628,27 +1629,54 @@ def to_latex(self, buf=None, columns=None, col_space=None, header=True,
`__ e.g 'rcl' for 3
columns
longtable : boolean, default will be read from the pandas config module
- default: False
+ Default: False.
Use a longtable environment instead of tabular. Requires adding
- a \\usepackage{longtable} to your LaTeX preamble.
+ a \usepackage{longtable} to your LaTeX preamble.
escape : boolean, default will be read from the pandas config module
- default: True
+ Default: True.
When set to False prevents from escaping latex special
characters in column names.
encoding : str, default None
A string representing the encoding to use in the output file,
defaults to 'ascii' on Python 2 and 'utf-8' on Python 3.
decimal : string, default '.'
- Character recognized as decimal separator, e.g. ',' in Europe
+ Character recognized as decimal separator, e.g. ',' in Europe.
.. versionadded:: 0.18.0
+ multicolumn : boolean, default True
+ Use \multicolumn to enhance MultiIndex columns.
+ The default will be read from the config module.
+
+ .. versionadded:: 0.20.0
+
+ multicolumn_format : str, default 'l'
+ The alignment for multicolumns, similar to `column_format`
+ The default will be read from the config module.
+
+ .. versionadded:: 0.20.0
+
+ multirow : boolean, default False
+ Use \multirow to enhance MultiIndex rows.
+ Requires adding a \usepackage{multirow} to your LaTeX preamble.
+ Will print centered labels (instead of top-aligned)
+ across the contained rows, separating groups via clines.
+ The default will be read from the pandas config module.
+
+ .. versionadded:: 0.20.0
+
"""
# Get defaults from the pandas config
if longtable is None:
longtable = get_option("display.latex.longtable")
if escape is None:
escape = get_option("display.latex.escape")
+ if multicolumn is None:
+ multicolumn = get_option("display.latex.multicolumn")
+ if multicolumn_format is None:
+ multicolumn_format = get_option("display.latex.multicolumn_format")
+ if multirow is None:
+ multirow = get_option("display.latex.multirow")
formatter = fmt.DataFrameFormatter(self, buf=buf, columns=columns,
col_space=col_space, na_rep=na_rep,
@@ -1660,7 +1688,9 @@ def to_latex(self, buf=None, columns=None, col_space=None, header=True,
index_names=index_names,
escape=escape, decimal=decimal)
formatter.to_latex(column_format=column_format, longtable=longtable,
- encoding=encoding)
+ encoding=encoding, multicolumn=multicolumn,
+ multicolumn_format=multicolumn_format,
+ multirow=multirow)
if buf is None:
return formatter.buf.getvalue()
diff --git a/pandas/formats/format.py b/pandas/formats/format.py
index 4c081770e0125..9dde3b0001c31 100644
--- a/pandas/formats/format.py
+++ b/pandas/formats/format.py
@@ -650,13 +650,17 @@ def _join_multiline(self, *strcols):
st = ed
return '\n\n'.join(str_lst)
- def to_latex(self, column_format=None, longtable=False, encoding=None):
+ def to_latex(self, column_format=None, longtable=False, encoding=None,
+ multicolumn=False, multicolumn_format=None, multirow=False):
"""
Render a DataFrame to a LaTeX tabular/longtable environment output.
"""
latex_renderer = LatexFormatter(self, column_format=column_format,
- longtable=longtable)
+ longtable=longtable,
+ multicolumn=multicolumn,
+ multicolumn_format=multicolumn_format,
+ multirow=multirow)
if encoding is None:
encoding = 'ascii' if compat.PY2 else 'utf-8'
@@ -824,11 +828,15 @@ class LatexFormatter(TableFormatter):
HTMLFormatter
"""
- def __init__(self, formatter, column_format=None, longtable=False):
+ def __init__(self, formatter, column_format=None, longtable=False,
+ multicolumn=False, multicolumn_format=None, multirow=False):
self.fmt = formatter
self.frame = self.fmt.frame
self.column_format = column_format
self.longtable = longtable
+ self.multicolumn = multicolumn
+ self.multicolumn_format = multicolumn_format
+ self.multirow = multirow
def write_result(self, buf):
"""
@@ -850,14 +858,21 @@ def get_col_type(dtype):
else:
return 'l'
+ # reestablish the MultiIndex that has been joined by _to_str_column
if self.fmt.index and isinstance(self.frame.index, MultiIndex):
clevels = self.frame.columns.nlevels
strcols.pop(0)
name = any(self.frame.index.names)
+ cname = any(self.frame.columns.names)
+ lastcol = self.frame.index.nlevels - 1
for i, lev in enumerate(self.frame.index.levels):
lev2 = lev.format()
blank = ' ' * len(lev2[0])
- lev3 = [blank] * clevels
+ # display column names in last index-column
+ if cname and i == lastcol:
+ lev3 = [x if x else '{}' for x in self.frame.columns.names]
+ else:
+ lev3 = [blank] * clevels
if name:
lev3.append(lev.name)
for level_idx, group in itertools.groupby(
@@ -885,10 +900,15 @@ def get_col_type(dtype):
buf.write('\\begin{longtable}{%s}\n' % column_format)
buf.write('\\toprule\n')
- nlevels = self.frame.columns.nlevels
+ ilevels = self.frame.index.nlevels
+ clevels = self.frame.columns.nlevels
+ nlevels = clevels
if any(self.frame.index.names):
nlevels += 1
- for i, row in enumerate(zip(*strcols)):
+ strrows = list(zip(*strcols))
+ self.clinebuf = []
+
+ for i, row in enumerate(strrows):
if i == nlevels and self.fmt.header:
buf.write('\\midrule\n') # End of header
if self.longtable:
@@ -910,8 +930,17 @@ def get_col_type(dtype):
if x else '{}') for x in row]
else:
crow = [x if x else '{}' for x in row]
+ if i < clevels and self.fmt.header and self.multicolumn:
+ # sum up columns to multicolumns
+ crow = self._format_multicolumn(crow, ilevels)
+ if (i >= nlevels and self.fmt.index and self.multirow and
+ ilevels > 1):
+ # sum up rows to multirows
+ crow = self._format_multirow(crow, ilevels, i, strrows)
buf.write(' & '.join(crow))
buf.write(' \\\\\n')
+ if self.multirow and i < len(strrows) - 1:
+ self._print_cline(buf, i, len(strcols))
if not self.longtable:
buf.write('\\bottomrule\n')
@@ -919,6 +948,80 @@ def get_col_type(dtype):
else:
buf.write('\\end{longtable}\n')
+ def _format_multicolumn(self, row, ilevels):
+ """
+ Combine columns belonging to a group to a single multicolumn entry
+ according to self.multicolumn_format
+
+ e.g.:
+ a & & & b & c &
+ will become
+ \multicolumn{3}{l}{a} & b & \multicolumn{2}{l}{c}
+ """
+ row2 = list(row[:ilevels])
+ ncol = 1
+ coltext = ''
+
+ def append_col():
+ # write multicolumn if needed
+ if ncol > 1:
+ row2.append('\\multicolumn{{{0:d}}}{{{1:s}}}{{{2:s}}}'
+ .format(ncol, self.multicolumn_format,
+ coltext.strip()))
+ # don't modify where not needed
+ else:
+ row2.append(coltext)
+ for c in row[ilevels:]:
+ # if next col has text, write the previous
+ if c.strip():
+ if coltext:
+ append_col()
+ coltext = c
+ ncol = 1
+ # if not, add it to the previous multicolumn
+ else:
+ ncol += 1
+ # write last column name
+ if coltext:
+ append_col()
+ return row2
+
+ def _format_multirow(self, row, ilevels, i, rows):
+ """
+ Check following rows, whether row should be a multirow
+
+ e.g.: becomes:
+ a & 0 & \multirow{2}{*}{a} & 0 &
+ & 1 & & 1 &
+ b & 0 & \cline{1-2}
+ b & 0 &
+ """
+ for j in range(ilevels):
+ if row[j].strip():
+ nrow = 1
+ for r in rows[i + 1:]:
+ if not r[j].strip():
+ nrow += 1
+ else:
+ break
+ if nrow > 1:
+ # overwrite non-multirow entry
+ row[j] = '\\multirow{{{0:d}}}{{*}}{{{1:s}}}'.format(
+ nrow, row[j].strip())
+ # save when to end the current block with \cline
+ self.clinebuf.append([i + nrow - 1, j + 1])
+ return row
+
+ def _print_cline(self, buf, i, icol):
+ """
+ Print clines after multirow-blocks are finished
+ """
+ for cl in self.clinebuf:
+ if cl[0] == i:
+ buf.write('\cline{{{0:d}-{1:d}}}\n'.format(cl[1], icol))
+ # remove entries that have been written to buffer
+ self.clinebuf = [x for x in self.clinebuf if x[0] != i]
+
class HTMLFormatter(TableFormatter):
diff --git a/pandas/tests/formats/test_to_latex.py b/pandas/tests/formats/test_to_latex.py
index 89e18e1cec06e..17e1e18f03dd6 100644
--- a/pandas/tests/formats/test_to_latex.py
+++ b/pandas/tests/formats/test_to_latex.py
@@ -168,6 +168,24 @@ def test_to_latex_multiindex(self):
assert result == expected
+ # GH 14184
+ df = df.T
+ df.columns.names = ['a', 'b']
+ result = df.to_latex()
+ expected = r"""\begin{tabular}{lrrrrr}
+\toprule
+a & \multicolumn{2}{l}{c1} & \multicolumn{2}{l}{c2} & c3 \\
+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 \\
+\bottomrule
+\end{tabular}
+"""
+ assert result == expected
+
# GH 10660
df = pd.DataFrame({'a': [0, 0, 1, 1],
'b': list('abab'),
@@ -189,16 +207,95 @@ def test_to_latex_multiindex(self):
assert result == expected
result = df.groupby('a').describe().to_latex()
- expected = ('\\begin{tabular}{lrrrrrrrr}\n\\toprule\n{} & c & '
- ' & & & & & & '
- '\\\\\n{} & count & mean & std & min & 25\\% & '
- '50\\% & 75\\% & max \\\\\na & & & '
- ' & & & & & \\\\\n\\midrule\n0 '
- '& 2.0 & 1.5 & 0.707107 & 1.0 & 1.25 & 1.5 & 1.75 '
- '& 2.0 \\\\\n1 & 2.0 & 3.5 & 0.707107 & 3.0 & 3.25 '
- '& 3.5 & 3.75 & 4.0 '
- '\\\\\n\\bottomrule\n\\end{tabular}\n')
+ expected = r"""\begin{tabular}{lrrrrrrrr}
+\toprule
+{} & \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 \\
+\bottomrule
+\end{tabular}
+"""
+
+ assert result == expected
+
+ def test_to_latex_multicolumnrow(self):
+ df = pd.DataFrame({
+ ('c1', 0): dict((x, x) for x in range(5)),
+ ('c1', 1): dict((x, x + 5) for x in range(5)),
+ ('c2', 0): dict((x, x) for x in range(5)),
+ ('c2', 1): dict((x, x + 5) for x in range(5)),
+ ('c3', 0): dict((x, x) for x in range(5))
+ })
+ result = df.to_latex()
+ expected = 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 \\
+\bottomrule
+\end{tabular}
+"""
+ assert result == expected
+ result = df.to_latex(multicolumn=False)
+ expected = 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 \\
+\bottomrule
+\end{tabular}
+"""
+ assert result == expected
+
+ result = df.T.to_latex(multirow=True)
+ expected = r"""\begin{tabular}{llrrrrr}
+\toprule
+ & & 0 & 1 & 2 & 3 & 4 \\
+\midrule
+\multirow{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 \\
+\cline{1-7}
+c3 & 0 & 0 & 1 & 2 & 3 & 4 \\
+\bottomrule
+\end{tabular}
+"""
+ assert result == expected
+
+ df.index = df.T.index
+ result = df.T.to_latex(multirow=True, multicolumn=True,
+ multicolumn_format='c')
+ expected = r"""\begin{tabular}{llrrrrr}
+\toprule
+ & & \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 \\
+\cline{1-7}
+\multirow{2}{*}{c2} & 0 & 0 & 1 & 2 & 3 & 4 \\
+ & 1 & 5 & 6 & 7 & 8 & 9 \\
+\cline{1-7}
+c3 & 0 & 0 & 1 & 2 & 3 & 4 \\
+\bottomrule
+\end{tabular}
+"""
assert result == expected
def test_to_latex_escape(self):