目录9
文章/Matplotlib/第 8 章
二维直方图
Matplotlib · 第 8 / 43 章
二维直方图 hist2d()
一维直方图的两个变量版本:把平面切成网格,数每个格子落了多少点。点太多、散点图糊成一团时用它。
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() 最常用 |
最小示例
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()
bins:三种给法
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=[40, 20] 这种写法里,第一个数是 x 方向的箱数。搞反了图会被拉长,但不报错。
range:固定范围,别让极值压平色阶
ax.hist2d(x, y, bins=40, range=[[-4, 4], [-4, 4]], cmap="Blues")不给 range 时,范围由数据的最小最大值决定。只要有一个离群点,所有格子就被挤到很窄的一段颜色里,整张图变成一片浅蓝。固定范围是最有效的一招。
cmin / cmax:只显示想看的区间
ax.hist2d(x, y, bins=40, cmin=5) # 少于 5 个点的格子不画(边缘立刻干净)
ax.hist2d(x, y, bins=40, cmax=300) # 高于 300 的格子不画(避免中心一个黑洞)cmin=1 是常用值,等于「空桶不画」。它只影响渲染,h 里的计数一个不少。
norm:低计数区也要可见
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 教程抄代码时最常踩这个。
返回的四元组
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 版本对照
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 求峰值
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()