DisparityBounds
where , , and (Bonferroni split).
Robust bounds on with specified coverage.
Input
- — first sample of measurements, where , requires sparity(x)
- — second sample of measurements, where , requires sparity(y)
- — probability that true disparity falls outside bounds in the long run (minimum depends on , ; see Algorithm)
Output
- Value interval bounding
- Unit dimensionless (spread units)
Notes
- Note Bonferroni split between shift and avg-spread bounds; no independence assumption needed; bounds may be unbounded when pooled spread cannot be certified positive
Properties
- Location invariance
- Scale invariance
- Antisymmetry (bounds reversed)
- Monotonicity in misrate smaller produces wider bounds
Example
DisparityBounds([1..30], [21..50], 10^(-3))returns bounds containingDisparity
See also: for comparing Disparity against practical thresholds with automatic verdict generation.
Algorithm
The estimator constructs bounds on by combining and through a Bonferroni split.
Misrate allocation
The total budget is split between the shift and avg-spread components. Let (minimum for ) and (minimum for ). The extra budget beyond the minimums is split equally:
Component bounds
Compute and . By Bonferronis inequality, the probability that both intervals simultaneously contain their respective true values is at least .
Interval division
When , the disparity bounds are obtained by dividing the shift interval by the avg-spread interval. Since dividing by a positive interval can flip the ordering depending on the sign of the numerator endpoints, the algorithm computes all four combinations and takes the extremes:
Edge cases
When (the avg-spread interval includes zero), the bounds become partially or fully unbounded depending on the sign of :
- :
- :
- :
- otherwise:
When (the avg-spread interval collapses to zero), only the sign of the shift determines the result.
using Pragmastat.Exceptions;
using Pragmastat.Internal;
using Pragmastat.Metrology;
using static Pragmastat.Functions.MinAchievableMisrate;
namespace Pragmastat.Estimators;
/// <summary>
/// Distribution-free bounds for disparity using Bonferroni combination.
/// </summary>
public class DisparityBoundsEstimator : ITwoSampleBoundsEstimator
{
public static readonly DisparityBoundsEstimator Instance = new();
/// <summary>
/// Raw native-array entry point. Returns unitless bounds.
/// </summary>
/// <param name="x">First sample values, in the order the disjoint-pair shuffle should run on.</param>
/// <param name="y">Second sample values, in the order the disjoint-pair shuffle should run on.</param>
/// <param name="misrate">Misclassification rate.</param>
/// <param name="assumeSorted">
/// When true, the inputs are treated as already sorted ascending and the embedded
/// order-independent sparity and shift-bounds sub-computations skip re-sorting. This flag is
/// inert (never changes the result) ONLY when the inputs are genuinely sorted: the embedded
/// shift-bounds consumes the inputs as a sorted view, so on UNSORTED input passing true is
/// undefined behavior and CAN change the result. The caller is responsible.
/// </param>
public Bounds Estimate(double[] x, double[] y, double misrate, bool assumeSorted = false) =>
EstimateRaw(x, assumeSorted ? x : null, y, assumeSorted ? y : null, misrate, null);
public Bounds Estimate(double[] x, double[] y, double misrate, string? seed, bool assumeSorted = false) =>
EstimateRaw(x, assumeSorted ? x : null, y, assumeSorted ? y : null, misrate, seed);
public Bounds Estimate(Sample x, Sample y, Probability misrate)
{
return Estimate(x, y, misrate, null);
}
public Bounds Estimate(Sample x, Sample y, Probability misrate, string? seed)
{
Assertion.NonWeighted("x", x);
Assertion.NonWeighted("y", y);
Assertion.CompatibleUnits(x, y);
(x, y) = Assertion.ConvertToFiner(x, y);
// The shuffle runs on the original order; the cached sorted views are sparity/shift-only.
var rb = EstimateRaw(x.Values, x.SortedValues, y.Values, y.SortedValues, misrate, seed);
return new Bounds(rb.Lower, rb.Upper, MeasurementUnit.Disparity);
}
/// <summary>
/// Single shared implementation. <paramref name="x"/>/<paramref name="y"/> are always in
/// ORIGINAL order; <paramref name="sortedX"/>/<paramref name="sortedY"/>, when non-null, are
/// pre-sorted views used only for the order-independent sparity and shift-bounds sub-computations.
/// Returns unitless bounds (the Sample path re-attaches the disparity unit).
/// </summary>
internal static Bounds EstimateRaw(
IReadOnlyList<double> x, IReadOnlyList<double>? sortedX,
IReadOnlyList<double> y, IReadOnlyList<double>? sortedY,
double misrate, string? seed)
{
Assertion.Validity(x, Subject.X);
Assertion.Validity(y, Subject.Y);
if (double.IsNaN(misrate) || misrate < 0 || misrate > 1)
throw AssumptionException.Domain(Subject.Misrate);
int n = x.Count;
int m = y.Count;
if (n < 2)
throw AssumptionException.Domain(Subject.X);
if (m < 2)
throw AssumptionException.Domain(Subject.Y);
double minShift = TwoSample(n, m);
double minX = OneSample(n / 2);
double minY = OneSample(m / 2);
double minAvg = 2.0 * Math.Max(minX, minY);
if (misrate < minShift + minAvg)
throw AssumptionException.Domain(Subject.Misrate);
double extra = misrate - (minShift + minAvg);
double alphaShift = minShift + extra / 2.0;
double alphaAvg = minAvg + extra / 2.0;
// The Spread > 0 sparity check is performed by AvgSpreadBoundsEstimator below (identical
// predicate, same Subject.X/Y order). ShiftBounds runs first but cannot throw for these inputs
// (alphaShift >= the two-sample minimum), so it cannot mask that sparity error.
// ShiftBounds is order-independent given sorted input; use the sorted views when present.
Bounds shiftBounds = sortedX != null && sortedY != null
? ShiftBoundsEstimator.EstimateRaw(sortedX, sortedY, alphaShift, assumeSorted: true)
: ShiftBoundsEstimator.EstimateRaw(x, y, alphaShift, assumeSorted: false);
Bounds avgBounds = AvgSpreadBoundsEstimator.EstimateRaw(x, sortedX, y, sortedY, alphaAvg, seed);
double la = avgBounds.Lower;
double ua = avgBounds.Upper;
double ls = shiftBounds.Lower;
double us = shiftBounds.Upper;
if (la > 0.0)
{
double r1 = ls / la;
double r2 = ls / ua;
double r3 = us / la;
double r4 = us / ua;
double lower = Math.Min(Math.Min(r1, r2), Math.Min(r3, r4));
double upper = Math.Max(Math.Max(r1, r2), Math.Max(r3, r4));
return new Bounds(lower, upper, MeasurementUnit.Number);
}
if (ua <= 0.0)
{
if (ls == 0.0 && us == 0.0)
return new Bounds(0.0, 0.0, MeasurementUnit.Number);
if (ls >= 0.0)
return new Bounds(0.0, double.PositiveInfinity, MeasurementUnit.Number);
if (us <= 0.0)
return new Bounds(double.NegativeInfinity, 0.0, MeasurementUnit.Number);
return new Bounds(double.NegativeInfinity, double.PositiveInfinity, MeasurementUnit.Number);
}
if (ls > 0.0)
return new Bounds(ls / ua, double.PositiveInfinity, MeasurementUnit.Number);
if (us < 0.0)
return new Bounds(double.NegativeInfinity, us / ua, MeasurementUnit.Number);
if (ls == 0.0 && us == 0.0)
return new Bounds(0.0, 0.0, MeasurementUnit.Number);
if (ls == 0.0 && us > 0.0)
return new Bounds(0.0, double.PositiveInfinity, MeasurementUnit.Number);
if (ls < 0.0 && us == 0.0)
return new Bounds(double.NegativeInfinity, 0.0, MeasurementUnit.Number);
return new Bounds(double.NegativeInfinity, double.PositiveInfinity, MeasurementUnit.Number);
}
}
Notes
Width Convergence
The table below shows how narrows as grows, for ( evenly spaced points on ) and . Dashes indicate too small to achieve the target misrate.
| N | Width |
|---|---|
| 2 | — |
| 3 | — |
| 4 | — |
| 5 | — |
| 10 | — |
| 20 | — |
| 30 | 18.0000 |
| 40 | 5.0000 |
| 50 | 3.1429 |
| 100 | 2.3077 |
| 200 | 1.2000 |
| 300 | 0.9123 |
| 400 | 0.5918 |
| 500 | 0.5893 |
| 1000 | 0.3817 |
| 10000 | 0.1050 |


Tests
The test suite contains 39 test cases (3 demo + 5 natural + 6 property + 5 edge + 5 misrate + 2 distro + 6 unsorted + 7 error). Since returns bounds rather than a point estimate, tests validate that the bounds contain and satisfy equivariance properties. Each test case output is a JSON object with lower and upper fields representing the interval bounds. Because the denominator () uses randomized , tests fix a seed to keep outputs deterministic.
Demo examples (, ) from manual introduction:
demo-1: , , baseline fixture misratedemo-2: , , stricter fixture misrate, wider boundsdemo-3: , , looser fixture misrate
These cases illustrate how tighter misrates produce wider bounds.
Natural sequences (reference fixture misrates) 5 tests:
natural-10-10: , , bounds containingnatural-10-15: ,natural-15-10: ,natural-15-15: , , bounds containingnatural-20-20: , , bounds containing
Property validation () 6 tests:
property-identity: , , expected output:property-location-shift: , , expected output:property-scale-2x: , (= 2× location-shift), expected output: (scale invariance of disparity)property-scale-neg: , (negated), expected output: (anti-symmetry under sign flip)property-symmetry: , , observed boundsproperty-symmetry-swapped: and swapped, bounds negated (anti-symmetry)
Edge cases boundary conditions (5 tests):
edge-small: (small samples)edge-negative: negative values for both samplesedge-mixed-signs: mixed positive/negative valuesedge-wide-range: extreme value rangeedge-asymmetric-10-20: , (unbalanced sizes)
Misrate variation (, ) 5 tests spanning progressively stricter fixture misrates:
These tests validate monotonicity: smaller misrates produce wider bounds.
Distribution tests ( varies) 2 tests:
additive-20-20: ,uniform-20-20: ,
Unsorted tests verify independent sorting of and (6 tests):
unsorted-reverse-x: X reversed, Y sortedunsorted-reverse-y: X sorted, Y reversedunsorted-reverse-both: both reversedunsorted-shuffle-x: X shuffled, Y sortedunsorted-shuffle-y: X sorted, Y shuffledunsorted-wide-range: wide value range, both unsorted
These tests validate that produces identical results regardless of input order.
Error cases inputs that violate assumptions (7 tests):
error-empty-x: (empty X array)error-empty-y: (empty Y array)error-single-element-x: (too few elements for pairing)error-single-element-y: (too few elements for pairing)error-constant-x: constant violates sparity ()error-constant-y: constant violates sparity ()error-misrate-below-min: misrate below minimum achievable