目录9

文章Matplotlib第 8 章

二维直方图

Matplotlib · 第 8 / 43 章

二维直方图 hist2d()

一维直方图的两个变量版本:把平面切成网格,数每个格子落了多少点。点太多、散点图糊成一团时用它。

Python11 行
Axes.hist2d(
    x, y,                # 两个等长一维数组
    bins=10,             # int / [nx, ny] / [xedges, yedges];默认 10(很粗)
    range=None,          # [[xmin,xmax],[ymin,ymax]]
    density=False,       # 归一化,使 sum(h) * dx * dy == 1
    weights=None,        # 每点权重,与 x 等长
    cmin=None,           # 计数低于此值的格子不画
    cmax=None,           # 计数高于此值的格子不画
    data=None,
    **kwargs             # 传给 QuadMesh,如 cmap、norm、alpha
)
参数 一句话 备注
bins 网格密度 默认 10,通常要调到 30~50
range 统计范围 固定色阶范围,避免极值把颜色拉平
cmin / cmax 显示阈值 只影响显示,不改变 h 里的计数
cmap 配色 只能用 matplotlib 自带的,seaborn 专有的会报错
norm 归一化对象 LogNorm() 最常用

最小示例

Python8 行
rng = np.random.default_rng(42)
x = rng.normal(0, 1, 8000)
y = 0.7 * x + np.sqrt(1 - 0.7 ** 2) * rng.normal(0, 1, 8000)

fig, ax = plt.subplots()
h, xedges, yedges, im = ax.hist2d(x, y, bins=30, cmap="Blues")
fig.colorbar(im, ax=ax, label="计数")
plt.show()

8000 个点、30×30 网格的二维直方图

bins:三种给法

Python4 行
ax.hist2d(x, y, bins=10)                     # 两向同数(默认,太粗)
ax.hist2d(x, y, bins=[40, 20])               # x 向 40 箱,y 向 20 箱
edges = np.arange(-4, 4.25, 0.25)
ax.hist2d(x, y, bins=[edges, edges])         # 给边界数组,两向可以不同

bins 的三种给法:同数、分开给、边界数组

bins=[40, 20] 这种写法里,第一个数是 x 方向的箱数。搞反了图会被拉长,但不报错。

range:固定范围,别让极值压平色阶

Python1 行
ax.hist2d(x, y, bins=40, range=[[-4, 4], [-4, 4]], cmap="Blues")

不给 range 时,范围由数据的最小最大值决定。只要有一个离群点,所有格子就被挤到很窄的一段颜色里,整张图变成一片浅蓝。固定范围是最有效的一招。

cmin / cmax:只显示想看的区间

Python2 行
ax.hist2d(x, y, bins=40, cmin=5)      # 少于 5 个点的格子不画(边缘立刻干净)
ax.hist2d(x, y, bins=40, cmax=300)    # 高于 300 的格子不画(避免中心一个黑洞)

cmin=1 是常用值,等于「空桶不画」。它只影响渲染,h 里的计数一个不少。

norm:低计数区也要可见

Python3 行
from matplotlib.colors import LogNorm

ax.hist2d(x, y, bins=40, norm=LogNorm(), cmap="Blues")

线性色阶下,中心密区占满整个色带,边缘稀疏区全是一个颜色。LogNorm() 把颜色映射改成对数,稀疏区也能分出层次——代价是不能用颜色深浅直接比较计数(差一倍和差十倍看起来差不多)。

cmap:只能用 matplotlib 自带的

hist2d 的 cmap 走的是普通 colormap 查表,可用的名字就是 plt.colormaps 里那些:"Blues" "viridis" "magma" "inferno" "cividis"…

"mako" / "rocket" 是 seaborn 专有的,纯 matplotlib 环境里会 ValueError: 'mako' is not a valid value for cmap。 从 seaborn 教程抄代码时最常踩这个。

返回的四元组

Python18 行
h, xedges, yedges, mesh = ax.hist2d(x, y, bins=30)
# h       : 2D 计数矩阵,shape == (len(xedges)-1, len(yedges)-1)
# xedges  : x 向箱边界,(nx+1,)
# yedges  : y 向箱边界,(ny+1,)
# mesh    : QuadMesh  ← 只有它才能传给 colorbar

fig.colorbar(mesh, ax=ax)      # ✅ 正确:传 mesh
# fig.colorbar(h, ax=ax)       # ❌ 报错:numpy 数组不是 Artist

# 不解包的老写法:四元组的第 4 个元素就是 mesh
res = ax.hist2d(x, y, bins=30)
fig.colorbar(res[3], ax=ax)    # ✅ 等价于传 mesh

# 拿到 h 就能做数值加工:找最密的格子
iy, ix = np.unravel_index(np.argmax(h), h.shape)
cx = (xedges[ix] + xedges[ix + 1]) / 2
cy = (yedges[iy] + yedges[iy + 1]) / 2
print(f"峰值在 ({cx:.2f}, {cy:.2f})")

注意 h[3] 和 res[3] 的区别:在没解包时(res = ax.hist2d(...)),res[3] 是那个 QuadMesh;一旦解包成 h, xedges, yedges, mesh,h 已经是计数矩阵,h[3] 只是矩阵的第 4 行,传进 colorbar 会报 'numpy.ndarray' object has no attribute 'cmap'。

要注意维度约定:h[i, j] 是「x 在第 i 箱、y 在第 j 箱」,而画图时会被转置。自己取数值时别和 np.histogram2d 的返回顺序搞混——两者索引含义一致,但 imshow(h.T) 之类的手工绘制很容易转错。

返回对象与 numpy 版本对照
Python8 行
h, xedges, yedges, mesh = ax.hist2d(x, y, bins=30)
fig.colorbar(mesh, ax=ax)              # ✅ 传 mesh
# fig.colorbar(h, ax=ax)               # ❌ numpy 数组不是 Artist

H, XE, YE = np.histogram2d(x, y, bins=30)     # 纯 numpy 版本,同一套分箱
print(H.shape, H.sum())
# H             : 计数矩阵,H.sum() == len(x)
# xedges/yedges : 与 hist2d 的含义相同

np.histogram2d 只算不画。想要数值而不是图时用它,省掉创建 Artist 的开销。

综合示例:固定范围 + 对数色阶 + 用 h 求峰值

Python50 行
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.colors import LogNorm

plt.rcParams["font.sans-serif"] = ["SimHei", "Microsoft YaHei", "DejaVu Sans"]
plt.rcParams["axes.unicode_minus"] = False

rng = np.random.default_rng(42)
N = 20000
x = rng.normal(0, 1, N)
y = 0.7 * x + np.sqrt(1 - 0.7 ** 2) * rng.normal(0, 1, N)   # 相关系数约 0.7

fig, axes = plt.subplots(1, 3, figsize=(16, 4.8))

# ---- (a) 基础:bins=[nx, ny] + cmin 隐藏空格子 ----
h1, xe1, ye1, mesh1 = axes[0].hist2d(
    x, y,
    bins=[40, 40],
    range=[[-4, 4], [-4, 4]],      # 固定范围,避免极值拉伸色阶
    cmap="Blues",
    cmin=1,                        # 计数 < 1 的格子不画 → 边缘干净
)
fig.colorbar(mesh1, ax=axes[0], label="计数")     # ★ 必须用 mesh,不能传 h
axes[0].set_title("(a) hist2d(bins=[40,40], cmin=1)")
axes[0].set_xlabel("X"); axes[0].set_ylabel("Y")
axes[0].set_aspect("equal")

# ---- (b) 对数色阶:兼顾密集区与稀疏区 ----
h2, xe2, ye2, mesh2 = axes[1].hist2d(x, y, bins=45, range=[[-4, 4], [-4, 4]],
                                     cmap="viridis", norm=LogNorm())
fig.colorbar(mesh2, ax=axes[1], label="计数(对数)")
axes[1].set_title("(b) norm=LogNorm() 对数色阶")
axes[1].set_xlabel("X"); axes[1].set_ylabel("Y")
axes[1].set_aspect("equal")

# ---- (c) 用返回的 h 做二次加工:标出密度峰值 ----
h3, xe3, ye3, mesh3 = axes[2].hist2d(x, y, bins=30, range=[[-4, 4], [-4, 4]],
                                     cmap="magma", density=True)
iy, ix = np.unravel_index(np.argmax(h3), h3.shape)
cx = (xe3[ix] + xe3[ix + 1]) / 2
cy = (ye3[iy] + ye3[iy + 1]) / 2
axes[2].plot(cx, cy, "c*", markersize=14, markeredgecolor="white",
             label=f"峰值 ({cx:.2f}, {cy:.2f})")
axes[2].legend(loc="upper left", fontsize=9)
axes[2].set_title("(c) density=True + 用 h 求峰值位置")
axes[2].set_xlabel("X"); axes[2].set_ylabel("Y")
axes[2].set_aspect("equal")

plt.tight_layout()
plt.show()

三张对照:基础 + cmin、对数色阶、标出峰值位置