Statistical annotations
Statistical inference annotations - significance brackets, omnibus labels, correlation readouts.
The annotation wrappers for what statistics.py computes: add_comparisons (pairwise
brackets and omnibus test labels) and add_correlation (coefficient readout + OLS fit line).
Pure computation stays in statistics.py (no Altair there); this module builds the Vega-Lite
layers that present it. Statistical results are registered in the statistics._REPORTS
registry and embedded into exports by save() via layer-name markers.
add_comparisons
Section titled “add_comparisons”def add_comparisons( df: pl.DataFrame | Any, xCol: str, yCol: str, pairs: list[tuple[str, str]] | None = None, *, test: str = 'mannwhitneyu', postHoc: str | None = None, pvalues: list[float] | None = None, correction: str | None = None, nComparisons: int | None = None, yPositions: list[float] | None = None, yStart: float | None = None, yStep: float | None = None, yPad: float | None = None, categories: list | None = None, chartWidth: int | None = None, bracketStyle: str | dict = 'bracket', labelStyle: str = 'p', tickHeight: float | None = None, strokeWidth: float | None = None, fontSize: int | None = None, reverse: list[tuple[str, str]] | None = None, sigFigs: int | None = None, notation: str | dict | None = None, testLabelPosition: str | None = 'auto', testLabel: str | None = None, omnibusVerbose: bool = False, testLabelOffsetX: int = 0, testLabelOffsetY: int = 0, testLabelX = None, testLabelY = None, report: bool = False, save: bool | str = False,) -> alt.LayerChart: ...Build p-value annotation layers for one or more group comparisons.
Two modes, selected by test:
- Pairwise (
'mannwhitneyu','ttest_ind','ttest_rel','wilcoxon','tukey_hsd') — draws a bracket per pair inpairs, stacked automatically so they don’t overlap (shorter-span pairs sit lower; overlapping spans are bumped up a level). - Omnibus (
'anova','kruskal','friedman','alexandergovern') — runs one “are any groups different?” test and places its result as a corner label viaadd_text(seetestLabelPosition). Ifpairsis also given, a post-hoc test (seepostHoc) fills the brackets.
A descriptive + effect-size report is generated on every call and queued for
the export metadata written by ds.save() (see report/save).
Combine with your chart using +: chart + add_comparisons(...).
Parameters
df(pl.DataFrame | Any) - Polars DataFrame containing the data.xCol(str) - Column name for the grouping variable (x-axis).yCol(str) - Column name for the value variable (y-axis). Used to run tests and to auto-place the first bracket.pairs(list[tuple[str, str]] | None) - List of(group1, group2)tuples identifying the comparisons to annotate with brackets. Required for pairwisetestvalues. Optional for omnibus tests — passNonefor an omnibus-only corner label, or a list to also draw post-hoc brackets.test(str) - Statistical test. Pairwise:'mannwhitneyu'(default),'ttest_ind','ttest_rel','wilcoxon'(run per pair), or'tukey_hsd'(one omnibus run, per-pair p-values from the matrix). Omnibus:'anova'(f_oneway),'kruskal','friedman','alexandergovern'. Ignored whenpvaluesis provided.postHoc(str | None) - Post-hoc test that fills the brackets whentestis omnibus andpairsis given.None(default) picks a sensible default per omnibus test:anova → 'tukey_hsd',alexandergovern → 'games_howell',kruskal → 'dunn',friedman → 'nemenyi'. May also be set to any pairwise test name. Dunn, Nemenyi, and Games-Howell are computed in-house (validated against scikit-posthocs / pingouin);correctionadjusts them over all unique pairs. Ignored for pairwisetest.pvalues(list[float] | None) - Pre-computed p-values, one per pair in the same order. Skips all statistical tests for the brackets when provided.correction(str | None) - Multiple comparison correction:'bonferroni','holm', orNone. For pairwise/post-hoc bracket p-values; ignored fortukey_hsd(correction is built in) and whenpvaluesis provided.nComparisons(int | None) - Total number of comparisons for Bonferroni correction. Defaults tolen(pairs)whencorrection='bonferroni'and not set explicitly.yPositions(list[float] | None) - Explicit y positions (data units) for each bracket, one per pair in the same order. When provided, overrides all auto-stacking logic (yStart,yStep,yPadare ignored).yStart(float | None) - Y position (data units) of the lowest bracket. Defaults tomax(y values for all annotated groups) + yPad.yStep(float | None) - Vertical distance (data units) between stacking levels. Defaults toyPad * 1.75, leaving clearance between a bracket’s label and the bracket stacked above it.yPad(float | None) - Padding above the data maximum whenyStartis auto-placed. Defaults to a visual gap of ~8 px (bracketStyle='line') or ~10 px (bracketStyle='bracket'), expressed in data units as a fraction of the full data extent overchartHeight. Using the full extent (not just the compared groups) keeps the spacing stable - and stops the brackets collapsing when an un-annotated group inflates the rendered domain - since the gap in pixels trackschartHeight / rendered domain.categories(list | None) - Ordered list of all x-axis categories. Inferred fromdf(sorted alphabetically) when not provided.chartWidth(int | None) - Width of the chart in pixels, used to compute text x positions. Auto-detected fromds.theme()when not set.bracketStyle(str | dict) -'bracket'(default; bar + end ticks) or'line'(horizontal bar only) applied to every bracket. Or adictmapping a pair to its style for per-pair control, e.g.{("A", "B"): "line", ("A", "C"): "bracket"}— keys match either pair order; pairs absent from the dict fall back to'bracket'.labelStyle(str) -'p'(default) rendersP = 0.012/P < 0.001.'asterisks'renders*/**/***/ns.tickHeight(float | None) - Height of bracket end ticks in data units. Defaults to the theme’stickSize(converted from px to data units), so bracket ticks match the axis ticks. Always positive, so it works with reverse (negative-yStep) brackets without an explicit override. Only used whenbracketStyle='bracket'.strokeWidth(float | None) - Stroke width of bracket lines. InheritsaxisWidthfromds.theme()when not set.fontSize(int | None) - Font size of the p-value / corner labels. Defaults to the theme’s primaryfontSize(7under the built-in defaults), matching the axis font.reverse(list[tuple[str, str]] | None) - List of(group1, group2)tuples identifying brackets to flip — text moves below the bar and ticks point upward.sigFigs(int | None) - Significant figures for p-value labels (and the correlation readout). Gives consistent visual precision across magnitudes — e.g.sigFigs=2renders bothP = 4.3×10⁻¹⁴andP = 0.68at two figures. Trailing zeros are stripped.None(default) reads the theme’ssigFigs(default3). Plain notation floors at a fixedP < 0.001;'power'is unaffected (integer exponent).notation(str | dict | None) - Format style for p-value labels whenlabelStyle='p'.None(default) usesP = 0.012/P < 0.001style.'scientific'usesP = 1.23×10⁻².'e'usesP = 1.23e-02.'power'rounds to the nearest power of 10 givingP ≈ 10⁻²— note that values within the same decade (e.g. 0.04 and 0.06) map to the same label; best for p-values spanning multiple orders of magnitude. A single value applies to every label; or pass adictfor per-pair notation, e.g.{("A", "B"): "scientific", "test": "power"}— tuple keys are pairs (matched either order, unlisted → plain), and the special"test"key sets the omnibus label’s notation.testLabelPosition(str | None) - Corner preset (anadd_textposition, e.g.'topLeft','bottomRight') for the single test label. Its content adapts: the omnibus result (ANOVA P = 0.003) for an omnibustest, or the pairwise test name (Mann-Whitney U) for a pairwisetest. Default'auto'→ shown at'topLeft'for omnibus, hidden for pairwise (opt-in). A preset draws it there;Nonehides it (the result is still computed for the report/metadata).testLabel(str | None) - Override string for the test label.None(default) builds it from the test result / name.omnibusVerbose(bool) - Applies to the omnibus label content:False(default) → terseANOVA P = 0.003;True→ANOVA F(2, 57) = 6.34, P = 0.003, η² = 0.18(statistic, df, p, and effect size).testLabelOffsetX(int) - Pixel nudges for the test label, forwarded toadd_text.testLabelOffsetY(int) - Pixel nudges for the test label, forwarded toadd_text.testLabelX- Explicit coordinates for the test label (data values, category names, oralt.value(px)), forwarded toadd_textwhere they override the preset.None(default) usestestLabelPosition.testLabelY- Explicit coordinates for the test label (data values, category names, oralt.value(px)), forwarded toadd_textwhere they override the preset.None(default) usestestLabelPosition.report(bool) -Trueprints the full descriptive + effect-size report (per-group n/mean/sd/median/IQR/range, the omnibus result, and the post-hoc comparisons) to stdout. DefaultFalse. For an omnibustestthe report lists all pairwise post-hoc comparisons — the full table, not just the pairs you bracket (and even whenpairs=None). For a pairwisetestit lists exactly the requestedpairs. The report is queued for the export metadata regardless of this flag (whends.save(..., saveMetadata=True)); it lands in the nextds.save().save(bool | str) -Truewrites the report todysonsphere_report_<timestamp>.txtin the current directory; a string writes it to that directory. DefaultFalse.
Examples
Single comparison::
CATEGORIES = ["A", "B", "C"] chart = ds.mark_strip(df, "group", "value", CATEGORIES) chart + ds.add_comparisons( df, "group", "value", pairs=[("A", "B")], categories=CATEGORIES, )
Multiple comparisons — brackets stacked automatically::
chart + ds.add_comparisons( df, "group", "value", pairs=[("A", "B"), ("A", "C"), ("B", "C")], test="mannwhitneyu", categories=CATEGORIES, )
Omnibus ANOVA in the corner + Tukey post-hoc brackets::
chart + ds.add_comparisons( df, "group", "value", pairs=[("A", "B"), ("A", "C")], test="anova", omnibusVerbose=True, categories=CATEGORIES, )
Omnibus-only (no brackets), report printed::
chart + ds.add_comparisons( df, "group", "value", test="kruskal", categories=CATEGORIES, report=True, )
From pre-computed p-values::
chart + ds.add_comparisons( df, "group", "value", pairs=[("A", "B"), ("A", "C")], pvalues=[0.012, 0.341], categories=CATEGORIES, )add_correlation
Section titled “add_correlation”def add_correlation( df: pl.DataFrame | Any, xCol: str, yCol: str, *, method: str = 'pearson', line: bool = True, position: str | None = 'topLeft', label: str | None = None, coefficient: str = 'r', includePvalue: bool = False, includeEquation: bool = False, verbose: bool = False, offsetX: int = 0, offsetY: int = 0, fontSize: int | None = None, sigFigs: int | None = None, notation: str | None = None, color: str | None = None, strokeWidth: float | None = None, strokeDash: bool | list[int] | None = None, opacity: float | None = None, lineStyle: dict | None = None, report: bool = False, save: bool | str = False,) -> alt.LayerChart: ...Annotate a scatter with a correlation coefficient (and an OLS fit line for Pearson).
Reports the coefficient as a corner label, and — for method="pearson"
only — draws the ordinary-least-squares regression line. A structured record
(kind="correlation") is queued for the export metadata (see ds.save),
exactly like add_comparisons.
Combine with your scatter using +: chart + add_correlation(...).
Parameters
df(pl.DataFrame | Any) - DataFrame containing the data (polars or pandas).xCol(str) - Column names for the two continuous variables.yCol(str) - Column names for the two continuous variables.method(str) -'pearson'(default) — linear correlationr+r²+ slope/intercept, with an OLS line.'spearman'— rank correlationρ.'kendall'— rank correlationτ. The rank methods report the coefficient only (nor², no line — a straight line isn’t their model). Matches pandas’DataFrame.corr.line(bool) - Draw the OLS fit line. DefaultTrue. Only applies tomethod="pearson"(a no-op for the rank methods). SetFalseto suppress it and, e.g., compose your own line from the returned/recorded slope and intercept.position(str | None) - Corner preset (anadd_textposition, e.g.'topLeft') for the readout. Default'topLeft'.Nonecomputes the result for the report/metadata but draws no label.label(str | None) - Override string for the corner readout.Nonebuilds it from the parts below.coefficient(str) - Pearson only — which statistic the readout shows:'r'(default),'r2'(justr², Excel-trendline style), or'both'. Ignored for the rank kinds (they always showρ/τ).includePvalue(bool) - Append the p-value to the readout. DefaultFalse.includeEquation(bool) - Pearson only — append the fit equation, y = 0.84x + 0.27. DefaultFalse.verbose(bool) - Shortcut for the fullest readout:Trueis equivalent tocoefficient="both", includePvalue=True, includeEquation=True(and overrides those three). DefaultFalse. So the default readout is justr = 0.87(Pearson) /ρ = 0.81(rank);verbose=Truegivesr = 0.87, r² = 0.76, P < 0.001, y = 0.84x + 0.27.offsetX(int) - Pixel nudges for the readout, forwarded toadd_text.offsetY(int) - Pixel nudges for the readout, forwarded toadd_text.fontSize(int | None) - Font size of the readout. Defaults to the theme’s primaryfontSize(7under the built-in defaults), matching the axis font.sigFigs(int | None) - Significant figures / number format for the readout (coefficient, r², p-value, and fit equation), as inadd_comparisons.sigFigs=Nonereads the theme.notation(int | None) - Significant figures / number format for the readout (coefficient, r², p-value, and fit equation), as inadd_comparisons.sigFigs=Nonereads the theme.color(str | None) - Curated style overrides for the fit line (same four knobs asadd_rule). Each defaults toNone→ the line inherits the theme’smark_lineconfig; set one to override just that property.strokeWidth(str | None) - Curated style overrides for the fit line (same four knobs asadd_rule). Each defaults toNone→ the line inherits the theme’smark_lineconfig; set one to override just that property.strokeDash(str | None) - Curated style overrides for the fit line (same four knobs asadd_rule). Each defaults toNone→ the line inherits the theme’smark_lineconfig; set one to override just that property.opacity(str | None) - Curated style overrides for the fit line (same four knobs asadd_rule). Each defaults toNone→ the line inherits the theme’smark_lineconfig; set one to override just that property.lineStyle(dict | None) - A dict of rawmark_lineproperties merged in last, so any Vega-Lite line property is reachable (e.g.{"interpolate": "monotone", "strokeCap": "round"}). Keys here override the curatedcolor/strokeWidth/etc. above.report(bool) -Trueprints the report (coefficient, r², p, fit, n) to stdout. DefaultFalse. The record is queued for export metadata regardless.save(bool | str) -Truewrites the report todysonsphere_report_<timestamp>.txtin the cwd; a string writes it to that directory.
Examples
::
scatter = alt.Chart(df).mark_point().encode(x="height:Q", y="weight:Q") scatter + ds.add_correlation(df, "height", "weight") # r + r² + OLS line scatter + ds.add_correlation(df, "height", "weight", method="spearman") # ρ, no line scatter + ds.add_correlation( df, "height", "weight", color="#c0392b", lineStyle={"strokeDash": [4, 2]}, )