np.meshgridの使い方を完全図解 — 格子点の仕組みからindexing・sparse・3D可視化まで

3Dサーフェスで関数の形を眺めたい、等高線図でデータの分布を可視化したい、ヒートマップで2変数の関係を表したい — このような「2次元平面の上に何かを描く」場面で、必ずといってよいほど登場するのがNumPyのnp.meshgridです。

一方で、meshgridは「なんとなく雰囲気で使っている」人がとても多い関数でもあります。返り値がなぜ2つあるのか、なぜ行列で返ってくるのか、indexing='xy''ij'は何が違うのか — このあたりを曖昧にしたまま使っていると、いざ自分の思いどおりの格子を作りたいときに「あれ、どうなってるんだっけ?」と手が止まってしまいます。

meshgridを正しく理解すると、応用範囲は一気に広がります。たとえば次のような場面です。

  • 関数の可視化: 2変数関数 $z = f(x, y)$ の3Dサーフェスや等高線図を1行で計算する
  • ベクトル場の描画: 電磁場や流れ場をplt.quiverで矢印表示する
  • 機械学習の決定境界: 平面を格子で埋め尽くして、各点での予測値を色分けする
  • 画像処理・数値計算: ピクセル座標や差分格子の生成

本記事の内容

  • meshgridが何をする関数なのかを図で直感的に理解する
  • 返り値X・Yの中身と「なぜ座標の行列が2枚必要なのか」
  • indexing='xy''ij'の違い(最重要ポイント)
  • 3Dサーフェス・等高線・ベクトル場での実用例
  • sparse=Trueとブロードキャストの関係、mgrid/ogridとの比較
  • よくあるハマりどころと対処法

前提知識

この記事を読む前に、以下の記事を読んでおくと理解が深まります。

np.meshgridとは — 格子点を作る関数

meshgridの動作を一言でいうと、「1次元配列を2本(以上)渡すと、その総当たりで格子点を作り、各点の座標を返す関数」 です。

身近なイメージで考えてみましょう。碁盤の目のような街路を思い浮かべてください。南北方向の通りが6本、東西方向の通りが4本あれば、交差点は 6 × 4 = 24 個できます。交差点の場所を全部リストアップしたければ、「南北の通りの位置」と「東西の通りの位置」の全組み合わせを作ればよいわけです。meshgridがやっているのは、まさにこの「全組み合わせの座標リストアップ」です。

次の図が、meshgridの動作の全体像です。

np.meshgridの概念図 — 1次元配列2本から2次元の格子点を作る

この図のポイントは2つあります。まず、入力はオレンジのx = [0, 1, 2, 3, 4, 5](6要素)と緑のy = [0, 1, 2, 3](4要素)という、ただの1次元配列が2本だけということです。そして出力は、その総当たりでできる青い格子点 6 × 4 = 24 個の「座標一覧」です。入力の要素数の積が、そのまま格子点の総数になります。

コードにすると、たったこれだけです。

import numpy as np

x = np.arange(0, 6, 1)  # [0 1 2 3 4 5]
y = np.arange(0, 4, 1)  # [0 1 2 3]
X, Y = np.meshgrid(x, y)

print(X.shape)  # (4, 6)
print(Y.shape)  # (4, 6)

実行すると、XYはどちらも (4, 6) という形の2次元配列になっています。「24点の格子点を作ったのだから、サイズ24の座標ペアのリストが1本返ってくるのかと思いきや、行列が2枚返ってきた」— ここがmeshgridの最初のつまずきポイントです。この2枚の行列の正体を、次のセクションでじっくり見ていきましょう。

返り値X・Yの中身 — 「座標の行列」が2枚

meshgridの返り値は、次のルールで作られています。

  • 1枚目のX: 各格子点のx座標だけを集めた行列
  • 2枚目のY: 各格子点のy座標だけを集めた行列

つまり、座標のペア (x, y) を「x成分の行列」と「y成分の行列」に分解して持っているのです。実際に中身を表示してみましょう。

import numpy as np

x = np.arange(0, 6, 1)
y = np.arange(0, 4, 1)
X, Y = np.meshgrid(x, y)

print(X)
# [[0 1 2 3 4 5]
#  [0 1 2 3 4 5]
#  [0 1 2 3 4 5]
#  [0 1 2 3 4 5]]
print(Y)
# [[0 0 0 0 0 0]
#  [1 1 1 1 1 1]
#  [2 2 2 2 2 2]
#  [3 3 3 3 3 3]]

この中身を色付きで可視化したのが次の図です。

meshgridの返り値XとYの行列の中身

図を見ると、Xは「xの配列 [0 1 2 3 4 5] を縦に4回コピーしたもの」、Yは「yの配列 [0 1 2 3] を縦ベクトルにして横に6回コピーしたもの」になっていることがわかります。x座標は横(列)方向にだけ変化し、y座標は縦(行)方向にだけ変化する — これが格子点の座標を行列で表現したときの自然な姿です。

そして重要なのは、XとYの「同じ位置 [i, j]」の要素を1つずつ取り出してペアにすると、それが1つの格子点の座標になるという対応関係です。

XとYの同じ位置の要素ペアが1つの格子点の座標になる

たとえば図中の赤い点では、X[2, 4] = 4Y[2, 4] = 2なので、この位置は格子点 (4, 2) を表しています。行列の1つの「マス」が平面上の1つの「点」に対応している、というこの感覚をつかめば、meshgridはもう怖くありません。

では、なぜわざわざこんな形式で座標を持つのでしょうか。「(x, y) ペアのリスト」のほうが素直に見えるのに、です。その理由を次に見ていきます。

なぜ座標の行列が2枚必要なのか — ベクトル化計算のため

答えは、NumPyのベクトル化計算(要素ごとの一括演算)と相性が抜群によいからです。

2変数関数 $z = f(x, y)$ を格子点全部で評価したいとします。XとYが「同じ形の行列」になっていれば、forループを一切書かずに、次の1行で全点の計算が終わります。

import numpy as np

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

# 1万点(100×100)の関数値を1行で一括計算
Z = np.sin(X) * np.cos(Y)

print(Z.shape)  # (100, 100)

XYは同じ形 (100, 100) の行列なので、np.sin(X) * np.cos(Y)はNumPyの要素ごとの演算として処理され、結果のZも同じ形 (100, 100) になります。Z[i, j]には「格子点 (X[i, j], Y[i, j]) における関数値」が入っています。つまり、X・Y・Zの3枚の行列はすべて同じ形で、同じ位置の要素が同じ格子点に対応しているわけです。

matplotlibのplot_surfacecontourpcolormeshなどの2次元描画関数は、まさにこの「同じ形のX・Y・Zの3枚セット」を引数に取る設計になっています。meshgridが返す形式は、この後続処理にそのまま渡せる形だったのです。

ここまでで、meshgridの基本動作と設計思想がわかりました。ただし、meshgridには動作モードを切り替える重要な引数indexingがあります。これを知らないと配列の形が「転置」されて混乱するので、次に詳しく見ていきましょう。

indexing=’xy’ と ‘ij’ の違い(最重要ポイント)

np.meshgridにはindexingという引数があり、'xy'(デフォルト)と'ij'の2つのモードを選べます。

  • indexing='xy'(デフォルト、デカルト座標流): 返り値の形は (len(y), len(x))。「行=y、列=x」という画像・グラフの慣習に合わせたモード
  • indexing='ij'(行列インデックス流): 返り値の形は (len(x), len(y))。「第1軸=1番目の入力、第2軸=2番目の入力」という行列の添字の慣習に合わせたモード

文章だけではわかりにくいので、x = [0, 1, 2, 3](4要素)、y = [0, 1, 2](3要素)の場合を図で比べてみます。

indexing='xy'と'ij'の返り値の違い(転置の関係)

上段の'xy'では形が (3, 4)、つまり「行数 = yの要素数、列数 = xの要素数」です。下段の'ij'では形が (4, 3) で、ちょうど上段を転置した形になっています。値の並び自体はどちらも同じ情報で、'xy''ij'の返り値は互いに転置の関係にあります。

コードでも確認しましょう。

import numpy as np

x = np.array([0, 1, 2, 3])
y = np.array([0, 1, 2])

X_xy, Y_xy = np.meshgrid(x, y, indexing='xy')  # デフォルト
X_ij, Y_ij = np.meshgrid(x, y, indexing='ij')

print(X_xy.shape)  # (3, 4) = (len(y), len(x))
print(X_ij.shape)  # (4, 3) = (len(x), len(y))

# 'xy'と'ij'は互いに転置
print(np.array_equal(X_xy, X_ij.T))  # True
print(np.array_equal(Y_xy, Y_ij.T))  # True

np.array_equalの結果が両方Trueになることから、2つのモードが転置の関係にあることが確かめられます。

使い分けの目安は次のとおりです。

用途 推奨モード 理由
matplotlibでの描画(contour, surface等) 'xy' 描画関数が「行=y、列=x」を前提にしている
数値計算・偏微分方程式の差分格子 'ij' A[i, j]の i が第1変数、j が第2変数に素直に対応する
scipy.interpolate.RegularGridInterpolator等との連携 'ij' SciPyの格子系APIは行列添字流が多い

なお、3次元以上の格子(np.meshgrid(x, y, z))も作れますが、'xy'モードでは最初の2軸だけが入れ替わるという少し不思議な仕様なので、3次元以上では'ij'を使うほうが混乱がありません

基本の仕組みと2つのモードを押さえたので、ここからはmeshgridの「使いどころ」を実例で見ていきます。まずは定番の3Dサーフェスです。

実用例1: 3Dサーフェスで2次元ガウス分布を描く

2変数関数の形を直感的につかむには、3Dサーフェスが効果的です。ここでは多変量ガウス分布(2次元)の確率密度関数を描いてみましょう。

import numpy as np
import matplotlib.pyplot as plt
from scipy.stats import multivariate_normal

N = 60
x = np.linspace(-3, 3, N)
y = np.linspace(-3, 4, N)
X, Y = np.meshgrid(x, y)

# 平均と共分散行列
mu = np.array([0.0, 1.0])
Sigma = np.array([[1.0, -0.5],
                  [-0.5, 1.5]])

# (N, N, 2) の配列に座標を詰めてpdfを一括評価
pos = np.dstack((X, Y))
Z = multivariate_normal(mu, Sigma).pdf(pos)

fig = plt.figure(figsize=(8, 6))
ax = fig.add_subplot(111, projection='3d')
ax.plot_surface(X, Y, Z, rstride=2, cstride=2,
                linewidth=0.3, antialiased=True, cmap='viridis')
ax.set_xlabel("x")
ax.set_ylabel("y")
ax.set_zlabel("確率密度")
plt.show()

meshgridで作った格子上に2次元ガウス分布を描いた3Dサーフェス

平均 (0, 1) を中心とした釣鐘型のサーフェスが描けました。共分散行列の非対角成分が負(-0.5)なので、山が斜め方向(xが大きいとyが小さい向き)に傾いているのも読み取れます。コードの流れは「meshgridで格子 → np.dstackでX・Yを (N, N, 2) に束ねる → pdfで全点一括評価 → plot_surface(X, Y, Z)」で、X・Y・Zが同じ形の3枚セットになっている点に注目してください。

なお、np.dstack((X, Y))は、SciPyのmultivariate_normal.pdfが「最後の軸が座標の次元」という形式の入力を要求するための変形です。古い記事やブログではfig.gca(projection='3d')という書き方も見かけますが、これは新しいmatplotlibでは削除されているので、上記のようにadd_subplot(111, projection='3d')を使ってください。

3Dサーフェスは見た目が派手な一方、定量的な読み取りには等高線図のほうが向いています。同じデータでそのまま描けるので、次に見てみましょう。

実用例2: 等高線図(contour / contourf)

contour(線のみ)とcontourf(塗りつぶし)は、どちらも(X, Y, Z)の3枚セットをそのまま受け取ります。

import numpy as np
import matplotlib.pyplot as plt
from scipy.stats import multivariate_normal

N = 60
x = np.linspace(-3, 3, N)
y = np.linspace(-3, 4, N)
X, Y = np.meshgrid(x, y)
Z = multivariate_normal([0.0, 1.0],
                        [[1.0, -0.5], [-0.5, 1.5]]).pdf(np.dstack((X, Y)))

fig, axes = plt.subplots(1, 2, figsize=(11, 4.6))

# 左: 等高線(線のみ)
cs = axes[0].contour(X, Y, Z, levels=10, cmap='viridis')
axes[0].clabel(cs, inline=True, fontsize=7)
axes[0].set_title("contour")

# 右: 塗りつぶし等高線
cf = axes[1].contourf(X, Y, Z, levels=20, cmap='viridis')
fig.colorbar(cf, ax=axes[1])
axes[1].set_title("contourf")
plt.show()

meshgridを使ったcontourとcontourfによる等高線図

等高線図にすると、3Dサーフェスでは見えにくかった情報がはっきりします。等高線が同心の楕円で、その楕円が左上がりに傾いている — つまり負の相関を持つガウス分布であることが、一目で定量的に読み取れます。levels引数で等高線の本数を、cmapで配色を調整できます。配色(カラーマップ)の選び方はmatplotlibのcolormap解説記事で詳しく扱っています。

スカラー値(高さ)の可視化を見てきましたが、meshgridは「各格子点にベクトルを置く」可視化でも主役です。次はベクトル場を描きます。

実用例3: ベクトル場(quiver)

電磁場・流れ場・勾配ベクトルなどの可視化では、「格子点ごとに矢印を1本描く」ことになります。plt.quiverは、格子点の座標 (X, Y) と、各点でのベクトル成分 (U, V) を受け取ります。ここでは回転場 $(u, v) = (-y, x)$ を描いてみます。

import numpy as np
import matplotlib.pyplot as plt

x = np.linspace(-2, 2, 17)
y = np.linspace(-2, 2, 17)
X, Y = np.meshgrid(x, y)

# 各格子点でのベクトル成分(反時計回りの回転場)
U = -Y
V = X
speed = np.sqrt(U**2 + V**2)  # 矢印の色用に大きさを計算

fig, ax = plt.subplots(figsize=(6.8, 6))
q = ax.quiver(X, Y, U, V, speed, cmap='plasma', scale=40)
fig.colorbar(q, ax=ax, label="ベクトルの大きさ")
ax.set_aspect('equal')
plt.show()

meshgridとquiverで描いた回転ベクトル場

原点を中心に反時計回りの渦が描けました。矢印の色は大きさ $\sqrt{u^2 + v^2} = \sqrt{x^2 + y^2}$ に対応していて、原点から離れるほど矢印が長く・明るくなっています。これは理論どおり、回転場の速さが中心からの距離に比例することを示しています。ここでも、U・VをmeshgridのX・YからU = -YV = X一括計算できている点が、meshgrid形式の威力です。quiverの細かい使い方はplt.quiverの解説記事を参照してください。

ここまでの例では、格子点の数だけ巨大な行列X・Yを正直に作っていました。しかし実は、メモリを節約できる賢いオプションがあります。それがsparse=Trueです。

sparse=True とブロードキャストの関係

格子が大きくなると、X・Yの行列もそれに比例して大きくなります。たとえば 10,000 × 10,000 の格子なら、X・Yそれぞれが1億要素(float64で800MB)です。しかしXの中身を思い出すと「同じ行が縦にコピーされているだけ」でした。コピーを持つのは明らかに無駄です。

そこでsparse=Trueを指定すると、meshgridはコピーを展開せず、Xは1行だけ(形 (1, len(x)))、Yは1列だけ(形 (len(y), 1)) の配列を返します。

sparse=Trueのmeshgridとブロードキャストによる展開

「1行と1列だけで、格子点全体の計算ができるのか?」と思うかもしれませんが、できます。図の右側に示したように、形 (1, 6) と (4, 1) の配列を演算すると、NumPyのブロードキャスト機能が自動的に両者を (4, 6) に展開して計算してくれるからです。つまり、計算結果は通常のmeshgridと完全に同じで、メモリ消費だけが劇的に減ります。

import numpy as np

x = np.linspace(-3, 3, 1000)
y = np.linspace(-3, 3, 1000)

# 通常のmeshgrid: 1000x1000の行列を2枚作る
X_full, Y_full = np.meshgrid(x, y)
# sparse=True: 1行と1列だけ
X_sp, Y_sp = np.meshgrid(x, y, sparse=True)

print(X_full.shape, Y_full.shape)  # (1000, 1000) (1000, 1000)
print(X_sp.shape, Y_sp.shape)      # (1, 1000) (1000, 1)

# メモリ使用量の比較
print(X_full.nbytes + Y_full.nbytes)  # 16000000 (16MB)
print(X_sp.nbytes + Y_sp.nbytes)      # 16000 (16KB)

# 計算結果は完全に一致する
Z_full = np.sin(X_full) * np.cos(Y_full)
Z_sp = np.sin(X_sp) * np.cos(Y_sp)  # ブロードキャストで(1000, 1000)に展開
print(Z_sp.shape)                   # (1000, 1000)
print(np.array_equal(Z_full, Z_sp))  # True

メモリ使用量は16MBから16KBへと1000分の1になり、それでいて計算結果Zは通常版と完全に一致しています。関数値の計算が目的ならsparse=Trueで十分ですが、plot_surfaceのように「X・Y自体が完全な行列であること」を要求する描画関数もあるので、描画用には通常のmeshgridを使うと安全です。

meshgridとブロードキャストの関係がわかると、NumPyにある「親戚」の関数たちも一気に理解できます。mgridogridです。

np.mgrid / np.ogrid との比較

NumPyには、meshgridと同じことをスライス記法で書けるnp.mgridnp.ogridがあります。対応関係は次のとおりです。

書き方 等価なmeshgrid 返り値
np.mgrid[0:4, 0:6] np.meshgrid(np.arange(4), np.arange(6), indexing='ij') 完全な行列(密)
np.ogrid[0:4, 0:6] np.meshgrid(np.arange(4), np.arange(6), indexing='ij', sparse=True) 1行・1列のみ(疎)
import numpy as np

# mgrid: スライス記法で密な格子(indexing='ij'相当)
Y1, X1 = np.mgrid[0:4, 0:6]
Y2, X2 = np.meshgrid(np.arange(4), np.arange(6), indexing='ij')
print(np.array_equal(Y1, Y2), np.array_equal(X1, X2))  # True True

# ogrid: スライス記法で疎な格子(sparse=True相当)
Yo, Xo = np.ogrid[0:4, 0:6]
print(Yo.shape, Xo.shape)  # (4, 1) (1, 6)

# 複素数ステップで「分割数」指定もできる(linspace相当)
Z1, Z2 = np.mgrid[0:1:5j, 0:2:3j]
print(Z1.shape)  # (5, 3)

ポイントは3つです。第一に、mgrid/ogrid常にindexing='ij'で動くため、返り値の第1軸が1番目のスライスに対応します。第二に、ステップに5jのような複素数を書くと「その区間を5分割」というlinspace相当の意味になります。第三に、ogridsparse=Trueのmeshgridと同じ疎な配列を返します。数値計算系のコードではmgridのほうが簡潔に書けるため好まれることも多いですが、中身はmeshgridと同じものだと理解しておけば十分です。

道具が出そろったところで、最後に、実際にmeshgridを使っていて「図が変だ」「エラーが出る」となりがちなパターンをまとめておきます。

よくあるハマりどころ

ハマりどころ1: Zのshapeと(X, Y)の対応の取り違え

最も多い事故は、自前で計算したZの形(行と列の対応)がXYとずれているケースです。デフォルトのindexing='xy'では行がy、列がxなので、「行がx」のつもりで作ったZを渡すと、図が転置されて描かれます。

Zのshapeと(X,Y)の対応を取り違えたときの図の崩れ

左が正しい図で、関数 $z = \exp(-(x-3)^2 – (y-0.5)^2)$ のピークがちゃんと (3, 0.5) にあります。右は同じZを転置して渡してしまった例で、ピークが (1, 1.5) 付近のまったく違う場所に現れています。恐ろしいのは、xyの要素数が同じだとエラーにならず、間違った図が黙って出力されることです。ピーク位置など「答えがわかっている点」を1つ決めて、図と一致するか確認する習慣をつけましょう。

ハマりどころ2: 要素数が違えばエラーで気づける

逆にいうと、デバッグの観点ではxyの要素数をわざと変えておくのが有効です。要素数が違えば、対応を間違えた瞬間にshape不一致のエラーが出るからです。

import numpy as np

x = np.linspace(0, 4, 50)   # 50要素
y = np.linspace(0, 2, 30)   # 30要素
X, Y = np.meshgrid(x, y)
print(X.shape)  # (30, 50) ← (len(y), len(x))。(50, 30)ではない!

(50, 30)を期待していると逆なので注意してください。「デフォルトのmeshgridでは、shapeの順序は入力と逆(y側が先)」とおぼえておきましょう。

ハマりどころ3: 格子点を「点のリスト」に変換したいとき

機械学習の決定境界の描画などでは、格子点を (N点, 2) の座標リストにしてmodel.predictに渡したい場面があります。ravelで平らにしてnp.column_stackで束ね、予測結果をreshapeで格子の形に戻すのが定石です。

import numpy as np

x = np.linspace(0, 1, 100)
y = np.linspace(0, 1, 80)
X, Y = np.meshgrid(x, y)

# (8000, 2) の座標リストに変換
points = np.column_stack([X.ravel(), Y.ravel()])
print(points.shape)  # (8000, 2)

# 各点で計算した結果(ここでは例として距離)を格子の形に戻す
dist = np.sqrt(points[:, 0]**2 + points[:, 1]**2)
Z = dist.reshape(X.shape)
print(Z.shape)  # (80, 100)

ravelで行き、reshape(X.shape)で帰る」という往復がペアになっている点が肝心です。途中で並び順を変えなければ、各点の計算結果は正しく元の格子位置に戻ります。

ハマりどころ4: imshowは座標を見ない

imshow(X, Y)を受け取らず、行列Zを「画像」として表示する関数です。軸の目盛りはピクセル番号になり、さらにデフォルトでは上の行が上に表示される(yが下向きに増える)ため、contourfと上下が逆に見えることがあります。座標軸を持つデータにはpcolormesh(X, Y, Z)contourf(X, Y, Z)を使うか、imshow(Z, extent=[x0, x1, y0, y1], origin='lower')のようにextentoriginを明示しましょう。

まとめ

本記事では、np.meshgridの仕組みと実用パターンを解説しました。要点をまとめます。

  • meshgridは1次元配列2本の総当たりで格子点を作り、x座標の行列Xとy座標の行列Yを返す関数。XとYの同じ位置 [i, j] のペアが1つの格子点に対応する
  • 座標を行列で持つのは、$Z = f(X, Y)$ というベクトル化された一括計算と、matplotlibの描画関数(surface/contour/quiver)にそのまま渡すため
  • デフォルトのindexing='xy'ではshapeが(len(y), len(x))になる。数値計算では'ij'(shapeが(len(x), len(y)))も使われ、両者は互いに転置の関係
  • sparse=Trueは1行・1列だけを持ち、ブロードキャストで展開されるためメモリ効率がよい。mgrid/ogridはそれぞれ密・疎な格子のスライス記法版(常に'ij'流)
  • ハマりどころは「Zの転置」「shapeの順序」「imshowの軸」— ピーク位置など答えのわかる点で必ず検算する

meshgridを使いこなせると、2次元の可視化や数値計算の表現力が大きく広がります。次のステップとして、以下の記事も参考にしてください。