matplotlibでよく使う手続き

# 動機
私は普段からmatplotlibを用いてグラフを描画しています。

作成できるグラフのクオリティには満足していますが、
頻繁に使う割に、必要なスクリプトが長い手続きがあります。

そういった手続きは、分析用の汎用モジュールとしてまとめて書いておくのが便利です。
この記事では汎用モジュールから幾つか抜粋してご紹介します。

汎用モジュールは以降plotTools.pyと表記します。
データの処理とグラフ描画はJupyter Notebookから実行することを想定しています。

# 0.プロット用データの準備
サンプルデータとして、Titanicを使います。
seabornにデータが同梱されています
（plotにはseabornのAPIは使いません）。

```python3:JupyterNotebook
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
%matplotlib inline

df = sns.load_dataset('titanic')
df['survived'] = df['survived'] == 1
```

# 1. 棒グラフのデータラベル表示
直接書くと意外と面倒なこの手続き。
下記のスクリプトを分析フォルダに置いておきます。

以下の部分を変更できるよう、引数として持ちます。

* ax : axisオブジェクト。指定がない場合はplt.gca()で取得する。
* formatter : データラベルの表示形式(数値を文字列に変換する関数。
    * ただしパーセンテージ表記は頻繁に使うため、単に'percent'とだけで指定できるようにしておく。
* orientation : 棒グラフの方向。垂直棒グラフなら'v', 水平棒グラフなら'h'とする。
    * axisオブジェクトからは垂直棒グラフなのか、水平棒グラフなのか判断できないため、指定が必要。
* stacked : 積上げ棒グラフかどうか。
* kind : 棒グラフかどうか。折れ線グラフに対しても適用できるようにしたいが、未実装。
* fontsize : データラベルの文字の大きさ。
* colors : フォントカラー。積上げ棒グラフのときは'auto'を指定すると、下地の輝度を計算し、黒字か白字かを自動判定する。
* weight : フォントスタイル。'bold'など。
* rotation : データラベルの角度。データ点が多く、データラベルが重なってしまう場合などに。
* total : 積上げ棒グラフのとき、合計値を表示するかどうか


```python3:plotTools.py
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt

def annotating_plots(ax = None, formatter = None, orientation = 'v', stacked = False, kind = 'bar', 
                     fontsize = 12, colors = None, weight='medium', rotation = 0, total=False):
    if ax is None:
        # axisが指定されていない場合、現在描画されているオブジェクトから取得する
        ax = plt.gca()
        
    if formatter is None:
        #デフォルトでは、数値をそのまま文字列として出力する
        formatter = lambda x : str(x)
        
    elif formatter == 'percent':
        # データラベルの表現が文字列でpercentと指定された場合、formatterを設定する
        formatter = lambda x : '{:.{}f}%'.format(x*100, 1)
        
        
    if colors is None:
        # 色が指定されていない場合、全て黒にする
        colors = ['black' for _ in range(len(ax.patches))]
        
    elif isinstance(colors, str):
        if colors != 'auto':
            # 色が文字列で指定されている場合、これを全てのデータラベルにブロードキャストする
            colors = [colors for _ in range(len(ax.patches))]
            
        else:
            #colors = 'auto'は、Stacked bar向けのオプション。rgbを輝度に変換し、0.5以下なら白字にする
            colors = ['white' if np.dot([0.3, 0.6, 0.1],p.get_facecolor()[:3]) <= 0.5 else 'black' for p in ax.patches]
        

    if kind == 'bar':
        # 棒グラフの場合
        if orientation == 'v':
            if not stacked:
                # Grouped bar
                for cnt, p in enumerate(ax.patches):
                    ax.annotate(formatter(p.get_height()),
                                (p.get_x() + p.get_width()/2, p.get_height()*1.005),
                                ha='center', 
                                va='bottom',
                                fontsize=fontsize,
                                color = colors[cnt],
                                weight = weight,
                                rotation = rotation
                                )

            else:
                # Stacked barはoffsetが必要なため、offsetデータ格納用配列を定義する
                length = pd.Series([p.get_x() for p in ax.patches]).nunique() #axisの長さ
                offset = np.zeros(length)
                for cnt, p in enumerate(ax.patches):
                    ax.annotate(formatter(p.get_height()),
                                (p.get_x() + p.get_width()/2, p.get_height()/2 + offset[cnt%length]),
                                horizontalalignment='center',
                                verticalalignment='center',
                                color = colors[cnt],
                                size = fontsize,
                                weight = weight,
                                rotation = rotation
                                )
                    offset[cnt%length] += p.get_height()
                if total:
                    # Stacked barに対し、合計値を表示
                    for idx, p in zip(range(length), ax.patches):
                        ax.annotate(formatter(offset[idx]), 
                                    (p.get_x() + p.get_width()/2, offset[idx]),
                                    horizontalalignment='center',
                                    verticalalignment='bottom',
                                    weight = weight,
                                    rotation = rotation
                                    )               

        elif orientation == 'h':
            if not stacked:
                for cnt, p in enumerate(ax.patches):
                    ax.annotate(formatter(p.get_width()),
                                (p.get_width(),p.get_y() + p.get_height()/2),
                                horizontalalignment='left',
                                verticalalignment='center',
                                fontsize=fontsize,
                                color = colors[cnt],
                                weight = weight,
                                rotation = rotation
                                )
                    
            else:
                # Stacked barはoffsetが必要なため、offsetデータ格納用配列を定義する
                length = pd.Series([p.get_y() for p in ax.patches]).nunique() #axisの長さ
                offset = np.zeros(length)
                for cnt, p in enumerate(ax.patches):
                    ax.annotate(formatter(p.get_width()),
                                (p.get_width()/2 + offset[cnt%length], p.get_y() + p.get_height()/2),
                                horizontalalignment='center',
                                verticalalignment='center',
                                color = colors[cnt],
                                size = fontsize,
                                weight = weight,
                                rotation = rotation
                                )
                    offset[cnt%length] += p.get_width()
                if total:
                    # Stacked barに対し、合計値を表示
                    for idx, p in zip(range(length), ax.patches):
                        ax.annotate(formatter(offset[idx]), 
                                    (offset[idx],
                                    p.get_y() + p.get_height()/2),
                                    horizontalalignment='left',
                                    verticalalignment='center',
                                    weight = weight,
                                    rotation = rotation
                                    )
```

titanicのデータを例としてグラフにしてみます。
上記のスクリプトがplotTools.pyという名前で保存されているとします。

### シンプルなの棒グラフの例
```python3:JupyterNotebook
import plotTools as pt

pd.crosstab(df['sex'], df['survived'], normalize='index')[True].plot.bar()
pt.annotating_plots(formatter='percent')
```
<img src = "https://qiita-image-store.s3.amazonaws.com/0/189855/07ba84b8-5b57-eec9-4e86-ad31ec73f661.png" width=50%>

### 積上げ棒グラフの例
```python3:JupyterNotebook
pd.crosstab(df['sex'], df['survived'], normalize='index').plot.bar(stacked=True)
pt.annotating_plots(formatter='percent',
                    stacked = True,
                    colors = 'auto')
```
<img src = "https://qiita-image-store.s3.amazonaws.com/0/189855/e0f37cbf-6645-8cb0-ff53-2a379e5caa7b.png" width=70%>


# 2. 軸のパーセント表記
こちらはデータラベルほど長くはなりませんが、よく使うため、汎用モジュールにまとめておきます。

* digits : 表示する桁数

```python3:plotTools.py
from matplotlib.ticker import FuncFormatter
def axis_percent(ax = None, axis=1, digits=0):
    if ax is None:
        ax = plt.gca()
    def _to_percent(x, position):
        return '{:.{}f}%'.format(x*100, digits)
    formatter = FuncFormatter(_to_percent)
    
    if axis == 0:
        ax.xaxis.set_major_formatter(formatter)        
    elif axis == 1:
        ax.yaxis.set_major_formatter(formatter)
    elif axis == 2:
        ax.zaxis.set_major_formatter(formatter)
```
上記の積上げ棒グラフの軸をパーセント表記に書き換えてみます。

```python3:JupyterNotebook
pd.crosstab(df['sex'], df['survived'], normalize='index').plot.bar(stacked=True, legend=False)
pt.annotating_plots(formatter='percent',
                    stacked = True,
                    colors = 'auto')
pt.axis_percent()
```
<img src = "https://qiita-image-store.s3.amazonaws.com/0/189855/85e3a9b5-efc0-ffa2-7e47-e393fe96066e.png" width=70%>


# 3. グラフのemf保存
日本ではたいていの場合、プレゼンテーションや資料提供にはPowerPointを用いると思います。
png形式でも構いませんが、印刷する場合にはベクタ形式でグラフを保存しておく方が
より綺麗な資料が出来上がります。

matplotlib.pyplot のsavefig関数は、svg形式には対応しているのですが、svg形式はPowerPointが対応していません。
PowerPointに埋め込むためには、emf形式にグラフ画像を変換する必要があります。

ここではinkscapeを利用することを考えます。
inkscapeをインストールしていない方は、下記のURLからダウンロードしてください。
[inkscapeのダウンロードページ](https://inkscape.org/en/release/0.91/platforms/)

inkscapeをインストールした上で、下記のスクリプトを汎用モジュールに追加します。

```python3:plotTools
import subprocess
import os

def savefig_as_emf(file, fig = None, transparent=False):
    if fig is None:
        figure = plt.gcf()
    inkscape_path = "C://Program Files//Inkscape//inkscape.exe"
    filepath = file

    if filepath is not None:
        path, filename = os.path.split(filepath)
        filename, extension = os.path.splitext(filename)

        svg_filepath = os.path.join(path, filename+'.svg')
        emf_filepath = os.path.join(path, filename+'.emf')

        figure.savefig(svg_filepath, format='svg', transparent = transparent, bbox_inches='tight')

        subprocess.call([inkscape_path, svg_filepath, '--export-emf', emf_filepath])
        os.remove(svg_filepath)
```

その後、以下のように実行することで、emf形式のグラフを出力することが出来るようになります。

```python3:JupyterNotebook
pd.crosstab(df['sex'], df['survived'], normalize='index').plot.bar(stacked=True, legend=False)
pt.annotating_plots(formatter='percent',
                    stacked = True,
                    colors = 'auto')
pt.axis_percent()

pt.savefig_as_emf('hoge')
```
# 終わりに
ここまで読んでくださってありがとうございます。
データ分析の仕事を進める際には、汎用的な処理は出来るだけモジュールにまとめておくようにしています。
各論が非常に多いデータ分析の仕事において、
経験を効率に変えるためには、何らかの工夫が必要だなといつも感じています。
皆さんの工夫をお知らせ頂けたら幸いです。