Jupyter NotebookでpandasのMultiIndexに色を付ける

この記事は [ACCESS Advent Calendar 2018](https://qiita.com/advent-calendar/2018/access) の5日目の記事です。

# はじめに

Jupyter NotebookでpandasのDataFrameを表示する際に、CSSでスタイルを設定して色付けすることができます。

本記事では、[公式ドキュメント](https://pandas.pydata.org/pandas-docs/stable/style.html) にない例として、MultiIndex構造のDataFrameに特定の条件で色を付けて表示する方法を紹介します。

# データの準備

これから色付けしていくベースとなるMultiIndex構造のDataFrameは、pandasのテスト用データセット`tips.csv`をクロス集計した表を使います。

```python
import pandas as pd

tips = pd.read_csv('https://raw.github.com/pandas-dev/pandas/master/pandas/tests/data/tips.csv')
cross = pd.crosstab([tips.sex, tips.smoker], [tips.day, tips.time])
cross
```

<img width="500" alt="cross_base.png" src="https://qiita-image-store.s3.amazonaws.com/0/54161/214db298-24fe-1f95-0d35-ecc1e90f95be.png">

以降では、このクロス集計表を元に特定の条件で色付けする例を紹介します。

# 特定の行全体

```python
def highlight(df):
  # 返り値は、スタイルで埋めたDataFrameです。引数のDataFrameと構造は同じなので`df`をコピーします。
  styles = df.copy()

  # DataFrame全体をスタイル無しで初期化します。
  styles.loc[:,:] = ''

  # `loc[(行の1層目, 行の2層目), (列の1層目, 列の2層目)]`の形式で、スタイルを設定する位置を選択します。
  styles.loc[('Male', 'No'), :] = 'background-color: lightgreen'

  return styles

# `apply`の第一引数の関数`highlight`でスタイルを設定します。
# `axis=None`にすることで、適用元のDataFrame全体が引数として渡されます。
cross.style.apply(highlight, axis=None)
```

<img width="500" alt="cross_highlight_row.png" src="https://qiita-image-store.s3.amazonaws.com/0/54161/60b003f0-bae0-d6f6-816f-d13b54af6444.png">

# 特定の列全体

```python
def highlight(df):
  styles = df.copy()
  styles.loc[:,:] = ''
  styles.loc[:, ('Sat', 'Dinner')] = 'background-color: lightskyblue'
  return styles

cross.style.apply(highlight, axis=None)
```

<img width="500" alt="cross_highlight_column.png" src="https://qiita-image-store.s3.amazonaws.com/0/54161/63e19455-ac83-9471-5d55-3bfd44b33faf.png">

# 特定の要素（行と列を指定）

```python
def highlight(df):
  styles = df.copy()
  styles.loc[:,:] = ''
  styles.loc[('Male', 'No'), ('Sat', 'Dinner')] = 'background-color: orange'
  return styles

cross.style.apply(highlight, axis=None)
```

<img width="500" alt="cross_highlight_cell.png" src="https://qiita-image-store.s3.amazonaws.com/0/54161/cb8eb5cb-c45f-351b-d64f-3bb327ab886e.png">

# 各行の中の最大値

```python
def highlight(s):
    # `is_max`は、最大の要素が`True`のSeriesです。
    # 例えば1行目は`[False, False, False, False, False, True]`になります。
    is_max = s >= s.max()
    styles = is_max.map(lambda x: 'background-color: mediumslateblue; color: white' if x else '')
    return styles

# `axis=columns`にすることで、列方向に各行に`highlight`を適用します。
# 各行はSeries型です。
cross.style.apply(highlight, axis='columns')
```

<img width="500" alt="cross_highlight_row_max.png" src="https://qiita-image-store.s3.amazonaws.com/0/54161/40ea8926-8a91-3c3b-6cd8-99b0001aa8ce.png">

# 各列の中の最大値

```python
# `axis=index`にすることで、行方向に各列に`highlight`を適用します。
# 各列はSeries型です。
cross.style.apply(highlight, axis='index')
```

<img width="500" alt="cross_highlight_column_max.png" src="https://qiita-image-store.s3.amazonaws.com/0/54161/a7388744-6dcb-ab4f-4cd7-923142babbbe.png">


# 行同士で比較

行`('Female', 'No')`と行`('Female', 'Yes')`を比較して、大きい方を色付けする例です。

```python
def highlight(s):
    colors = [None] * len(s)

    if s[('Female', 'No')] > s[('Female', 'Yes')]:
        # s.index.get_locで行番号を取得してスタイルを設定します。
        loc = s.index.get_loc(('Female', 'No'))
        colors[loc] = 'yellow'
    elif s[('Female', 'No')] < s[('Female', 'Yes')]:
        loc = s.index.get_loc(('Female', 'Yes'))
        colors[loc] = 'tomato'

    return ['' if c is None else 'background-color: {}'.format(c) for c in colors]

cross.style.apply(highlight, axis='index')
```

<img width="500" alt="cross_highlight_compare.png" src="https://qiita-image-store.s3.amazonaws.com/0/54161/b727a646-fc46-f93a-5dc1-03e2ffc8bac9.png">

---

明日は @shotasakamoto さんです。お楽しみに！