Matplotlibを使うと、データの可視化が簡単になります。でも、だからといって同じコードを何度も書くのは面倒じゃありませんか? そんなときには関数を定義することで、自分が書いたコードを何でも使えるようになりますよ。
本シリーズ「Pythonデータ処理入門」は、Pythonの基礎をマスターした人を対象に以下のような、Pythonを使ってデータを処理しようというときに便利に使えるツールやライブラリ、フレームワークの使い方の基礎を説明するものです。
【Matplotlib超入門:OOインタフェース編】は【Matplotlib超入門:pyplot編】に続きオブジェクト指向インタフェースを用いて、グラフを描画する方法を紹介する連載です。pyplot編と同様、以下のバージョンのPythonとMatplotlibを使用しています。
なお、以下ではMatplotlibのpyplotインタフェースやNumPy、pandasをインポートする以下の行を実行してあるものとします。
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
今回は3種類のデータを用意して、それらをpyplotインタフェースとOOインタフェースを使って可視化してみます。
1つ目のデータはサイン曲線にノイズを混ぜたもので、pyplot.plot関数で折れ線グラフを描くことを想定しています(コード上はmatplotlib.pyplotモジュールを「plt」としてインポートしているので、実際には「plt.plot関数」を呼び出す点には留意してください。以下同様)。
# データの準備
rng = np.random.default_rng(0)
# 折れ線グラフ用のデータ(ノイズ入りのサイン曲線)
x0 = np.linspace(0, 2*np.pi, 80)
y0 = np.sin(x0) + rng.normal(0, 0.12, size=x0.size)
実際に可視化してみましょう。
plt.figure(figsize=(3, 2))
plt.plot(x0, y0, label='line plot', color='blue')
plt.title('line plot sample')
plt.legend()
plt.show()
pyplot.plot関数には先ほど作成したデータ(X軸、Y軸)と凡例を表示する際に使われるラベル、描画色を指定しています。また、pyplot.title関数でこのグラフのタイトルも指定しています。pyplot.legend関数は今述べたように凡例を表示するためのものです。
実行結果は次の通りです。
2つ目は散布図を描くことを想定したデータです。
x1 = rng.normal(0, 1.0, 120)
y1 = 0.8 * x1 + rng.normal(0, 0.6, 120)
size = 30 + 120 * rng.random(120)
plt.figure(figsize=(3, 2))
plt.scatter(x1, y1, s=size, label='scatter plot',color='orange', alpha=0.6)
plt.title('scatter plot sample')
plt.legend()
plt.show()
散布図はもちろんpyplot.scatter関数で描画します。このときには、データ(X軸、Y軸)と散布図の各点の大きさを示すデータ(size変数)、凡例で使われるラベル、描画色、アルファ値を指定しています。それ以外は折れ線グラフと同様です。
これを実行すると、次のような散布図が描かれます。
最後に棒グラフ用のデータです。
categories = ['A', 'B', 'C', 'D', 'E']
values = rng.integers(5, 20, size=len(categories))
plt.figure(figsize=(3, 2))
plt.bar(categories, values, label='Bar Plot', color='green', alpha=0.7)
plt.title('bar plot example')
plt.legend()
plt.show()
pyplot.bar関数で棒グラフを描くときには、X軸の値としてカテゴリ('A'や'B'など)、Y軸の値としてそのカテゴリの値、凡例用のラベル、描画色、アルファ値を指定しています。これも折れ線グラフや散布図と共通しているところが多々ありますね。
こちらを可視化すると、次のような棒グラフが描かれます。
単純にデータを可視化したいだけなら、pyplotインタフェースが提供する各種の関数にデータ(とラベルや描画色など)を渡すだけで簡単にグラフを描けることは、これまでにも見てきた通りです。では、次にpyplotインタフェースを使って、1つのFigureオブジェクトに複数のグラフをグリッド状に描画してみます。
Matplotlibではグラフなどを描画する土台となるのがFigureオブジェクトであり、その上に1つ以上のAxesオブジェクトを作成して、そこにグラフ(や何かの画像)を描画していきます。pyplotインタフェースでは、描画対象のAxesオブジェクトを切り替えるのにpyplot.subplot関数を使うのでした。このとき、描画対象のオブジェクトなどは内部で自動的に管理されるので、上で見たように何も考えずにそこにあるデータを可視化するときには便利に使えます(対して、OOインタフェースでは描画対象のAxesオブジェクトのメソッドを直接呼び出すことで、どのAxesオブジェクトに何を描画するのかを明示します)。
そこでちょっと面倒ですが、pyplotインタフェースを使って複数のグラフを1つのFigureオブジェクトに描画してみましょう。以下はそのコードです。
plt.figure(figsize=(8, 6))
# 折れ線グラフ
plt.subplot(2, 2, 1)
plt.plot(x0, y0, label='line plot', color='blue')
plt.title('line plot sample')
plt.xlabel('x')
plt.ylabel('y')
plt.legend()
# 散布図
plt.subplot(2, 2, 2)
plt.scatter(x1, y1, s=size, label='scatter plot',color='orange', alpha=0.6)
plt.xlabel('x')
plt.ylabel('y')
plt.title('scatter plot sample')
plt.legend()
# 棒グラフ
plt.subplot(2, 2, 3)
plt.bar(categories, values, label='Bar Plot', color='green', alpha=0.7)
plt.xlabel('categories')
plt.ylabel('values')
plt.title('bar plot example')
plt.legend()
# Figure全体の設定
plt.suptitle('graphs on one figure', fontsize=16)
plt.tight_layout()
plt.show()
最初のpyplot.figure関数ではグラフ全体(Figureオブジェクト)のサイズを指定しています。その後は、先ほど見たコードとほとんど同じです(加えて、軸ラベルの設定もするようにしました)。ただし、各グラフを描く前にpyplot.subplot関数を呼び出している点に注意してください。最初の2つの引数は全て「2, 2」となっていますが、これはFigureを2行2列の領域に分割することを意味しています。そして、次の引数(1、2、3)では2行2列に分割した領域の左上を1、右下を4として、どこにグラフを描くかを指定しています(これにより、そこにAxesオブジェクトが作成され、pyplotインタフェースがそこをこれから描画する対象だなと判断します)。
よって、折れ線グラフを描く前に「plt.subplot(2, 2, 1)」とすることで、Figureオブジェクトの左上が描画対象になっているわけです。
最後のpyplot.suptitle関数はグラフ全体のタイトルを付けるもので、pyplot.tight_layout関数は複数のグラフ間のパディングなどを自動で調整して、いい感じにグラフが表示されるようにしてくれます。なお、suptitle関数とtight_layout関数がいい感じにグラフを表示してくれないこともあります。そうしたときには「plt.tight_layout(rect=[0, 0, 1, 0.95])」のようにrectパラメーターを指定することでうまくいく場合もあります(後で見るFigureオブジェクトも同様。ただし、ここでは特に何も指定しません)。
実行結果はこんな感じです。
カンタンですよね。pyplot.subplot関数での描画対象の切り替えさえ忘れなければ。でも、グラフの数が増えてきたら、似たようなコードを繰り返し書くのも面倒になってくるはずです。そこで、グラフ描画のコードを関数にまとめてしまいましょう。
ここではplot_graph関数にグラフの描画、グラフのタイトルの追加、凡例の表示、それから(先ほどはやっていませんでしたが)X軸とY軸のラベルの指定を関数にまとめちゃいます。
def plot_graph(x, y, kind=None,
title='', xlabel='', ylabel='',
show_legend=True, **kwargs,):
...
この関数には幾つかのパラメーターがあります。
pyplot.plot関数やpyplot.scatter関数、pyplot.bar関数にはキーワード引数として描画色などを指定できます。plot_graph関数はそうした引数をkwargsに受け取って、それをそのまま、pyplot.plot関数などに引き渡すようにしています。これにより、plot_graph関数のユーザーがグラフ描画に必要な引数を自由に渡せるようになるので、読者の皆さんが同様な関数を独自に定義するときには、このやり方をぜひ真似てみてください。
というわけで実際の関数のコードを以下に示します。
def plot_graph(x, y, kind=None,
title='', xlabel='', ylabel='',
show_legend=True, **kwargs,):
if kind is None:
raise ValueError("kind unspecified. Use: 'line', 'scatter', or 'bar'.")
if kind == 'line':
plt.plot(x, y, **kwargs)
elif kind == 'scatter':
plt.scatter(x, y, **kwargs)
elif kind == 'bar':
plt.bar(x, y, **kwargs)
else:
raise ValueError("Unsupported kind. Use 'line', 'scatter', or 'bar'.")
plt.title(title)
plt.xlabel(xlabel)
plt.ylabel(ylabel)
if show_legend and 'label' in kwargs:
plt.legend()
kindパラメーターがNoneであれば、描画するグラフの種類が指定されていないので例外を発生させます。また、'line'、'scatter'、'bar'のいずれでもない場合にはこの関数ではサポートされないグラフの種類が指定されたとしてやはり例外を発生させます。
そして、kindが'line'であればpyplot.plot関数を、'scatter'であればpyplot.scatter関数を、'bar'であればpyplot.bar関数を呼び出しています(ここでkwargsに受け取ったキーワード引数をそのまま、それらの関数に引き渡している点に注意)。その後はグラフのタイトルや軸ラベルの設定と、凡例の表示を行っています。ただし、show_legendがTrueであっても、凡例に使用するラベル(labelパラメーター)がなければならないのでここでは「if show_legend and 'label' in kwargs:」とshow_legendの値とラベルがキーワード引数として渡されているかどうかをチェックしている点には注意してください。
実際にこれを呼び出すコードは例えば、次のようになります。
plt.figure(figsize=(8, 6))
plt.subplot(2, 2, 1)
plot_graph(x0, y0, kind='line', title='line plot sample',
xlabel='x', ylabel='y', label='line plot', color='blue')
plt.subplot(2, 2, 2)
plot_graph(x1, y1, kind='scatter', title='scatter plot sample',
xlabel='x', ylabel='y', s=size, label='scatter plot',
color='orange', alpha=0.6)
plt.subplot(2, 2, 3)
plot_graph(categories, values, kind='bar', title='bar plot example',
xlabel='categories', ylabel='values', label='Bar Plot',
color='green', alpha=0.7)
plt.suptitle('graphs on one figure', fontsize=16)
plt.tight_layout()
plt.show()
先ほどまではpyplot.plot関数などを呼び出した後に、pyplot.title関数などを呼び出してタイトルの設定などを行っていましたが、ここではplot_graph関数に必要なデータを渡すようになっているのがポイントです。
しかし、それ以上に重要なのは、plot_graph関数を呼び出す前に、pyplot.subplot関数を呼び出して、グラフの描画領域を切り替えているところです。pyplotインタフェースは単純なグラフなら便利に使えて、細々とした要素はpyplotが内部で自動的に管理をしてくれるようになっています。しかし、このようにグラフを描画するコードを関数にまとめたとしても、複数の領域にグラフを描画したいというときには自分で、そのことを管理する必要があるのが難しいところです。例えば、pyplot.subplot関数の呼び出しを忘れて、次のようなコードを書いてしまったとします。
plt.figure(figsize=(8, 6))
plt.subplot(2, 2, 1)
plot_graph(x0, y0, kind='line', title='line plot sample',
xlabel='x', ylabel='y', label='line plot', color='blue')
plot_graph(x1, y1, kind='scatter', title='scatter plot sample',
xlabel='x', ylabel='y', s=size, label='scatter plot',
color='orange', alpha=0.6)
plt.suptitle('graphs on one figure', fontsize=16)
plt.tight_layout()
plt.show()
これを実行すると結果は次のようになります。
2つのグラフが重ねて描画されてしまいました。これが意図したことであれば問題はありません。しかし、2つのグラフはX軸の範囲もY軸の範囲も異なっている上に、それらを重ねて表示することに意味はありません。これらは別々のグラフとして描画されるべきです。pyplotインタフェースを使っても描画領域を自分で管理することは可能ですが、そういう場面ではOOインタフェースの方が適しています。
そういうわけで、ここではOOインタフェースを使い、上で定義したplot_graph関数を書き直しますが、その前に、ちょっと見てもらいたいものがあります。それはplot_graph関数にグラフ描画のコードをまとめる前に書いたベタ書きでFigureオブジェクトに複数のグラフを描くコードです。
plt.figure(figsize=(8, 6))
# 折れ線グラフ
plt.subplot(2, 2, 1)
plt.plot(x0, y0, label='line plot', color='blue')
plt.title('line plot sample')
plt.xlabel('x')
plt.ylabel('y')
plt.legend()
# 散布図
plt.subplot(2, 2, 2)
plt.scatter(x1, y1, s=size, label='scatter plot',color='orange', alpha=0.6)
plt.xlabel('x')
plt.ylabel('y')
plt.title('scatter plot sample')
plt.legend()
# 棒グラフ
plt.subplot(2, 2, 3)
plt.bar(categories, values, label='Bar Plot', color='green', alpha=0.7)
plt.xlabel('categories')
plt.ylabel('values')
plt.title('bar plot example')
plt.legend()
# Figure全体の設定
plt.suptitle('graphs on one figure', fontsize=16)
plt.tight_layout()
plt.show()
pyplot.subplot関数で描画領域を切り替えて、後はpyplotインタフェースが提供する各種の関数を呼び出すだけでした。そして、これと同じことをOOインタフェースで書き直すと次のようになります。
fig, ax = plt.subplots(2, 2, figsize=(8, 6))
# 折れ線グラフ
ax[0, 0].plot(x0, y0, label='line plot', color='blue')
ax[0, 0].set_title('line plot sample')
ax[0, 0].set_xlabel('x')
ax[0, 0].set_ylabel('y')
ax[0, 0].legend()
# 散布図
ax[0, 1].scatter(x1, y1, s=size, label='scatter plot',color='orange', alpha=0.6)
ax[0, 1].set_xlabel('x')
ax[0, 1].set_ylabel('y')
ax[0, 1].set_title('scatter plot sample')
ax[0, 1].legend()
# 棒グラフ
ax[1, 0].bar(categories, values, label='Bar Plot', color='green', alpha=0.7)
ax[1, 0].set_xlabel('categories')
ax[1, 0].set_ylabel('values')
ax[1, 0].set_title('bar plot example')
ax[1, 0].legend()
# Figure全体の設定
fig.suptitle('graphs on one figure', fontsize=16)
fig.tight_layout()
plt.show()
詳しい説明は省略しますが、以下に注目してください。
重要なのは、pyplotインタフェースではpyplot.subplot関数で描画領域を切り替えていたのに対して、OOインタフェースでは「ax[0, 0]」のように描画対象のAxesオブジェクトを明示的に指定しているところです。pyplotインタフェースを使っているときには、描画対象となる(アクティブな)Axesオブジェクトは暗黙的にpyplotインタフェースが管理してくれます。しかし、OOインタフェースでは「どのAxesに描くか」を引数やメソッド呼び出しの対象として明示するため、描画対象の切り替えを忘れるような「暗黙の状態」が発端となるエラーを減らせるでしょう。
このことを念頭に置いて、先ほどのplot_graph関数を書き替えてみたものが以下のplot_graph_ax関数です。
def plot_graph_ax(ax, x, y, kind=None,
title='', xlabel='', ylabel='',
show_legend=True, **kwargs,):
if kind is None:
raise ValueError("kind unspecified. Use: 'line', 'scatter', or 'bar'.")
if kind == 'line':
ax.plot(x, y, **kwargs)
elif kind == 'scatter':
ax.scatter(x, y, **kwargs)
elif kind == 'bar':
ax.bar(x, y, **kwargs)
else:
raise ValueError("Unsupported kind. Use 'line', 'scatter', or 'bar'.")
ax.set_title(title)
ax.set_xlabel(xlabel)
ax.set_ylabel(ylabel)
if show_legend and 'label' in kwargs:
ax.legend()
書き直したplot_graph_ax関数は描画対象のAxesオブジェクトを受け取るようになっています。そして、そのAxesオブジェクトのメソッドを呼び出して、グラフを描画しています。タイトルや軸ラベルの設定はメソッドの名前がpyplotインタフェースのそれと異なっていますが、説明する必要はないでしょう。
これを呼び出すコードは次の通りです。
fig, ax = plt.subplots(2, 2, figsize=(8, 6))
plot_graph_ax(ax[0, 0], x0, y0, kind='line', title='line plot sample',
xlabel='x', ylabel='y', label='line plot', color='blue')
plot_graph_ax(ax[0, 1], x1, y1, kind='scatter', title='scatter plot sample',
xlabel='x', ylabel='y', s=size, label='scatter plot',
color='orange', alpha=0.6)
plot_graph_ax(ax[1, 0], categories, values, kind='bar',
title='bar plot example',
xlabel='categories', ylabel='values', label='Bar Plot',
color='green', alpha=0.7)
fig.suptitle('graphs on one figure', fontsize=16)
fig.tight_layout()
plt.show()
最初にpyplot.subplots関数を呼び出して、FigureオブジェクトとAxesオブジェクトの配列を得ています。その後は、pyplot.subplot関数で描画対象を切り替える代わりに、「ax[0, 0]」のように描画対象となるAxesオブジェクトを配列から取り出して、それをplot_graph_ax関数に(他の引数と一緒に)渡しているのが大きな違いです。
それ以外はpyplotインタフェース版のplot_graph関数と同様な呼び出し方ができます。
この関数を使えば、折れ線グラフと散布図、棒グラフについては、何度も同じコードを書くことなく、1つのFigureオブジェクトに好きなだけグラフを描画できるようになりました。これが関数を定義して、コードを再利用するということです。ノートブック環境なら必要なコードをコピペして、ちょっと修正するという形のコードの再利用も簡単ですが、このように関数を作ることでもコードは再利用できます。少しずつ、こうしたやり方になれていくことをオススメします。
そして……上のような関数を定義することで、グラフ描画がさらに簡単になります。
これまでは自分でデータや引数を指定して、plot_graph_ax関数を呼び出していました。グラフを3つ描くのであれば、これを3回繰り返すわけです。そうではなくて、データや引数のセットアップだけしておいて、それを基にグラフを自動的に必要な数だけ描画する関数も作成できます。
例えば、それをplot_multiple_graphs関数としましょう。この関数に3種類のデータと、それぞれのグラフに必要な引数を渡すと、それが内部でplot_graph_ax関数を呼んでくれるという仕組みです。
まずはplot_multiple_graphs関数をどんな形式で呼び出すかを示します。
def plot_multiple_graphs(fig, axes, specs):
pass
plot_spec = [
{'x': x0, 'y': y0, 'kind': 'line',
'title': 'line plot sample',
'xlabel': 'x', 'ylabel': 'y',
'label': 'line plot', 'color': 'blue'},
{'x': x1, 'y': y1, 'kind': 'scatter',
'title': 'scatter plot sample',
'xlabel': 'x', 'ylabel': 'y',
's': size, 'label': 'scatter plot',
'color': 'orange', 'alpha': 0.6},
{'x': categories, 'y': values, 'kind': 'bar',
'title': 'bar plot example',
'xlabel': 'categories', 'ylabel': 'values',
'label': 'Bar Plot', 'color': 'green', 'alpha': 0.7}
]
fig, axes = plt.subplots(2, 2, figsize=(8, 6))
plot_multiple_graphs(fig, axes, plot_spec)
plt.show()
つまり、3種類のデータとそれぞれの引数を辞書にまとめて、それらを要素とするリストを作成したものがplot_specです。これに加えて、pyplot.subplots関数で取得したFigureオブジェクトとAxes配列をplot_multiple_graphs関数に渡せばよいでしょう。ここでミソなのは、辞書のキーはplot_graph_ax関数のキーワード引数の名前に合致するようにしているところです。後から見ますが、plot_multiple_graphs関数の中では、辞書の内容をキーワード引数としてplot_graph_ax関数に渡すので、こうしておくことで呼び出しがスムーズになります。
上ではplot_multiple_graphs関数の中身はありませんでしたが、以下にそのコードを示します。
def plot_multiple_graphs(fig, axes, specs):
for ax, spec in zip(axes.flat, specs):
plot_graph_ax(ax, **spec)
fig.suptitle('graphs on one Figure')
fig.tight_layout()
plot_multiple_graphs関数はAxes配列のflat属性を使っている点に注目です。「plt.subplots(2, 2, figsize=...)」とした場合、Figureオブジェクトは2行2列の領域に分割されます。そしてその戻り値も2行2列のAxes配列ですが、flat属性はこの配列を反復する際に、それがあたかも1次元のオブジェクトであるかのように振る舞います。specsに受け取るのは、辞書を要素とする(1次元の)リストなので、for文でaxes.flatとspecsをzip関数に渡すことで、4つの描画領域と3つの辞書がループ変数のaxとspecに渡されます(実際にはzip関数は引数として渡した反復可能オブジェクトのどれかの要素が尽きたところで反復を終了するので渡される描画領域も3つだけです)。
このとき、axは描画対象となるAxesオブジェクトです(左上から右下に向かって反復されます)。また、specにはX軸とY軸の値や描画色、グラフのタイトルなどの情報が辞書の形で格納されています。なので、for文の中では描画対象のaxと、「**spec」として辞書をキーワード引数の形でplot_graph_ax関数に渡すだけです。そして、最後にfigオブジェクトを使ってFigureオブジェクト全体のタイトルとレイアウトを指定しています。
念のために、実行結果を以下に示します。
今回はグラフを描画するコードを関数とすることで、呼び出し一発でスムーズにグラフを描画する方法を紹介しました。pyplotインタフェースとOOインタフェースでは、描画対象の切り替え方法が大きく異なる点には注意が必要ですが、どちらのインタフェースでも関数を作ることで、タイプ量を大きく減らせますし、コードの見通しもよくなるはずです。今はまだコピペ中心だという方も、少しずつ関数を活用してみることをオススメします。そして、Matplotlib超入門は今回で終了となります。これからはより実践的なAI活用記事に注力していく予定なので、そちらも楽しんでくれると幸いです。
Copyright© Digital Advantage Corp. All Rights Reserved.