HOWTO · Matplotlib

Matplotlib で表面プロットを作成する方法

plot_surface()メソッドを使用して Matplotlib で表面プロットを作成できます。

このページの内容

Matplotlibで3D曲面図を作成するには、互換性のある2次元配列 X、Y、Z を用意し、projection="3d" を指定したAxesを作成して、配列を Axes3D.plot_surface に渡します。以下の例では、数式から座標グリッドを作り、軸ラベルとカラーバーを追加して、図を保存します。座標と高さを長方形のグリッドで表せる場合は、実測値にも同じ手順を使えます。

実行環境を準備する

配列やグリッドの操作にはNumPyを、プロットにはMatplotlibを使います。使用するPython環境で、ターミナルから python -m pip install numpy matplotlib を実行してインストールしてください。環境やノートブックでパッケージを管理している場合は、スクリプトを実行するインタープリターから利用できるよう、その環境に両方のパッケージをインストールします。

例ではNumPyと matplotlib.pyplot をインポートします。Matplotlibには3Dプロット機能が含まれており、projection="3d" を指定すると3D Axesを作成できます。そのため、Axes3D を別途インポートする必要はありません。このAxesで使うメソッドは Axes3D.plot_surface です。

データを準備する

曲面図では、2つの座標からなる平面上の各位置に高さを割り当てます。等間隔の座標には np.linspace() で1次元の値を作り、np.meshgrid() で2次元の X、Y グリッドに展開します。そのグリッドから Z を計算するか、同じグリッド位置に対応する実測の高さを用意します。

3つの配列は互換性のある形状である必要があります。各 Z[i, j] は、座標の組 X[i, j]、Y[i, j] における高さを表します。現在のMatplotlibは1次元の x、y 座標ベクトルも受け付け、2次元の Z に合わせて展開しますが、高さの配列自体はグリッドの寸法と一致していなければなりません。X や Y とは異なるグリッドで計算した Z を渡すと ValueError が発生します。これは後のトラブルシューティングの節で、実際の実行結果とともに示します。まず座標グリッドを作り、それと同じグリッド上で高さを計算するか、形状を変換してください。測定点が散在していて長方形のグリッドを構成しない場合は、plot_surface を使う前にグリッドデータを用意するか、補間します。

曲面図を作成する

次の一通りそろった例では、滑らかな動径関数 sin(sqrt(x² + y²)) を使います。グリッドを作成して曲面を描き、3つの軸にラベルを付け、カラースケールを追加したうえで、結果を現在のディレクトリに surface-plot.png として保存します。NumPyとMatplotlibをインストールした環境で実行してください。この例はコンソールに文字列を出力しません。結果は保存された画像と、使用中のバックエンドが表示に対応している場合のMatplotlibウィンドウです。

import numpy as np
import matplotlib.pyplot as plt

x = np.linspace(-5, 5, 100)
y = np.linspace(-5, 5, 100)
X, Y = np.meshgrid(x, y)
Z = np.sin(np.sqrt(X**2 + Y**2))

fig = plt.figure()
ax = fig.add_subplot(111, projection="3d")
surface = ax.plot_surface(X, Y, Z, cmap="viridis")
ax.set_xlabel("X")
ax.set_ylabel("Y")
ax.set_zlabel("Z")
ax.set_title("Surface Plot")
fig.colorbar(surface, ax=ax, label="Z value")
fig.savefig("surface-plot.png", dpi=150, bbox_inches="tight")
plt.show()

viridisカラーマップで描いた動径サイン関数の3D曲面図。Z値が-1から1の範囲を示すカラーバー付き。

このスクリプトは文字列を出力しないため、コンソール出力のブロックはありません。カラーバーは曲面の色と値の対応を示します。グリッドが形状を決め、カラーマップが見た目を決めます。ノートブックでは通常、プロット環境が図を表示します。スクリプトでは plt.show() により、設定済みのMatplotlibバックエンドに表示を要求します。コード内で指定された軸ラベルなどの表示文字列は変更されず、英語のまま表示されます。

曲面図をカスタマイズする

次の例では、同じ曲面に最も一般的なカスタマイズを適用します。カラーマップを plasma に切り替え、メッシュの辺を取り除き、カメラを固定した仰角と方位角へ回転させてから、カラーバーを追加し、図を customized-surface-plot.png として保存します。

import numpy as np
import matplotlib.pyplot as plt

x = np.linspace(-5, 5, 100)
y = np.linspace(-5, 5, 100)
X, Y = np.meshgrid(x, y)
Z = np.sin(np.sqrt(X**2 + Y**2))

fig = plt.figure()
ax = fig.add_subplot(111, projection="3d")
surface = ax.plot_surface(X, Y, Z, cmap="plasma", edgecolor="none")
ax.set_xlabel("X")
ax.set_ylabel("Y")
ax.set_zlabel("Z")
ax.set_title("Customized Surface Plot")
ax.view_init(elev=30, azim=45)
fig.colorbar(surface, ax=ax, label="Z value")
fig.savefig("customized-surface-plot.png", dpi=150, bbox_inches="tight")
plt.show()

plasmaカラーマップを使い、メッシュの辺をなくし、カメラを仰角30度・方位角45度に回転させた同じ曲面図。

Z の変化を読み取りやすいカラーマップを選びます。この例では、cmap="plasma" が最初の例の viridis の色の変化に置き換わり、カラーバーで色と値の対応を確認できます。edgecolor="none" オプションを指定すると、密なグリッドで連続した曲面の邪魔になるメッシュの辺が取り除かれます。サンプリンググリッド自体を見せたい場合は、既定の辺のままにしてください。

視点を変えるには、図を保存または表示する前に ax.view_init(elev=30, azim=45) を呼び出します。elev は仰角を変え、azim は垂直軸を中心に視点を回転させます。形状と軸が分かりやすくなるように値を調整してください。適した角度はデータや見せたい内容によって異なります。最初の図と比べると、2番目の図の回転したカメラは、中央の峰の向こう側の斜面をより多く見せます。

グリッドが密な場合、plot_surface では rcount や ccount などを使って行と列のサンプリング数を指定できます。数を減らすと描画する行と列が少なくなり、大きな曲面をより速く描画できる場合がありますが、表示できる細部は減ります。データの重要な変化が残るよう、十分なサンプル数を確保してください。共有用の画像を保存するには、fig.savefig("surface-plot.png", dpi=150, bbox_inches="tight") を使います。表示後に図を閉じるバックエンドとの互換性を保つため、plt.show() より前に保存してください。複数の版を残す場合は、別のファイル名またはディレクトリを指定します。

ワイヤーフレーム図との比較

ワイヤーフレームは、面を塗りつぶしたりカラーマップを使ったりせず、同じ曲面のグリッド線だけを描きます。Axes3D.plot_wireframe は同じ X、Y、Z グリッドを受け付けるため、新しいデータを用意しなくても2つの表示を切り替えられます。ワイヤーフレームは、グリッド構造自体が重要な場合、塗りつぶした曲面では形状の向こう側が隠れてしまう場合、または未知のデータを本格的な曲面描画の前に手早く構造確認したい場合に役立ちます。

import numpy as np
import matplotlib.pyplot as plt

x = np.linspace(-5, 5, 50)
y = np.linspace(-5, 5, 50)
X, Y = np.meshgrid(x, y)
Z = np.sin(np.sqrt(X**2 + Y**2))

fig = plt.figure()
ax = fig.add_subplot(111, projection="3d")
ax.plot_wireframe(X, Y, Z, rstride=2, cstride=2, color="tab:blue")
ax.set_xlabel("X")
ax.set_ylabel("Y")
ax.set_zlabel("Z")
ax.set_title("Wireframe Plot")
fig.savefig("wireframe-plot.png", dpi=150, bbox_inches="tight")
plt.show()

青いグリッド線だけで描き、面を塗りつぶしていない、同じ動径サイン曲面のワイヤーフレーム図。

rstride と cstride 引数は行と列を間引くため、線が読みやすくなります。この例では、50×50グリッドの行と列を1つおきに描いています。plot_surface と異なり、ワイヤーフレームには面の色がないため、カラーバーで説明する対象がありません。最終的なプレゼンテーションでは、塗りつぶした曲面のほうが高さの差を伝えやすく、ワイヤーフレームはグリッド自体のサンプリングのされ方をより明確に示します。

形状の不一致エラーを修正する

plot_surface で最も多い失敗は、Z 配列の形状が座標グリッドと一致しないことです。次のスクリプトは Z を50×50グリッドで計算しながら、100×100の X、Y グリッドと一緒に渡しています:

import numpy as np
import matplotlib.pyplot as plt

x = np.linspace(-5, 5, 100)
y = np.linspace(-5, 5, 100)
X, Y = np.meshgrid(x, y)

x_small = np.linspace(-5, 5, 50)
y_small = np.linspace(-5, 5, 50)
X_small, Y_small = np.meshgrid(x_small, y_small)
Z = np.sin(np.sqrt(X_small**2 + Y_small**2))

fig = plt.figure()
ax = fig.add_subplot(111, projection="3d")
ax.plot_surface(X, Y, Z, cmap="viridis")
plt.show()

スクリプトを実行すると、plot_surface の呼び出しで次のエラーが発生して停止します:

ValueError: shape mismatch: objects cannot be broadcast to a single shape.  Mismatch is between arg 0 with shape (100, 100) and arg 2 with shape (50, 50).

修正するには、最初の例のように、plot_surface に渡すのと同じグリッドから Z を計算し、Z.shape が X.shape と Y.shape に等しくなるようにします。不安な場合は、描画前に3つの形状を表示して比較してください。Z の各要素には、ちょうど1組の座標が必要です。多すぎても少なすぎてもいけません。

まとめ

基本の手順は、座標グリッドを作り、Z の形状をグリッドに合わせ、3D Axesを追加して Axes3D.plot_surface を呼び出すことです。カラーマップとカラーバーで高さの値を読み取りやすくし、視点とサンプリング数を調整して形状を見やすくできます。グリッド自体を確認したい場合は、plot_wireframe が同じデータを線で描画します。画像ファイルが必要なら図を保存し、プロットするデータに合わせてグリッドの解像度や視点を調整してください。

よくある質問

  1. 曲面図にはどのようなデータを使えますか?
    2つの座標で定義され、各高さが長方形グリッド上の位置に対応する値を使います。説明用には関数が便利です。実測値も、互換性のある座標グリッドに並べれば利用できます。散在する点は、先にグリッドへ変換してください。

  2. 必要なライブラリはどのようにインストールしますか?
    スクリプトを実行する環境で python -m pip install numpy matplotlib を実行し、NumPyとMatplotlibをインストールします。

  3. 曲面の色は変更できますか?
    はい。plot_surface の cmap 引数を設定し、fig.colorbar(surface, ax=ax) でカラーバーを追加すると、色と値の対応を示せます。

  4. 視点はどのように変更しますか?
    ax.view_init(elev=..., azim=...) を使います。仰角はカメラの高さを変え、方位角はプロットの周りでカメラを回転させます。

  5. なぜ X、Y、Z の形状を合わせる必要がありますか?
    Matplotlibはグリッド上の各位置について、座標の組と高さを必要とします。Z が別のグリッドで計算されていると、plot_surface は一緒にブロードキャストできない形状についての ValueError を発生させます。np.meshgrid() で座標グリッドを作り、それぞれの要素が対応するよう、その同じグリッドから Z を計算してください。