bio-data-visualization-statistical-annotation — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited bio-data-visualization-statistical-annotation (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
Reference examples tested with: ggpubr 0.6+, ggsignif 0.6+, rstatix 0.7+, statannotations 0.6+ (Python), seaborn 0.13+.
Before using code patterns, verify installed versions match. If versions differ:
packageVersion('<pkg>') then ?function_namepip show <package> then help(module.function) to check signaturesIf code throws ImportError, AttributeError, or TypeError, introspect the installed package and adapt the example to match the actual API rather than retrying.
"Add p-values to my plot" -> Render pairwise group comparisons as brackets with the correct statistical test (parametric vs non-parametric, paired vs unpaired, independent vs nested), adjusted for multiple testing, with rendering of significance as either numerical p OR asterisks. The choices that matter: which test is appropriate for the data, what multiple-testing adjustment applies, and whether to show n.s. (non-significant) results.
ggpubr::stat_compare_means, ggsignif::geom_signif, rstatix::t_test/wilcox_teststatannotations.Annotator, scipy.stats directlyTool defaults are NOT data-appropriate. ggpubr::stat_compare_means(method='t.test') uses Welch's two-sample t-test assuming approximate normality and unequal variances. This is wrong when:
method='wilcox.test').paired = TRUE).The bracket-and-asterisk visual is the same; the underlying statistics are not. Choose the test deliberately.
| Question | Recommended test | Function |
|---|---|---|
| 2 unpaired groups, normal, N≥30 | Welch t-test | t.test(), stat_compare_means(method='t.test') |
| 2 unpaired groups, non-normal or small N | Mann-Whitney U (Wilcoxon rank-sum) | wilcox.test(), stat_compare_means(method='wilcox.test') |
| 2 paired groups | Paired t-test OR Wilcoxon signed-rank | paired = TRUE |
| 3+ groups, normal | One-way ANOVA + Tukey HSD post-hoc | aov(), TukeyHSD() |
| 3+ groups, non-normal | Kruskal-Wallis + Dunn post-hoc | kruskal.test(), dunn.test() |
| Nested data (cells in patients) | Linear mixed model | lme4::lmer() |
| Time-course / repeated measures | Repeated-measures ANOVA OR LMM | nlme::lme() |
| Two-way factorial | Two-way ANOVA + interaction term | aov(y ~ a*b) |
| Survival / time-to-event | Log-rank, NOT t-test | survdiff() |
| Categorical outcome | Chi-square OR Fisher exact | chisq.test(), fisher.test() |
For pairwise comparisons among K groups, there are K(K-1)/2 unadjusted p-values. Without adjustment, family-wise error rate inflates rapidly:
# rstatix supports per-comparison adjustment
library(rstatix)
df %>%
pairwise_wilcox_test(value ~ group, p.adjust.method = 'bonferroni') %>%
add_xy_position(x = 'group')p.adjust.method options:
'bonferroni' — strictest; controls FWER'holm' — stepwise Bonferroni; uniformly more powerful than Bonferroni'BH' (Benjamini-Hochberg) — FDR; less strict than FWER; standard for genomics'fdr' — alias for BHFor figure annotations, holm is the modern default — controls FWER and is more powerful than bonferroni. For a small number of pre-planned comparisons (≤3), Bonferroni is fine.
Goal: Add per-comparison p-value brackets between groups on a distribution plot, using a test appropriate to data shape and adjusting for multiple comparisons.
Approach: Build the base plot with ggboxplot(); add stat_compare_means() with explicit method, comparisons, p.adjust.method, and label arguments; render as asterisks (p.signif) for terse display or numeric (p.format) for precise display.
library(ggpubr)
# Default boxplot + p-value bracket(s)
ggboxplot(df, x = 'group', y = 'value', color = 'group',
add = 'jitter', palette = 'npg') +
stat_compare_means(method = 'wilcox.test', # explicit; default is t-test
comparisons = list(c('Control', 'Treatment'),
c('Control', 'Vehicle'),
c('Treatment', 'Vehicle')),
label = 'p.signif', # 'p.signif' for asterisks; 'p.format' for numeric
p.adjust.method = 'holm',
method.args = list(alternative = 'two.sided'))For an overall test plus pairwise:
# Overall + per-comparison
ggboxplot(df, x = 'group', y = 'value', color = 'group') +
stat_compare_means(method = 'kruskal.test', # overall test
label.y = 1.05 * max(df$value)) +
stat_compare_means(comparisons = pairs,
method = 'wilcox.test',
p.adjust.method = 'holm',
label = 'p.signif')library(ggsignif)
ggplot(df, aes(group, value, fill = group)) +
geom_boxplot() +
geom_signif(comparisons = list(c('Control', 'Treatment')),
test = 'wilcox.test',
map_signif_level = TRUE, # asterisks vs numeric p
step_increase = 0.1) +
scale_fill_manual(values = c('#0072B2', '#D55E00'))map_signif_level = TRUE converts p-values to asterisks per Wasserstein-Lazar 2016 convention:
*** p < 0.001** p < 0.01* p < 0.05ns p ≥ 0.05For literal p-values, set FALSE.
import seaborn as sns
from statannotations.Annotator import Annotator
ax = sns.boxplot(x='group', y='value', data=df, palette=['#0072B2', '#D55E00', '#009E73'])
pairs = [('Control', 'Treatment'),
('Control', 'Vehicle'),
('Treatment', 'Vehicle')]
annotator = Annotator(ax, pairs, data=df, x='group', y='value')
annotator.configure(test='Mann-Whitney', # 't-test_ind', 't-test_paired', 'Wilcoxon', etc
comparisons_correction='holm',
text_format='star', # 'star', 'simple', 'full'
line_height=0.02,
text_offset=0.5)
annotator.apply_and_annotate()Trigger: stat_compare_means(method='t.test') (default) on log-distributed expression.
Mechanism: t-test assumes approximate normality; non-normal data with small N inflates Type-I.
Symptom: Significant p where rank test gives p > 0.05.
Fix: Switch to method='wilcox.test' for non-normal or small-N data. Verify normality with Shapiro-Wilk if borderline.
Trigger: Multiple bracket annotations with raw p-values.
Mechanism: K(K-1)/2 comparisons inflate FWER without adjustment.
Symptom: All-pairwise significant at nominal 0.05; doesn't replicate.
Fix: p.adjust.method = 'holm' (or 'bonferroni' or 'BH'). Document choice.
Trigger: Before/after measurements in same subjects, tested with unpaired t-test.
Mechanism: Ignores within-subject correlation; loses power.
Symptom: Non-significant p where paired test gives significant.
Fix: paired = TRUE (R) or t-test_paired (statannotations). Verify subjects are correctly matched.
Trigger: Hundreds of cells per patient, tested as if each cell is independent.
Mechanism: Pseudoreplication — within-patient correlation ignored; p-values dramatically over-significant.
Symptom: p < 1e-50 from a dataset where the actual N is ~10 patients.
Fix: Linear mixed model (lme4::lmer(value ~ group + (1|patient_id))); aggregate to per-patient median first; or pseudobulk.
Trigger: label = 'p.signif' exclusively.
Mechanism: Reader cannot recover the actual p-value.
Symptom: Reviewer asks for exact p; not in figure or supplementary.
Fix: Either show numeric p (label = 'p.format') or include test results table in supplementary.
Trigger: Showing only significant brackets, omitting non-significant.
Mechanism: Selective reporting biases interpretation.
Symptom: Reader assumes untested pairs were significant.
Fix: Either annotate all comparisons (with n.s. for non-significant) OR pre-specify which pairs are tested in the legend/caption.
Trigger: Conclusion "highly significant difference" from p = 1e-10 on a tiny effect.
Mechanism: Large N inflates significance for trivial differences.
Symptom: Effect size negligible despite extreme p.
Fix: Always report effect size (Cohen's d, Cliff's delta, median difference with CI) alongside p. The bracket should convey direction AND magnitude, not just significance.
| Pattern | Cause | Action |
|---|---|---|
| t-test significant; Wilcoxon n.s. | Outliers driving t-test; rank test robust | Trust Wilcoxon for non-normal data |
| Unpaired n.s.; paired significant | Within-subject correlation matters | Use paired if subjects are matched |
| Pairwise all-significant; ANOVA n.s. | Multiple-testing inflation in pairwise | ANOVA / Kruskal-Wallis is the overall test; pairwise post-hoc only after omnibus significant |
| Pseudo-replicated p < 1e-50; LMM p = 0.1 | Pseudoreplication | LMM is correct; pseudo-replicated p is meaningless |
| Bonferroni-adjusted n.s.; raw p < 0.05 | Adjustment correctly identified borderline | Trust adjusted; document the test family |
| Threshold | Value | Source |
|---|---|---|
| α for FWER control | 0.05 family-wise | Standard |
| α for FDR control | 0.05 expected FDR (BH) | Benjamini-Hochberg 1995 |
| Asterisk convention | <0.05, <0.01, ** <0.001 | Common practice |
| Bonferroni cutoff | 0.05 / K(K-1)/2 | Standard |
| Holm step-down | better than Bonferroni for all K | Holm 1979 |
| FDR (BH) | less strict than FWER | Genomics standard |
| Error / symptom | Cause | Solution |
|---|---|---|
| Reviewer asks "why t-test?" | Default not justified | Pre-justify test choice |
| Many pairwise-significant; doesn't replicate | No multiple-testing adjustment | Holm or BH |
| Effect "highly significant" but tiny | Large N inflates p | Report effect size |
| Asterisks only; no p-values | label = 'p.signif' exclusively | Show p.format OR provide table |
| Pseudoreplication inflated p | Cells treated as independent | LMM or pseudobulk |
| n.s. comparisons hidden | Selective reporting | Annotate all pre-specified pairs |
| Numeric p truncated to '<2.22e-16' | R default precision | Manual formatting or report as < 2e-16 |
~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.