データ分析やデータ加工にGoogle Colabを使用するメリットの一つに、Matplotlibを使った可視化がやりやすい点があります。
Google Driveから読み込んだCSVやBigQueryでのクエリー実行結果から、最低限のコードで手軽にグラフを作ることができ、視覚的にデータの内容をチェックできます。
複数のグラフを並べてレイアウトしたり、seabornを使ってより高度な可視化をすることもできます。
Google Colab + Matplotlib + PandasのDataFrameを使ったデータの可視化はとても手軽で便利な一方で、 「流儀」 の問題があるため、学習で躓きやすいという問題があり自分自身も学習の初期ではその点に悩まされました。ここでは改めて流儀について書きたいと思います。
事前準備: 日本語対応とMatplotlibのバージョンのアップデート
流儀の話に入る前に、データの可視化の作業がやりやすくなるように環境を少し整えておきます。
Google ColabでMatplotlibを使ってデータの可視化をやろうとする時に直面しがちな二つの問題があります。作業に取り掛かる前にこれらの問題の対応として、Matplotlibのバージョンアップと日本語化対応の設定をしておくことをお勧めします。
一つが日本語対応の問題です。上の図のように、デフォルトの状態ではグラフのラベルなどで日本語を使おうとすると全て□で表示されてしまいます。
もう一つが、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」ボタンを押して実行マシンを再起動する必要があります。 このボタンを押してマシンを再起動しない限り、デフォルトのバージョンの古いMatplotlibが動作し続けることになります。「すべてのセルを実行」などで一括でNotebookを実行する場合には特に注意が必要です。
2つの流儀
Google ColabでのMatplotlibを使ったデータの可視化については、公式サンプルをはじめとして多くのドキュメント、サンプルが存在します。簡単なグラフの描画であれば、最小限のコードで、あるいはサンプルからのコピペで目的を実現でき、とてもとっつきやすいツールだと思いますが、流儀が二つあるということが学習を進める上での一つの大きなハードルになる場合があります。MatplotlibにはExplicit(オブジェクト指向)とImplicit(MATLAB風)という二つの流儀があり、同じようなことをそれぞれの流儀で実装することができます。世の中にはそれぞれの流儀で書かれたサンプルコードが大量にありますが、二つの流儀を混ぜて使おうとするととてもややこしいことになります・どちらの流儀で書かれたコード・ドキュメントなのかを意識して読まないと、混乱の原因となります。
例えば、Pandasで読み込んだDataFrameの内容からシンプルな棒グラフを描画したい場合、下記のような実装方法が存在します。
# 流儀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軸ラベルや凡例の表示に違いがありますが、データを棒グラフに書きたいだけであれば上記のどの方法を使ってグラフを描画しても問題ありません。ただし、グラフのカスタマイズやレイアウトの調整などを行いたくなった場合に、どの流儀を使っているかで実現方法が少し変わります。
例えば、棒グラフの描画ではグラフ上の各バーに対して数値を表示したいことがよくあります。Matplotlib 3.3まではかなり面倒な処理が必要だったのですが、3.4でbar_label
という機能が追加され簡単に数値を描画できるようになっています。
各流儀でそれぞれbar_label
を使う場合下記のようなコードになります。
# 流儀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というメソッドになります。一方流儀2のplt
はmatplotlib.pyplot
であり、呼び出しているメソッドもmatplotlib.pyplot.bar_labelで、流儀1とは別のメソッドになります。ちなみに参考として載せたdf.plot.barh
の返り値はAxesSubplot
型ですが、AxesSubplot
にはbar_label
メソッドはないため、直接呼び出すことはできません。
# 流儀1と流儀2が混ざった実装
fig, ax = plt.subplots()
bc = ax.barh(df["date"], df["num_records"])
plt.bar_label(bc)
bar_label
に関して言えば、メソッドの呼び出し方や引数の型が偶然二つの流儀で一致しているため、仮に二つの流儀のサンプルコードの流儀を意識せずに上のように混ぜてしまったとしても、期待通りの結果が得られてしまいます。ただし、このような実装をしてしまうとより複雑なことをやろうとして流儀によって実装方法が変わってきた際に混乱のもととなるため、避けましょう。
ややこしいですが、
- 同じ「棒グラフを描画する」メソッドでも流儀によって返り値の型が違うこと
- 返り値の型によって、できること(呼び出せるメソッド)に違いがあること
- 型が違ってもそれぞれ似たようなメソッドが用意されていて同じように呼び出せることもあるが、実際には別メソッドを呼び出していること
を頭に入れながらサンプルコードを読まないと、後々混乱の元となります。
流儀によってコードがもう少し大きく変わる例を紹介します。
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という機能があり、それを使うとかなり手軽にMatplotlibでグラフを描画することができるため、データ可視化のサンプルコードで使用されることも珍しくありません。(正確に書くと、デフォルトのグラフ描画バックエンドがMatplotlibになっているだけで、pandas.DataFrame.plotはMatplotlibに特化した機能ではありません)
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
は似たようなグラフ結果を出力しますが、内部的にやっていることは異なるため「代替」として使用するには注意が必要になります。
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
の中では図のレイアウトなども含めた独自の処理が行われるため、期待したような出力結果にはなりません。
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は非常に便利ですが、使用する時は上記の点を意識・注意して使用しましょう。