Google Colab + Matplotlibを使ったデータの可視化における2つの流儀とPandasのDataFrameのplotの話

データ分析やデータ加工にGoogle Colabを使用するメリットの一つに、Matplotlibを使った可視化がやりやすい点があります。

![Bar plot of random data](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/14097/3bc5fb57-9b03-2e95-c3c8-cd7ed51d3bb7.png)

Google Driveから読み込んだCSVやBigQueryでのクエリー実行結果から、最低限のコードで手軽にグラフを作ることができ、視覚的にデータの内容をチェックできます。

![download-1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/14097/b1f02dd9-7f26-311c-8b7d-eb504729c578.png)

複数のグラフを並べてレイアウトしたり、[seaborn](https://seaborn.pydata.org)を使ってより高度な可視化をすることもできます。

Google Colab + Matplotlib + PandasのDataFrameを使ったデータの可視化はとても手軽で便利な一方で、 **「流儀」** の問題があるため、学習で躓きやすいという問題があり自分自身も学習の初期ではその点に悩まされました。ここでは改めて流儀について書きたいと思います。

# 事前準備: 日本語対応とMatplotlibのバージョンのアップデート
流儀の話に入る前に、データの可視化の作業がやりやすくなるように環境を少し整えておきます。

Google ColabでMatplotlibを使ってデータの可視化をやろうとする時に直面しがちな二つの問題があります。作業に取り掛かる前にこれらの問題の対応として、Matplotlibのバージョンアップと日本語化対応の設定をしておくことをお勧めします。

![Japanese in Matplotlib chart](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/14097/f39d7529-bda0-f9f8-8f73-67bfb3c6466b.png)

一つが日本語対応の問題です。上の図のように、デフォルトの状態ではグラフのラベルなどで日本語を使おうとすると全て□で表示されてしまいます。

![Bar label](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/14097/94547cb2-1252-0f4e-f1b5-49cd6cbf2f36.png)

もう一つが、Matplotlibの新機能についてです。例えば上のグラフのように棒グラフのバーの横に数字を出す`bar_label`という機能はMatplotlibの`3.4.0`以降で使用できるのですが、Google ColabのデフォルトのMatplotlibのバージョンは`3.2.2`(2022年12月現在)のため、このままではこれらの便利な機能を利用することができません。

```
!pip install matplotlib --upgrade
!pip install japanize-matplotlib

import japanize_matplotlib
japanize_matplotlib.japanize()
```

幸い、Google Colabでは`pip`コマンドを使って`matplotlib`のアップデートと`japanize-matplotlib`のインストールが可能です。Notebookの一番最初にこのコマンドを実行して環境をアップデートすることをお勧めします。

![Restart runtime after pip commands](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/14097/655e420a-df06-d1a8-2f29-1e837b2c1a31.png)

**アップデートを反映させるには、実行結果に出てくる「RESTART RUNTIME」ボタンを押して実行マシンを再起動する必要があります。** このボタンを押してマシンを再起動しない限り、デフォルトのバージョンの古いMatplotlibが動作し続けることになります。「すべてのセルを実行」などで一括でNotebookを実行する場合には特に注意が必要です。

# 2つの流儀
Google ColabでのMatplotlibを使ったデータの可視化については、[公式サンプル](https://colab.research.google.com/notebooks/charts.ipynb)をはじめとして多くのドキュメント、サンプルが存在します。簡単なグラフの描画であれば、最小限のコードで、あるいはサンプルからのコピペで目的を実現でき、とてもとっつきやすいツールだと思いますが、**流儀が二つある**ということが学習を進める上での一つの大きなハードルになる場合があります。Matplotlibには[Explicit(オブジェクト指向)とImplicit(MATLAB風)](https://matplotlib.org/stable/tutorials/introductory/lifecycle.html#a-note-on-the-explicit-vs-implicit-interfaces)という二つの流儀があり、同じようなことをそれぞれの流儀で実装することができます。世の中にはそれぞれの流儀で書かれたサンプルコードが大量にありますが、二つの流儀を混ぜて使おうとするととてもややこしいことになります・**どちらの流儀で書かれたコード・ドキュメントなのかを意識して読まないと、混乱の原因となります。**

例えば、Pandasで読み込んだDataFrameの内容からシンプルな棒グラフを描画したい場合、下記のような実装方法が存在します。

```Python
# 流儀1(Explicit オブジェクト指向な流儀)
fig, ax = plt.subplots()
ax.barh(df["date"], df["num_records"])

# 流儀2(Implicit MATLAB風の流儀)
plt.barh(df["date"], df["num_records"])

# 参考(上の二つの流儀以外でも、DataFrameの拡張を使ってもグラフを描画できる : 後述)
df.plot.barh(x="date", y="num_records")
```

X軸ラベルや凡例の表示に違いがありますが、データを棒グラフに書きたいだけであれば上記のどの方法を使ってグラフを描画しても問題ありません。ただし、グラフのカスタマイズやレイアウトの調整などを行いたくなった場合に、どの流儀を使っているかで実現方法が少し変わります。

![Using bar_label](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/14097/ede52de0-645e-376d-5563-79734d77e69e.png)

例えば、棒グラフの描画ではグラフ上の各バーに対して数値を表示したいことがよくあります。Matplotlib 3.3まではかなり面倒な処理が必要だったのですが、3.4で`bar_label`という機能が追加され簡単に数値を描画できるようになっています。

各流儀でそれぞれ`bar_label`を使う場合下記のようなコードになります。

```Python
# 流儀1
fig, ax = plt.subplots()
bc = ax.barh(df["date"], df["num_records"])
ax.bar_label(bc)

# 流儀2
bc = plt.barh(df["date"], df["num_records"])
plt.bar_label(bc)
```

一見このコードを見ると、流儀1も流儀2も同じ`bar_label`メソッドを呼び出しているように見えてしまいますが、流儀1と流儀2では実際に呼び出しているメソッドは別のメソッドになります。流儀1の`ax`は`matplotlib.axes.Axes`型の変数であり、ここで呼び出しているメソッドは[matplotlib.axes.Axes.bar_label](https://matplotlib.org/stable/api/_as_gen/matplotlib.axes.Axes.bar_label.html)というメソッドになります。一方流儀2の`plt`は`matplotlib.pyplot`であり、呼び出しているメソッドも[matplotlib.pyplot.bar_label](https://matplotlib.org/stable/api/_as_gen/matplotlib.pyplot.bar_label.html)で、流儀1とは別のメソッドになります。ちなみに参考として載せた`df.plot.barh`の返り値は`AxesSubplot`型ですが、`AxesSubplot`には`bar_label`メソッドはないため、直接呼び出すことはできません。

```Python
# 流儀1と流儀2が混ざった実装
fig, ax = plt.subplots()
bc = ax.barh(df["date"], df["num_records"])
plt.bar_label(bc)
```

`bar_label`に関して言えば、メソッドの呼び出し方や引数の型が**偶然**二つの流儀で一致しているため、仮に二つの流儀のサンプルコードの流儀を意識せずに上のように混ぜてしまったとしても、期待通りの結果が得られてしまいます。ただし、このような実装をしてしまうとより複雑なことをやろうとして流儀によって実装方法が変わってきた際に混乱のもととなるため、避けましょう。

ややこしいですが、

* 同じ「棒グラフを描画する」メソッドでも流儀によって返り値の型が違うこと
* 返り値の型によって、できること(呼び出せるメソッド)に違いがあること
* 型が違ってもそれぞれ似たようなメソッドが用意されていて同じように呼び出せることもあるが、実際には別メソッドを呼び出していること

を頭に入れながらサンプルコードを読まないと、後々混乱の元となります。

流儀によってコードがもう少し大きく変わる例を紹介します。

![Two charts in a row](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/14097/8fca92d2-c3c9-918b-61f0-e05ffa04634d.png)

```Python
df = pd.DataFrame({'product': ['A', 'B', 'C', 'D', 'E'],
                   'cost': np.random.randint(100, 200, 5),
                   'sales': np.random.randint(100, 200, 5)})

# 流儀1
plt.subplot(1, 2, 1)
plt.barh(df["product"], df["cost"])
plt.title('cost')

plt.subplot(1, 2, 2)
plt.barh(df["product"], df["sales"])
plt.title('sales')


# 流儀2
fix, axes = plt.subplots(1, 2)
axes[0].barh(df["product"], df["cost"])
axes[0].set_title('cost')

axes[1].barh(df["product"], df["sales"])
axes[1].set_title('sales')
```

上のコードでは二つのグラフを並べて描画する処理を、それぞれの流儀で記述しています。グラフの描画自体は共に`barh`という同じ名前、同じ型の引数をとるメソッドで描画していますが、各グラフにタイトルをつける処理は流儀1では`title`、流儀2では`set_title`とメソッド名も変わります。`matplotlib.pyplot`である`plt`に対して`set_title`というメソッドを呼び出すことはできませんし、`matplotlib.axes.Axes`型である`axes[0]`もしくは`axes[1]`に対して`title`というメソッドを呼び出すこともできません。流儀を意識せずにサンプルコードを組み合わせて使おうとしたり、ドキュメントの一部分だけを読んで実装しようとすると、このような場面で「なぜ動作しないのか」迷子になってしまいます。サンプルコードやドキュメントを読む際は、どちらの流儀の話なのかを意識するようにしましょう。

# PandasのPlot機能

さて、Matplotlibを使ったグラフの描画においては、もう一つ「便利だがややこしい」問題が存在します。それが上のコードでも「参考」として記載したPandasのDataFrameのplot機能です。PandasのDataFrameには[plot](https://pandas.pydata.org/docs/reference/api/pandas.DataFrame.plot.html)という機能があり、それを使うとかなり手軽にMatplotlibでグラフを描画することができるため、データ可視化のサンプルコードで使用されることも珍しくありません。(正確に書くと、デフォルトのグラフ描画バックエンドがMatplotlibになっているだけで、pandas.DataFrame.plotはMatplotlibに特化した機能ではありません)

```Python
df.plot.barh(x="date", y="num_records")

#もしくは

df.plot(x="date", y="num_records", kind="bar")
```

この機能はMatplotlibの機能ではなく、**PandasのDataFrameがMatplotlibの機能を使ってグラフを描画する**機能となるため、上に書いたMatplotlibの2つの流儀とはさらに別の次元/流儀として存在する機能になります。`df.plot.bar`と`plt.barh`あるいは`ax.barh`は似たようなグラフ結果を出力しますが、内部的にやっていることは異なるため「代替」として使用するには注意が必要になります。

```Python
plt.subplot(1, 2, 1)
plt.title('cost')
df.plot.barh(x="product", y="cost")

plt.subplot(1, 2, 2)
plt.title('sales')
df.plot.barh(x="product", y="sales")
```

これを例えば上の二つのグラフを描画するコードの`plt.barh(df["product"], df["cost"])`の代替として使用しても、`df.plot.barh`の中では図のレイアウトなども含めた独自の処理が行われるため、期待したような出力結果にはなりません。

```Python
fix, axes = plt.subplots(1, 2)
df.plot.barh(x="product", y="cost", ax=axes[0])
axes[0].set_title('cost')

df.plot.barh(x="product", y="sales", ax=axes[1])
axes[1].set_title('sales')
```

DataFrame.plotを使って複数のグラフを描画したい場合は、`ax`というパラメータに描画したい場所の`Axes`を指定します。このやり方は、Matplotlibの二つの流儀とはまた別の流儀になります。Pandas.DataFrame.plotは非常に便利ですが、使用する時は上記の点を意識・注意して使用しましょう。


# リンク
* [Charts in Colaboratory](https://colab.research.google.com/notebooks/charts.ipynb)
* [pandas.DataFrame.plot](https://pandas.pydata.org/pandas-docs/stable/reference/api/pandas.DataFrame.plot.html)
* [早く知っておきたかったmatplotlibの基礎知識、あるいは見た目の調整が捗るArtistの話](https://qiita.com/skotaro/items/08dc0b8c5704c94eafb9)
* [A note on the explicit vs. implicit interfaces](https://matplotlib.org/stable/tutorials/introductory/lifecycle.html#a-note-on-the-explicit-vs-implicit-interfaces)