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の動作の全体像です。

この図のポイントは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)
実行すると、XとYはどちらも (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]]
この中身を色付きで可視化したのが次の図です。

図を見ると、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[2, 4] = 4とY[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)
XとYは同じ形 (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_surface・contour・pcolormeshなどの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要素)の場合を図で比べてみます。

上段の'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()

平均 (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()

等高線図にすると、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()

原点を中心に反時計回りの渦が描けました。矢印の色は大きさ $\sqrt{u^2 + v^2} = \sqrt{x^2 + y^2}$ に対応していて、原点から離れるほど矢印が長く・明るくなっています。これは理論どおり、回転場の速さが中心からの距離に比例することを示しています。ここでも、U・VをmeshgridのX・YからU = -Y、V = 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)) の配列を返します。

「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にある「親戚」の関数たちも一気に理解できます。mgridとogridです。
np.mgrid / np.ogrid との比較
NumPyには、meshgridと同じことをスライス記法で書けるnp.mgridとnp.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相当の意味になります。第三に、ogridはsparse=Trueのmeshgridと同じ疎な配列を返します。数値計算系のコードではmgridのほうが簡潔に書けるため好まれることも多いですが、中身はmeshgridと同じものだと理解しておけば十分です。
道具が出そろったところで、最後に、実際にmeshgridを使っていて「図が変だ」「エラーが出る」となりがちなパターンをまとめておきます。
よくあるハマりどころ
ハマりどころ1: Zのshapeと(X, Y)の対応の取り違え
最も多い事故は、自前で計算したZの形(行と列の対応)がX・Yとずれているケースです。デフォルトのindexing='xy'では行がy、列がxなので、「行がx」のつもりで作ったZを渡すと、図が転置されて描かれます。

左が正しい図で、関数 $z = \exp(-(x-3)^2 – (y-0.5)^2)$ のピークがちゃんと (3, 0.5) にあります。右は同じZを転置して渡してしまった例で、ピークが (1, 1.5) 付近のまったく違う場所に現れています。恐ろしいのは、xとyの要素数が同じだとエラーにならず、間違った図が黙って出力されることです。ピーク位置など「答えがわかっている点」を1つ決めて、図と一致するか確認する習慣をつけましょう。
ハマりどころ2: 要素数が違えばエラーで気づける
逆にいうと、デバッグの観点ではxとyの要素数をわざと変えておくのが有効です。要素数が違えば、対応を間違えた瞬間に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')のようにextentとoriginを明示しましょう。
まとめ
本記事では、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次元の可視化や数値計算の表現力が大きく広がります。次のステップとして、以下の記事も参考にしてください。