Skip to content

ENH: Add Styler.where #17474

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Sep 11, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions doc/source/api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -2062,6 +2062,7 @@ Style Application

Styler.apply
Styler.applymap
Styler.where
Styler.format
Styler.set_precision
Styler.set_table_styles
Expand Down
1 change: 1 addition & 0 deletions doc/source/whatsnew/v0.21.0.txt
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@ Other Enhancements
- `read_*` methods can now infer compression from non-string paths, such as ``pathlib.Path`` objects (:issue:`17206`).
- :func:`pd.read_sas()` now recognizes much more of the most frequently used date (datetime) formats in SAS7BDAT files (:issue:`15871`).
- :func:`DataFrame.items` and :func:`Series.items` is now present in both Python 2 and 3 and is lazy in all cases (:issue:`13918`, :issue:`17213`)
- :func:`Styler.where` has been implemented. It is as a convenience for :func:`Styler.applymap` and enables simple DataFrame styling on the Jupyter notebook (:issue:`17474`).



Expand Down
42 changes: 42 additions & 0 deletions pandas/io/formats/style.py
Original file line number Diff line number Diff line change
Expand Up @@ -618,11 +618,53 @@ def applymap(self, func, subset=None, **kwargs):
-------
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, updating 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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can you add a See Also here (pointing to .applymap) and the reciprical one there

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.
Expand Down
58 changes: 58 additions & 0 deletions pandas/tests/io/formats/test_style.py
Original file line number Diff line number Diff line change
Expand Up @@ -265,6 +265,64 @@ def f(x):
col in self.df.loc[slice_].columns)
assert result == expected

def test_where_with_one_style(self):
# GH 17474
def f(x):
return x > 0.5

style1 = 'foo: bar'

result = self.df.style.where(f, style1)._compute().ctx
expected = dict(((r, c),
[style1 if f(self.df.loc[row, col]) else ''])
for r, row in enumerate(self.df.index)
for c, col in enumerate(self.df.columns))
assert result == expected

def test_where_subset(self):
# GH 17474
def f(x):
return x > 0.5

style1 = 'foo: bar'
style2 = 'baz: foo'

slices = [pd.IndexSlice[:], pd.IndexSlice[:, ['A']],
pd.IndexSlice[[1], :], pd.IndexSlice[[1], ['A']],
pd.IndexSlice[:2, ['A', 'B']]]

for slice_ in slices:
result = self.df.style.where(f, style1, style2,
subset=slice_)._compute().ctx
expected = dict(((r, c),
[style1 if f(self.df.loc[row, col]) else style2])
for r, row in enumerate(self.df.index)
for c, col in enumerate(self.df.columns)
if row in self.df.loc[slice_].index and
col in self.df.loc[slice_].columns)
assert result == expected

def test_where_subset_compare_with_applymap(self):
# GH 17474
def f(x):
return x > 0.5

style1 = 'foo: bar'
style2 = 'baz: foo'

def g(x):
return style1 if f(x) else style2

slices = [pd.IndexSlice[:], pd.IndexSlice[:, ['A']],
pd.IndexSlice[[1], :], pd.IndexSlice[[1], ['A']],
pd.IndexSlice[:2, ['A', 'B']]]

for slice_ in slices:
result = self.df.style.where(f, style1, style2,
subset=slice_)._compute().ctx
expected = self.df.style.applymap(g, subset=slice_)._compute().ctx
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These tests are fine, but could you add one test that checks the generated values directly against the dictionary?

assert result == expected

def test_empty(self):
df = pd.DataFrame({'A': [1, 0]})
s = df.style
Expand Down