目录4

文章绘图第 29 章

PARALLEL PLOT

绘图 · 第 29 / 36 章

基础平行坐标图

chart_111

基础平行坐标图
Python52 行
import matplotlib.pyplot as plt
import numpy as np

COLORS = ["#A8552E", "#35666E", "#D0A64F", "#3F2E18", "#7E8F52"]
BG = "#F7F3EC"

np.random.seed(42)
n = 60
n_dims = 5
# 模拟 5 维数据
X = np.random.normal(0, 1, (n, n_dims))
# 第 2 维与第 1 维相关
X[:, 1] = X[:, 0] * 0.7 + np.random.normal(0, 0.5, n)

dims = [f"Dim {i+1}" for i in range(n_dims)]

# 归一化到 0~1
X_norm = (X - X.min(axis=0)) / (X.max(axis=0) - X.min(axis=0))

fig, ax = plt.subplots(figsize=(11, 6))

x_positions = np.arange(n_dims)

# 画每条数据线
for i in range(n):
    ax.plot(x_positions, X_norm[i],
            color=COLORS[i % len(COLORS)],
            linewidth=1.2, alpha=0.5)

# 坐标轴(竖线)
for x in x_positions:
    ax.axvline(x, color=COLORS[3], linewidth=1.2,
               alpha=0.6, zorder=0)

# 刻度标签
ax.set_xticks(x_positions)
ax.set_xticklabels(dims, fontsize=11)
ax.set_yticks([])   # 平行坐标图通常隐藏 y 刻度

ax.set_ylim(-0.05, 1.05)
ax.set_xlim(-0.3, n_dims - 0.7)

ax.set_title("Parallel Plot — Basic",
             fontsize=15, color=COLORS[3], pad=12)
ax.tick_params(axis="x", colors=COLORS[3])

for s in ("top", "right", "left"):
    ax.spines[s].set_visible(False)
ax.spines["bottom"].set_visible(False)

plt.tight_layout()
plt.show()

按分类着色

chart_112

按分类着色
Python64 行
import matplotlib.pyplot as plt
import numpy as np

COLORS = ["#A8552E", "#35666E", "#D0A64F", "#3F2E18", "#7E8F52"]
BG = "#F7F3EC"

np.random.seed(0)
n_per_class = 30
n_dims = 6

# 三类数据,各有不同的多维模式
class_data = {
    "Class A": np.random.normal(0, 0.5, (n_per_class, n_dims)),
    "Class B": np.random.normal(0.8, 0.5, (n_per_class, n_dims)),
    "Class C": np.random.normal(-0.6, 0.5, (n_per_class, n_dims)),
}
# 给各类加特征
class_data["Class A"][:, 1] += 1.0
class_data["Class B"][:, 3] -= 1.2
class_data["Class C"][:, 4] += 1.5

X = np.vstack(list(class_data.values()))
dims = [f"Dim {i+1}" for i in range(n_dims)]

# 归一化
X_norm = (X - X.min(axis=0)) / (X.max(axis=0) - X.min(axis=0))

fig, ax = plt.subplots(figsize=(12, 6.5))

x_positions = np.arange(n_dims)

# 按类画线
start = 0
for (name, data), c in zip(class_data.items(), COLORS):
    end = start + len(data)
    for i in range(start, end):
        ax.plot(x_positions, X_norm[i],
                color=c, linewidth=1.4, alpha=0.55,
                label=name if i == start else None)
    start = end

# 竖轴
for x in x_positions:
    ax.axvline(x, color=COLORS[3], linewidth=1.2,
               alpha=0.6, zorder=0)

ax.set_xticks(x_positions)
ax.set_xticklabels(dims, fontsize=11)
ax.set_yticks([])
ax.set_ylim(-0.05, 1.05)
ax.set_xlim(-0.3, n_dims - 0.7)

ax.set_title("Parallel Plot by Class",
             fontsize=15, color=COLORS[3], pad=12)
ax.tick_params(axis="x", colors=COLORS[3])
ax.legend(frameon=False, fontsize=11, labelcolor=COLORS[3],
          loc="upper right", ncol=3)

for s in ("top", "right", "left"):
    ax.spines[s].set_visible(False)
ax.spines["bottom"].set_visible(False)

plt.tight_layout()
plt.show()

按数值渐变着色

chart_113

按数值渐变着色
Python62 行
import matplotlib.pyplot as plt
import numpy as np
from matplotlib.colors import LinearSegmentedColormap
from matplotlib.cm import ScalarMappable

BG = "#F7F3EC"
CMAP = LinearSegmentedColormap.from_list(
    "theme", ["#D0A64F", "#A8552E", "#3F2E18"], N=256
)

np.random.seed(1)
n = 80
n_dims = 6
X = np.random.normal(0, 1, (n, n_dims))

# 用一个连续变量(这里用第 1 维)映射颜色
color_var = X[:, 0]
norm = (color_var - color_var.min()) / (color_var.max() - color_var.min())

X_norm = (X - X.min(axis=0)) / (X.max(axis=0) - X.min(axis=0))

dims = [f"Dim {i+1}" for i in range(n_dims)]

fig, ax = plt.subplots(figsize=(12, 6.5))

x_positions = np.arange(n_dims)

# 先画浅色的线在底层
order = np.argsort(norm)   # 按颜色值排序,让高值在上层
for i in order:
    ax.plot(x_positions, X_norm[i],
            color=CMAP(norm[i]),
            linewidth=1.6, alpha=0.7)

for x in x_positions:
    ax.axvline(x, color="#3F2E18", linewidth=1.2,
               alpha=0.6, zorder=0)

ax.set_xticks(x_positions)
ax.set_xticklabels(dims, fontsize=11)
ax.set_yticks([])
ax.set_ylim(-0.05, 1.05)
ax.set_xlim(-0.3, n_dims - 0.7)

# 色条
sm = ScalarMappable(cmap=CMAP, norm=plt.Normalize(0, 1))
sm.set_array([])
cbar = fig.colorbar(sm, ax=ax, shrink=0.7, pad=0.02)
cbar.set_label("Color Variable", fontsize=11, color="#3F2E18")
cbar.ax.tick_params(colors="#3F2E18")
cbar.outline.set_edgecolor("#3F2E18")

ax.set_title("Parallel Plot — Continuous Color",
             fontsize=15, color="#3F2E18", pad=12)
ax.tick_params(axis="x", colors="#3F2E18")

for s in ("top", "right", "left"):
    ax.spines[s].set_visible(False)
ax.spines["bottom"].set_visible(False)

plt.tight_layout()
plt.show()

按维度独立归一化(真实量纲)

chart_114

按维度独立归一化(真实量纲)
Python58 行
import matplotlib.pyplot as plt
import numpy as np

COLORS = ["#A8552E", "#35666E", "#D0A64F", "#3F2E18", "#7E8F52"]
BG = "#F7F3EC"

np.random.seed(2)
n = 50
# 各维度量纲差异大
X = np.column_stack([
    np.random.normal(170, 10, n),      # 身高 cm
    np.random.normal(70, 12, n),       # 体重 kg
    np.random.normal(25, 5, n),        # BMI
    np.random.normal(100, 15, n),      # 血压
    np.random.normal(5.5, 1, n),       # 血糖
])
dims = ["Height\n(cm)", "Weight\n(kg)", "BMI", "BP\n(mmHg)", "Glucose"]

# 每列独立归一化
X_norm = (X - X.min(axis=0)) / (X.max(axis=0) - X.min(axis=0))

fig, ax = plt.subplots(figsize=(12, 6.5))

x_positions = np.arange(len(dims))

for i in range(n):
    ax.plot(x_positions, X_norm[i],
            color=COLORS[0], linewidth=1.2, alpha=0.35)

# 竖轴 + min/max 标注
for x, dim, col in zip(x_positions, dims, range(len(dims))):
    ax.axvline(x, color=COLORS[3], linewidth=1.2,
               alpha=0.6, zorder=0)
    # 顶部标 max
    ax.text(x, 1.04, f"{X[:, col].max():.1f}",
            ha="center", va="bottom",
            fontsize=9, color=COLORS[3])
    # 底部标 min
    ax.text(x, -0.04, f"{X[:, col].min():.1f}",
            ha="center", va="top",
            fontsize=9, color=COLORS[3])

ax.set_xticks(x_positions)
ax.set_xticklabels(dims, fontsize=11)
ax.set_yticks([])
ax.set_ylim(-0.1, 1.1)
ax.set_xlim(-0.3, len(dims) - 0.7)

ax.set_title("Parallel Plot — Per-Axis Scale",
             fontsize=15, color=COLORS[3], pad=12)
ax.tick_params(axis="x", colors=COLORS[3])

for s in ("top", "right", "left"):
    ax.spines[s].set_visible(False)
ax.spines["bottom"].set_visible(False)

plt.tight_layout()
plt.show()