craftbeerpi4-pione/venv3/lib/python3.7/site-packages/pandas/io/formats/latex.py

373 lines
13 KiB
Python
Raw Normal View History

2021-03-03 23:49:41 +01:00
"""
Module for formatting output data in Latex.
"""
from typing import IO, List, Optional, Tuple
import numpy as np
from pandas.core.dtypes.generic import ABCMultiIndex
from pandas.io.formats.format import DataFrameFormatter, TableFormatter
class LatexFormatter(TableFormatter):
"""
Used to render a DataFrame to a LaTeX tabular/longtable environment output.
Parameters
----------
formatter : `DataFrameFormatter`
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
longtable : boolean, default False
Use a longtable environment instead of tabular.
See Also
--------
HTMLFormatter
"""
def __init__(
self,
formatter: DataFrameFormatter,
column_format: Optional[str] = None,
longtable: bool = False,
multicolumn: bool = False,
multicolumn_format: Optional[str] = None,
multirow: bool = False,
caption: Optional[str] = None,
label: Optional[str] = None,
):
self.fmt = formatter
self.frame = self.fmt.frame
self.bold_rows = self.fmt.bold_rows
self.column_format = column_format
self.longtable = longtable
self.multicolumn = multicolumn
self.multicolumn_format = multicolumn_format
self.multirow = multirow
self.caption = caption
self.label = label
self.escape = self.fmt.escape
def write_result(self, buf: IO[str]) -> None:
"""
Render a DataFrame to a LaTeX tabular, longtable, or table/tabular
environment output.
"""
# string representation of the columns
if len(self.frame.columns) == 0 or len(self.frame.index) == 0:
info_line = (
f"Empty {type(self.frame).__name__}\n"
f"Columns: {self.frame.columns}\n"
f"Index: {self.frame.index}"
)
strcols = [[info_line]]
else:
strcols = self.fmt._to_str_columns()
def get_col_type(dtype):
if issubclass(dtype.type, np.number):
return "r"
else:
return "l"
# reestablish the MultiIndex that has been joined by _to_str_column
if self.fmt.index and isinstance(self.frame.index, ABCMultiIndex):
out = self.frame.index.format(
adjoin=False,
sparsify=self.fmt.sparsify,
names=self.fmt.has_index_names,
na_rep=self.fmt.na_rep,
)
# index.format will sparsify repeated entries with empty strings
# so pad these with some empty space
def pad_empties(x):
for pad in reversed(x):
if pad:
break
return [x[0]] + [i if i else " " * len(pad) for i in x[1:]]
out = (pad_empties(i) for i in out)
# Add empty spaces for each column level
clevels = self.frame.columns.nlevels
out = [[" " * len(i[-1])] * clevels + i for i in out]
# Add the column names to the last index column
cnames = self.frame.columns.names
if any(cnames):
new_names = [i if i else "{}" for i in cnames]
out[self.frame.index.nlevels - 1][:clevels] = new_names
# Get rid of old multiindex column and add new ones
strcols = out + strcols[1:]
if self.column_format is None:
dtypes = self.frame.dtypes._values
column_format = "".join(map(get_col_type, dtypes))
if self.fmt.index:
index_format = "l" * self.frame.index.nlevels
column_format = index_format + column_format
elif not isinstance(self.column_format, str): # pragma: no cover
raise AssertionError(
f"column_format must be str or unicode, not {type(column_format)}"
)
else:
column_format = self.column_format
if self.longtable:
self._write_longtable_begin(buf, column_format)
else:
self._write_tabular_begin(buf, column_format)
buf.write("\\toprule\n")
ilevels = self.frame.index.nlevels
clevels = self.frame.columns.nlevels
nlevels = clevels
if self.fmt.has_index_names and self.fmt.show_index_names:
nlevels += 1
strrows = list(zip(*strcols))
self.clinebuf: List[List[int]] = []
for i, row in enumerate(strrows):
if i == nlevels and self.fmt.header:
buf.write("\\midrule\n") # End of header
if self.longtable:
buf.write("\\endhead\n")
buf.write("\\midrule\n")
buf.write(
f"\\multicolumn{{{len(row)}}}{{r}}"
"{{Continued on next page}} \\\\\n"
)
buf.write("\\midrule\n")
buf.write("\\endfoot\n\n")
buf.write("\\bottomrule\n")
buf.write("\\endlastfoot\n")
if self.escape:
# escape backslashes first
crow = [
(
x.replace("\\", "\\textbackslash ")
.replace("_", "\\_")
.replace("%", "\\%")
.replace("$", "\\$")
.replace("#", "\\#")
.replace("{", "\\{")
.replace("}", "\\}")
.replace("~", "\\textasciitilde ")
.replace("^", "\\textasciicircum ")
.replace("&", "\\&")
if (x and x != "{}")
else "{}"
)
for x in row
]
else:
crow = [x if x else "{}" for x in row]
if self.bold_rows and self.fmt.index:
# bold row labels
crow = [
f"\\textbf{{{x}}}"
if j < ilevels and x.strip() not in ["", "{}"]
else x
for j, x in enumerate(crow)
]
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 self.longtable:
self._write_longtable_end(buf)
else:
self._write_tabular_end(buf)
def _format_multicolumn(self, row: List[str], ilevels: int) -> List[str]:
r"""
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(
f"\\multicolumn{{{ncol:d}}}{{{self.multicolumn_format}}}"
f"{{{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: List[str], ilevels: int, i: int, rows: List[Tuple[str, ...]]
) -> List[str]:
r"""
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] = f"\\multirow{{{nrow:d}}}{{*}}{{{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: IO[str], i: int, icol: int) -> None:
"""
Print clines after multirow-blocks are finished.
"""
for cl in self.clinebuf:
if cl[0] == i:
buf.write(f"\\cline{{{cl[1]:d}-{icol:d}}}\n")
# remove entries that have been written to buffer
self.clinebuf = [x for x in self.clinebuf if x[0] != i]
def _write_tabular_begin(self, buf, column_format: str):
"""
Write the beginning of a tabular environment or
nested table/tabular environments including caption and label.
Parameters
----------
buf : string or file handle
File path or object. If not specified, the result is returned as
a string.
column_format : str
The columns format as specified in `LaTeX table format
<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:
# then write output in a nested table/tabular environment
if self.caption is None:
caption_ = ""
else:
caption_ = f"\n\\caption{{{self.caption}}}"
if self.label is None:
label_ = ""
else:
label_ = f"\n\\label{{{self.label}}}"
buf.write(f"\\begin{{table}}\n\\centering{caption_}{label_}\n")
else:
# then write output only in a tabular environment
pass
buf.write(f"\\begin{{tabular}}{{{column_format}}}\n")
def _write_tabular_end(self, buf):
"""
Write the end of a tabular environment or nested table/tabular
environment.
Parameters
----------
buf : string or file handle
File path or object. If not specified, the result is returned as
a string.
"""
buf.write("\\bottomrule\n")
buf.write("\\end{tabular}\n")
if self.caption is not None or self.label is not None:
buf.write("\\end{table}\n")
else:
pass
def _write_longtable_begin(self, buf, column_format: str):
"""
Write the beginning of a longtable environment including caption and
label if provided by user.
Parameters
----------
buf : string or file handle
File path or object. If not specified, the result is returned as
a string.
column_format : str
The columns format as specified in `LaTeX table format
<https://en.wikibooks.org/wiki/LaTeX/Tables>`__ e.g 'rcl'
for 3 columns
"""
buf.write(f"\\begin{{longtable}}{{{column_format}}}\n")
if self.caption is not None or self.label is not None:
if self.caption is None:
pass
else:
buf.write(f"\\caption{{{self.caption}}}")
if self.label is None:
pass
else:
buf.write(f"\\label{{{self.label}}}")
# a double-backslash is required at the end of the line
# as discussed here:
# https://tex.stackexchange.com/questions/219138
buf.write("\\\\\n")
else:
pass
@staticmethod
def _write_longtable_end(buf):
"""
Write the end of a longtable environment.
Parameters
----------
buf : string or file handle
File path or object. If not specified, the result is returned as
a string.
"""
buf.write("\\end{longtable}\n")