1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528 |
- """
- Module for applying conditional formatting to
- DataFrames and Series.
- """
- from collections import defaultdict
- from contextlib import contextmanager
- import copy
- from functools import partial
- from itertools import product
- from typing import Any, Callable, DefaultDict, Dict, List, Optional, Sequence, Tuple
- from uuid import uuid1
- import numpy as np
- from pandas._config import get_option
- from pandas._libs import lib
- from pandas.compat._optional import import_optional_dependency
- from pandas.util._decorators import Appender
- from pandas.core.dtypes.common import is_float
- import pandas as pd
- from pandas.api.types import is_dict_like, is_list_like
- import pandas.core.common as com
- from pandas.core.generic import _shared_docs
- from pandas.core.indexing import _maybe_numeric_slice, _non_reducing_slice
- jinja2 = import_optional_dependency("jinja2", extra="DataFrame.style requires jinja2.")
- try:
- import matplotlib.pyplot as plt
- from matplotlib import colors
- has_mpl = True
- except ImportError:
- has_mpl = False
- no_mpl_message = "{0} requires matplotlib."
- @contextmanager
- def _mpl(func):
- if has_mpl:
- yield plt, colors
- else:
- raise ImportError(no_mpl_message.format(func.__name__))
- class Styler:
- """
- Helps style a DataFrame or Series according to the data with HTML and CSS.
- Parameters
- ----------
- data : Series or DataFrame
- Data to be styled - either a Series or DataFrame.
- precision : int
- Precision to round floats to, defaults to pd.options.display.precision.
- table_styles : list-like, default None
- List of {selector: (attr, value)} dicts; see Notes.
- uuid : str, default None
- A unique identifier to avoid CSS collisions; generated automatically.
- caption : str, default None
- Caption to attach to the table.
- table_attributes : str, default None
- Items that show up in the opening ``<table>`` tag
- in addition to automatic (by default) id.
- cell_ids : bool, default True
- If True, each cell will have an ``id`` attribute in their HTML tag.
- The ``id`` takes the form ``T_<uuid>_row<num_row>_col<num_col>``
- where ``<uuid>`` is the unique identifier, ``<num_row>`` is the row
- number and ``<num_col>`` is the column number.
- na_rep : str, optional
- Representation for missing values.
- If ``na_rep`` is None, no special formatting is applied
- .. versionadded:: 1.0.0
- Attributes
- ----------
- env : Jinja2 jinja2.Environment
- template : Jinja2 Template
- loader : Jinja2 Loader
- See Also
- --------
- DataFrame.style : Return a Styler object containing methods for building
- a styled HTML representation for the DataFrame.
- Notes
- -----
- Most styling will be done by passing style functions into
- ``Styler.apply`` or ``Styler.applymap``. Style functions should
- return values with strings containing CSS ``'attr: value'`` that will
- be applied to the indicated cells.
- If using in the Jupyter notebook, Styler has defined a ``_repr_html_``
- to automatically render itself. Otherwise call Styler.render to get
- the generated HTML.
- CSS classes are attached to the generated HTML
- * Index and Column names include ``index_name`` and ``level<k>``
- where `k` is its level in a MultiIndex
- * Index label cells include
- * ``row_heading``
- * ``row<n>`` where `n` is the numeric position of the row
- * ``level<k>`` where `k` is the level in a MultiIndex
- * Column label cells include
- * ``col_heading``
- * ``col<n>`` where `n` is the numeric position of the column
- * ``evel<k>`` where `k` is the level in a MultiIndex
- * Blank cells include ``blank``
- * Data cells include ``data``
- """
- loader = jinja2.PackageLoader("pandas", "io/formats/templates")
- env = jinja2.Environment(loader=loader, trim_blocks=True)
- template = env.get_template("html.tpl")
- def __init__(
- self,
- data,
- precision=None,
- table_styles=None,
- uuid=None,
- caption=None,
- table_attributes=None,
- cell_ids=True,
- na_rep: Optional[str] = None,
- ):
- self.ctx: DefaultDict[Tuple[int, int], List[str]] = defaultdict(list)
- self._todo: List[Tuple[Callable, Tuple, Dict]] = []
- if not isinstance(data, (pd.Series, pd.DataFrame)):
- raise TypeError("``data`` must be a Series or DataFrame")
- if data.ndim == 1:
- data = data.to_frame()
- if not data.index.is_unique or not data.columns.is_unique:
- raise ValueError("style is not supported for non-unique indices.")
- self.data = data
- self.index = data.index
- self.columns = data.columns
- self.uuid = uuid
- self.table_styles = table_styles
- self.caption = caption
- if precision is None:
- precision = get_option("display.precision")
- self.precision = precision
- self.table_attributes = table_attributes
- self.hidden_index = False
- self.hidden_columns: Sequence[int] = []
- self.cell_ids = cell_ids
- self.na_rep = na_rep
- # display_funcs maps (row, col) -> formatting function
- def default_display_func(x):
- if self.na_rep is not None and pd.isna(x):
- return self.na_rep
- elif is_float(x):
- display_format = f"{x:.{self.precision}f}"
- return display_format
- else:
- return x
- self._display_funcs: DefaultDict[
- Tuple[int, int], Callable[[Any], str]
- ] = defaultdict(lambda: default_display_func)
- def _repr_html_(self):
- """
- Hooks into Jupyter notebook rich display system.
- """
- return self.render()
- @Appender(
- _shared_docs["to_excel"]
- % dict(
- axes="index, columns",
- klass="Styler",
- axes_single_arg="{0 or 'index', 1 or 'columns'}",
- optional_by="""
- by : str or list of str
- Name or list of names which refer to the axis items.""",
- versionadded_to_excel="\n .. versionadded:: 0.20",
- )
- )
- def to_excel(
- self,
- excel_writer,
- sheet_name="Sheet1",
- na_rep="",
- float_format=None,
- columns=None,
- header=True,
- index=True,
- index_label=None,
- startrow=0,
- startcol=0,
- engine=None,
- merge_cells=True,
- encoding=None,
- inf_rep="inf",
- verbose=True,
- freeze_panes=None,
- ):
- from pandas.io.formats.excel import ExcelFormatter
- formatter = ExcelFormatter(
- self,
- na_rep=na_rep,
- cols=columns,
- header=header,
- float_format=float_format,
- index=index,
- index_label=index_label,
- merge_cells=merge_cells,
- inf_rep=inf_rep,
- )
- formatter.write(
- excel_writer,
- sheet_name=sheet_name,
- startrow=startrow,
- startcol=startcol,
- freeze_panes=freeze_panes,
- engine=engine,
- )
- def _translate(self):
- """
- Convert the DataFrame in `self.data` and the attrs from `_build_styles`
- into a dictionary of {head, body, uuid, cellstyle}.
- """
- table_styles = self.table_styles or []
- caption = self.caption
- ctx = self.ctx
- precision = self.precision
- hidden_index = self.hidden_index
- hidden_columns = self.hidden_columns
- uuid = self.uuid or str(uuid1()).replace("-", "_")
- ROW_HEADING_CLASS = "row_heading"
- COL_HEADING_CLASS = "col_heading"
- INDEX_NAME_CLASS = "index_name"
- DATA_CLASS = "data"
- BLANK_CLASS = "blank"
- BLANK_VALUE = ""
- def format_attr(pair):
- return f"{pair['key']}={pair['value']}"
- # for sparsifying a MultiIndex
- idx_lengths = _get_level_lengths(self.index)
- col_lengths = _get_level_lengths(self.columns, hidden_columns)
- cell_context = dict()
- n_rlvls = self.data.index.nlevels
- n_clvls = self.data.columns.nlevels
- rlabels = self.data.index.tolist()
- clabels = self.data.columns.tolist()
- if n_rlvls == 1:
- rlabels = [[x] for x in rlabels]
- if n_clvls == 1:
- clabels = [[x] for x in clabels]
- clabels = list(zip(*clabels))
- cellstyle = []
- head = []
- for r in range(n_clvls):
- # Blank for Index columns...
- row_es = [
- {
- "type": "th",
- "value": BLANK_VALUE,
- "display_value": BLANK_VALUE,
- "is_visible": not hidden_index,
- "class": " ".join([BLANK_CLASS]),
- }
- ] * (n_rlvls - 1)
- # ... except maybe the last for columns.names
- name = self.data.columns.names[r]
- cs = [
- BLANK_CLASS if name is None else INDEX_NAME_CLASS,
- f"level{r}",
- ]
- name = BLANK_VALUE if name is None else name
- row_es.append(
- {
- "type": "th",
- "value": name,
- "display_value": name,
- "class": " ".join(cs),
- "is_visible": not hidden_index,
- }
- )
- if clabels:
- for c, value in enumerate(clabels[r]):
- cs = [
- COL_HEADING_CLASS,
- f"level{r}",
- f"col{c}",
- ]
- cs.extend(
- cell_context.get("col_headings", {}).get(r, {}).get(c, [])
- )
- es = {
- "type": "th",
- "value": value,
- "display_value": value,
- "class": " ".join(cs),
- "is_visible": _is_visible(c, r, col_lengths),
- }
- colspan = col_lengths.get((r, c), 0)
- if colspan > 1:
- es["attributes"] = [
- format_attr({"key": "colspan", "value": colspan})
- ]
- row_es.append(es)
- head.append(row_es)
- if (
- self.data.index.names
- and com.any_not_none(*self.data.index.names)
- and not hidden_index
- ):
- index_header_row = []
- for c, name in enumerate(self.data.index.names):
- cs = [INDEX_NAME_CLASS, f"level{c}"]
- name = "" if name is None else name
- index_header_row.append(
- {"type": "th", "value": name, "class": " ".join(cs)}
- )
- index_header_row.extend(
- [{"type": "th", "value": BLANK_VALUE, "class": " ".join([BLANK_CLASS])}]
- * (len(clabels[0]) - len(hidden_columns))
- )
- head.append(index_header_row)
- body = []
- for r, idx in enumerate(self.data.index):
- row_es = []
- for c, value in enumerate(rlabels[r]):
- rid = [
- ROW_HEADING_CLASS,
- f"level{c}",
- f"row{r}",
- ]
- es = {
- "type": "th",
- "is_visible": (_is_visible(r, c, idx_lengths) and not hidden_index),
- "value": value,
- "display_value": value,
- "id": "_".join(rid[1:]),
- "class": " ".join(rid),
- }
- rowspan = idx_lengths.get((c, r), 0)
- if rowspan > 1:
- es["attributes"] = [
- format_attr({"key": "rowspan", "value": rowspan})
- ]
- row_es.append(es)
- for c, col in enumerate(self.data.columns):
- cs = [DATA_CLASS, f"row{r}", f"col{c}"]
- cs.extend(cell_context.get("data", {}).get(r, {}).get(c, []))
- formatter = self._display_funcs[(r, c)]
- value = self.data.iloc[r, c]
- row_dict = {
- "type": "td",
- "value": value,
- "class": " ".join(cs),
- "display_value": formatter(value),
- "is_visible": (c not in hidden_columns),
- }
- # only add an id if the cell has a style
- if self.cell_ids or not (len(ctx[r, c]) == 1 and ctx[r, c][0] == ""):
- row_dict["id"] = "_".join(cs[1:])
- row_es.append(row_dict)
- props = []
- for x in ctx[r, c]:
- # have to handle empty styles like ['']
- if x.count(":"):
- props.append(x.split(":"))
- else:
- props.append(["", ""])
- cellstyle.append({"props": props, "selector": f"row{r}_col{c}"})
- body.append(row_es)
- table_attr = self.table_attributes
- use_mathjax = get_option("display.html.use_mathjax")
- if not use_mathjax:
- table_attr = table_attr or ""
- if 'class="' in table_attr:
- table_attr = table_attr.replace('class="', 'class="tex2jax_ignore ')
- else:
- table_attr += ' class="tex2jax_ignore"'
- return dict(
- head=head,
- cellstyle=cellstyle,
- body=body,
- uuid=uuid,
- precision=precision,
- table_styles=table_styles,
- caption=caption,
- table_attributes=table_attr,
- )
- def format(self, formatter, subset=None, na_rep: Optional[str] = None):
- """
- Format the text display value of cells.
- Parameters
- ----------
- formatter : str, callable, dict or None
- If ``formatter`` is None, the default formatter is used
- subset : IndexSlice
- An argument to ``DataFrame.loc`` that restricts which elements
- ``formatter`` is applied to.
- na_rep : str, optional
- Representation for missing values.
- If ``na_rep`` is None, no special formatting is applied
- .. versionadded:: 1.0.0
- Returns
- -------
- self : Styler
- Notes
- -----
- ``formatter`` is either an ``a`` or a dict ``{column name: a}`` where
- ``a`` is one of
- - str: this will be wrapped in: ``a.format(x)``
- - callable: called with the value of an individual cell
- The default display value for numeric values is the "general" (``g``)
- format with ``pd.options.display.precision`` precision.
- Examples
- --------
- >>> df = pd.DataFrame(np.random.randn(4, 2), columns=['a', 'b'])
- >>> df.style.format("{:.2%}")
- >>> df['c'] = ['a', 'b', 'c', 'd']
- >>> df.style.format({'c': str.upper})
- """
- if formatter is None:
- assert self._display_funcs.default_factory is not None
- formatter = self._display_funcs.default_factory()
- if subset is None:
- row_locs = range(len(self.data))
- col_locs = range(len(self.data.columns))
- else:
- subset = _non_reducing_slice(subset)
- if len(subset) == 1:
- subset = subset, self.data.columns
- sub_df = self.data.loc[subset]
- row_locs = self.data.index.get_indexer_for(sub_df.index)
- col_locs = self.data.columns.get_indexer_for(sub_df.columns)
- if is_dict_like(formatter):
- for col, col_formatter in formatter.items():
- # formatter must be callable, so '{}' are converted to lambdas
- col_formatter = _maybe_wrap_formatter(col_formatter, na_rep)
- col_num = self.data.columns.get_indexer_for([col])[0]
- for row_num in row_locs:
- self._display_funcs[(row_num, col_num)] = col_formatter
- else:
- # single scalar to format all cells with
- formatter = _maybe_wrap_formatter(formatter, na_rep)
- locs = product(*(row_locs, col_locs))
- for i, j in locs:
- self._display_funcs[(i, j)] = formatter
- return self
- def render(self, **kwargs):
- """
- Render the built up styles to HTML.
- Parameters
- ----------
- **kwargs
- Any additional keyword arguments are passed
- through to ``self.template.render``.
- This is useful when you need to provide
- additional variables for a custom template.
- Returns
- -------
- rendered : str
- The rendered HTML.
- Notes
- -----
- ``Styler`` objects have defined the ``_repr_html_`` method
- which automatically calls ``self.render()`` when it's the
- last item in a Notebook cell. When calling ``Styler.render()``
- directly, wrap the result in ``IPython.display.HTML`` to view
- the rendered HTML in the notebook.
- Pandas uses the following keys in render. Arguments passed
- in ``**kwargs`` take precedence, so think carefully if you want
- to override them:
- * head
- * cellstyle
- * body
- * uuid
- * precision
- * table_styles
- * caption
- * table_attributes
- """
- self._compute()
- # TODO: namespace all the pandas keys
- d = self._translate()
- # filter out empty styles, every cell will have a class
- # but the list of props may just be [['', '']].
- # so we have the neested anys below
- trimmed = [x for x in d["cellstyle"] if any(any(y) for y in x["props"])]
- d["cellstyle"] = trimmed
- d.update(kwargs)
- return self.template.render(**d)
- def _update_ctx(self, attrs):
- """
- Update the state of the Styler.
- Collects a mapping of {index_label: ['<property>: <value>']}.
- attrs : Series or DataFrame
- should contain strings of '<property>: <value>;<prop2>: <val2>'
- Whitespace shouldn't matter and the final trailing ';' shouldn't
- matter.
- """
- for row_label, v in attrs.iterrows():
- for col_label, col in v.items():
- i = self.index.get_indexer([row_label])[0]
- j = self.columns.get_indexer([col_label])[0]
- for pair in col.rstrip(";").split(";"):
- self.ctx[(i, j)].append(pair)
- def _copy(self, deepcopy=False):
- styler = Styler(
- self.data,
- precision=self.precision,
- caption=self.caption,
- uuid=self.uuid,
- table_styles=self.table_styles,
- na_rep=self.na_rep,
- )
- if deepcopy:
- styler.ctx = copy.deepcopy(self.ctx)
- styler._todo = copy.deepcopy(self._todo)
- else:
- styler.ctx = self.ctx
- styler._todo = self._todo
- return styler
- def __copy__(self):
- """
- Deep copy by default.
- """
- return self._copy(deepcopy=False)
- def __deepcopy__(self, memo):
- return self._copy(deepcopy=True)
- def clear(self):
- """
- Reset the styler, removing any previously applied styles.
- Returns None.
- """
- self.ctx.clear()
- self._todo = []
- def _compute(self):
- """
- Execute the style functions built up in `self._todo`.
- Relies on the conventions that all style functions go through
- .apply or .applymap. The append styles to apply as tuples of
- (application method, *args, **kwargs)
- """
- r = self
- for func, args, kwargs in self._todo:
- r = func(self)(*args, **kwargs)
- return r
- def _apply(self, func, axis=0, subset=None, **kwargs):
- subset = slice(None) if subset is None else subset
- subset = _non_reducing_slice(subset)
- data = self.data.loc[subset]
- if axis is not None:
- result = data.apply(func, axis=axis, result_type="expand", **kwargs)
- result.columns = data.columns
- else:
- result = func(data, **kwargs)
- if not isinstance(result, pd.DataFrame):
- raise TypeError(
- f"Function {repr(func)} must return a DataFrame when "
- f"passed to `Styler.apply` with axis=None"
- )
- if not (
- result.index.equals(data.index) and result.columns.equals(data.columns)
- ):
- raise ValueError(
- f"Result of {repr(func)} must have identical "
- f"index and columns as the input"
- )
- result_shape = result.shape
- expected_shape = self.data.loc[subset].shape
- if result_shape != expected_shape:
- raise ValueError(
- f"Function {repr(func)} returned the wrong shape.\n"
- f"Result has shape: {result.shape}\n"
- f"Expected shape: {expected_shape}"
- )
- self._update_ctx(result)
- return self
- def apply(self, func, axis=0, subset=None, **kwargs):
- """
- Apply a function column-wise, row-wise, or table-wise.
- Updates the HTML representation with the result.
- Parameters
- ----------
- func : function
- ``func`` should take a Series or DataFrame (depending
- on ``axis``), and return an object with the same shape.
- Must return a DataFrame with identical index and
- column labels when ``axis=None``.
- axis : {0 or 'index', 1 or 'columns', None}, default 0
- Apply to each column (``axis=0`` or ``'index'``), to each row
- (``axis=1`` or ``'columns'``), or to the entire DataFrame at once
- with ``axis=None``.
- subset : IndexSlice
- A valid indexer to limit ``data`` to *before* applying the
- function. Consider using a pandas.IndexSlice.
- **kwargs : dict
- Pass along to ``func``.
- Returns
- -------
- self : Styler
- Notes
- -----
- The output shape of ``func`` should match the input, i.e. if
- ``x`` is the input row, column, or table (depending on ``axis``),
- then ``func(x).shape == x.shape`` should be true.
- This is similar to ``DataFrame.apply``, except that ``axis=None``
- applies the function to the entire DataFrame at once,
- rather than column-wise or row-wise.
- Examples
- --------
- >>> def highlight_max(x):
- ... return ['background-color: yellow' if v == x.max() else ''
- for v in x]
- ...
- >>> df = pd.DataFrame(np.random.randn(5, 2))
- >>> df.style.apply(highlight_max)
- """
- self._todo.append(
- (lambda instance: getattr(instance, "_apply"), (func, axis, subset), kwargs)
- )
- return self
- def _applymap(self, func, subset=None, **kwargs):
- func = partial(func, **kwargs) # applymap doesn't take kwargs?
- if subset is None:
- subset = pd.IndexSlice[:]
- subset = _non_reducing_slice(subset)
- result = self.data.loc[subset].applymap(func)
- self._update_ctx(result)
- return self
- def applymap(self, func, subset=None, **kwargs):
- """
- Apply a function elementwise.
- Updates the HTML representation with the result.
- Parameters
- ----------
- func : function
- ``func`` should take a scalar and return a scalar.
- subset : IndexSlice
- A valid indexer to limit ``data`` to *before* applying the
- function. Consider using a pandas.IndexSlice.
- **kwargs : dict
- Pass along to ``func``.
- Returns
- -------
- self : Styler
- See Also
- --------
- Styler.where
- """
- self._todo.append(
- (lambda instance: getattr(instance, "_applymap"), (func, subset), kwargs)
- )
- return self
- def where(self, cond, value, other=None, subset=None, **kwargs):
- """
- Apply a function elementwise.
- Updates the HTML representation with a style which is
- selected in accordance with the return value of a function.
- .. versionadded:: 0.21.0
- Parameters
- ----------
- cond : callable
- ``cond`` should take a scalar and return a boolean.
- value : str
- Applied when ``cond`` returns true.
- other : str
- Applied when ``cond`` returns false.
- subset : IndexSlice
- A valid indexer to limit ``data`` to *before* applying the
- function. Consider using a pandas.IndexSlice.
- **kwargs : dict
- Pass along to ``cond``.
- Returns
- -------
- self : Styler
- See Also
- --------
- Styler.applymap
- """
- if other is None:
- other = ""
- return self.applymap(
- lambda val: value if cond(val) else other, subset=subset, **kwargs
- )
- def set_precision(self, precision):
- """
- Set the precision used to render.
- Parameters
- ----------
- precision : int
- Returns
- -------
- self : Styler
- """
- self.precision = precision
- return self
- def set_table_attributes(self, attributes):
- """
- Set the table attributes.
- These are the items that show up in the opening ``<table>`` tag
- in addition to to automatic (by default) id.
- Parameters
- ----------
- attributes : str
- Returns
- -------
- self : Styler
- Examples
- --------
- >>> df = pd.DataFrame(np.random.randn(10, 4))
- >>> df.style.set_table_attributes('class="pure-table"')
- # ... <table class="pure-table"> ...
- """
- self.table_attributes = attributes
- return self
- def export(self):
- """
- Export the styles to applied to the current Styler.
- Can be applied to a second style with ``Styler.use``.
- Returns
- -------
- styles : list
- See Also
- --------
- Styler.use
- """
- return self._todo
- def use(self, styles):
- """
- Set the styles on the current Styler.
- Possibly uses styles from ``Styler.export``.
- Parameters
- ----------
- styles : list
- List of style functions.
- Returns
- -------
- self : Styler
- See Also
- --------
- Styler.export
- """
- self._todo.extend(styles)
- return self
- def set_uuid(self, uuid):
- """
- Set the uuid for a Styler.
- Parameters
- ----------
- uuid : str
- Returns
- -------
- self : Styler
- """
- self.uuid = uuid
- return self
- def set_caption(self, caption):
- """
- Set the caption on a Styler.
- Parameters
- ----------
- caption : str
- Returns
- -------
- self : Styler
- """
- self.caption = caption
- return self
- def set_table_styles(self, table_styles):
- """
- Set the table styles on a Styler.
- These are placed in a ``<style>`` tag before the generated HTML table.
- Parameters
- ----------
- table_styles : list
- Each individual table_style should be a dictionary with
- ``selector`` and ``props`` keys. ``selector`` should be a CSS
- selector that the style will be applied to (automatically
- prefixed by the table's UUID) and ``props`` should be a list of
- tuples with ``(attribute, value)``.
- Returns
- -------
- self : Styler
- Examples
- --------
- >>> df = pd.DataFrame(np.random.randn(10, 4))
- >>> df.style.set_table_styles(
- ... [{'selector': 'tr:hover',
- ... 'props': [('background-color', 'yellow')]}]
- ... )
- """
- self.table_styles = table_styles
- return self
- def set_na_rep(self, na_rep: str) -> "Styler":
- """
- Set the missing data representation on a Styler.
- .. versionadded:: 1.0.0
- Parameters
- ----------
- na_rep : str
- Returns
- -------
- self : Styler
- """
- self.na_rep = na_rep
- return self
- def hide_index(self):
- """
- Hide any indices from rendering.
- .. versionadded:: 0.23.0
- Returns
- -------
- self : Styler
- """
- self.hidden_index = True
- return self
- def hide_columns(self, subset):
- """
- Hide columns from rendering.
- .. versionadded:: 0.23.0
- Parameters
- ----------
- subset : IndexSlice
- An argument to ``DataFrame.loc`` that identifies which columns
- are hidden.
- Returns
- -------
- self : Styler
- """
- subset = _non_reducing_slice(subset)
- hidden_df = self.data.loc[subset]
- self.hidden_columns = self.columns.get_indexer_for(hidden_df.columns)
- return self
- # -----------------------------------------------------------------------
- # A collection of "builtin" styles
- # -----------------------------------------------------------------------
- @staticmethod
- def _highlight_null(v, null_color):
- return f"background-color: {null_color}" if pd.isna(v) else ""
- def highlight_null(self, null_color="red"):
- """
- Shade the background ``null_color`` for missing values.
- Parameters
- ----------
- null_color : str
- Returns
- -------
- self : Styler
- """
- self.applymap(self._highlight_null, null_color=null_color)
- return self
- def background_gradient(
- self,
- cmap="PuBu",
- low=0,
- high=0,
- axis=0,
- subset=None,
- text_color_threshold=0.408,
- vmin: Optional[float] = None,
- vmax: Optional[float] = None,
- ):
- """
- Color the background in a gradient style.
- The background color is determined according
- to the data in each column (optionally row). Requires matplotlib.
- Parameters
- ----------
- cmap : str or colormap
- Matplotlib colormap.
- low : float
- Compress the range by the low.
- high : float
- Compress the range by the high.
- axis : {0 or 'index', 1 or 'columns', None}, default 0
- Apply to each column (``axis=0`` or ``'index'``), to each row
- (``axis=1`` or ``'columns'``), or to the entire DataFrame at once
- with ``axis=None``.
- subset : IndexSlice
- A valid slice for ``data`` to limit the style application to.
- text_color_threshold : float or int
- Luminance threshold for determining text color. Facilitates text
- visibility across varying background colors. From 0 to 1.
- 0 = all text is dark colored, 1 = all text is light colored.
- .. versionadded:: 0.24.0
- vmin : float, optional
- Minimum data value that corresponds to colormap minimum value.
- When None (default): the minimum value of the data will be used.
- .. versionadded:: 1.0.0
- vmax : float, optional
- Maximum data value that corresponds to colormap maximum value.
- When None (default): the maximum value of the data will be used.
- .. versionadded:: 1.0.0
- Returns
- -------
- self : Styler
- Raises
- ------
- ValueError
- If ``text_color_threshold`` is not a value from 0 to 1.
- Notes
- -----
- Set ``text_color_threshold`` or tune ``low`` and ``high`` to keep the
- text legible by not using the entire range of the color map. The range
- of the data is extended by ``low * (x.max() - x.min())`` and ``high *
- (x.max() - x.min())`` before normalizing.
- """
- subset = _maybe_numeric_slice(self.data, subset)
- subset = _non_reducing_slice(subset)
- self.apply(
- self._background_gradient,
- cmap=cmap,
- subset=subset,
- axis=axis,
- low=low,
- high=high,
- text_color_threshold=text_color_threshold,
- vmin=vmin,
- vmax=vmax,
- )
- return self
- @staticmethod
- def _background_gradient(
- s,
- cmap="PuBu",
- low=0,
- high=0,
- text_color_threshold=0.408,
- vmin: Optional[float] = None,
- vmax: Optional[float] = None,
- ):
- """
- Color background in a range according to the data.
- """
- if (
- not isinstance(text_color_threshold, (float, int))
- or not 0 <= text_color_threshold <= 1
- ):
- msg = "`text_color_threshold` must be a value from 0 to 1."
- raise ValueError(msg)
- with _mpl(Styler.background_gradient) as (plt, colors):
- smin = np.nanmin(s.to_numpy()) if vmin is None else vmin
- smax = np.nanmax(s.to_numpy()) if vmax is None else vmax
- rng = smax - smin
- # extend lower / upper bounds, compresses color range
- norm = colors.Normalize(smin - (rng * low), smax + (rng * high))
- # matplotlib colors.Normalize modifies inplace?
- # https://github.com/matplotlib/matplotlib/issues/5427
- rgbas = plt.cm.get_cmap(cmap)(norm(s.to_numpy(dtype=float)))
- def relative_luminance(rgba):
- """
- Calculate relative luminance of a color.
- The calculation adheres to the W3C standards
- (https://www.w3.org/WAI/GL/wiki/Relative_luminance)
- Parameters
- ----------
- color : rgb or rgba tuple
- Returns
- -------
- float
- The relative luminance as a value from 0 to 1
- """
- r, g, b = (
- x / 12.92 if x <= 0.03928 else ((x + 0.055) / 1.055 ** 2.4)
- for x in rgba[:3]
- )
- return 0.2126 * r + 0.7152 * g + 0.0722 * b
- def css(rgba):
- dark = relative_luminance(rgba) < text_color_threshold
- text_color = "#f1f1f1" if dark else "#000000"
- return f"background-color: {colors.rgb2hex(rgba)};color: {text_color};"
- if s.ndim == 1:
- return [css(rgba) for rgba in rgbas]
- else:
- return pd.DataFrame(
- [[css(rgba) for rgba in row] for row in rgbas],
- index=s.index,
- columns=s.columns,
- )
- def set_properties(self, subset=None, **kwargs):
- """
- Method to set one or more non-data dependent properties or each cell.
- Parameters
- ----------
- subset : IndexSlice
- A valid slice for ``data`` to limit the style application to.
- **kwargs : dict
- A dictionary of property, value pairs to be set for each cell.
- Returns
- -------
- self : Styler
- Examples
- --------
- >>> df = pd.DataFrame(np.random.randn(10, 4))
- >>> df.style.set_properties(color="white", align="right")
- >>> df.style.set_properties(**{'background-color': 'yellow'})
- """
- values = ";".join(f"{p}: {v}" for p, v in kwargs.items())
- f = lambda x: values
- return self.applymap(f, subset=subset)
- @staticmethod
- def _bar(s, align, colors, width=100, vmin=None, vmax=None):
- """
- Draw bar chart in dataframe cells.
- """
- # Get input value range.
- smin = np.nanmin(s.to_numpy()) if vmin is None else vmin
- smax = np.nanmax(s.to_numpy()) if vmax is None else vmax
- if align == "mid":
- smin = min(0, smin)
- smax = max(0, smax)
- elif align == "zero":
- # For "zero" mode, we want the range to be symmetrical around zero.
- smax = max(abs(smin), abs(smax))
- smin = -smax
- # Transform to percent-range of linear-gradient
- normed = width * (s.to_numpy(dtype=float) - smin) / (smax - smin + 1e-12)
- zero = -width * smin / (smax - smin + 1e-12)
- def css_bar(start, end, color):
- """
- Generate CSS code to draw a bar from start to end.
- """
- css = "width: 10em; height: 80%;"
- if end > start:
- css += "background: linear-gradient(90deg,"
- if start > 0:
- css += f" transparent {start:.1f}%, {color} {start:.1f}%, "
- e = min(end, width)
- css += f"{color} {e:.1f}%, transparent {e:.1f}%)"
- return css
- def css(x):
- if pd.isna(x):
- return ""
- # avoid deprecated indexing `colors[x > zero]`
- color = colors[1] if x > zero else colors[0]
- if align == "left":
- return css_bar(0, x, color)
- else:
- return css_bar(min(x, zero), max(x, zero), color)
- if s.ndim == 1:
- return [css(x) for x in normed]
- else:
- return pd.DataFrame(
- [[css(x) for x in row] for row in normed],
- index=s.index,
- columns=s.columns,
- )
- def bar(
- self,
- subset=None,
- axis=0,
- color="#d65f5f",
- width=100,
- align="left",
- vmin=None,
- vmax=None,
- ):
- """
- Draw bar chart in the cell backgrounds.
- Parameters
- ----------
- subset : IndexSlice, optional
- A valid slice for `data` to limit the style application to.
- axis : {0 or 'index', 1 or 'columns', None}, default 0
- Apply to each column (``axis=0`` or ``'index'``), to each row
- (``axis=1`` or ``'columns'``), or to the entire DataFrame at once
- with ``axis=None``.
- color : str or 2-tuple/list
- If a str is passed, the color is the same for both
- negative and positive numbers. If 2-tuple/list is used, the
- first element is the color_negative and the second is the
- color_positive (eg: ['#d65f5f', '#5fba7d']).
- width : float, default 100
- A number between 0 or 100. The largest value will cover `width`
- percent of the cell's width.
- align : {'left', 'zero',' mid'}, default 'left'
- How to align the bars with the cells.
- - 'left' : the min value starts at the left of the cell.
- - 'zero' : a value of zero is located at the center of the cell.
- - 'mid' : the center of the cell is at (max-min)/2, or
- if values are all negative (positive) the zero is aligned
- at the right (left) of the cell.
- vmin : float, optional
- Minimum bar value, defining the left hand limit
- of the bar drawing range, lower values are clipped to `vmin`.
- When None (default): the minimum value of the data will be used.
- .. versionadded:: 0.24.0
- vmax : float, optional
- Maximum bar value, defining the right hand limit
- of the bar drawing range, higher values are clipped to `vmax`.
- When None (default): the maximum value of the data will be used.
- .. versionadded:: 0.24.0
- Returns
- -------
- self : Styler
- """
- if align not in ("left", "zero", "mid"):
- raise ValueError("`align` must be one of {'left', 'zero',' mid'}")
- if not (is_list_like(color)):
- color = [color, color]
- elif len(color) == 1:
- color = [color[0], color[0]]
- elif len(color) > 2:
- raise ValueError(
- "`color` must be string or a list-like "
- "of length 2: [`color_neg`, `color_pos`] "
- "(eg: color=['#d65f5f', '#5fba7d'])"
- )
- subset = _maybe_numeric_slice(self.data, subset)
- subset = _non_reducing_slice(subset)
- self.apply(
- self._bar,
- subset=subset,
- axis=axis,
- align=align,
- colors=color,
- width=width,
- vmin=vmin,
- vmax=vmax,
- )
- return self
- def highlight_max(self, subset=None, color="yellow", axis=0):
- """
- Highlight the maximum by shading the background.
- Parameters
- ----------
- subset : IndexSlice, default None
- A valid slice for ``data`` to limit the style application to.
- color : str, default 'yellow'
- axis : {0 or 'index', 1 or 'columns', None}, default 0
- Apply to each column (``axis=0`` or ``'index'``), to each row
- (``axis=1`` or ``'columns'``), or to the entire DataFrame at once
- with ``axis=None``.
- Returns
- -------
- self : Styler
- """
- return self._highlight_handler(subset=subset, color=color, axis=axis, max_=True)
- def highlight_min(self, subset=None, color="yellow", axis=0):
- """
- Highlight the minimum by shading the background.
- Parameters
- ----------
- subset : IndexSlice, default None
- A valid slice for ``data`` to limit the style application to.
- color : str, default 'yellow'
- axis : {0 or 'index', 1 or 'columns', None}, default 0
- Apply to each column (``axis=0`` or ``'index'``), to each row
- (``axis=1`` or ``'columns'``), or to the entire DataFrame at once
- with ``axis=None``.
- Returns
- -------
- self : Styler
- """
- return self._highlight_handler(
- subset=subset, color=color, axis=axis, max_=False
- )
- def _highlight_handler(self, subset=None, color="yellow", axis=None, max_=True):
- subset = _non_reducing_slice(_maybe_numeric_slice(self.data, subset))
- self.apply(
- self._highlight_extrema, color=color, axis=axis, subset=subset, max_=max_
- )
- return self
- @staticmethod
- def _highlight_extrema(data, color="yellow", max_=True):
- """
- Highlight the min or max in a Series or DataFrame.
- """
- attr = f"background-color: {color}"
- if max_:
- extrema = data == np.nanmax(data.to_numpy())
- else:
- extrema = data == np.nanmin(data.to_numpy())
- if data.ndim == 1: # Series from .apply
- return [attr if v else "" for v in extrema]
- else: # DataFrame from .tee
- return pd.DataFrame(
- np.where(extrema, attr, ""), index=data.index, columns=data.columns
- )
- @classmethod
- def from_custom_template(cls, searchpath, name):
- """
- Factory function for creating a subclass of ``Styler``.
- Uses a custom template and Jinja environment.
- Parameters
- ----------
- searchpath : str or list
- Path or paths of directories containing the templates.
- name : str
- Name of your custom template to use for rendering.
- Returns
- -------
- MyStyler : subclass of Styler
- Has the correct ``env`` and ``template`` class attributes set.
- """
- loader = jinja2.ChoiceLoader([jinja2.FileSystemLoader(searchpath), cls.loader])
- class MyStyler(cls):
- env = jinja2.Environment(loader=loader)
- template = env.get_template(name)
- return MyStyler
- def pipe(self, func, *args, **kwargs):
- """
- Apply ``func(self, *args, **kwargs)``, and return the result.
- .. versionadded:: 0.24.0
- Parameters
- ----------
- func : function
- Function to apply to the Styler. Alternatively, a
- ``(callable, keyword)`` tuple where ``keyword`` is a string
- indicating the keyword of ``callable`` that expects the Styler.
- *args : optional
- Arguments passed to `func`.
- **kwargs : optional
- A dictionary of keyword arguments passed into ``func``.
- Returns
- -------
- object :
- The value returned by ``func``.
- See Also
- --------
- DataFrame.pipe : Analogous method for DataFrame.
- Styler.apply : Apply a function row-wise, column-wise, or table-wise to
- modify the dataframe's styling.
- Notes
- -----
- Like :meth:`DataFrame.pipe`, this method can simplify the
- application of several user-defined functions to a styler. Instead
- of writing:
- .. code-block:: python
- f(g(df.style.set_precision(3), arg1=a), arg2=b, arg3=c)
- users can write:
- .. code-block:: python
- (df.style.set_precision(3)
- .pipe(g, arg1=a)
- .pipe(f, arg2=b, arg3=c))
- In particular, this allows users to define functions that take a
- styler object, along with other parameters, and return the styler after
- making styling changes (such as calling :meth:`Styler.apply` or
- :meth:`Styler.set_properties`). Using ``.pipe``, these user-defined
- style "transformations" can be interleaved with calls to the built-in
- Styler interface.
- Examples
- --------
- >>> def format_conversion(styler):
- ... return (styler.set_properties(**{'text-align': 'right'})
- ... .format({'conversion': '{:.1%}'}))
- The user-defined ``format_conversion`` function above can be called
- within a sequence of other style modifications:
- >>> df = pd.DataFrame({'trial': list(range(5)),
- ... 'conversion': [0.75, 0.85, np.nan, 0.7, 0.72]})
- >>> (df.style
- ... .highlight_min(subset=['conversion'], color='yellow')
- ... .pipe(format_conversion)
- ... .set_caption("Results with minimum conversion highlighted."))
- """
- return com.pipe(self, func, *args, **kwargs)
- def _is_visible(idx_row, idx_col, lengths):
- """
- Index -> {(idx_row, idx_col): bool}).
- """
- return (idx_col, idx_row) in lengths
- def _get_level_lengths(index, hidden_elements=None):
- """
- Given an index, find the level length for each element.
- Optional argument is a list of index positions which
- should not be visible.
- Result is a dictionary of (level, initial_position): span
- """
- levels = index.format(sparsify=lib.no_default, adjoin=False, names=False)
- if hidden_elements is None:
- hidden_elements = []
- lengths = {}
- if index.nlevels == 1:
- for i, value in enumerate(levels):
- if i not in hidden_elements:
- lengths[(0, i)] = 1
- return lengths
- for i, lvl in enumerate(levels):
- for j, row in enumerate(lvl):
- if not get_option("display.multi_sparse"):
- lengths[(i, j)] = 1
- elif (row is not lib.no_default) and (j not in hidden_elements):
- last_label = j
- lengths[(i, last_label)] = 1
- elif row is not lib.no_default:
- # even if its hidden, keep track of it in case
- # length >1 and later elements are visible
- last_label = j
- lengths[(i, last_label)] = 0
- elif j not in hidden_elements:
- lengths[(i, last_label)] += 1
- non_zero_lengths = {
- element: length for element, length in lengths.items() if length >= 1
- }
- return non_zero_lengths
- def _maybe_wrap_formatter(formatter, na_rep: Optional[str]):
- if isinstance(formatter, str):
- formatter_func = lambda x: formatter.format(x)
- elif callable(formatter):
- formatter_func = formatter
- else:
- msg = f"Expected a template string or callable, got {formatter} instead"
- raise TypeError(msg)
- if na_rep is None:
- return formatter_func
- elif isinstance(na_rep, str):
- return lambda x: na_rep if pd.isna(x) else formatter_func(x)
- else:
- msg = f"Expected a string, got {na_rep} instead"
- raise TypeError(msg)
|