Skip to content
dysonsphere

Statistical annotations

add_comparisons() and add_correlation() compute statistics with scipy and draw them as layers. Every call also builds a structured report - descriptives, exact p-values, and effect sizes - that save() embeds in the export metadata, so a figure carries its own analysis. Pass report=True to print it, or save=True to write a text file.

add_comparisons() classifies test= into pairwise (mannwhitneyu, ttest_ind, ttest_rel, wilcoxon, tukey_hsd) or omnibus (anova, kruskal, friedman, alexandergovern). Here an ANOVA with a corner label plus two post-hoc brackets. The corner label sits at the top of the plot, so pad the y domain and lift the brackets with yStart= to keep everything clear:

import altair as alt
import dysonsphere as ds
from vega_datasets import data
ds.theme()
cars = ds.ensure_polars(data.cars()).drop_nulls(["Miles_per_Gallon"])
origins = ["Europe", "Japan", "USA"]
box = alt.Chart(cars).mark_boxplot().encode(
x=alt.X("Origin:N", sort=origins, title=None),
# Pad the y domain so the corner label clears the stacked brackets below it.
y=alt.Y("Miles_per_Gallon:Q", scale=alt.Scale(domain=[0, 75]), title="Miles per gallon"),
color=alt.Color("Origin:N", legend=None),
)
chart = box + ds.add_comparisons(
cars, "Origin", "Miles_per_Gallon",
[("Europe", "USA"), ("Japan", "USA")],
test="anova", categories=origins, yStart=50,
)

Give a list of pairs; brackets stack automatically. correction= applies Holm or Bonferroni multiple-comparison adjustment.

import dysonsphere as ds
from vega_datasets import data
ds.theme()
cars = ds.ensure_polars(data.cars()).drop_nulls(["Horsepower"])
origins = ["Europe", "Japan", "USA"]
# Pairwise Mann-Whitney U with Holm correction; brackets stack automatically.
chart = ds.mark_strip(
cars, "Origin", "Horsepower", origins,
) + ds.add_comparisons(
cars, "Origin", "Horsepower",
[("USA", "Europe"), ("Europe", "Japan"), ("USA", "Japan")],
test="mannwhitneyu", correction="holm", categories=origins,
)

labelStyle="asterisks" renders * / ** / *** / ns; bracketStyle="line" drops the end ticks.

import dysonsphere as ds
from vega_datasets import data
ds.theme()
cars = ds.ensure_polars(data.cars()).drop_nulls(["Horsepower"])
origins = ["Europe", "Japan", "USA"]
# Asterisk labels (* / ** / *** / ns) and plain-line brackets.
chart = ds.mark_strip(
cars, "Origin", "Horsepower", origins,
) + ds.add_comparisons(
cars, "Origin", "Horsepower",
[("USA", "Europe"), ("Europe", "Japan")],
test="mannwhitneyu", correction="holm",
labelStyle="asterisks", bracketStyle="line", categories=origins,
)

notation= controls the p-value format ("scientific", "e", "power"), and sigFigs= sets the precision.

import dysonsphere as ds
from vega_datasets import data
ds.theme()
cars = ds.ensure_polars(data.cars()).drop_nulls(["Horsepower"])
origins = ["Europe", "Japan", "USA"]
# Scientific notation for small p-values, 2 significant figures.
chart = ds.mark_strip(
cars, "Origin", "Horsepower", origins,
) + ds.add_comparisons(
cars, "Origin", "Horsepower",
[("USA", "Japan")],
test="ttest_ind", notation="scientific", sigFigs=2, categories=origins,
)

Both bracketStyle and notation also accept a dict for per-pair control (keys are pairs, matched regardless of order); notation additionally takes a "test" key for the omnibus label:

import dysonsphere as ds
from vega_datasets import data
ds.theme()
cars = ds.ensure_polars(data.cars()).drop_nulls(["Horsepower"])
origins = ["Europe", "Japan", "USA"]
# bracketStyle and notation accept per-pair dicts (keys matched regardless of
# order); the special "test" notation key styles the omnibus/test label.
chart = ds.mark_strip(
cars, "Origin", "Horsepower", origins,
) + ds.add_comparisons(
cars, "Origin", "Horsepower",
[("USA", "Europe"), ("USA", "Japan")],
test="mannwhitneyu", correction="holm",
bracketStyle={("USA", "Japan"): "line"},
notation={("USA", "Japan"): "scientific"},
categories=origins,
)

For an omnibus test, the corner label reports the omnibus result and the brackets are filled by the matching post-hoc (Kruskal-Wallis → Dunn here). omnibusVerbose=True adds the statistic, degrees of freedom, and effect size.

import altair as alt
import dysonsphere as ds
from vega_datasets import data
# The verbose omnibus label is long - widen the canvas so it fits.
ds.theme(chartWidth=200)
cars = ds.ensure_polars(data.cars()).drop_nulls(["Miles_per_Gallon"])
origins = ["Europe", "Japan", "USA"]
box = alt.Chart(cars).mark_boxplot().encode(
x=alt.X("Origin:N", sort=origins, title=None),
# Pad the y domain so the corner label clears the stacked brackets below it.
y=alt.Y("Miles_per_Gallon:Q", scale=alt.Scale(domain=[0, 75]), title="Miles per gallon"),
color=alt.Color("Origin:N"),
)
chart = box + ds.add_comparisons(
cars, "Origin", "Miles_per_Gallon",
[("Europe", "USA"), ("Japan", "USA")],
test="kruskal", omnibusVerbose=True, categories=origins,
yStart=50,
)

add_correlation() adds a coefficient, optional p-value, and (for Pearson) an OLS fit line to a scatter. The default readout is a bare r = …; rank methods report the coefficient only.

import altair as alt
import dysonsphere as ds
from vega_datasets import data
ds.theme()
cars = ds.ensure_polars(data.cars()).drop_nulls(["Miles_per_Gallon", "Horsepower"])
scatter = alt.Chart(cars).mark_point().encode(
x=alt.X("Horsepower:Q"),
y=alt.Y("Miles_per_Gallon:Q", title="Miles per gallon"),
)
# The default readout is a bare r = ...; Pearson also draws the OLS fit line.
chart = scatter + ds.add_correlation(cars, "Horsepower", "Miles_per_Gallon")

The readout is composed from independent parts, so you can show exactly what you want: coefficient="both" gives r and r², includePvalue=True adds P, includeEquation=True adds the fit line’s equation, and verbose=True is a shortcut that turns all of them on. The longer the readout, the wider the chart should be (theme(chartWidth=…)):

import altair as alt
import dysonsphere as ds
from vega_datasets import data
# The three-part readout is wide - give it a wider canvas.
ds.theme(chartWidth=150)
cars = ds.ensure_polars(data.cars()).drop_nulls(["Miles_per_Gallon", "Horsepower"])
scatter = alt.Chart(cars).mark_point().encode(
x=alt.X("Horsepower:Q"),
y=alt.Y("Miles_per_Gallon:Q", title="Miles per gallon"),
)
# Compose the readout from independent parts: coefficient="both" shows r and r-squared,
# includePvalue adds P. The default (coefficient="r") shows just r = ...; verbose=True is a
# shortcut that turns all of them on plus the fit equation.
chart = scatter + ds.add_correlation(
cars, "Horsepower", "Miles_per_Gallon",
coefficient="both", includePvalue=True,
)

Rank correlations (method="spearman" / "kendall") report only the coefficient (ρ / τ) and its p-value - no fit line, since a straight line is not their model:

import altair as alt
import dysonsphere as ds
from vega_datasets import data
ds.theme()
cars = ds.ensure_polars(data.cars()).drop_nulls(["Miles_per_Gallon", "Weight_in_lbs"])
scatter = alt.Chart(cars).mark_point().encode(
x=alt.X("Weight_in_lbs:Q", title="Weight (lbs)"),
y=alt.Y("Miles_per_Gallon:Q", title="Miles per gallon"),
)
# Rank correlations report the coefficient only - no fit line, since a straight
# line is not their model.
chart = scatter + ds.add_correlation(
cars, "Weight_in_lbs", "Miles_per_Gallon",
method="spearman", includePvalue=True, position="bottomLeft",
)