散点图是探索数据关系时最直觉的工具。但很多开发者只会画最基础的 x-y 点阵,遇到多变量场景就束手无策——要么画一堆子图,要么把信息硬塞进图例。plt.scatter() 其实提供了四种视觉通道:大小、颜色、形状、透明度,足够在一张图里同时编码四到五个维度。
下面从基础用法开始,逐步叠加这些通道,最后给出一个可直接运行的综合示例。
最简散点图:两个维度就够了
import matplotlib.pyplot as plt
import numpy as np
np.random.seed(42)
x = np.random.normal(5, 2, 80)
y = x * 1.5 + np.random.normal(0, 3, 80)
plt.scatter(x, y)
plt.xlabel("收入(万元)")
plt.ylabel("消费(万元)")
plt.title("收入与消费散点图")
plt.tight_layout()
plt.show()
这是入门级用法:只传 x 和 y。点的大小、颜色、形状全是默认值。信息密度低,但够用——前提是你只关心两个变量。
用颜色编码第三个维度
当数据自带分组标签或连续数值时,c 参数是最自然的扩展方式。
# 假设每条数据有一个"年龄段"标签:0=青年, 1=中年, 2=老年
age_group = np.random.randint(0, 3, 80)
plt.scatter(x, y, c=age_group, cmap="Set2", edgecolors="black")
plt.colorbar(label="年龄段(0=青年 1=中年 2=老年)")
plt.xlabel("收入(万元)")
plt.ylabel("消费(万元)")
plt.tight_layout()
plt.show()
几个要点:
c接受与数据等长的数组,可以是分类标签也可以是连续值。cmap控制颜色映射方案。分类数据用"Set2"、"tab10"这类离散色板;连续数据更适合"viridis"、"coolwarm"。edgecolors给点加描边,防止浅色点在白底上消失。plt.colorbar()是必须的——没有色条,颜色就是哑巴信息。
用大小编码第四个维度
s 参数控制每个点的面积(注意是像素面积,不是半径)。用它编码一个连续变量,比如"家庭人口数":
family_size = np.random.randint(1, 8, 80)
plt.scatter(
x, y,
c=age_group,
s=family_size * 30, # 放大系数让差异肉眼可见
cmap="Set2",
edgecolors="black",
alpha=0.7
)
plt.colorbar(label="年龄段")
plt.xlabel("收入(万元)")
plt.ylabel("消费(万元)")
plt.tight_layout()
plt.show()
实操建议:
- 原始数值往往太小或太大,需要乘一个系数做视觉缩放。先画一次看效果,再调整。
s的值域跨度太大时,小点会被大点完全遮挡——这时就该引入透明度。
透明度:解决遮挡的最后一块拼图
alpha 取值 0 到 1,控制点的透明程度。数据量大或大小差异显著时,设 alpha=0.5 到 0.7 是常见做法:
- 重叠区域会自然变暗,形成密度热力效果。
- 小点不再被大点吞掉。
不要设到 0.2 以下——点太淡等于没画。
形状:第五个维度的可选通道
marker 参数可以改变点的形状,但 plt.scatter() 的 marker 是全局参数,不支持逐点设置。要做多形状散点图,需要按组拆分多次调用:
markers = ["o", "s", "D"] # 圆形、方形、菱形
labels = ["青年", "中年", "老年"]
for g in range(3):
mask = age_group == g
plt.scatter(
x[mask], y[mask],
marker=markers[g],
s=family_size[mask] * 30,
label=labels[g],
alpha=0.7,
edgecolors="black"
)
plt.legend()
plt.xlabel("收入(万元)")
plt.ylabel("消费(万元)")
plt.tight_layout()
plt.show()
形状通道适合分类标签少于 5 种的场景。超过 5 种形状,人眼很难快速区分,不如回到颜色。
综合实战:一张图编码五个变量
把上面的技巧组合起来,下面是完整可运行示例。模拟一个"城市居民消费"数据集,同时展示收入、消费、年龄段、家庭规模、信用评分五个维度:
import matplotlib.pyplot as plt
import numpy as np
np.random.seed(42)
n = 200
# 生成模拟数据
income = np.random.normal(8, 3, n) # 收入(万元)
spending = income * 1.2 + np.random.normal(0, 4, n) # 消费(万元)
age_group = np.random.randint(0, 3, n) # 年龄段
family_size = np.random.randint(1, 7, n) # 家庭人口
credit_score = np.clip(income * 10 + np.random.normal(0, 20, n), 300, 900) # 信用评分
markers = ["o", "s", "D"]
group_labels = ["青年", "中年", "老年"]
fig, ax = plt.subplots(figsize=(8, 6))
for g in range(3):
mask = age_group == g
scatter = ax.scatter(
income[mask],
spending[mask],
c=credit_score[mask],
s=family_size[mask] * 40,
marker=markers[g],
cmap="coolwarm",
alpha=0.65,
edgecolors="black",
linewidths=0.5,
label=group_labels[g],
)
cbar = fig.colorbar(scatter, ax=ax)
cbar.set_label("信用评分")
# 图例中用统一大小,避免大小干扰图例识别
handles, labels = ax.get_legend_handles_labels()
for h in handles:
h.set_sizes([80])
ax.legend(handles, labels, title="年龄段")
ax.set_xlabel("收入(万元)")
ax.set_ylabel("消费(万元)")
ax.set_title("城市居民消费多维散点图")
ax.tight_layout()
plt.show()
这张图里:
- x 轴:收入
- y 轴:消费
- 颜色:信用评分(连续,coolwarm 色板)
- 大小:家庭人口
- 形状:年龄段(三类)
五个维度,一张图,没有子图堆叠,没有信息遗漏。
实用清单
画多维散点图前快速检查这几项:
| 检查点 | 建议 |
|---|---|
| 维度超过 5 个? | 不要硬塞,拆成两张图或换用交互式工具(Plotly、Bokeh) |
| 点数超过 1000? | 必须设 alpha,否则重叠区一片糊 |
| 颜色编码连续值? | 加 colorbar,选感知均匀色板(viridis、coolwarm) |
| 颜色编码分类值? | 用离散色板(Set2、tab10),加 legend |
| 大小差异太极端? | 对 s 值做对数缩放:s=np.log(values)*50 |
| 形状超过 5 种? | 放弃形状通道,回到颜色或拆图 |
| 图例里大小混乱? | 手动 h.set_sizes([80]) 统一图例点尺寸 |
最后一点容易被忽略:图例里的点如果保留了原始大小,大点会撑破图例布局。统一图例点尺寸是专业散点图的基本礼仪。
plt.scatter() 的参数组合远不止这些——linewidths、edgecolors、zorder 都能在特定场景下发挥作用。但大小、颜色、透明度、形状这四个核心通道,已经覆盖了绝大多数多维可视化的需求。下次画散点图时,先问自己"我到底要同时展示几个变量",再决定用哪些通道,而不是默认只画 x-y。