Repository: pandas-dev/pandas

Issue:
DataFrame.style.applymap: subset kwarg fails with MultiIndexSlice 
#### Code Sample, a copy-pastable example if possible

```python
import pandas as pd


def color_negative_red(val):
    """
    Takes a scalar and returns a string with
    the css property `'color: red'` for negative
    strings, black otherwise.
    """
    color = 'red' if val < 0 else 'black'
    return 'color: %s' % color


dic = {('a', 'd'): [-1.12, 2.11],
       ('a', 'c'): [2.78, -2.88],
       ('b', 'c'): [-3.99, 3.77],
       ('b', 'd'): [4.21, -1.22],
}

idx = pd.IndexSlice
df = pd.DataFrame(dic, index=[0, 1])

html = (
        df.style
        # .applymap(color_negative_red)
        .applymap(color_negative_red, subset=idx[:, idx['b', 'd']])
        .render()
    )


```
#### Problem description

The subset keyword argument does not work with a MultiIndex IndexSlice.  The relevant documentation says "For row and column slicing, any valid indexer to .loc will work" ([source](https://pandas.pydata.org/pandas-docs/stable/style.html#Finer-Control:-Slicing)).

#### Actual Output

```
Traceback (most recent call last):
  File "<stdin>", line 5, in <module>
  File "c:\Users\user\AppData\Local\Continuum\Miniconda3\envs\lg\lib\site-packages\pandas\io\formats\style.py", line 434, in render
    self._compute()
  File "c:\Users\user\AppData\Local\Continuum\Miniconda3\envs\lg\lib\site-packages\pandas\io\formats\style.py", line 502, in _compute
    r = func(self)(*args, **kwargs)
  File "c:\Users\user\AppData\Local\Continuum\Miniconda3\envs\lg\lib\site-packages\pandas\io\formats\style.py", line 591, in _applymap
    result = self.data.loc[subset].applymap(func)
  File "c:\Users\user\AppData\Local\Continuum\Miniconda3\envs\lg\lib\site-packages\pandas\core\generic.py", line 3081, in __getattr__
    return object.__getattribute__(self, name)
AttributeError: 'Series' object has no attribute 'applymap'
```

#### Expected Output

`color_negative_red` applied to the cell(s) corresponding to the MultiIndex IndexSlice.

#### Output of ``pd.show_versions()``

<details>

[paste the output of ``pd.show_versions()`` here below this line]

INSTALLED VERSIONS
------------------
commit: None
python: 3.6.3.final.0
python-bits: 64
OS: Windows
OS-release: 7
machine: AMD64
processor: Intel64 Family 6 Model 78 Stepping 3, GenuineIntel
byteorder: little
LC_ALL: None
LANG: None
LOCALE: None.None
pandas: 0.22.0
pytest: 3.3.2
pip: 9.0.1
setuptools: 38.4.0
Cython: None
numpy: 1.14.0
scipy: 1.0.0
pyarrow: 0.8.0
xarray: None
IPython: None
sphinx: 1.6.6
patsy: None
dateutil: 2.6.1
pytz: 2017.3
blosc: None
bottleneck: None
tables: None
numexpr: None
feather: 0.4.0
matplotlib: None
openpyxl: None
xlrd: None
xlwt: None
xlsxwriter: None
lxml: None
bs4: None
html5lib: None
sqlalchemy: None
pymysql: None
psycopg2: None
jinja2: 2.10
s3fs: None
fastparquet: None
pandas_gbq: None
pandas_datareader: None

</details>
DataFrame.style.applymap: subset kwarg fails with MultiIndexSlice 
#### Code Sample, a copy-pastable example if possible

```python
import pandas as pd


def color_negative_red(val):
    """
    Takes a scalar and returns a string with
    the css property `'color: red'` for negative
    strings, black otherwise.
    """
    color = 'red' if val < 0 else 'black'
    return 'color: %s' % color


dic = {('a', 'd'): [-1.12, 2.11],
       ('a', 'c'): [2.78, -2.88],
       ('b', 'c'): [-3.99, 3.77],
       ('b', 'd'): [4.21, -1.22],
}

idx = pd.IndexSlice
df = pd.DataFrame(dic, index=[0, 1])

html = (
        df.style
        # .applymap(color_negative_red)
        .applymap(color_negative_red, subset=idx[:, idx['b', 'd']])
        .render()
    )


```
#### Problem description

The subset keyword argument does not work with a MultiIndex IndexSlice.  The relevant documentation says "For row and column slicing, any valid indexer to .loc will work" ([source](https://pandas.pydata.org/pandas-docs/stable/style.html#Finer-Control:-Slicing)).

#### Actual Output

```
Traceback (most recent call last):
  File "<stdin>", line 5, in <module>
  File "c:\Users\user\AppData\Local\Continuum\Miniconda3\envs\lg\lib\site-packages\pandas\io\formats\style.py", line 434, in render
    self._compute()
  File "c:\Users\user\AppData\Local\Continuum\Miniconda3\envs\lg\lib\site-packages\pandas\io\formats\style.py", line 502, in _compute
    r = func(self)(*args, **kwargs)
  File "c:\Users\user\AppData\Local\Continuum\Miniconda3\envs\lg\lib\site-packages\pandas\io\formats\style.py", line 591, in _applymap
    result = self.data.loc[subset].applymap(func)
  File "c:\Users\user\AppData\Local\Continuum\Miniconda3\envs\lg\lib\site-packages\pandas\core\generic.py", line 3081, in __getattr__
    return object.__getattribute__(self, name)
AttributeError: 'Series' object has no attribute 'applymap'
```

#### Expected Output

`color_negative_red` applied to the cell(s) corresponding to the MultiIndex IndexSlice.

#### Output of ``pd.show_versions()``

<details>

[paste the output of ``pd.show_versions()`` here below this line]

INSTALLED VERSIONS
------------------
commit: None
python: 3.6.3.final.0
python-bits: 64
OS: Windows
OS-release: 7
machine: AMD64
processor: Intel64 Family 6 Model 78 Stepping 3, GenuineIntel
byteorder: little
LC_ALL: None
LANG: None
LOCALE: None.None
pandas: 0.22.0
pytest: 3.3.2
pip: 9.0.1
setuptools: 38.4.0
Cython: None
numpy: 1.14.0
scipy: 1.0.0
pyarrow: 0.8.0
xarray: None
IPython: None
sphinx: 1.6.6
patsy: None
dateutil: 2.6.1
pytz: 2017.3
blosc: None
bottleneck: None
tables: None
numexpr: None
feather: 0.4.0
matplotlib: None
openpyxl: None
xlrd: None
xlwt: None
xlsxwriter: None
lxml: None
bs4: None
html5lib: None
sqlalchemy: None
pymysql: None
psycopg2: None
jinja2: 2.10
s3fs: None
fastparquet: None
pandas_gbq: None
pandas_datareader: None

</details>


Discussion:
Can you try changing your indexer to `subset=idx[:, [idx['b', 'd']]`? note the extra brackets around the column slice.

We could probably detect `_non_reducing_slice` to account for a MI in the columns. Want to take a look? https://github.com/pandas-dev/pandas/blob/01e99decf14b55409cea0789ffcc615afed45bac/pandas/core/indexing.py#L2156-L2162
Thanks for the prompt response.

> Can you try changing your indexer to subset=idx[:, [idx['b', 'd']]]? note the extra brackets around the column slice.

Yes, this works and will serve for now.

> We could probably detect _non_reducing_slice to account for a MI in the columns. Want to take a look? 

I can have a look at it, yes.
Can you try changing your indexer to `subset=idx[:, [idx['b', 'd']]`? note the extra brackets around the column slice.

We could probably detect `_non_reducing_slice` to account for a MI in the columns. Want to take a look? https://github.com/pandas-dev/pandas/blob/01e99decf14b55409cea0789ffcc615afed45bac/pandas/core/indexing.py#L2156-L2162
Thanks for the prompt response.

> Can you try changing your indexer to subset=idx[:, [idx['b', 'd']]]? note the extra brackets around the column slice.

Yes, this works and will serve for now.

> We could probably detect _non_reducing_slice to account for a MI in the columns. Want to take a look? 

I can have a look at it, yes.

Resolving patch:
diff --git a/pandas/core/indexing.py b/pandas/core/indexing.py
--- a/pandas/core/indexing.py
+++ b/pandas/core/indexing.py
@@ -2732,8 +2732,10 @@ def _non_reducing_slice(slice_):
         slice_ = IndexSlice[:, slice_]
 
     def pred(part):
-        # true when slice does *not* reduce
-        return isinstance(part, slice) or is_list_like(part)
+        # true when slice does *not* reduce, False when part is a tuple,
+        # i.e. MultiIndex slice
+        return ((isinstance(part, slice) or is_list_like(part))
+                and not isinstance(part, tuple))
 
     if not is_list_like(slice_):
         if not isinstance(slice_, slice):


Test patch:
diff --git a/pandas/tests/indexing/test_indexing.py b/pandas/tests/indexing/test_indexing.py
--- a/pandas/tests/indexing/test_indexing.py
+++ b/pandas/tests/indexing/test_indexing.py
@@ -812,6 +812,23 @@ def test_non_reducing_slice(self):
             tslice_ = _non_reducing_slice(slice_)
             assert isinstance(df.loc[tslice_], DataFrame)
 
+    def test_non_reducing_slice_on_multiindex(self):
+        # GH 19861
+        dic = {
+            ('a', 'd'): [1, 4],
+            ('a', 'c'): [2, 3],
+            ('b', 'c'): [3, 2],
+            ('b', 'd'): [4, 1]
+        }
+        df = pd.DataFrame(dic, index=[0, 1])
+        idx = pd.IndexSlice
+        slice_ = idx[:, idx['b', 'd']]
+        tslice_ = _non_reducing_slice(slice_)
+
+        result = df.loc[tslice_]
+        expected = pd.DataFrame({('b', 'd'): [4, 1]})
+        tm.assert_frame_equal(result, expected)
+
     def test_list_slice(self):
         # like dataframe getitem
         slices = [['A'], Series(['A']), np.array(['A'])]
diff --git a/pandas/tests/io/formats/test_style.py b/pandas/tests/io/formats/test_style.py
--- a/pandas/tests/io/formats/test_style.py
+++ b/pandas/tests/io/formats/test_style.py
@@ -274,6 +274,32 @@ def f(x):
                         col in self.df.loc[slice_].columns}
             assert result == expected
 
+    def test_applymap_subset_multiindex(self):
+        # GH 19861
+        # Smoke test for applymap
+        def color_negative_red(val):
+            """
+            Takes a scalar and returns a string with
+            the css property `'color: red'` for negative
+            strings, black otherwise.
+            """
+            color = 'red' if val < 0 else 'black'
+            return 'color: %s' % color
+
+        dic = {
+            ('a', 'd'): [-1.12, 2.11],
+            ('a', 'c'): [2.78, -2.88],
+            ('b', 'c'): [-3.99, 3.77],
+            ('b', 'd'): [4.21, -1.22],
+        }
+
+        idx = pd.IndexSlice
+        df = pd.DataFrame(dic, index=[0, 1])
+
+        (df.style
+         .applymap(color_negative_red, subset=idx[:, idx['b', 'd']])
+         .render())
+
     def test_where_with_one_style(self):
         # GH 17474
         def f(x):