import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
import plotly.graph_objects as go雷达图 (Python)
雷达图的 Python 对应版本——雷达图(蛛网图、网图)以 \(p\) 条辐射轴各代表一个变量,把一条或多条序列画成闭合多边形,从而直观比较多维画像。在生物医学中,它是免疫信号面板 [1]、肿瘤功能评估、患者报告结局画像的标准汇总图:画像的形状往往与任何单一数值同等重要。
matplotlib 通过极坐标轴构建雷达图;plotly.graph_objects 的 Scatterpolar 提供适用于探索性仪表盘的交互版本 [2]最常见且足以毁掉真实分析的陷阱:雷达各轴必须处于可比较的量纲。当变量取值范围不同(0-1 比例 vs 0-100 评分)时,直接用原始值作图会扭曲每条画像——务必先做逐轴归一化。
本教程涵盖三种工作流:
- matplotlib 极坐标投影手工构建单序列雷达图;
- 逐轴 0-1 归一化的多序列雷达图;
- 面向仪表盘的
plotly交互雷达图。
Example

Setup
- 系统要求:跨平台(Linux/MacOS/Windows)
- 编程语言:Python
- 依赖包:
matplotlib、numpy、pandas、plotly
Data Preparation
我们模拟一个免疫反卷积面板 [1]:三种肿瘤免疫亚型在六种免疫细胞类型上的信号评分(0-1)。Immune-hot 亚型在适应性免疫与 NK 标志上得分高,Immune-cold 全面偏低,Fibrotic 呈髓系偏移——三种雷达图应能一眼区分的形状。
rng = np.random.default_rng(42)
cells = ["CD8 T cell", "CD4 T cell", "B cell", "NK cell", "Macrophage", "Dendritic"]
subtypes = ["Immune-hot", "Immune-cold", "Fibrotic"]
base = np.array([
[0.85, 0.55, 0.30, 0.70, 0.60, 0.45], # immune-hot:适应性 + NK 高
[0.25, 0.30, 0.15, 0.20, 0.35, 0.20], # immune-cold:全面偏低
[0.45, 0.50, 0.40, 0.25, 0.85, 0.70], # fibrotic:髓系偏移
])
scores = pd.DataFrame(base + rng.normal(0, 0.05, base.shape),
index=subtypes, columns=cells).clip(lower=0, upper=1)
# 极坐标脚手架:每种细胞一条轴,多边形通过在末尾重复第一个角度/取值闭合
angles = [n / len(cells) * 2 * np.pi for n in range(len(cells))]
angles += angles[:1]
scores.head()| CD8 T cell | CD4 T cell | B cell | NK cell | Macrophage | Dendritic | |
|---|---|---|---|---|---|---|
| Immune-hot | 0.865236 | 0.498001 | 0.337523 | 0.747028 | 0.502448 | 0.384891 |
| Immune-cold | 0.256392 | 0.284188 | 0.149160 | 0.157348 | 0.393970 | 0.238890 |
| Fibrotic | 0.453302 | 0.556362 | 0.423375 | 0.207035 | 0.868438 | 0.652056 |
Visualization
1. 单序列雷达图
values = scores.loc["Immune-hot"].tolist()
values += values[:1]
fig, ax = plt.subplots(figsize=(5.5, 5.2), subplot_kw={"projection": "polar"})
ax.plot(angles, values, color="#2166AC", linewidth=2)
ax.fill(angles, values, color="#2166AC", alpha=0.25)
ax.set_xticks(angles[:-1]); ax.set_xticklabels(cells, fontsize=10)
ax.set_ylim(0, 1); ax.set_yticks([0.25, 0.5, 0.75, 1.0])
ax.set_yticklabels(["0.25", "0.5", "0.75", "1.0"], fontsize=8, color="grey")
ax.set_title("Immune-hot subtype: signature scores", pad=20)
fig.tight_layout()
plt.show()
2. 逐轴归一化的多序列雷达图
逐轴 min-max 缩放到 0-1 后,所有轴共用同一量程,即使原始变量离散程度不同,形状比较依然诚实。
norm = (scores - scores.min()) / (scores.max() - scores.min())
colors = {"Immune-hot": "#B2182B", "Immune-cold": "#2166AC",
"Fibrotic": "#1B7837"}
fig, ax = plt.subplots(figsize=(5.8, 5.4), subplot_kw={"projection": "polar"})
for st in subtypes:
v = norm.loc[st].tolist(); v += v[:1]
ax.plot(angles, v, color=colors[st], linewidth=2, label=st)
ax.fill(angles, v, color=colors[st], alpha=0.12)
ax.set_xticks(angles[:-1]); ax.set_xticklabels(cells, fontsize=10)
ax.set_ylim(0, 1); ax.set_yticks([0.25, 0.5, 0.75, 1.0])
ax.set_yticklabels(["0.25", "0.5", "0.75", "1.0"], fontsize=8, color="grey")
ax.legend(loc="upper right", bbox_to_anchor=(1.3, 1.1), fontsize=9)
ax.set_title("Normalized per axis (0-1)", pad=20)
fig.tight_layout()
plt.show()
3. plotly 交互雷达图
fig = go.Figure()
for st in subtypes:
v = norm.loc[st].tolist(); v += v[:1]
fig.add_trace(go.Scatterpolar(
r=v, theta=cells + [cells[0]], fill="toself", name=st,
line=dict(color=colors[st])))
fig.update_layout(
polar=dict(radialaxis=dict(range=[0, 1], tickvals=[0.25, 0.5, 0.75, 1.0])),
width=560, height=480,
title="Immune signature radar (hover to compare)")
fig- 一定要
set_ylim():matplotlib 极坐标的 r 轴上限默认随数据变化,来自不同子集的两张雷达图会悄悄失去可比性。 - 严禁在不同量纲上叠加原始值——0-100 的评分会把所有 0-1 的比例压到圆心(见 Application 面板)。
- 叠加超过 3-4 条序列后雷达图将不可读;同一矩阵改用斜率/哑铃图或热图。
- 轴的排布应携带含义(细胞谱系顺序、流程顺序)——按字母排序的轴只会产生无意义的锯齿多边形。
Application
下面的面板复现了临床报告中最常见的雷达图错误:A 面板直接叠加三条亚型画像,其中一条的原始分数被放大 100 倍——该多边形撑满共享的 r 轴上限,其余序列全部塌缩成中心一团不可读的斑点。B 面板逐轴 min-max 归一化后,三种画像形状重新显现。

Reference
[1] Becht E, Giraldo NA, Lacroix L, et al. Estimating the abundance of cell populations from deconvolution of immune cell infiltrates. Genome Biol. 2016;17:218. doi:10.1186/s13059-016-1070-5
[2] Sievert C. Interactive Web-Based Data Visualization with R, plotly, and shiny. Chapman and Hall/CRC; 2020. doi:10.1201/9780429447273
