HOWTO · Matplotlib

在 Matplotlib 散点图中设置标记大小

了解如何在 Matplotlib 散点图中设置统一或逐点标记大小、正确缩放标记面积,并修复长度不匹配的大小数组。

本页内容

在 Matplotlib 散点图中设置标记大小时,请使用 Axes.scatter 的 s 参数。标量会为所有点应用相同的名义面积;一维列表或数组则为每个点指定面积,长度必须与 x 和 y 相同。

使用 s 设置标记大小

要在 Matplotlib 散点图中设置标记大小,请将 s 参数传给 Axes.scatter。s=80 这样的标量会让所有点使用相同的标称面积。一维列表或数组可以为每个点指定不同面积,但其值的数量必须与 x 和 y 相同。例如,一种大小可使用 ax.scatter(x, y, s=80),四个点使用不同大小可写成 ax.scatter(x, y, s=[20, 40, 80, 160])。

当每个观测值需要不同大小时,请使用 scatter。相关的 plot 方法则使用 markersize 指定一个线性标记尺寸。

为所有点设置相同大小或逐点设置大小

下面这个可重复的示例并排绘制一个采用标量大小的集合和一个根据数据计算大小的集合。它还把相同的值映射到颜色,并使用 alpha 让重叠的标记更容易辨认。代码会保存结果图像,因此不依赖交互式后端也可以检查结果。

import matplotlib.pyplot as plt

x = [1, 2, 3, 4, 5]
y = [2, 3, 5, 7, 11]
values = [1, 2, 4, 7, 11]
sizes = [40 + value * 20 for value in values]

fig, (top, bottom) = plt.subplots(2, 1, figsize=(6, 7), layout="constrained")
uniform = top.scatter(x, y, s=80, color="tab:blue")
varying = bottom.scatter(x, y, s=sizes, c=values, cmap="viridis", alpha=0.75)

top.set(title="s = 80", xlabel="x", ylabel="y")
bottom.set(title="s = [60, 80, 120, 180, 260]", xlabel="x", ylabel="y")
fig.colorbar(varying, ax=bottom)

print("scalar sizes:", uniform.get_sizes().tolist())
print("per-point sizes:", varying.get_sizes().tolist())
fig.savefig("scatter-marker-sizes.webp", dpi=120)

两个 PathCollection 报告 Matplotlib 实际保留的 s 值:

scalar sizes: [80]
per-point sizes: [60, 80, 120, 180, 260]

两个 Matplotlib 散点图,对比统一的标量标记面积与五个逐点标记面积。

这个转换从 40 开始,每增加一个数据单位就增加 20。因此,最小值仍然可见,最大值也不会盖住坐标轴。对于实际数据,应选用有明确含义的大小上下限,而不要直接传入极端的原始值。颜色可以编码同一变量,也可以编码另一个变量;只要颜色表示数量,就应添加颜色条。

理解平方点和默认值

Matplotlib 的 scatter 参考文档规定,s 的单位为平方点(pt²),其中一个排版点等于 1/72 英寸。默认值是 rcParams['lines.markersize'] ** 2。因此,s 与标称标记面积成比例,但 s=100 并不保证可见形状严格为 10×10 点。最终轮廓还会受到标记路径、图像 DPI 和边缘样式的影响。

边缘对小标记的影响尤其明显。当 linewidths 大于零并且 edgecolors 可见时,描边以标记边界为中心,有效外部尺寸会增加半个线宽。比较小型实心标记且不希望边缘改变其视觉大小时,可以使用 linewidths=0 或 edgecolors="none"。

标记大小是显示空间中的度量,而不是 x 轴或 y 轴的数据单位。因此,缩放视图或更改坐标轴范围并不会让标记表示数据坐标中的半径。

正确缩放标记面积和线性宽度

将 s 乘以 2 会使标称面积加倍。由于线性外部尺寸大致按面积的平方根增长,对于形状相同的标记,把 s 乘以 4 会让宽度约为原来的两倍。这是比例关系上的视觉规则,不是对每一种标记路径的精确几何保证。

import matplotlib.pyplot as plt

base = 36
area_sizes = [base, 2 * base, 4 * base]

fig, (top, bottom) = plt.subplots(2, 1, figsize=(6, 7), layout="constrained")
top.scatter([1, 2, 3], [1, 1, 1], s=area_sizes, linewidths=0)
top.set(title="s = [36, 72, 144]", yticks=[])
top.set_xticks([1, 2, 3], ["1", "2", "4"])

bottom.scatter([1], [1], s=10**2, linewidths=0)
(line,) = bottom.plot([2], [1], marker="o", linestyle="none", markersize=10,
                     color="tab:orange")
bottom.set(title="scatter: s = 10**2    plot: markersize = 10", xlim=(0.5, 2.5), yticks=[])

print("scatter areas:", area_sizes)
print("plot markersize:", line.get_markersize())
fig.savefig("scatter-size-scaling.webp", dpi=120)

所选值和 Line2D 标记尺寸如下:

scatter areas: [36, 72, 144]
plot markersize: 10.0

Matplotlib 输出,对比散点面积倍数,以及 scatter 的 s 和 plot 的 markersize。

选择 scatter(s=...) 或 plot(markersize=...)

绘制散点图时应使用 scatter(s=...),特别是每个观测值需要不同面积或颜色时。如果要绘制所有节点共享一种标记大小的 Line2D,请使用 plot(marker="o", markersize=10)。正如 Matplotlib 的 plot 参考文档所述,markersize(或 ms)是以点为单位的线性大小,并不是 scatter 接受的平方点面积。

对于圆形标记,scatter(s=10**2) 和 plot(markersize=10) 表达的是相互对应的标称尺寸,但不要依赖像素级完全一致。由于规范化路径、边缘、渲染后端和栅格化方式不同,集合标记和线条标记可能会有差异。

修复与点数不匹配的大小数组

非标量 s 的长度必须与所绘制点的数量相同。下面的示例捕获当前 Matplotlib 的诊断信息,以便在不显示完整回溯的情况下输出错误:

import matplotlib.pyplot as plt

x = [1, 2, 3]
y = [1, 4, 9]

try:
    plt.scatter(x, y, s=[40, 80])
except ValueError as error:
    print(f"{type(error).__name__}: {error}")
else:
    raise AssertionError("a mismatched size array should fail")

在 Matplotlib 3.11.2 中,输出如下:

ValueError: s (size 2) cannot be broadcast to match x and y (size 3)

较早版本的 Matplotlib 可能使用不同的 ValueError 文案,但修复方式相同:三个 (x, y) 点应提供三个大小值,或者只传入一个标量大小。