关于 statistical-significance-annotation
本指南介绍如何用括号和星号标注比较图中的统计检验结果,适用于准备科研图表的研究者。内容涵盖比较对象选择、多重检验校正,以及可复用的 matplotlib 实现。
主要功能
- 01将 p 值映射为星号标记
- 02选择需要标注的比较
- 03处理多重检验校正
- 04放置并堆叠图表括号
- 05使用 matplotlib 添加标注
- 06报告检验方法与样本信息
适用场景
- 01标注处理组与对照组的比较
- 02展示选定的事后比较结果
- 03在图中呈现校正后的 p 值
- 04避免多组图表标注过密
安装
复制下面的命令,在终端中运行。
$
npx skills add https:/ / github.com/ jaechang-hits/ sciagent-skills --skill statistical-significance-annotation安装后在 Claude Code 中输入/statistical-significance-annotation确认已加载。
详细介绍
图表上的统计显著性标注
概述
统计显著性标注(星号表示法)是放置在比较图上的视觉标记,用于指示组间假设检验的结果。它们由连接两组的括号和表示 p 值范围的星号组成。恰当的标注可确保图表中的视觉结论与定量证据一致,使图表达到可发表的标准并符合科学严谨性。本指南介绍标准惯例、何时以及如何进行标注,并提供可复用的 matplotlib 实现。
核心概念
标准星号表示法
广泛采用的惯例是将 p 值范围映射为星号:
| 符号 | P 值范围 | 含义 |
|---|---|---|
| ns | p > 0.05 | 不显著 |
| * | p <= 0.05 | 显著 |
| ** | p <= 0.01 | 高度显著 |
| *** | p <= 0.001 | 极显著 |
| **** | p <= 0.0001 | 极其显著 |
转换函数:
def pvalue_to_asterisk(p: float) -> str:
"""Convert a p-value to standard asterisk notation."""
if p <= 0.0001:
return "****"
elif p <= 0.001:
return "***"
elif p <= 0.01:
return "**"
elif p <= 0.05:
return "*"
else:
return "ns"
校正后的 p 值与原始 p 值
- 单次比较(一次 t 检验):使用原始 p 值。
- 多重比较(针对 3 组或更多组进行成对检验、比较多个基因):使用校正后的 p 值(FDR/Benjamini-Hochberg 或 Bonferroni)。使用原始 p 值进行标注会夸大显著性。
- 预先计算的结果(DESeq2 的
padj、ANOVA 事后检验):使用已提供的校正值。
选择比较对象
并非每一对组都需要标注。选择符合以下条件的比较:
- 直接支持分析文本中提出的结论
- 具有生物学意义(例如,处理组与对照组,而非对照组 A 与对照组 B)
- 数量有限,以保持图表易读(通常每个面板 1-5 组比较)
决策框架
Does the plot compare groups?
├── No (scatter, heatmap, PCA, line trend) → Do NOT annotate
└── Yes (box, violin, bar, strip)
├── Does the analysis claim significance? → Annotate the claimed comparisons
├── Exploratory (no specific claim) → Annotate vs control only, or skip
└── Too many groups (>6 pairwise) → Annotate key comparisons only
| 场景 | 是否标注? | 比较对象 |
|---|---|---|
| DEG 箱线图:处理组与对照组 | 是 | 处理组与对照组 |
| 多组 ANOVA,带事后检验 | 是 | 仅标注显著的事后检验比较 |
| 10 种细胞类型的基因表达 | 有选择地标注 | 仅与参考细胞类型比较 |
| PCA 或 UMAP | 否 | N/A |
| 热图或火山图 | 否 | N/A |
| 相关性散点图 | 否 | 在正文/图例中报告 r 和 p |
| 探索性柱状图,无假设 | 可选 | 若适用,与对照组比较 |
最佳实践
- 让标注与文本结论一致:图上的每个星号都必须对应分析中描述的统计检验。未经计算检验,切勿进行标注。
- 多重比较时使用校正后的 p 值:检验不止一对时,务必使用经过 FDR 或 Bonferroni 校正的 p 值。在图例中注明校正方法。
- 限制标注的比较数量:仅标注与分析结论相关的比较。过度标注会使图表显得杂乱并分散重点。
- 清晰放置括号:将括号放在数据范围上方,并留出足够的垂直间距,避免与数据点、误差线或其他括号重叠。多个括号应以一致的间距堆叠。
- 注明统计检验:务必在图表标题、说明或图例中注明所用检验(t 检验、Mann-Whitney U、Wilcoxon、ANOVA + Tukey HSD 等)。
- 注明样本量:在坐标轴标签(例如,“对照组(n=30)”)或图例中显示每组的 n 值。
- 使用粗体标题:将图表标题设为
fontweight='bold',以符合发表要求。
常见陷阱
-
在多组图中标注所有成对比较
- 如何避免:只选择由假设驱动的比较。对于 k 个组,k*(k-1)/2 个比较很快就会让图变得难以阅读。只显示与对照组的比较或特定对比。
-
进行多重比较时使用原始 p 值
- 如何避免:使用
statsmodels.stats.multitest.multipletests(pvals, method='fdr_bh'),或使用上游工具提供的调整后 p 值(DESeq2 的padj)。
- 如何避免:使用
-
括号与数据或其他括号重叠
- 如何避免:为堆叠的括号使用递增的垂直偏移量。将第一个括号放在数据最大值加误差线的上方,然后为每个后续括号增加固定偏移量。
-
只标星号,却不说明使用了哪种检验
- 如何避免:始终在图标题或注释中注明检验名称(例如,“Mann-Whitney U 检验”或“Tukey HSD 事后检验”)。
-
不同图中的标注方式不一致
- 如何避免:在整个分析中使用相同的
pvalue_to_asterisk()函数。定义一次并重复使用。
- 如何避免:在整个分析中使用相同的
-
为每组不显著的比较都标注“ns”
- 如何避免:只有当不显著本身是值得关注的发现时才显示“ns”(例如,显示两种处理之间没有差异)。对于未进行比较的组对,不要标注 ns。
-
将注释放在数据下方
- 如何避免:始终将括号和星号放在被比较组的上方,绝不放在下方。
工作流
步骤 1:执行统计检验
在绘图前运行适当的检验并收集 p 值:
from scipy import stats
# Two-group comparison
stat, pval = stats.mannwhitneyu(group_a, group_b, alternative='two-sided')
# or for normal data:
stat, pval = stats.ttest_ind(group_a, group_b)
# Multi-group: ANOVA + post-hoc
from scipy.stats import f_oneway
stat, pval_anova = f_oneway(group_a, group_b, group_c)
# Post-hoc pairwise (if ANOVA significant)
from itertools import combinations
from statsmodels.stats.multitest import multipletests
pairs = list(combinations(["A", "B", "C"], 2))
groups = {"A": group_a, "B": group_b, "C": group_c}
raw_pvals = []
for g1, g2 in pairs:
_, p = stats.mannwhitneyu(groups[g1], groups[g2], alternative='two-sided')
raw_pvals.append(p)
# Adjust for multiple comparisons
rejected, adj_pvals, _, _ = multipletests(raw_pvals, method='fdr_bh')
步骤 2:在图中添加括号注释
使用此辅助函数在任意 matplotlib 坐标轴上绘制带星号的括号:
def add_significance_bracket(ax, x1, x2, y, p_value, dh=0.02, barh=0.015, fontsize=11):
"""Draw a significance bracket with asterisk notation between two x positions.
Args:
ax: matplotlib Axes object.
x1, x2: x-axis positions of the two groups (0-indexed).
y: y-coordinate for the bracket (top of bracket line).
p_value: p-value for the comparison.
dh: vertical offset above bracket for the text (in axes fraction).
barh: height of the bracket tips (in axes fraction).
fontsize: font size for the asterisk text.
"""
asterisk = pvalue_to_asterisk(p_value)
# Draw bracket: two tips and a connecting line
ax.plot([x1, x1, x2, x2], [y - barh, y, y, y - barh],
lw=1.2, color='black')
# Place asterisk text centered above the bracket
ax.text((x1 + x2) / 2, y + dh, asterisk,
ha='center', va='bottom', fontsize=fontsize, fontweight='bold')
步骤 3:与 Seaborn 图集成
import seaborn as sns
import matplotlib.pyplot as plt
import pandas as pd
import numpy as np
# Example: box plot with significance annotation
fig, ax = plt.subplots(figsize=(6, 5))
sns.boxplot(data=df, x="group", y="value", ax=ax, palette="Set2")
sns.stripplot(data=df, x="group", y="value", ax=ax,
color="black", alpha=0.4, size=3, jitter=True)
# Determine bracket y-position from data
y_max = df["value"].max()
y_range = df["value"].max() - df["value"].min()
offset = y_range * 0.08 # spacing between brackets
# Add brackets for each significant comparison
# pairs_with_pvals: list of (group1_idx, group2_idx, p_value)
pairs_with_pvals = [(0, 1, 0.003), (0, 2, 0.042)]
for i, (x1, x2, pval) in enumerate(pairs_with_pvals):
bracket_y = y_max + offset * (i + 1)
add_significance_bracket(ax, x1, x2, bracket_y, pval)
ax.set_title("Gene Expression by Treatment", fontweight='bold', fontsize=14)
ax.set_ylabel("Expression (log2 CPM)")
# Extend y-axis to fit brackets
ax.set_ylim(top=y_max + offset * (len(pairs_with_pvals) + 1.5))
plt.tight_layout()
plt.savefig("expression_comparison.png", dpi=150, bbox_inches='tight')
步骤 4:为分组柱状图添加注释
对于带误差线的柱状图,将括号放在误差线上方:
fig, ax = plt.subplots(figsize=(7, 5))
bar_plot = sns.barplot(data=df, x="gene", y="fold_change", hue="condition",
ax=ax, palette="Set2", ci="sd", capsize=0.05)
# For grouped bars, calculate x positions manually
# Each gene has multiple bars offset by group
n_groups = df["condition"].nunique()
n_genes = df["gene"].nunique()
bar_width = 0.8 / n_groups
for gene_idx in range(n_genes):
# x positions of the two bars within this gene group
x1 = gene_idx - bar_width / 2
x2 = gene_idx + bar_width / 2
# Get the max value + error for this gene
gene_data = df[df["gene"] == df["gene"].unique()[gene_idx]]
y_top = gene_data["fold_change"].mean() + gene_data["fold_change"].std()
p_val = pvals_per_gene[gene_idx] # pre-computed
if p_val <= 0.05: # only annotate significant results
add_significance_bracket(ax, x1, x2, y_top + 0.1, p_val)
ax.set_title("Fold Change by Condition", fontweight='bold', fontsize=14)
plt.tight_layout()
plt.savefig("fold_change_comparison.png", dpi=150, bbox_inches='tight')
操作指南
- 始终先执行检验再绘图:应先运行统计检验并保存结果,再编写任何绘图代码。不要在绘图代码块内计算 p 值。
- 使用一致的样式:在同一分析的所有图中使用相同的
add_significance_bracket函数和pvalue_to_asterisk转换方式。 - 在解读文字中报告检验详情:展示图表时,说明所用检验、每组的样本数,以及 p 值是否经过调整。
- 调整 y 轴范围:添加括号后,扩大 y 轴上限以避免内容被裁切。使用
ax.set_ylim(top=...)或ax.margins(y=0.15)。 - 对于 DESeq2/edgeR 结果:直接使用
padj(调整后 p 值)。不要对原始计数重新进行检验。
延伸阅读
- Graphpad:如何报告统计显著性 — 星号标注惯例
- Nature Methods:显著性要点 — 图表的统计学最佳实践
- Weissgerber 等人(2015)超越柱状图和折线图 — 为什么应在统计注释旁显示单个数据点
相关技能
seaborn-statistical-plots— Seaborn 绘图基础;可在 Seaborn 图表上使用本指南中的注释工作流scientific-visualization— 通用科学图表设计原则
评论
支持 Markdown