Return automatically chosen histogram edges as UniformEdges and fix silently dropped observations - #1011
Open
andreasnoack wants to merge 3 commits into
Open
Return automatically chosen histogram edges as UniformEdges and fix silently dropped observations#1011andreasnoack wants to merge 3 commits into
andreasnoack wants to merge 3 commits into
Conversation
Pass `lt = <` to `searchsortedfirst`/`searchsortedlast` in `_edge_binindex` instead of relying on `isless`. Since `-0.0 < 0.0` is false, -0.0 and 0.0 are binned identically without normalizing the inputs, so `_normalize_zero`, the separate `AbstractRange` method, and the constructor check rejecting ranges containing -0.0 can all go. `<` is also cheaper than `isless`: `fit(Histogram)` is about 20% faster with range edges and about 2x faster with vector edges.
…ed observations `fit(Histogram, v; nbins)` could silently drop observations at the extremes of the data (#1009): for Float32 and Float16 data the endpoint checks in `histrange` were done in the element type while the edges were Float64, and for Float64 data with `closed=:right` the last element of the returned TwicePrecision range evaluated one ulp below the value that was checked. The edges are now the decimal numbers `k * 10^e` for consecutive multiples of a nice width, each rounded to the nearest value of the data's float type `F`, and returned as a `Vector{F}`. Only integers and `F` are involved, with one conversion per edge and no arithmetic on the converted values, so the same code works for every AbstractFloat. The endpoints are adjusted by comparing `lo` and `hi` against the `F` edges themselves, which is what `binindex` compares against, so all observations are inside the edges by construction. The width is clamped to three times the floating-point spacing of the data so that the edges are strictly increasing. Identical values get a unit-width bin with decimal edges. The conversion of `k * 10^e` is a single IEEE operation when both operands are exact in `F`; beyond about 1e±22 (Float64) a correctly rounded power of ten is taken from a table and the result is within two ulps of the decimal. The docstring of `fit` now documents that observations outside supplied edges are not counted, and `show` abbreviates long edge vectors.
A plain `Vector` loses what `histrange` knows about the edges it produced:
that the bins have equal width, and what that width is. `UniformEdges{F}` is an
`AbstractVector{F}` wrapping the stored edges together with the width. `step`
returns the width, `binvolume` uses it so that bin volumes are exact, `show`
prints first edge, width and last edge, and `binindex` estimates the bin from
the width and corrects it against the stored edges, which is faster than both
the range arithmetic and a binary search. User-supplied edges are unaffected.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Return automatically chosen histogram edges as
UniformEdgesand fix silently dropped observationsFixes #1009. Includes #1010, which can be closed if this is merged.
Problem
fit(Histogram, v; nbins)could silently drop observations at the extremes of the data (#1009). Two independent causes:Float32andFloat16data,histrangedid its endpoint checks in the element type while returningFloat64edges.Float32(0.7)is0.69999998as aFloat64, so an edge at0.7passed the check inFloat32and then excluded the observation inbinindex.Float64data withclosed=:right, the last edge was checked as(start + (len-1)*step)/divisorbut the returnedTwicePrecisionrange evaluated it one ulp lower, e.g.0.19999999999999998for data[0.0, 0.2]with 9 bins.Both come from the same source: the edges were represented as a float range whose elements are computed by arithmetic that does not reproduce the decimal numbers the edges are meant to be, and the checks were done against something other than what
binindexcompares against.Approach
The automatically chosen edges are now the decimal numbers
k * 10^efor consecutive multipleskof a "nice" width (1, 2 or 5 times a power of ten, as before), each rounded to the nearest value of the data's float typeF, and stored in aVector{F}. There are only two representations involved, integers on the decimal side andFon the binary side, with one conversion per edge and no arithmetic on the converted values.The edges are returned as
UniformEdges{F} <: AbstractVector{F}, which wraps the stored vector together with the bin width. The type records whathistrangeknows and a plain vector would lose, namely that the bins have equal width and what that width is:step(edges)returns it,binvolumeandnormalizeuse it so bin volumes are exact rather than differences of rounded edges,showprints the edges as first edge, width, last edge like a range, andbinindexestimates the bin from the width and corrects against the stored edges, which is faster than both the previous range arithmetic and a binary search. The elements are the storedFvalues, so no arithmetic progression is represented and the numerics do not depend on the type. User-supplied edges are untouched, so a non-range vector of edges continues to mean bins of possibly unequal width.F, so the same code works forFloat16,Float32,Float64,BigFloatand any otherAbstractFloat.round.(x, digits=2)lands in the bin one expects:0.7f0is in the bin starting at0.7f0.loandhiagainst theFedges themselves, i.e. against exactly whatbinindexsees, so containment holds by construction for every type.[1.0, 2.0]for data equal to1.05, instead of[1.05, 2.05].The conversion of
k * 10^etoFis a single IEEE multiplication or division whenkand10^|e|are exactly representable, which is correctly rounded and covers everything up to about1e±22forFloat64and1e±10forFloat32. Beyond that a correctly rounded power of ten is taken from a small table and at most three roundings occur, so edges are within two ulps of the decimal. Exact conversion for all exponents is the job of a decimal parser and is not attempted here. Note thatF(k // 10^d)cannot be used: Base performs that division inFwith rounded operands (JuliaLang/julia#49749).binindexnow compares with<instead ofisless(#1010), which treats-0.0and0.0as equal without the_normalize_zeroworkaround and lets NaN fall outside the edges as before.Behaviour changes
histrange, and thereforeedgesof histograms fitted withnbins, returnsUniformEdges{F}instead of aStepRangeLen. It is anAbstractVector{F}withstep, but not anAbstractRange. ForFloat32andFloat16data the element type is nowFrather thanFloat64.-0.0in user-supplied ranges is accepted instead of throwing.show(::Histogram)prints user-supplied edge vectors with:limit => true;UniformEdgesprint as first edge, width, last edge.midpointsofUniformEdgesare averages of neighbouring edges and may differ from the previous range-based midpoints by an ulp.fitwithnbinsis faster: on 1e6Float64observations, 13.3 → 10.0 ms with 10 bins and 13.2 → 10.0 ms with 100 bins.histrangeitself is about 20x faster (0.5 µs for 100 bins).The docstring of
fitnow states that observations outside suppliededgesare not counted, which has been the behaviour since 2014 but was undocumented, and that edges should have the element type of the data for decimal-rounded observations and edges to compare as intended.Downstream packages
Checked by reading their sources. Consumers that call
step(h.edges[1])onnbinshistograms keep working becauseUniformEdgessupportsstep: PairPlots, HistTools, EvoId, Octofitter's legacy Makie extension. Would break:@assert isa(h.edges[1], AbstractRange)on histograms passed to its plot recipes)Ulp-level output changes only: AlgebraOfGraphics (its
midpoints(::AbstractVector)method is now taken; its tests use≈), Makie's datashader, UnicodePlots labels forFloat32data. Transparent: Plots, StatsPlots, Makiehist/stephist, BAT, FHist, LegendSpecFits. Gadfly and the Plotly packages do not useStatsBase.Histogram.Before merging, RadiationSpectra should be fixed to not require an
AbstractRange, and this should be released as a minor version.Tests
Histograms #1009 overFloat16,Float32,Float64andBigFloat, bothclosedvalues and 1 to 12 bins: edges have element typeF, contain the data, andsum(weights)equals the number of observations.stepand exact edges forFloat32,Float16andBigFloatinput; strictly increasing edges for data spanning 0 to 40 ulps at several magnitudes and up to 1000 requested bins; the two-ulp bound at extreme magnitudes; non-finite data throws;binindexonUniformEdgesagrees with the generic search for all inputs including signed zeros and infinities, and NaN is outside the bins on both paths;binvolumeequalsstep;showofUniformEdgesand of long user-supplied edge vectors.first(r) == xfor identical values is replaced by containment and strictness checks, since the edge coinciding with the value was an artifact of the old implementation.histrangeto ranges with==are unchanged and pass, as nearest-rounded edges coincide with the previous values.🤖 Generated with Claude Code