Skip to content

FIX: Normalise stacked histograms by the total of their stack - #174

Merged
thclark merged 1 commit into
bars-and-histogramsfrom
hists-common-norm
Oct 26, 2026
Merged

thclark merged 1 commit into
bars-and-histogramsfrom
hists-common-norm

Conversation

@thclark

@thclark thclark commented Oct 26, 2026

Copy link
Copy Markdown
Owner

Summary

hists normalised every member by its own total. For a stacked column this was wrong: each layer integrated to 1, so a stack of m density histograms enclosed an area of m, and its outline was not the density of anything.

The members of a stacked column are now normalised by the pooled total of that column. This is what Matplotlib's hist(stacked=True, density=True) does, and what seaborn's histplot(multiple="stack") does with its default common_norm=True. The top of the stack is therefore the normalised histogram of the pooled sample, and each layer is its member's share of it:

  • a stack of densities encloses unit area;
  • a stack of probabilities sums to 1, and a stack of percentages sums to 100;
  • a cumulative stack ends at the pooled total, and a CDF stack ends at 1;
  • counts and count densities are unchanged, because they are not divided by a total.

Members that stand side by side, each in a column of its own, are still normalised by their own totals, because they are placed side by side to compare their shapes.

The new setter HistsMut::common_norm(bool) overrides either rule, as seaborn's argument of that name does. true normalises every member by the pooled total of all the members, and false normalises each member by its own total. The result is the same whether it is called before or after the members are added.

Each member's Binning.total now holds the total its values were normalised by, which for a member of a stack is the pooled total of the stack. A datatip therefore still recovers the member's own count, and reports as its density the member's contribution to the density of the stack. The excluded_non_finite and excluded_out_of_range counts remain those of the member's own sample.

Tests

  • hists_normalize_and_range_apply_to_every_member_whenever_they_are_set now requires a stack, rather than each layer, to enclose unit area, and the records of a stack to hold the pooled total.
  • the_top_of_a_stack_is_the_normalised_histogram_of_the_pooled_sample checks every normalisation on bins of unequal width.
  • common_norm_overrides_the_layout_whenever_it_is_set checks both layouts with the setter unset, true and false, called before and after the members are added.
  • the_members_of_a_stack_record_the_pooled_total_of_their_stack_and_their_own_exclusions checks that a stack pools only its own column and that exclusions stay per member.
  • the_datatip_of_a_stacked_member_reports_its_own_count_and_its_share_of_the_density checks the datatip under density, probability and CDF normalisation.

Documentation

The getting-started guide has a new subsection, "Normalising stacked and side-by-side histograms", with examples. The equivalent-functions reference maps Matplotlib's and seaborn's stacked normalisation and common_norm to hists. The Binning section of the figure schema states that total is the total the values were normalised by.

Gallery

The rendered gallery is unchanged. The only difference is in histogram.fig, whose stacked count histograms now record the pooled total of 800 samples instead of 400 each; their drawn values are the same.

Normalise the members of a stacked column of `hists` by the pooled total
of the column, so that the top of the stack is the normalised histogram
of the pooled sample and each layer is its member's share of it, as
Matplotlib's `hist(stacked=True, density=True)` and seaborn's default
`common_norm=True` do. Members in columns of their own are still
normalised by their own totals.

Add `HistsMut::common_norm(bool)`, which normalises every member by the
pooled total of all the members, or each by its own, whatever the layout.

Record in each member's `Binning.total` the total its values were
normalised by, so that a datatip recovers the member's own count.
@thclark
thclark merged commit 7df80b1 into bars-and-histograms Oct 26, 2026
7 checks passed
thclark added a commit that referenced this pull request Oct 26, 2026
Normalise the members of a stacked column of `hists` by the pooled total
of the column, so that the top of the stack is the normalised histogram
of the pooled sample and each layer is its member's share of it, as
Matplotlib's `hist(stacked=True, density=True)` and seaborn's default
`common_norm=True` do. Members in columns of their own are still
normalised by their own totals.

Add `HistsMut::common_norm(bool)`, which normalises every member by the
pooled total of all the members, or each by its own, whatever the layout.

Record in each member's `Binning.total` the total its values were
normalised by, so that a datatip recovers the member's own count.
@thclark
thclark deleted the hists-common-norm branch October 26, 2026 14:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant