-- tdigest_percentile_of() interpolates between two centroids by computing the -- slope of the line connecting their means -- -- m = (curr->mean - prev->mean) / (curr->count / 2.0 + prev->count / 2.0); -- x = (value - prev->mean) / m; -- -- and both the difference of the means and the slope may leave the float8 -- range even though every input is an ordinary finite number: -- -- * (curr->mean - prev->mean) overflows to infinity when the means are -- large and have opposite signs. The slope is then infinity, and x is -- either 0 (silently wrong) or NaN. -- -- * the slope underflows to zero when the means are tiny (subnormal) and -- the counts are huge. x is then infinity, and so is the result. -- -- tdigest_compute_quantiles() was reworked to use a convex combination -- exactly to avoid this. tdigest_compute_quantiles_of() still uses the slope, -- and it is the last place in the code that does. -- -- A percentile is a probability, so the result always has to be a finite -- number in the [0, 1] range. It also must not depend on the scale of the -- data - scaling all the means and the probed value by the same power of two -- has to produce exactly the same percentiles. \set VERBOSITY terse SET extra_float_digits = 0; CREATE TABLE tdigest_extreme_digests (id int, descr text, d tdigest, probes double precision[]); INSERT INTO tdigest_extreme_digests VALUES (1, 'means of opposite sign, the difference overflows', 'flags 1 count 2 compression 10000 centroids 2 (-1.7e308, 1) (1.7e308, 1)', ARRAY[-1.7e308, -1e308, -1e307, 0, 1e307, 1e308, 1.7e308]), (2, 'the same, with uneven counts', 'flags 1 count 10 compression 10000 centroids 2 (-1.5e308, 7) (1.5e308, 3)', ARRAY[-1.5e308, -1e308, 0, 1e308, 1.5e308]), (3, 'the same, with a centroid in the middle', 'flags 1 count 3 compression 10000 centroids 3 (-1.7e308, 1) (0, 1) (1.7e308, 1)', ARRAY[-1.7e308, -1e308, -1e-300, 0, 1e-300, 1e308, 1.7e308]), (4, 'subnormal means and huge counts, the slope underflows', 'flags 1 count 9223372036854775806 compression 10000 centroids 2 (1e-320, 4611686018427387903) (2e-320, 4611686018427387903)', ARRAY[1e-320, 1.2e-320, 1.5e-320, 1.8e-320, 2e-320]), (5, 'almost adjacent means and huge counts', 'flags 1 count 9223372036854775806 compression 10000 centroids 2 (1, 4611686018427387903) (1.0000000000000004, 4611686018427387903)', ARRAY[1, 1.0000000000000002, 1.0000000000000004]), (6, 'large means and huge counts', 'flags 1 count 4611686018427387903 compression 10000 centroids 2 (-1e308, 2305843009213693951) (1e308, 2305843009213693952)', ARRAY[-1e308, -5e307, 0, 5e307, 1e308]), (7, 'ordinary means, for comparison', 'flags 1 count 10 compression 10000 centroids 2 (-1.5, 7) (1.5, 3)', ARRAY[-1.5, -1, 0, 1, 1.5]); -- Every result has to be a probability. Currently some of them are NaN -- (the difference of the means overflowed) or Infinity (the slope underflowed -- to zero). WITH q AS ( SELECT id, descr, probes, tdigest_percentile_of(d, probes) AS res FROM tdigest_extreme_digests GROUP BY id, descr, probes ) SELECT id, descr, v AS value, r AS percentile_of FROM q, unnest(q.probes, q.res) WITH ORDINALITY AS x(v, r, ord) WHERE NOT (r BETWEEN 0 AND 1) ORDER BY id, ord; -- And the result still has to be a non-decreasing function of the value. WITH q AS ( SELECT id, descr, probes, tdigest_percentile_of(d, probes) AS res FROM tdigest_extreme_digests GROUP BY id, descr, probes ), u AS ( SELECT id, descr, ord, v, r FROM q, unnest(q.probes, q.res) WITH ORDINALITY AS x(v, r, ord) ), s AS ( SELECT id, descr, v, r, lag(v) OVER w AS prev_v, lag(r) OVER w AS prev_r FROM u WINDOW w AS (PARTITION BY id ORDER BY ord) ) SELECT id, descr, count(*) AS decreasing_steps FROM s WHERE r < prev_r GROUP BY id, descr ORDER BY id; -- This is reachable without writing the digest by hand - two ordinary finite -- values are enough to build a digest whose centroid means are far enough -- apart. WITH q AS ( SELECT tdigest_percentile_of(v, 10000, ARRAY[-1e307, 0.0, 1e307]::double precision[]) AS res FROM (VALUES (-1.7e308::double precision), (1.7e308::double precision)) t(v) ) SELECT bool_and(r BETWEEN 0 AND 1) AS all_probabilities FROM q, unnest(q.res) AS r; WITH q AS ( SELECT tdigest_percentile_of(v, c, 10000, ARRAY[-1e307, 0.0, 1e307]::double precision[]) AS res FROM (VALUES (-1.7e308::double precision, 7::bigint), (1.7e308::double precision, 3::bigint)) t(v, c) ) SELECT bool_and(r BETWEEN 0 AND 1) AS all_probabilities FROM q, unnest(q.res) AS r; -- The percentile only depends on where the value sits between the two -- centroid means, so multiplying the means and the probed values by the same -- power of two must not change the result in any way. 2^1022 pushes the -- difference of the means (4) just past the float8 range, while every single -- mean still fits comfortably. The comparison allows for a rounding error of -- a couple of ULPs, the difference we are looking for here is 0.0625. WITH s(k) AS ( VALUES (1::double precision), (2::double precision ^ 1022) ), r AS ( SELECT s.k, tdigest_percentile_of(t.v * s.k, t.c, 10000, ARRAY[-3*s.k, -2*s.k, -1*s.k, 0*s.k, 1*s.k]) AS q FROM s, (VALUES (-3.0::double precision, 3::bigint), (1.0::double precision, 1::bigint)) t(v, c) GROUP BY s.k ), z AS ( SELECT a.q AS unscaled, b.q AS scaled FROM r a, r b WHERE a.k = 1 AND b.k <> 1 ) SELECT bool_and(abs(u - s) <= 1e-15) AS scale_invariant FROM z, unnest(z.unscaled, z.scaled) AS x(u, s); DROP TABLE tdigest_extreme_digests;