Statistics and comparison brackets

Compute reporting rows with effect sizes and intervals, then attach the same results to a plot.

4 min readPlotting

Two-group comparisons

Each two-group test takes two samples (a, b), orients signs as a − b, and returns one reporting row: a dict ready to save as a CSV in the plot’s _stats dissection.

import polars as pl
import fluxplot as fp

# sd_values and sleep_values are the two samples from your analysis.
row = fp.stats.welch_hedges(sd_values, sleep_values, names=("SD", "sleep"))
pl.DataFrame([row]).write_csv("plots/_dissections/app_gapdh/_stats/welch_ttest.csv")

The tests are two-sided unless you pass alternative=. A row’s keys, in the order of fp.stats.REPORT_COLUMNS, are:

Function Design Test
welch_hedges independent samples, means Welch’s t-test
mann_whitney_cliff independent samples, ranks Mann–Whitney U (the U of a)
paired_t_hedges paired samples, mean difference paired t-test
wilcoxon_rank_biserial paired samples, ranks Wilcoxon signed-rank (W+, zeros dropped)

Each reports this effect size with a 95% confidence interval:

Function Effect size and 95% CI
welch_hedges Hedges’ g with the non-pooled SD, sqrt((var_a + var_b) / 2); Bonett (2008) CI
mann_whitney_cliff Cliff’s delta; Newcombe (2006) Method 5 score CI
paired_t_hedges Hedges’ g_z (SD of the differences); exact noncentral-t CI
wilcoxon_rank_biserial Kerby’s matched-pairs rank-biserial r; score CI

The CIs target the population effect size, so they are never multiplied by Hedges’ J. Both rank-based CIs stay non-degenerate at complete separation (delta or r = ±1), which is common at n = 6. Rank tests report dof = None. fluxplot’s test suite (tests/test_stats.py) checks each test against SciPy and against the equation that defines its interval.

Multiple comparisons

A row on its own has p_corrected_holm == p-value (a family of one). Holm’s step-down correction needs every p-value in the family, so it is a separate pass over the rows of the comparisons that belong together (e.g. every measure tested on the same animals in one figure); extra keys such as a measure name ride along:

# sd and sleep map each measure name to its samples.
rows = [dict(measure=m, **fp.stats.welch_hedges(sd[m], sleep[m])) for m in measures]
# Fill p_corrected_holm across the family; the row order is kept.
rows = fp.stats.holm(rows)

fp.stats.holm_adjusted(p) does the same for a bare array of p-values; fp.stats.bh(rows) / bh_adjusted(p) fill p_corrected_bh with Benjamini–Hochberg (false-discovery-rate) values for a screen of many measures. NaN p-values pass through both untouched.

Three or more groups

The omnibus tests return one row, in which groups lists every group and F tests carry dof and dof_error. The post-hoc tests return one row per pair. pairwise runs any two-group test above over the pairs of a {name: sample} family and corrects across them.

Function Test Effect size and 95% CI
anova_oneway one-way ANOVA η² or ω² (effect="eta2" or "omega2"); Steiger’s noncentral-F CI
welch_anova Welch’s ANOVA ω²; noncentral-F CI on Welch’s dof
kruskal_epsilon Kruskal–Wallis ε² = H / (N − 1); seeded bootstrap CI
rm_anova repeated-measures ANOVA partial η²; noncentral-F CI
friedman_kendall Friedman Kendall’s W; seeded bootstrap CI
tukey_hsd Tukey HSD Hedges’ g, pooled SD
games_howell Games–Howell Hedges’ g, non-pooled SD; Bonett CI
dunn Dunn’s test Cliff’s delta; Newcombe CI
pairwise any two-group test, per pair the chosen test’s

Notes on the tests:

The group-based functions take the samples as positional arguments with optional names=, for example fp.stats.welch_anova(ctl, drug, sham, names=("ctl", "drug", "sham")). The repeated-measures functions take a long table and its column names: fp.stats.rm_anova(table, subject, within, dv). Exact signatures are in the API reference. fluxplot’s test suite (tests/test_stats_multi.py) checks them against pingouin and scikit-posthocs reference values.

Draw the comparisons

fp.brackets(ax, rows, positions=…) draws one significance bracket per post-hoc row and stacks them automatically: shortest span first, each bracket one step above the data it spans and above every bracket it overlaps in x (multiplicative steps on a log axis), so nothing crosses. positions maps group name → x; a glowbar / fluxbox result provides it, and gb.brackets(rows) is the one-liner:

# ctl, drug and sham are the samples; df["group"] holds the same three names.
rows = fp.stats.pairwise(fp.stats.welch_hedges,
                         {"ctl": ctl, "drug": drug, "sham": sham})
gb = fp.glowbar(data=df, x="group", y="value", ax=ax)

# Stars (***, **, *, ns) from p_corrected_holm:
gb.brackets(rows)
# Or "p = 0.003" labels from the BH-corrected p-values, without the ns pairs:
gb.brackets(rows, label="p", ns=False, p_column="p_corrected_bh")

label is "stars", "p", "both" or a callable on the row. thresholds, top, step and tip shape the stack, and extra keywords are passed to fp.significance_bracket. Each bracket’s manifest overlay carries between: [a, b], p and a stats block — test, statistic, raw and corrected p, correction, effect size and its CI, sizes — so the figure states exactly which test each star came from.

Search the docs

Search guides, workspaces, and reference topics.

↑ ↓ to move · Enter to open · Esc to close