diff --git a/doc/source/whatsnew/v1.5.0.rst b/doc/source/whatsnew/v1.5.0.rst index 1ae76984484af..8a720882a72e0 100644 --- a/doc/source/whatsnew/v1.5.0.rst +++ b/doc/source/whatsnew/v1.5.0.rst @@ -20,6 +20,7 @@ Styler ^^^^^^ - New method :meth:`.Styler.to_string` for alternative customisable output methods (:issue:`44502`) + - New keyword argument ``aliases`` added to :meth:`.Styler.format_index` to allow simple label string replacement (:issue:`45288`) - Various bug fixes, see below. .. _whatsnew_150.enhancements.enhancement2: diff --git a/pandas/core/generic.py b/pandas/core/generic.py index b8a7864a3a7e0..1898c98eb5063 100644 --- a/pandas/core/generic.py +++ b/pandas/core/generic.py @@ -157,7 +157,6 @@ Window, ) -from pandas.io.formats import format as fmt from pandas.io.formats.format import ( DataFrameFormatter, DataFrameRenderer, @@ -2159,7 +2158,7 @@ def _repr_latex_(self): Returns a LaTeX representation for a particular object. Mainly for use with nbconvert (jupyter notebook conversion to pdf). """ - if config.get_option("display.latex.repr"): + if config.get_option("styler.render.repr") == "latex": return self.to_latex() else: return None @@ -3171,20 +3170,19 @@ class (index) object 'bird' 'bird' 'mammal' 'mammal' return xarray.Dataset.from_dataframe(self) @final - @doc(returns=fmt.return_docstring) def to_latex( self, buf=None, columns=None, col_space=None, - header=True, - index=True, - na_rep="NaN", + header=None, + index=None, + na_rep=None, formatters=None, float_format=None, sparsify=None, - index_names=True, - bold_rows=False, + index_names=None, + bold_rows=None, column_format=None, longtable=None, escape=None, @@ -3196,6 +3194,11 @@ def to_latex( caption=None, label=None, position=None, + *, + hide: dict | list[dict] | None = None, + format: dict | list[dict] | None = None, + format_index: dict | list[dict] | None = None, + render_kwargs: dict = {}, ): r""" Render object to a LaTeX tabular, longtable, or nested table. @@ -3210,67 +3213,107 @@ def to_latex( .. versionchanged:: 1.2.0 Added position argument, changed meaning of caption argument. + .. versionchanged:: 1.5.0 + Uses the `Styler` implementation and has had an arguments' signature + overhaul. See notes. + Parameters ---------- buf : str, Path or StringIO-like, optional, default None Buffer to write to. If None, the output is returned as a string. columns : list of label, optional The subset of columns to write. Writes all columns by default. + + .. deprecated:: 1.5.0 col_space : int, optional The minimum width of each column. + + .. deprecated:: 1.5.0 header : bool or list of str, default True Write out the column names. If a list of strings is given, it is assumed to be aliases for the column names. + + .. deprecated:: 1.5.0 index : bool, default True Write row names (index). + + .. deprecated:: 1.5.0 na_rep : str, default 'NaN' Missing data representation. + + .. deprecated:: 1.5.0 formatters : list of functions or dict of {{str: function}}, optional Formatter functions to apply to columns' elements by position or name. The result of each function must be a unicode string. List must be of length equal to the number of columns. + + .. deprecated:: 1.5.0 float_format : one-parameter function or str, optional, default None Formatter for floating point numbers. For example ``float_format="%.2f"`` and ``float_format="{{:0.2f}}".format`` will both result in 0.1234 being formatted as 0.12. + + .. deprecated:: 1.5.0 sparsify : bool, optional Set to False for a DataFrame with a hierarchical index to print every multiindex key at each row. By default, the value will be read from the config module. + + .. deprecated:: 1.5.0 index_names : bool, default True Prints the names of the indexes. + + .. deprecated:: 1.5.0 bold_rows : bool, default False Make the row labels bold in the output. + + .. deprecated:: 1.5.0 column_format : str, optional The columns format as specified in `LaTeX table format `__ e.g. 'rcl' for 3 columns. By default, 'l' will be used for all columns except columns of numbers, which default to 'r'. + + .. deprecated:: 1.5.0 longtable : bool, optional By default, the value will be read from the pandas config module. Use a longtable environment instead of tabular. Requires adding a \usepackage{{longtable}} to your LaTeX preamble. + + .. deprecated:: 1.5.0 escape : bool, optional By default, the value will be read from the pandas config module. When set to False prevents from escaping latex special characters in column names. + + .. deprecated:: 1.5.0 encoding : str, optional A string representing the encoding to use in the output file, defaults to 'utf-8'. + + .. deprecated:: 1.5.0 decimal : str, default '.' Character recognized as decimal separator, e.g. ',' in Europe. + + .. deprecated:: 1.5.0 multicolumn : bool, default True Use \multicolumn to enhance MultiIndex columns. The default will be read from the config module. + + .. deprecated:: 1.5.0 multicolumn_format : str, default 'l' The alignment for multicolumns, similar to `column_format` The default will be read from the config module. + + .. deprecated:: 1.5.0 multirow : bool, 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. + + .. deprecated:: 1.5.0 caption : str or tuple, optional Tuple (full_caption, short_caption), which results in ``\caption[short_caption]{{full_caption}}``; @@ -3281,17 +3324,47 @@ def to_latex( .. versionchanged:: 1.2.0 Optionally allow caption to be a tuple ``(full_caption, short_caption)``. + .. deprecated:: 1.5.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. .. versionadded:: 1.0.0 + + .. deprecated:: 1.5.0 position : str, optional The LaTeX positional argument for tables, to be placed after ``\begin{{}}`` in the output. .. versionadded:: 1.2.0 - {returns} + + .. deprecated:: 1.5.0 + + hide : dict, list of dict + Keyword args to pass to the method call of ``Styler.hide``. If a list will + call the method numerous times. + + .. versionadded:: 1.5.0 + format : dict, list of dict + Keyword args to pass to the method call of ``Styler.format``. If a list will + call the method numerous times. + + .. versionadded:: 1.5.0 + format_index : dict, list of dict + Keyword args to pass to the method call of ``Styler.format_index``. If a + list will call the method numerous times. + + .. versionadded:: 1.5.0 + render_kwargs : dict + Keyword args to pass to the method call of ``Styler.to_latex``. + + .. versionadded:: 1.5.0 + + Returns + ------- + str or None + If buf is None, returns the result as a string. Otherwise returns None. + See Also -------- Styler.to_latex : Render a DataFrame to LaTeX with conditional formatting. @@ -3299,29 +3372,141 @@ def to_latex( tabular output. DataFrame.to_html : Render a DataFrame as an HTML table. + Notes + ----- + In futures versions this method will exclusively use the `Styler` + implementation, which is more flexible and contains more features. The + signature: + + .. code-block:: python + + df.to_latex(buf, hide=hide, format=format, format_index=format_index, + render_kwargs=render_kwargs) + + will be refactored internally into the following: + + .. code-block:: python + + styler = df.style + styler.hide(**hide) + styler.format(**format) + styler.format_index(**format_index) + styler.to_latex(buf, **render_kwargs) + + The respective `Styler` methods :meth:`.Styler.hide`, :meth:`.Styler.format`, + :meth:`.Styler.format_index`, and :meth:`.Styler.to_latex` each give + extensive documentation on the arguments that can be passed. + + Below we give examples of how to refactor each deprecated argument to the + new signature, or indeed, utilise the underlying `Styler` implementation, + which is effectively the same. + Examples -------- + Convert a general DataFrame to LaTeX with formatting: + >>> df = pd.DataFrame(dict(name=['Raphael', 'Donatello'], - ... mask=['red', 'purple'], - ... weapon=['sai', 'bo staff'])) - >>> print(df.to_latex(index=False)) # doctest: +SKIP - \begin{{tabular}}{{lll}} - \toprule - name & mask & weapon \\ - \midrule - Raphael & red & sai \\ - Donatello & purple & bo staff \\ + ... age=[26, 45], + ... height=[181.23, 177.65])) + >>> print(df.to_latex(hide={"axis": "index"}, + ... format={"formatter": {"name": str.upper}, + ... "precision": 1}, + ... render_kwargs={"hrules": True} + ... ) # doctest: +SKIP + \begin{{tabular}}{{lrr}} + \toprule + name & age & height \\ + \\midrule + RAPHAEL & 26 & 181.2 \\ + DONATELLO & 45 & 177.7 \\ \bottomrule - \end{{tabular}} + \\end{{tabular}} + + Specifying a set of ``columns`` to display, e.g. ``["a", "b"]``. + + >>> df.to_latex(hide={"axis": "columns", + ... "subset": [c for c in df.columns if c not in ["a", "b"]]} + ... ) # doctest: +SKIP + + Setting the ``index`` to ``False``. + + >>> df.to_latex(hide={"axis": "index"}) # doctest: +SKIP + + Not displaying any column headers. + + >>> df.to_latex(hide={"axis": "columns"}) # doctest: +SKIP + + Setting ``index_names`` to ``False`` across both axes. + + >>> df.to_latex(hide=[{"axis": "index", "names": True}, + ... {"axis": "columns", "names": True}]) # doctest: +SKIP + + Adding render options, such as ``longtable``, ``column_format``, ``position``, + ``caption``, ``label``, ``multirow``, ``multicolumn``, ``multicolumn_format``, + as well as ``sparsify`` and other new options. + + >>> df.to_latex(render_kwargs={"environment": "longtable", + ... "column_format": "rcl", + ... "position": "h!", + ... "position_float": "centering", + ... "hrules": True, + ... "label": "my-label", + ... "caption": ("full caption", "short caption"), + ... "sparse_index": True, + ... "sparse_columns": True, + ... "multirow_align": "t", + ... "multicol_align": "r", + ... "clines": "skip-last;data"} + ... ) # doctest: +SKIP + + Adding specific ``formatters`` by column and setting the effective + ``float_format``, ``decimal`` and ``na_rep`` for data values. + + >>> df.to_latex(format={"formatter": {"a": str.upper}, + ... "precision": 2, + ... "decimal": ",", + ... "thousands": ".", + ... "na_rep": "missing"} + ... ) # doctest: +SKIP """ msg = ( - "In future versions `DataFrame.to_latex` is expected to utilise the base " - "implementation of `Styler.to_latex` for formatting and rendering. " - "The arguments signature may therefore change. It is recommended instead " - "to use `DataFrame.style.to_latex` which also contains additional " - "functionality." + "`col_space` is deprecated. Whitespace in LaTeX does not impact " + "the rendered version, and this argument is ignored." + ) + if col_space is not None: + warnings.warn(msg, DeprecationWarning, stacklevel=find_stack_level()) + + deprecated_arg_used = any( + [ + formatters is not None, + float_format is not None, + columns is not None, + header is not None, + index is not None, + na_rep is not None, + sparsify is not None, + index_names is not None, + bold_rows is not None, + column_format is not None, + longtable is not None, + escape is not None, + encoding is not None, + decimal != ".", + multicolumn is not None, + multicolumn_format is not None, + multirow is not None, + caption is not None, + label is not None, + position is not None, + ] ) - warnings.warn(msg, FutureWarning, stacklevel=find_stack_level()) + + # reset defaults + index = True if index is None else index + na_rep = "NaN" if na_rep is None else na_rep + header = True if header is None else header + bold_rows = False if bold_rows is None else bold_rows + index_names = True if index_names is None else index_names # Get defaults from the pandas config if self.ndim == 1: @@ -3338,34 +3523,209 @@ def to_latex( multirow = config.get_option("display.latex.multirow") self = cast("DataFrame", self) - formatter = DataFrameFormatter( - self, - columns=columns, - col_space=col_space, - na_rep=na_rep, - header=header, - index=index, - formatters=formatters, - float_format=float_format, - bold_rows=bold_rows, - sparsify=sparsify, - index_names=index_names, - escape=escape, - decimal=decimal, + + # Use styler implementation refactoring original kwargs + if deprecated_arg_used: + msg = ( + "Deprecated arguments supplied to `DataFrame.to_latex`. " + "Review the documentation for " + "advice on how to restructure arguments to suit the new Styler " + "implementation, which may be exclusively used in future " + "versions." + ) + warnings.warn(msg, FutureWarning, stacklevel=find_stack_level()) + + base_format_ = { + "na_rep": na_rep, + "escape": "latex" if escape else None, + "decimal": decimal, + } + index_format_: dict[str, Any] = {"axis": 0, **base_format_} + column_format_: dict[str, Any] = {"axis": 1, **base_format_} + + # refactor formatters to Styler structure + if isinstance(float_format, str): + float_format_ = lambda x: float_format % x + else: + float_format_ = float_format + + def _wrap(x, alt_format_): + if isinstance(x, (float, complex)) and float_format_ is not None: + return float_format_(x) + else: + return alt_format_(x) + + if isinstance(formatters, list): + formatters = { + c: functools.partial(_wrap, alt_format_=formatters[i]) + for i, c in enumerate(self.columns) + } + elif isinstance(formatters, dict): + index_formatter = formatters.pop("__index__", None) + column_formatter = formatters.pop("__columns__", None) + if index_formatter is not None: + index_format_.update({"formatter": index_formatter}) + if column_formatter is not None: + column_format_.update({"formatter": column_formatter}) + + formatters = { + k: functools.partial(_wrap, alt_format_=v) + for k, v in formatters.items() + } + elif formatters is None and float_format is not None: + formatters = functools.partial(_wrap, alt_format_=lambda v: v) + else: + formatters = None + + if hide is None: + hide = [] + elif isinstance(hide, dict): + hide = [hide] + + if columns: + hide.append( + { + "subset": [c for c in self.columns if c not in columns], + "axis": "columns", + } + ) + if header is False: + hide.append({"axis": "columns"}) + elif isinstance(header, (list, tuple)): + column_format_ = {"axis": "columns", "aliases": header} + + if index is False: + hide.append({"axis": "index"}) + if index_names is False: + hide.append({"names": True, "axis": "index"}) + + format_ = {"formatter": formatters, **base_format_} + data_format_ = [format_] + if isinstance(format, dict): + data_format_.append(format) + elif isinstance(format, list): + data_format_.extend(format) + + render_kwargs = { + "hrules": True, + "sparse_index": sparsify, + "sparse_columns": sparsify, + "environment": "longtable" if longtable else None, + "column_format": column_format, + "multicol_align": multicolumn_format + if multicolumn + else f"naive-{multicolumn_format}", + "multirow_align": "t" if multirow else "naive", + "encoding": encoding, + "caption": caption, + "label": label, + "position": position, + "column_format": column_format, + "clines": "skip-last;data" if multirow else None, + "bold_rows": bold_rows, + **render_kwargs, + } + + return self._to_latex_via_styler( + buf, + hide=hide, + format=data_format_, + format_index=[index_format_, column_format_], + render_kwargs=render_kwargs, ) - return DataFrameRenderer(formatter).to_latex( - buf=buf, - column_format=column_format, - longtable=longtable, - encoding=encoding, - multicolumn=multicolumn, - multicolumn_format=multicolumn_format, - multirow=multirow, - caption=caption, - label=label, - position=position, + + def _to_latex_via_styler( + self, + buf=None, + *, + hide: dict | list[dict] | None = None, + format: dict | list[dict] | None = None, + format_index: dict | list[dict] | None = None, + render_kwargs: dict = {}, + ): + """ + Render object to a LaTeX tabular, longtable, or nested table. + + Uses the ``Styler`` implementation with the following, ordered, method chaining: + + .. code-block:: python + + styler = Styler(DataFrame) + styler.hide(**hide) + styler.format(**format) + styler.format_index(**format_index) + styler.to_latex(buf=buf, **render_kwargs) + + Parameters + ---------- + buf : str, Path or StringIO-like, optional, default None + Buffer to write to. If None, the output is returned as a string. + hide : dict, list of dict + Keyword args to pass to the method call of ``Styler.hide``. If a list will + call the method numerous times. + format : dict, list of dict + Keyword args to pass to the method call of ``Styler.format``. If a list will + call the method numerous times. + format_index : dict, list of dict + Keyword args to pass to the method call of ``Styler.format_index``. If a + list will call the method numerous times. + render_kwargs : dict + Keyword args to pass to the method call of ``Styler.to_latex``. + + Returns + ------- + str or None + If buf is None, returns the result as a string. Otherwise returns None. + + See Also + -------- + Styler.to_latex : Render a DataFrame to LaTeX with conditional formatting. + DataFrame.to_string : Render a DataFrame to a console-friendly + tabular output. + DataFrame.to_html : Render a DataFrame as an HTML table. + + Examples + -------- + Convert a general DataFrame to LaTeX with formatting: + >>> df = pd.DataFrame(dict(name=['Raphael', 'Donatello'], + ... age=[26, 45], + ... height=[181.23, 177.65])) + >>> print(df.to_latex(hide={"axis": "index"}, + ... format={"formatter": {"name": str.upper}, + ... "precision": 1}, + ... render_kwargs={"hrules": True} + ... ) # doctest: +SKIP + \begin{{tabular}}{{lrr}} + \toprule + name & age & height \\ + \\midrule + RAPHAEL & 26 & 181.2 \\ + DONATELLO & 45 & 177.7 \\ + \bottomrule + \\end{{tabular}} + """ + from pandas.io.formats.style import Styler + + self = cast("DataFrame", self) + styler = Styler( + self, + uuid="", ) + for kw_name in ["hide", "format", "format_index"]: + kw = vars()[kw_name] + if isinstance(kw, dict): + getattr(styler, kw_name)(**kw) + elif isinstance(kw, list): + for sub_kw in kw: + getattr(styler, kw_name)(**sub_kw) + + # should be removed on deprecation cycle for DataFrame.to_latex(bold_rows) + if render_kwargs.pop("bold_rows"): + styler.applymap_index(lambda v: "textbf:--rwrap;") + + return styler.to_latex(buf=buf, **render_kwargs) + @final @doc( storage_options=_shared_docs["storage_options"], diff --git a/pandas/io/formats/style.py b/pandas/io/formats/style.py index 6293ad6ae3ddf..4d37412bba57b 100644 --- a/pandas/io/formats/style.py +++ b/pandas/io/formats/style.py @@ -222,7 +222,7 @@ def __init__( precision: int | None = None, table_styles: CSSStyles | None = None, uuid: str | None = None, - caption: str | tuple | None = None, + caption: str | tuple | list | None = None, table_attributes: str | None = None, cell_ids: bool = True, na_rep: str | None = None, @@ -963,6 +963,8 @@ def to_latex( if column_format is not None: # add more recent setting to table_styles + if not isinstance(column_format, str): + raise ValueError("`column_format` must be str or unicode") obj.set_table_styles( [{"selector": "column_format", "props": f":{column_format}"}], overwrite=False, @@ -1048,10 +1050,12 @@ def to_latex( clines=clines, ) - encoding = encoding or get_option("styler.render.encoding") - return save_to_buffer( - latex, buf=buf, encoding=None if buf is None else encoding + encoding = ( + (encoding or get_option("styler.render.encoding")) + if isinstance(buf, str) # i.e. a filepath + else encoding ) + return save_to_buffer(latex, buf=buf, encoding=encoding) def to_html( self, @@ -2116,13 +2120,13 @@ def set_uuid(self, uuid: str) -> Styler: self.uuid = uuid return self - def set_caption(self, caption: str | tuple) -> Styler: + def set_caption(self, caption: str | list | tuple) -> Styler: """ Set the text added to a ```` HTML element. Parameters ---------- - caption : str, tuple + caption : str, tuple, list For HTML output either the string input is used or the first element of the tuple. For LaTeX the string input provides a caption and the additional tuple input allows for full captions and short captions, in that order. @@ -2132,7 +2136,7 @@ def set_caption(self, caption: str | tuple) -> Styler: self : Styler """ msg = "`caption` must be either a string or 2-tuple of strings." - if isinstance(caption, tuple): + if isinstance(caption, (list, tuple)): if ( len(caption) != 2 or not isinstance(caption[0], str) diff --git a/pandas/io/formats/style_render.py b/pandas/io/formats/style_render.py index 1fe36a34903ab..1559def2bb75a 100644 --- a/pandas/io/formats/style_render.py +++ b/pandas/io/formats/style_render.py @@ -83,7 +83,7 @@ def __init__( uuid_len: int = 5, table_styles: CSSStyles | None = None, table_attributes: str | None = None, - caption: str | tuple | None = None, + caption: str | tuple | list | None = None, cell_ids: bool = True, precision: int | None = None, ): @@ -1063,6 +1063,7 @@ def format_index( thousands: str | None = None, escape: str | None = None, hyperlinks: str | None = None, + aliases: list[str] | list[list[str]] | None = None, ) -> StylerRenderer: r""" Format the text display value of index labels or column headers. @@ -1074,9 +1075,12 @@ def format_index( formatter : str, callable, dict or None Object to define how values are displayed. See notes. axis : {0, "index", 1, "columns"} - Whether to apply the formatter to the index or column headers. + Whether to apply the ``formatter`` or ``aliases`` to the index or column + headers. level : int, str, list - The level(s) over which to apply the generic formatter. + The level(s) over which to apply the generic ``formatter``, or ``aliases``. + In the case of ``aliases`` defaults to the last level of a MultiIndex, + for the reason that the last level is never sparsified. na_rep : str, optional Representation for missing values. If ``na_rep`` is None, no special formatting is applied. @@ -1098,6 +1102,17 @@ def format_index( Convert string patterns containing https://, http://, ftp:// or www. to HTML tags as clickable URL hyperlinks if "html", or LaTeX \href commands if "latex". + aliases : list of str, list of list of str + Values to replace the existing index or column headers. If specifying + more than one ``level`` then this should be a list containing sub-lists for + each identified level, in the respective order. + Cannot be used simultaneously with ``formatter`` and the associated + arguments; ``thousands``, ``decimal``, ``escape``, ``hyperlinks``, + ``na_rep`` and ``precision``. + This list (or each sub-list) must be of length equal to the number of + visible columns, see examples. + + .. versionadded:: 1.5.0 Returns ------- @@ -1129,6 +1144,10 @@ def format_index( When using a ``formatter`` string the dtypes must be compatible, otherwise a `ValueError` will be raised. + Since it is not possible to apply a generic function which will return an + arbitrary set of column aliases, the argument ``aliases`` provides the + ability to automate this, across individual index levels if necessary. + Examples -------- Using ``na_rep`` and ``precision`` with the default ``formatter`` @@ -1182,18 +1201,41 @@ def format_index( {} & {\textbf{123}} & {\textbf{\textasciitilde }} & {\textbf{\$\%\#}} \\ 0 & 1 & 2 & 3 \\ \end{tabular} + + Using ``aliases`` to overwrite column names. + + >>> df = pd.DataFrame([[1, 2, 3]], columns=[1, 2, 3]) + >>> df.style.format_index(axis=1, aliases=["A", "B", "C"]) # doctest: +SKIP + A B C + 0 1 2 3 + + Using ``aliases`` to overwrite column names of remaining **visible** items. + + >>> df = pd.DataFrame([[1, 2, 3]], + ... columns=pd.MultiIndex.from_product([[1, 2, 3], ["X"]])) + >>> styler = df.style # doctest: +SKIP + 1 2 3 + X X X + 0 1 2 3 + + >>> styler.hide([2], axis=1) # hides a column as a `subset` hide + ... .hide(level=1, axis=1) # hides the entire axis level + ... .format_index(axis=1, aliases=["A", "C"], level=0) # doctest: +SKIP + A C + 0 1 3 """ axis = self.data._get_axis_number(axis) if axis == 0: display_funcs_, obj = self._display_funcs_index, self.index + hidden_labels = self.hidden_rows else: display_funcs_, obj = self._display_funcs_columns, self.columns + hidden_labels = self.hidden_columns levels_ = refactor_levels(level, obj) - if all( + formatting_args_unset = all( ( formatter is None, - level is None, precision is None, decimal == ".", thousands is None, @@ -1201,31 +1243,75 @@ def format_index( escape is None, hyperlinks is None, ) - ): + ) + + if formatting_args_unset and level is None and aliases is None: + # clear the formatter / revert to default and avoid looping display_funcs_.clear() - return self # clear the formatter / revert to default and avoid looping - if not isinstance(formatter, dict): - formatter = {level: formatter for level in levels_} - else: - formatter = { - obj._get_level_number(level): formatter_ - for level, formatter_ in formatter.items() - } + elif aliases is not None: # then apply a formatting function from arg: aliases + if not formatting_args_unset: + raise ValueError( + "``aliases`` cannot be supplied together with any of " + "``formatter``, ``precision``, ``decimal``, ``na_rep``, " + "``escape``, or ``hyperlinks``." + ) + else: + visible_len = len(obj) - len(set(hidden_labels)) + if level is None: + levels_ = [obj.nlevels - 1] # default to last level + elif len(levels_) > 1 and len(aliases) != len(levels_): + raise ValueError( + f"``level`` specifies {len(levels_)} levels but the length of " + f"``aliases``, {len(aliases)}, does not match." + ) - for lvl in levels_: - format_func = _maybe_wrap_formatter( - formatter.get(lvl), - na_rep=na_rep, - precision=precision, - decimal=decimal, - thousands=thousands, - escape=escape, - hyperlinks=hyperlinks, - ) + def alias(x, value): + return value + + for i, lvl in enumerate(levels_): + level_aliases = aliases[i] if len(levels_) > 1 else aliases + if len(level_aliases) != visible_len: + raise ValueError( + "``aliases`` must be of length equal to the number of " + "visible labels along ``axis``. If ``level`` is given and " + "contains more than one level ``aliases`` should be a " + "list of lists with each sub-list having length equal to" + "the number of visible labels along ``axis``." + ) + for ai, idx in enumerate( + [ + (i, lvl) if axis == 0 else (lvl, i) + for i in range(len(obj)) + if i not in hidden_labels + ] + ): + display_funcs_[idx] = partial(alias, value=level_aliases[ai]) + + else: # then apply a formatting function from arg: formatter + if not isinstance(formatter, dict): + formatter = {level: formatter for level in levels_} + else: + formatter = { + obj._get_level_number(level): formatter_ + for level, formatter_ in formatter.items() + } + + for lvl in levels_: + format_func = _maybe_wrap_formatter( + formatter.get(lvl), + na_rep=na_rep, + precision=precision, + decimal=decimal, + thousands=thousands, + escape=escape, + hyperlinks=hyperlinks, + ) - for idx in [(i, lvl) if axis == 0 else (lvl, i) for i in range(len(obj))]: - display_funcs_[idx] = format_func + for idx in [ + (i, lvl) if axis == 0 else (lvl, i) for i in range(len(obj)) + ]: + display_funcs_[idx] = format_func return self diff --git a/pandas/tests/frame/test_repr_info.py b/pandas/tests/frame/test_repr_info.py index f19edf5722ca1..153ebc21d8d51 100644 --- a/pandas/tests/frame/test_repr_info.py +++ b/pandas/tests/frame/test_repr_info.py @@ -287,18 +287,22 @@ def test_repr_column_name_unicode_truncation_bug(self): @pytest.mark.filterwarnings("ignore::FutureWarning") def test_latex_repr(self): - result = r"""\begin{tabular}{llll} + pytest.importorskip("jinja2") + expected = r"""\begin{tabular}{llll} \toprule -{} & 0 & 1 & 2 \\ + & 0 & 1 & 2 \\ \midrule -0 & $\alpha$ & b & c \\ -1 & 1 & 2 & 3 \\ +0 & $\alpha$ & b & c \\ +1 & 1 & 2 & 3 \\ \bottomrule \end{tabular} """ - with option_context("display.latex.escape", False, "display.latex.repr", True): + with option_context( + "display.latex.escape", False, "styler.render.repr", "latex" + ): df = DataFrame([[r"$\alpha$", "b", "c"], [1, 2, 3]]) - assert result == df._repr_latex_() + result = df._repr_latex_() + assert result == expected # GH 12182 assert df._repr_latex_() is None diff --git a/pandas/tests/io/formats/style/test_format.py b/pandas/tests/io/formats/style/test_format.py index 5207be992d606..667e18d2d3701 100644 --- a/pandas/tests/io/formats/style/test_format.py +++ b/pandas/tests/io/formats/style/test_format.py @@ -434,3 +434,83 @@ def test_1level_multiindex(): assert ctx["body"][0][0]["is_visible"] is True assert ctx["body"][1][0]["display_value"] == "2" assert ctx["body"][1][0]["is_visible"] is True + + +def test_basic_alias(styler): + styler.format_index(axis=1, aliases=["alias1", "alias2"]) + ctx = styler._translate(True, True) + assert ctx["head"][0][1]["value"] == "A" + assert ctx["head"][0][1]["display_value"] == "alias1" # alias + assert ctx["head"][0][2]["value"] == "B" + assert ctx["head"][0][2]["display_value"] == "alias2" # alias + + +def test_basic_alias_hidden_column(styler): + styler.hide(subset="A", axis=1) + styler.format_index(axis=1, aliases=["alias2"]) + ctx = styler._translate(True, True) + assert ctx["head"][0][1]["value"] == "A" + assert ctx["head"][0][1]["display_value"] == "A" # no alias for hidden + assert ctx["head"][0][2]["value"] == "B" + assert ctx["head"][0][2]["display_value"] == "alias2" # alias + + +@pytest.mark.parametrize("level", [None, 0, 1]) +def test_alias_single_levels(df, level): + df.columns = MultiIndex.from_tuples([("X", "A"), ("Y", "B")]) + styler = Styler(df, cell_ids=False, uuid_len=0) + styler.format_index(axis=1, level=level, aliases=["alias1", "alias2"]) + ctx = styler._translate(True, True) + print(ctx["head"]) + assert len(ctx["head"]) == 2 # MultiIndex levels + + level = 1 if level is None else level # defaults to last + assert f"level{level}" in ctx["head"][level][1]["class"] + assert ctx["head"][level][1]["display_value"] == "alias1" + assert ctx["head"][level][2]["display_value"] == "alias2" + + +@pytest.mark.parametrize("level", [[0, 1], [1, 0]]) +def test_alias_multi_levels_order(df, level): + df.columns = MultiIndex.from_tuples([("X", "A"), ("Y", "B")]) + styler = Styler(df, cell_ids=False, uuid_len=0) + styler.format_index(axis=1, level=level, aliases=[["a1", "a2"], ["b1", "b2"]]) + ctx = styler._translate(True, True) + + assert ctx["head"][1 - level[1]][1]["display_value"] == "a1" + assert ctx["head"][1 - level[1]][2]["display_value"] == "a2" + assert ctx["head"][1 - level[0]][1]["display_value"] == "b1" + assert ctx["head"][1 - level[0]][2]["display_value"] == "b2" + + +@pytest.mark.parametrize( + "level, aliases", + [ + ([0, 1], ["alias1", "alias2"]), # no sublists + ([0], ["alias1"]), # too short + (None, ["alias1", "alias2", "alias3"]), # too long + ([0, 1], [["alias1", "alias2"], ["alias1"]]), # sublist too short + ([0, 1], [["a1", "a2"], ["a1", "a2", "a3"]]), # sublist too long + ], +) +def test_alias_warning(df, level, aliases): + df.columns = MultiIndex.from_tuples([("X", "A"), ("Y", "B")]) + styler = Styler(df, cell_ids=False, uuid_len=0) + msg = "``aliases`` must be of length equal to" + with pytest.raises(ValueError, match=msg): + styler.format_index(axis=1, level=level, aliases=aliases) + + +@pytest.mark.parametrize( + "level, aliases", + [ + ([0, 1], [["a1", "a2"]]), # too few sublists + ([0, 1], [["a1", "a2"], ["a1", "a2"], ["a1", "a2"]]), # too many sublists + ], +) +def test_alias_warning2(df, level, aliases): + df.columns = MultiIndex.from_tuples([("X", "A"), ("Y", "B")]) + styler = Styler(df, cell_ids=False, uuid_len=0) + msg = "``level`` specifies 2 levels but the length of" + with pytest.raises(ValueError, match=msg): + styler.format_index(axis=1, level=level, aliases=aliases) diff --git a/pandas/tests/io/formats/test_format.py b/pandas/tests/io/formats/test_format.py index 99378654c6c11..0d6985009d61d 100644 --- a/pandas/tests/io/formats/test_format.py +++ b/pandas/tests/io/formats/test_format.py @@ -3309,7 +3309,7 @@ def test_repr_html_ipython_config(ip): assert not result.error_in_exec -@pytest.mark.filterwarnings("ignore:In future versions `DataFrame.to_latex`") +@pytest.mark.filterwarnings("ignore:Deprecated arguments supplied:FutureWarning") @pytest.mark.parametrize("method", ["to_string", "to_html", "to_latex"]) @pytest.mark.parametrize( "encoding, data", @@ -3324,6 +3324,8 @@ def test_filepath_or_buffer_arg( filepath_or_buffer_id, ): df = DataFrame([data]) + if method in ["to_latex"]: # uses styler implementation + pytest.importorskip("jinja2") if filepath_or_buffer_id not in ["string", "pathlike"] and encoding is not None: with pytest.raises( @@ -3344,6 +3346,8 @@ def test_filepath_or_buffer_arg( @pytest.mark.filterwarnings("ignore::FutureWarning") @pytest.mark.parametrize("method", ["to_string", "to_html", "to_latex"]) def test_filepath_or_buffer_bad_arg_raises(float_frame, method): + if method in ["to_latex"]: # uses styler implementation + pytest.importorskip("jinja2") msg = "buf is not a file name and it has no write method" with pytest.raises(TypeError, match=msg): getattr(float_frame, method)(buf=object()) diff --git a/pandas/tests/io/formats/test_printing.py b/pandas/tests/io/formats/test_printing.py index 4fc8a46bad777..e363f6bb5cabc 100644 --- a/pandas/tests/io/formats/test_printing.py +++ b/pandas/tests/io/formats/test_printing.py @@ -138,7 +138,7 @@ def test_publishes(self, ip): formatted = ipython.display_formatter.format(obj) assert set(formatted[0].keys()) == expected - with_latex = pd.option_context("display.latex.repr", True) + with_latex = pd.option_context("styler.render.repr", "latex") with opt, with_latex: formatted = ipython.display_formatter.format(obj) diff --git a/pandas/tests/io/formats/test_to_latex.py b/pandas/tests/io/formats/test_to_latex.py index 01bc94bf594d9..e317519fb4092 100644 --- a/pandas/tests/io/formats/test_to_latex.py +++ b/pandas/tests/io/formats/test_to_latex.py @@ -19,7 +19,10 @@ RowStringConverter, ) -pytestmark = pytest.mark.filterwarnings("ignore::FutureWarning") +pytest.importorskip("jinja2") +pytestmark = pytest.mark.filterwarnings( + "ignore:Deprecated arguments supplied:FutureWarning" +) def _dedent(string): @@ -68,10 +71,10 @@ def test_to_latex_tabular_with_index(self): r""" \begin{tabular}{lrl} \toprule - {} & a & b \\ + & a & b \\ \midrule - 0 & 1 & b1 \\ - 1 & 2 & b2 \\ + 0 & 1 & b1 \\ + 1 & 2 & b2 \\ \bottomrule \end{tabular} """ @@ -85,10 +88,10 @@ def test_to_latex_tabular_without_index(self): r""" \begin{tabular}{rl} \toprule - a & b \\ + a & b \\ \midrule - 1 & b1 \\ - 2 & b2 \\ + 1 & b1 \\ + 2 & b2 \\ \bottomrule \end{tabular} """ @@ -101,7 +104,7 @@ def test_to_latex_tabular_without_index(self): ) def test_to_latex_bad_column_format(self, bad_column_format): df = DataFrame({"a": [1, 2], "b": ["b1", "b2"]}) - msg = r"column_format must be str or unicode" + msg = r"`column_format` must be str or unicode" with pytest.raises(ValueError, match=msg): df.to_latex(column_format=bad_column_format) @@ -116,10 +119,10 @@ def test_to_latex_column_format(self): r""" \begin{tabular}{lcr} \toprule - {} & a & b \\ + & a & b \\ \midrule - 0 & 1 & b1 \\ - 1 & 2 & b2 \\ + 0 & 1 & b1 \\ + 1 & 2 & b2 \\ \bottomrule \end{tabular} """ @@ -134,10 +137,10 @@ def test_to_latex_float_format_object_col(self): r""" \begin{tabular}{ll} \toprule - {} & 0 \\ + & 0 \\ \midrule 0 & 1,000 \\ - 1 & test \\ + 1 & test \\ \bottomrule \end{tabular} """ @@ -151,9 +154,7 @@ def test_to_latex_empty_tabular(self): r""" \begin{tabular}{l} \toprule - Empty DataFrame - Columns: Index([], dtype='object') - Index: Index([], dtype='object') \\ + \midrule \bottomrule \end{tabular} """ @@ -167,11 +168,11 @@ def test_to_latex_series(self): r""" \begin{tabular}{ll} \toprule - {} & 0 \\ + & 0 \\ \midrule - 0 & a \\ - 1 & b \\ - 2 & c \\ + 0 & a \\ + 1 & b \\ + 2 & c \\ \bottomrule \end{tabular} """ @@ -187,10 +188,10 @@ def test_to_latex_midrule_location(self): r""" \begin{tabular}{lr} \toprule - {} & a \\ + & a \\ \midrule - 0 & 1 \\ - 1 & 2 \\ + 0 & 1 \\ + 1 & 2 \\ \bottomrule \end{tabular} """ @@ -206,9 +207,17 @@ def test_to_latex_empty_longtable(self): r""" \begin{longtable}{l} \toprule - Empty DataFrame - Columns: Index([], dtype='object') - Index: Index([], dtype='object') \\ + \midrule + \endfirsthead + \toprule + \midrule + \endhead + \midrule + \multicolumn{1}{r}{Continued on next page} \\ + \midrule + \endfoot + \bottomrule + \endlastfoot \end{longtable} """ ) @@ -221,23 +230,21 @@ def test_to_latex_longtable_with_index(self): r""" \begin{longtable}{lrl} \toprule - {} & a & b \\ + & a & b \\ \midrule \endfirsthead - \toprule - {} & a & b \\ + & a & b \\ \midrule \endhead \midrule - \multicolumn{3}{r}{{Continued on next page}} \\ + \multicolumn{3}{r}{Continued on next page} \\ \midrule \endfoot - \bottomrule \endlastfoot - 0 & 1 & b1 \\ - 1 & 2 & b2 \\ + 0 & 1 & b1 \\ + 1 & 2 & b2 \\ \end{longtable} """ ) @@ -250,23 +257,21 @@ def test_to_latex_longtable_without_index(self): r""" \begin{longtable}{rl} \toprule - a & b \\ + a & b \\ \midrule \endfirsthead - \toprule - a & b \\ + a & b \\ \midrule \endhead \midrule - \multicolumn{2}{r}{{Continued on next page}} \\ + \multicolumn{2}{r}{Continued on next page} \\ \midrule \endfoot - \bottomrule \endlastfoot - 1 & b1 \\ - 2 & b2 \\ + 1 & b1 \\ + 2 & b2 \\ \end{longtable} """ ) @@ -294,8 +299,9 @@ def test_to_latex_no_header_with_index(self): r""" \begin{tabular}{lrl} \toprule - 0 & 1 & b1 \\ - 1 & 2 & b2 \\ + \midrule + 0 & 1 & b1 \\ + 1 & 2 & b2 \\ \bottomrule \end{tabular} """ @@ -310,6 +316,7 @@ def test_to_latex_no_header_without_index(self): r""" \begin{tabular}{rl} \toprule + \midrule 1 & b1 \\ 2 & b2 \\ \bottomrule @@ -326,10 +333,10 @@ def test_to_latex_specified_header_with_index(self): r""" \begin{tabular}{lrl} \toprule - {} & AA & BB \\ + & AA & BB \\ \midrule - 0 & 1 & b1 \\ - 1 & 2 & b2 \\ + 0 & 1 & b1 \\ + 1 & 2 & b2 \\ \bottomrule \end{tabular} """ @@ -346,8 +353,8 @@ def test_to_latex_specified_header_without_index(self): \toprule AA & BB \\ \midrule - 1 & b1 \\ - 2 & b2 \\ + 1 & b1 \\ + 2 & b2 \\ \bottomrule \end{tabular} """ @@ -370,7 +377,7 @@ def test_to_latex_number_of_items_in_header_missmatch_raises( ): # GH 7124 df = DataFrame({"a": [1, 2], "b": ["b1", "b2"]}) - msg = f"Writing 2 cols but got {num_aliases} aliases" + msg = "``aliases`` must be of length equal to the number of visible labels" with pytest.raises(ValueError, match=msg): df.to_latex(header=header) @@ -382,10 +389,10 @@ def test_to_latex_decimal(self): r""" \begin{tabular}{lrl} \toprule - {} & a & b \\ + & a & b \\ \midrule - 0 & 1,0 & b1 \\ - 1 & 2,1 & b2 \\ + 0 & 1,000000 & b1 \\ + 1 & 2,100000 & b2 \\ \bottomrule \end{tabular} """ @@ -402,10 +409,10 @@ def test_to_latex_bold_rows(self): r""" \begin{tabular}{lrl} \toprule - {} & a & b \\ + & a & b \\ \midrule - \textbf{0} & 1 & b1 \\ - \textbf{1} & 2 & b2 \\ + \textbf{0} & 1 & b1 \\ + \textbf{1} & 2 & b2 \\ \bottomrule \end{tabular} """ @@ -420,10 +427,10 @@ def test_to_latex_no_bold_rows(self): r""" \begin{tabular}{lrl} \toprule - {} & a & b \\ + & a & b \\ \midrule - 0 & 1 & b1 \\ - 1 & 2 & b2 \\ + 0 & 1 & b1 \\ + 1 & 2 & b2 \\ \bottomrule \end{tabular} """ @@ -463,14 +470,13 @@ def test_to_latex_caption_only(self, df_short, caption_table): expected = _dedent( r""" \begin{table} - \centering \caption{a table in a \texttt{table/tabular} environment} \begin{tabular}{lrl} \toprule - {} & a & b \\ + & a & b \\ \midrule - 0 & 1 & b1 \\ - 1 & 2 & b2 \\ + 0 & 1 & b1 \\ + 1 & 2 & b2 \\ \bottomrule \end{tabular} \end{table} @@ -484,14 +490,13 @@ def test_to_latex_label_only(self, df_short, label_table): expected = _dedent( r""" \begin{table} - \centering \label{tab:table_tabular} \begin{tabular}{lrl} \toprule - {} & a & b \\ + & a & b \\ \midrule - 0 & 1 & b1 \\ - 1 & 2 & b2 \\ + 0 & 1 & b1 \\ + 1 & 2 & b2 \\ \bottomrule \end{tabular} \end{table} @@ -505,15 +510,14 @@ def test_to_latex_caption_and_label(self, df_short, caption_table, label_table): expected = _dedent( r""" \begin{table} - \centering \caption{a table in a \texttt{table/tabular} environment} \label{tab:table_tabular} \begin{tabular}{lrl} \toprule - {} & a & b \\ + & a & b \\ \midrule - 0 & 1 & b1 \\ - 1 & 2 & b2 \\ + 0 & 1 & b1 \\ + 1 & 2 & b2 \\ \bottomrule \end{tabular} \end{table} @@ -531,14 +535,13 @@ def test_to_latex_caption_and_shortcaption( expected = _dedent( r""" \begin{table} - \centering \caption[a table]{a table in a \texttt{table/tabular} environment} \begin{tabular}{lrl} \toprule - {} & a & b \\ + & a & b \\ \midrule - 0 & 1 & b1 \\ - 1 & 2 & b2 \\ + 0 & 1 & b1 \\ + 1 & 2 & b2 \\ \bottomrule \end{tabular} \end{table} @@ -567,15 +570,14 @@ def test_to_latex_caption_shortcaption_and_label( expected = _dedent( 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 \\ + & a & b \\ \midrule - 0 & 1 & b1 \\ - 1 & 2 & b2 \\ + 0 & 1 & b1 \\ + 1 & 2 & b2 \\ \bottomrule \end{tabular} \end{table} @@ -596,7 +598,7 @@ def test_to_latex_caption_shortcaption_and_label( def test_to_latex_bad_caption_raises(self, bad_caption): # test that wrong number of params is raised df = DataFrame({"a": [1]}) - msg = "caption must be either a string or a tuple of two strings" + msg = "`caption` must be either a string or 2-tuple of strings" with pytest.raises(ValueError, match=msg): df.to_latex(caption=bad_caption) @@ -607,14 +609,13 @@ def test_to_latex_two_chars_caption(self, df_short): expected = _dedent( r""" \begin{table} - \centering \caption{xy} \begin{tabular}{lrl} \toprule - {} & a & b \\ + & a & b \\ \midrule - 0 & 1 & b1 \\ - 1 & 2 & b2 \\ + 0 & 1 & b1 \\ + 1 & 2 & b2 \\ \bottomrule \end{tabular} \end{table} @@ -630,25 +631,24 @@ def test_to_latex_longtable_caption_only(self, df_short, caption_longtable): expected = _dedent( r""" \begin{longtable}{lrl} - \caption{a table in a \texttt{longtable} environment}\\ + \caption{a table in a \texttt{longtable} environment} \\ \toprule - {} & a & b \\ + & a & b \\ \midrule \endfirsthead \caption[]{a table in a \texttt{longtable} environment} \\ \toprule - {} & a & b \\ + & a & b \\ \midrule \endhead \midrule - \multicolumn{3}{r}{{Continued on next page}} \\ + \multicolumn{3}{r}{Continued on next page} \\ \midrule \endfoot - \bottomrule \endlastfoot - 0 & 1 & b1 \\ - 1 & 2 & b2 \\ + 0 & 1 & b1 \\ + 1 & 2 & b2 \\ \end{longtable} """ ) @@ -660,25 +660,23 @@ def test_to_latex_longtable_label_only(self, df_short, label_longtable): expected = _dedent( r""" \begin{longtable}{lrl} - \label{tab:longtable}\\ + \label{tab:longtable} \\ \toprule - {} & a & b \\ + & a & b \\ \midrule \endfirsthead - \toprule - {} & a & b \\ + & a & b \\ \midrule \endhead \midrule - \multicolumn{3}{r}{{Continued on next page}} \\ + \multicolumn{3}{r}{Continued on next page} \\ \midrule \endfoot - \bottomrule \endlastfoot - 0 & 1 & b1 \\ - 1 & 2 & b2 \\ + 0 & 1 & b1 \\ + 1 & 2 & b2 \\ \end{longtable} """ ) @@ -698,29 +696,27 @@ def test_to_latex_longtable_caption_and_label( ) expected = _dedent( r""" - \begin{longtable}{lrl} - \caption{a table in a \texttt{longtable} environment} - \label{tab:longtable}\\ - \toprule - {} & a & b \\ - \midrule - \endfirsthead - \caption[]{a table in a \texttt{longtable} environment} \\ - \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} - """ + \begin{longtable}{lrl} + \caption{a table in a \texttt{longtable} environment} \label{tab:longtable} \\ + \toprule + & a & b \\ + \midrule + \endfirsthead + \caption[]{a table in a \texttt{longtable} environment} \\ + \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 == expected @@ -739,29 +735,27 @@ def test_to_latex_longtable_caption_shortcaption_and_label( ) expected = _dedent( r""" - \begin{longtable}{lrl} - \caption[a table]{a table in a \texttt{longtable} environment} - \label{tab:longtable}\\ - \toprule - {} & a & b \\ - \midrule - \endfirsthead - \caption[]{a table in a \texttt{longtable} environment} \\ - \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} - """ +\begin{longtable}{lrl} +\caption[a table]{a table in a \texttt{longtable} environment} \label{tab:longtable} \\ +\toprule + & a & b \\ +\midrule +\endfirsthead +\caption[]{a table in a \texttt{longtable} environment} \\ +\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 == expected @@ -780,10 +774,10 @@ def test_to_latex_escape_false(self, df_with_symbols): r""" \begin{tabular}{lll} \toprule - {} & co$e^x$ & co^l1 \\ + & co$e^x$ & co^l1 \\ \midrule - a & a & a \\ - b & b & b \\ + a & a & a \\ + b & b & b \\ \bottomrule \end{tabular} """ @@ -796,10 +790,10 @@ def test_to_latex_escape_default(self, df_with_symbols): r""" \begin{tabular}{lll} \toprule - {} & co\$e\textasciicircum x\$ & co\textasciicircum l1 \\ + & co\$e\textasciicircum x\$ & co\textasciicircum l1 \\ \midrule - a & a & a \\ - b & b & b \\ + a & a & a \\ + b & b & b \\ \bottomrule \end{tabular} """ @@ -813,11 +807,11 @@ def test_to_latex_special_escape(self): r""" \begin{tabular}{ll} \toprule - {} & 0 \\ + & 0 \\ \midrule - 0 & a\textbackslash b\textbackslash c \\ - 1 & \textasciicircum a\textasciicircum b\textasciicircum c \\ - 2 & \textasciitilde a\textasciitilde b\textasciitilde c \\ + 0 & a\textbackslash b\textbackslash c \\ + 1 & \textasciicircum a\textasciicircum b\textasciicircum c \\ + 2 & \textasciitilde a\textasciitilde b\textasciitilde c \\ \bottomrule \end{tabular} """ @@ -832,18 +826,18 @@ def test_to_latex_escape_special_chars(self): r""" \begin{tabular}{ll} \toprule - {} & 0 \\ - \midrule - 0 & \& \\ - 1 & \% \\ - 2 & \$ \\ - 3 & \# \\ - 4 & \_ \\ - 5 & \{ \\ - 6 & \} \\ - 7 & \textasciitilde \\ - 8 & \textasciicircum \\ - 9 & \textbackslash \\ + & 0 \\ + \midrule + 0 & \& \\ + 1 & \% \\ + 2 & \$ \\ + 3 & \# \\ + 4 & \_ \\ + 5 & \{ \\ + 6 & \} \\ + 7 & \textasciitilde \\ + 8 & \textasciicircum \\ + 9 & \textbackslash \\ \bottomrule \end{tabular} """ @@ -858,10 +852,10 @@ def test_to_latex_specified_header_special_chars_without_escape(self): r""" \begin{tabular}{lrl} \toprule - {} & $A$ & $B$ \\ + & $A$ & $B$ \\ \midrule - 0 & 1 & b1 \\ - 1 & 2 & b2 \\ + 0 & 1 & b1 \\ + 1 & 2 & b2 \\ \bottomrule \end{tabular} """ @@ -877,13 +871,12 @@ def test_to_latex_position(self): expected = _dedent( r""" \begin{table}[h] - \centering \begin{tabular}{lrl} \toprule - {} & a & b \\ + & a & b \\ \midrule - 0 & 1 & b1 \\ - 1 & 2 & b2 \\ + 0 & 1 & b1 \\ + 1 & 2 & b2 \\ \bottomrule \end{tabular} \end{table} @@ -899,23 +892,21 @@ def test_to_latex_longtable_position(self): r""" \begin{longtable}[t]{lrl} \toprule - {} & a & b \\ + & a & b \\ \midrule \endfirsthead - \toprule - {} & a & b \\ + & a & b \\ \midrule \endhead \midrule - \multicolumn{3}{r}{{Continued on next page}} \\ + \multicolumn{3}{r}{Continued on next page} \\ \midrule \endfoot - \bottomrule \endlastfoot - 0 & 1 & b1 \\ - 1 & 2 & b2 \\ + 0 & 1 & b1 \\ + 1 & 2 & b2 \\ \end{longtable} """ ) @@ -950,11 +941,11 @@ def test_to_latex_with_formatters(self): r""" \begin{tabular}{llrrl} \toprule - {} & datetime64 & float & int & object \\ + & datetime64 & float & int & object \\ \midrule - index: 0 & 2016-01 & [ 1.0] & 0x1 & -(1, 2)- \\ - index: 1 & 2016-02 & [ 2.0] & 0x2 & -True- \\ - index: 2 & 2016-03 & [ 3.0] & 0x3 & -False- \\ + index: 0 & 2016-01 & [ 1.0] & 0x1 & -(1, 2)- \\ + index: 1 & 2016-02 & [ 2.0] & 0x2 & -True- \\ + index: 2 & 2016-03 & [ 3.0] & 0x3 & -False- \\ \bottomrule \end{tabular} """ @@ -969,7 +960,7 @@ def test_to_latex_float_format_no_fixed_width_3decimals(self): r""" \begin{tabular}{lr} \toprule - {} & x \\ + & x \\ \midrule 0 & 0.200 \\ \bottomrule @@ -986,7 +977,7 @@ def test_to_latex_float_format_no_fixed_width_integer(self): r""" \begin{tabular}{lr} \toprule - {} & x \\ + & x \\ \midrule 0 & 100 \\ \bottomrule @@ -1009,10 +1000,10 @@ def test_to_latex_na_rep_and_float_format(self, na_rep): fr""" \begin{{tabular}}{{llr}} \toprule - {{}} & Group & Data \\ + & Group & Data \\ \midrule - 0 & A & 1.22 \\ - 1 & A & {na_rep} \\ + 0 & A & 1.22 \\ + 1 & A & {na_rep} \\ \bottomrule \end{{tabular}} """ @@ -1056,10 +1047,10 @@ def test_to_latex_multindex_header(self): r""" \begin{tabular}{llrr} \toprule - & & r1 & r2 \\ - a & b & & \\ + & & r1 & r2 \\ + a & b & & \\ \midrule - 0 & 1 & 2 & 3 \\ + 0 & 1 & 2 & 3 \\ \bottomrule \end{tabular} """ @@ -1075,8 +1066,8 @@ def test_to_latex_multiindex_empty_name(self): r""" \begin{tabular}{lrrrr} \toprule - & 0 & 1 & 2 & 3 \\ - {} & & & & \\ + & 0 & 1 & 2 & 3 \\ + & & & & \\ \midrule 1 & -1 & -1 & -1 & -1 \\ 2 & -1 & -1 & -1 & -1 \\ @@ -1093,10 +1084,10 @@ def test_to_latex_multiindex_column_tabular(self): r""" \begin{tabular}{ll} \toprule - {} & x \\ - {} & y \\ + & x \\ + & y \\ \midrule - 0 & a \\ + 0 & a \\ \bottomrule \end{tabular} """ @@ -1110,9 +1101,9 @@ def test_to_latex_multiindex_small_tabular(self): r""" \begin{tabular}{lll} \toprule - & & 0 \\ + & & 0 \\ \midrule - x & y & a \\ + x & y & a \\ \bottomrule \end{tabular} """ @@ -1125,13 +1116,13 @@ def test_to_latex_multiindex_tabular(self, multiindex_frame): r""" \begin{tabular}{llrrrr} \toprule - & & 0 & 1 & 2 & 3 \\ + & & 0 & 1 & 2 & 3 \\ \midrule - c1 & 0 & 0 & 1 & 2 & 3 \\ - & 1 & 4 & 5 & 6 & 7 \\ - c2 & 0 & 0 & 1 & 2 & 3 \\ - & 1 & 4 & 5 & 6 & 7 \\ - c3 & 0 & 0 & 1 & 2 & 3 \\ + c1 & 0 & 0 & 1 & 2 & 3 \\ + & 1 & 4 & 5 & 6 & 7 \\ + c2 & 0 & 0 & 1 & 2 & 3 \\ + & 1 & 4 & 5 & 6 & 7 \\ + c3 & 0 & 0 & 1 & 2 & 3 \\ \bottomrule \end{tabular} """ @@ -1148,12 +1139,12 @@ def test_to_latex_multicolumn_tabular(self, multiindex_frame): \begin{tabular}{lrrrrr} \toprule a & \multicolumn{2}{l}{c1} & \multicolumn{2}{l}{c2} & c3 \\ - b & 0 & 1 & 0 & 1 & 0 \\ + 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 \\ + 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} """ @@ -1168,13 +1159,13 @@ def test_to_latex_index_has_name_tabular(self): r""" \begin{tabular}{llr} \toprule - & & c \\ - a & b & \\ + & & c \\ + a & b & \\ \midrule - 0 & a & 1 \\ - & b & 2 \\ - 1 & a & 3 \\ - & b & 4 \\ + 0 & a & 1 \\ + & b & 2 \\ + 1 & a & 3 \\ + & b & 4 \\ \bottomrule \end{tabular} """ @@ -1184,17 +1175,17 @@ def test_to_latex_index_has_name_tabular(self): def test_to_latex_groupby_tabular(self): # GH 10660 df = DataFrame({"a": [0, 0, 1, 1], "b": list("abab"), "c": [1, 2, 3, 4]}) - result = df.groupby("a").describe().to_latex() + result = df.groupby("a").describe().to_latex(format={"precision": 1}) expected = _dedent( r""" \begin{tabular}{lrrrrrrrr} \toprule - {} & \multicolumn{8}{l}{c} \\ - {} & count & mean & std & min & 25\% & 50\% & 75\% & max \\ - a & & & & & & & & \\ + & \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 \\ + 0 & 2.0 & 1.5 & 0.7 & 1.0 & 1.2 & 1.5 & 1.8 & 2.0 \\ + 1 & 2.0 & 3.5 & 0.7 & 3.0 & 3.2 & 3.5 & 3.8 & 4.0 \\ \bottomrule \end{tabular} """ @@ -1217,10 +1208,10 @@ def test_to_latex_multiindex_dupe_level(self): r""" \begin{tabular}{lll} \toprule - & & col \\ + & & col \\ \midrule - A & c & NaN \\ - B & c & NaN \\ + A & c & NaN \\ + B & c & NaN \\ \bottomrule \end{tabular} """ @@ -1233,14 +1224,14 @@ def test_to_latex_multicolumn_default(self, multicolumn_frame): 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 \\ + & \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} """ @@ -1253,14 +1244,14 @@ def test_to_latex_multicolumn_false(self, multicolumn_frame): 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 \\ + & 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} """ @@ -1273,15 +1264,16 @@ def test_to_latex_multirow_true(self, multicolumn_frame): r""" \begin{tabular}{llrrrrr} \toprule - & & 0 & 1 & 2 & 3 & 4 \\ + & & 0 & 1 & 2 & 3 & 4 \\ \midrule - \multirow{2}{*}{c1} & 0 & 0 & 1 & 2 & 3 & 4 \\ - & 1 & 5 & 6 & 7 & 8 & 9 \\ + \multirow[t]{2}{*}{c1} & 0 & 0 & 1 & 2 & 3 & 4 \\ + & 1 & 5 & 6 & 7 & 8 & 9 \\ + \cline{1-7} + \multirow[t]{2}{*}{c2} & 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 \\ + c3 & 0 & 0 & 1 & 2 & 3 & 4 \\ \cline{1-7} - c3 & 0 & 0 & 1 & 2 & 3 & 4 \\ \bottomrule \end{tabular} """ @@ -1299,16 +1291,17 @@ def test_to_latex_multicolumnrow_with_multicol_format(self, multicolumn_frame): r""" \begin{tabular}{llrrrrr} \toprule - & & \multicolumn{2}{c}{c1} & \multicolumn{2}{c}{c2} & c3 \\ - & & 0 & 1 & 0 & 1 & 0 \\ + & & \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 \\ + \multirow[t]{2}{*}{c1} & 0 & 0 & 1 & 2 & 3 & 4 \\ + & 1 & 5 & 6 & 7 & 8 & 9 \\ + \cline{1-7} + \multirow[t]{2}{*}{c2} & 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 \\ + c3 & 0 & 0 & 1 & 2 & 3 & 4 \\ \cline{1-7} - c3 & 0 & 0 & 1 & 2 & 3 & 4 \\ \bottomrule \end{tabular} """ @@ -1326,24 +1319,23 @@ def test_to_latex_multiindex_names(self, name0, name1, axes): for idx in axes: df.axes[idx].names = names - idx_names = tuple(n or "{}" for n in names) + idx_names = tuple(n or "" for n in names) idx_names_row = ( - f"{idx_names[0]} & {idx_names[1]} & & & & \\\\\n" + f"{idx_names[0]} & {idx_names[1]} & & & & \\\\\n" if (0 in axes and any(names)) else "" ) - placeholder = "{}" if any(names) and 1 in axes else " " - col_names = [n if (bool(n) and 1 in axes) else placeholder for n in names] + col_names = [n if (bool(n) and 1 in axes) else "" for n in names] observed = df.to_latex() expected = r"""\begin{tabular}{llrrrr} \toprule - & %s & \multicolumn{2}{l}{1} & \multicolumn{2}{l}{2} \\ - & %s & 3 & 4 & 3 & 4 \\ + & %s & \multicolumn{2}{l}{1} & \multicolumn{2}{l}{2} \\ + & %s & 3 & 4 & 3 & 4 \\ %s\midrule 1 & 3 & -1 & -1 & -1 & -1 \\ - & 4 & -1 & -1 & -1 & -1 \\ + & 4 & -1 & -1 & -1 & -1 \\ 2 & 3 & -1 & -1 & -1 & -1 \\ - & 4 & -1 & -1 & -1 & -1 \\ + & 4 & -1 & -1 & -1 & -1 \\ \bottomrule \end{tabular} """ % tuple( @@ -1362,14 +1354,14 @@ def test_to_latex_multiindex_nans(self, one_row): r""" \begin{tabular}{llr} \toprule - & & c \\ - a & b & \\ + & & c \\ + a & b & \\ \midrule - NaN & 2 & 4 \\ + NaN & 2 & 4 \\ """ ) if not one_row: - expected += r"""1.0 & 3 & 5 \\ + expected += r"""1.000000 & 3 & 5 \\ """ expected += r"""\bottomrule \end{tabular} @@ -1384,11 +1376,11 @@ def test_to_latex_non_string_index(self): r""" \begin{tabular}{llr} \toprule - & & 2 \\ - 0 & 1 & \\ + & & 2 \\ + 0 & 1 & \\ \midrule - 1 & 2 & 3 \\ - & 2 & 3 \\ + 1 & 2 & 3 \\ + & 2 & 3 \\ \bottomrule \end{tabular} """ @@ -1406,27 +1398,26 @@ def test_to_latex_multiindex_multirow(self): r""" \begin{tabular}{lll} \toprule - & & \\ i & val0 & val1 \\ \midrule - \multirow{6}{*}{0.0} & \multirow{2}{*}{3.0} & 0 \\ - & & 1 \\ + \multirow[t]{6}{*}{0.000000} & \multirow[t]{2}{*}{3.000000} & 0 \\ + & & 1 \\ \cline{2-3} - & \multirow{2}{*}{2.0} & 0 \\ - & & 1 \\ + & \multirow[t]{2}{*}{2.000000} & 0 \\ + & & 1 \\ \cline{2-3} - & \multirow{2}{*}{1.0} & 0 \\ - & & 1 \\ - \cline{1-3} + & \multirow[t]{2}{*}{1.000000} & 0 \\ + & & 1 \\ + \cline{1-3} \cline{2-3} + \multirow[t]{6}{*}{1.000000} & \multirow[t]{2}{*}{3.000000} & 0 \\ + & & 1 \\ \cline{2-3} - \multirow{6}{*}{1.0} & \multirow{2}{*}{3.0} & 0 \\ - & & 1 \\ + & \multirow[t]{2}{*}{2.000000} & 0 \\ + & & 1 \\ \cline{2-3} - & \multirow{2}{*}{2.0} & 0 \\ - & & 1 \\ - \cline{2-3} - & \multirow{2}{*}{1.0} & 0 \\ - & & 1 \\ + & \multirow[t]{2}{*}{1.000000} & 0 \\ + & & 1 \\ + \cline{1-3} \cline{2-3} \bottomrule \end{tabular} """ @@ -1517,14 +1508,17 @@ def test_get_strrow_multindex_multicolumn(self, row_num, expected): assert row_string_converter.get_strrow(row_num=row_num) == expected - def test_future_warning(self): + @pytest.mark.parametrize( + "deprecated_arg, value", + [ + ("col_space", 10), + ], + ) + def test_deprecation_warning(self, deprecated_arg, value): df = DataFrame([[1]]) msg = ( - "In future versions `DataFrame.to_latex` is expected to utilise the base " - "implementation of `Styler.to_latex` for formatting and rendering. " - "The arguments signature may therefore change. It is recommended instead " - "to use `DataFrame.style.to_latex` which also contains additional " - "functionality." - ) - with tm.assert_produces_warning(FutureWarning, match=msg): - df.to_latex() + "`col_space` is deprecated. Whitespace in LaTeX does not impact " + "the rendered version, and this argument is ignored." + ) + with tm.assert_produces_warning(DeprecationWarning, match=msg): + df.to_latex(**{deprecated_arg: value}) diff --git a/pandas/tests/io/test_common.py b/pandas/tests/io/test_common.py index b458f3351c860..469741d465193 100644 --- a/pandas/tests/io/test_common.py +++ b/pandas/tests/io/test_common.py @@ -351,6 +351,8 @@ def test_read_fspath_all(self, reader, module, path, datapath): ], ) def test_write_fspath_all(self, writer_name, writer_kwargs, module): + if writer_name in ["to_latex"]: + pytest.importorskip("jinja2") p1 = tm.ensure_clean("string") p2 = tm.ensure_clean("fspath") df = pd.DataFrame({"A": [1, 2]}) diff --git a/pandas/tests/series/test_repr.py b/pandas/tests/series/test_repr.py index a12bc1df37269..ddc479692d056 100644 --- a/pandas/tests/series/test_repr.py +++ b/pandas/tests/series/test_repr.py @@ -208,17 +208,20 @@ def test_timeseries_repr_object_dtype(self): @pytest.mark.filterwarnings("ignore::FutureWarning") def test_latex_repr(self): + pytest.importorskip("jinja2") # uses Styler implementation result = r"""\begin{tabular}{ll} \toprule -{} & 0 \\ + & 0 \\ \midrule -0 & $\alpha$ \\ -1 & b \\ -2 & c \\ +0 & $\alpha$ \\ +1 & b \\ +2 & c \\ \bottomrule \end{tabular} """ - with option_context("display.latex.escape", False, "display.latex.repr", True): + with option_context( + "display.latex.escape", False, "styler.render.repr", "latex" + ): s = Series([r"$\alpha$", "b", "c"]) assert result == s._repr_latex_()