Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Guide for annotating statistical significance (p-value asterisks) on comparison plots. Covers standard notation (ns, *, **, ***, ****), matplotlib bracket+asterisk implementation, and use with seaborn box/violin/bar plots. Use when preparing publication-ready figures with significance markers.
.claude/skills/jaechang-hits-statistical-significance-annotation/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-22 | ✗→✓ | ▲ Improved | 88% | 0% |
| case-05 | ✓→✓ | = Same ✓ | 150% | 0% |
| case-01 | ✓→✓ | = Same ✓ | 146% | 0% |
| case-02 | ✓→✓ | = Same ✓ | 143% | 0% |
| case-03 | ✓→✓ | = Same ✓ | 296% | 0% |
Statistical significance annotations (asterisk notation) are visual markers placed on comparison plots to indicate the results of hypothesis tests between groups. They consist of brackets connecting two groups and asterisk symbols denoting the p-value range. Proper annotation ensures that the visual claims in a figure match the quantitative evidence, making plots publication-ready and scientifically rigorous. This guide covers the standard conventions, when and how to annotate, and a reusable matplotlib implementation.
The widely adopted convention maps p-value ranges to asterisk symbols:
| Symbol | P-value Range | Meaning | |--------|--------------|---------| | ns | p > 0.05 | Not significant | | \ | p <= 0.05 | Significant | | \\ | p <= 0.01 | Highly significant | | \\\ | p <= 0.001 | Very highly significant | | \\\\ | p <= 0.0001 | Extremely significant |
The conversion function:
pythondef 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"
padj, ANOVA post-hoc): Use the adjusted values already provided.Not every pair of groups needs annotation. Select comparisons that:
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| Scenario | Annotate? | Which pairs | |----------|-----------|-------------| | DEG box plot: treatment vs control | Yes | Treatment vs Control | | Multi-group ANOVA with post-hoc | Yes | Significant post-hoc pairs only | | Gene expression across 10 cell types | Selectively | vs reference cell type only | | PCA or UMAP | No | N/A | | Heatmap or volcano plot | No | N/A | | Correlation scatter | No | Report r and p in text/legend | | Exploratory bar plot, no hypothesis | Optional | vs control if applicable |
fontweight='bold' on figure titles for publication readiness.statsmodels.stats.multitest.multipletests(pvals, method='fdr_bh') or use adjusted p-values from upstream tools (DESeq2 padj).pvalue_to_asterisk() function throughout the analysis. Define it once and reuse.Run the appropriate test and collect p-values before plotting:
pythonfrom 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')
Use this helper function to draw brackets with asterisks on any matplotlib axes:
pythondef 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')
pythonimport 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')
For bar plots with error bars, position brackets above the error bars:
pythonfig, 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')
add_significance_bracket function and pvalue_to_asterisk conversion across all figures in an analysis.ax.set_ylim(top=...) or ax.margins(y=0.15).padj (adjusted p-value) directly. Do not re-test the raw counts.seaborn-statistical-plots — Seaborn plotting fundamentals; use this guide's annotation workflow on top of seaborn figuresscientific-visualization — General scientific figure design principles| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-05 | pass→pass | 11,019 | 9,037 | -18% | 1 | 1 | 0% | 1,971 | 4,931 | +150% | 0 | 0 | — |
case-01 | pass→pass | 9,480 | 9,601 | +1% | 1 | 1 | 0% | 1,776 | 4,374 | +146% | 0 | 0 | — |
case-02 | pass→pass | 10,434 | 8,135 | -22% | 1 | 1 | 0% | 1,913 | 4,652 | +143% | 0 | 0 | — |
case-03 | pass→pass | 4,568 | 2,979 | -35% | 1 | 1 | 0% | 959 | 3,796 | +296% | 0 | 0 | — |
case-04 | pass→pass | 4,945 | 2,831 | -43% | 1 | 1 | 0% | 821 | 3,730 | +354% | 0 | 0 | — |
case-06 | pass→pass | 11,745 | 9,732 | -17% | 1 | 1 | 0% | 1,975 | 5,003 | +153% | 0 | 0 | — |
case-07 | pass→pass | 13,017 | 7,970 | -39% | 1 | 1 | 0% | 2,162 | 4,509 | +109% | 0 | 0 | — |
case-08 | pass→pass | 10,426 | 8,999 | -14% | 1 | 1 | 0% | 1,816 | 4,755 | +162% | 0 | 0 | — |
case-09 | pass→pass | 10,233 | 9,704 | -5% | 1 | 1 | 0% | 1,865 | 4,914 | +163% | 0 | 0 | — |
case-10 | pass→pass | 15,226 | 14,334 | -6% | 1 | 1 | 0% | 2,367 | 5,512 | +133% | 0 | 0 | — |
case-11 | pass→pass | 11,268 | 8,407 | -25% | 1 | 1 | 0% | 1,829 | 4,591 | +151% | 0 | 0 | — |
case-12 | pass→pass | 9,566 | 8,286 | -13% | 1 | 1 | 0% | 1,691 | 4,688 | +177% | 0 | 0 | — |
case-13 | pass→pass | 15,348 | 10,943 | -29% | 1 | 1 | 0% | 2,829 | 5,333 | +89% | 0 | 0 | — |
case-14 | pass→pass | 12,161 | 11,838 | -3% | 1 | 1 | 0% | 2,192 | 5,273 | +141% | 0 | 0 | — |
case-24 | pass→pass | 14,177 | 9,185 | -35% | 1 | 1 | 0% | 2,695 | 4,877 | +81% | 0 | 0 | — |
case-15 | pass→pass | 13,383 | 12,456 | -7% | 1 | 1 | 0% | 2,330 | 5,360 | +130% | 0 | 0 | — |
case-16 | pass→pass | 5,463 | 2,584 | -53% | 1 | 1 | 0% | 864 | 3,599 | +317% | 0 | 0 | — |
case-17 | pass→pass | 10,855 | 4,805 | -56% | 1 | 1 | 0% | 1,744 | 3,882 | +123% | 0 | 0 | — |
case-18 | pass→pass | 12,235 | 19,857 | +62% | 1 | 1 | 0% | 2,319 | 7,096 | +206% | 0 | 0 | — |
case-19 | pass→pass | 15,480 | 13,949 | -10% | 1 | 1 | 0% | 2,560 | 5,661 | +121% | 0 | 0 | — |
case-20 | pass→pass | 12,076 | 7,940 | -34% | 1 | 1 | 0% | 2,184 | 4,639 | +112% | 0 | 0 | — |
case-21 | pass→pass | 10,183 | 8,847 | -13% | 1 | 1 | 0% | 2,002 | 4,945 | +147% | 0 | 0 | — |
case-22 | fail→pass | 19,945 | 18,094 | -9% | 1 | 1 | 0% | 3,532 | 6,656 | +88% | 0 | 0 | — |
case-23 | pass→pass | 15,400 | 17,796 | +16% | 1 | 1 | 0% | 2,557 | 6,693 | +162% | 0 | 0 | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 24 cases were attempted. The headline lift of +4 percentage points is the difference between those two pass rates over the 24 comparable cases.
Without the skill loaded, the model failed this case. With it loaded, the same prompt on the same model passed. This is one improved case from the latest verified run; every case, including any that regressed, is in the table above.
Other measured skills in the registry, with their headline benchmark lift.