Disparity
where is the weighted average of dispersions (pooled scale).
Robust effect size (shift normalized by pooled spread).
Input
- — first sample of measurements, requires sparity(x)
- — second sample of measurements, requires sparity(y)
Output
- Value —
- Unit — spread units
Notes
- Robust alternative to — Cohen’s d (Cohen 1988; uses robust shift and spread instead of mean difference and pooled standard deviation)
Properties
- Location invariance
- Scale invariance
- Antisymmetry
Example
Disparity(x, y) = 0.4whereShift = 2,AvgSpread = 5Disparity(x + c, y + c) = Disparity(x, y)Disparity(kx, ky) = Disparity(x, y)
expresses a difference between groups in a way that does not depend on the original measurement units. A disparity of 0.5 means the groups differ by half a spread unit; 1.0 means one full spread unit. Being dimensionless allows comparison of effect sizes across different studies, metrics, or measurement scales. What counts as a “large” or “small” disparity depends entirely on the domain and what matters practically in a given application. Do not rely on universal thresholds; interpret the number in context.
Algorithm
The estimator is a composition of Shift and Spread:
where is the pooled scale.
The algorithm proceeds as follows:
-
Compute Spread for each sample — Delegate to the Spread algorithm for and independently.
-
Compute AvgSpread — Form the weighted average .
-
Domain check — Verify that . If the pooled spread is zero, the division is undefined.
-
Compute Shift — Delegate to the Shift algorithm for the pair .
-
Divide — Return .
using Pragmastat.Algorithms;
using Pragmastat.Exceptions;
using Pragmastat.Internal;
using Pragmastat.Metrology;
namespace Pragmastat.Estimators;
public class DisparityEstimator : ITwoSampleEstimator
{
public static readonly DisparityEstimator Instance = new();
/// <summary>
/// Raw native-array entry point. Returns a unitless disparity estimate.
/// </summary>
/// <param name="x">First sample values.</param>
/// <param name="y">Second sample values.</param>
/// <param name="assumeSorted">
/// When true, both <paramref name="x"/> and <paramref name="y"/> are assumed already sorted
/// ascending and the internal sort is skipped. This changes the computation path; passing true
/// on unsorted input is undefined behavior and yields a wrong result. The caller is responsible.
/// </param>
public double Estimate(double[] x, double[] y, bool assumeSorted = false) => EstimateRaw(x, y, assumeSorted);
public Measurement Estimate(Sample x, Sample y)
{
Assertion.NonWeighted("x", x);
Assertion.NonWeighted("y", y);
Assertion.CompatibleUnits(x, y);
(x, y) = Assertion.ConvertToFiner(x, y);
return EstimateRaw(x.SortedValues, y.SortedValues, assumeSorted: true).WithUnit(MeasurementUnit.Disparity);
}
/// <summary>
/// Single shared implementation. Both the raw and Sample entry points call this.
/// </summary>
internal static double EstimateRaw(IReadOnlyList<double> x, IReadOnlyList<double> y, bool assumeSorted)
{
Assertion.Validity(x, Subject.X);
Assertion.Validity(y, Subject.Y);
int n = x.Count;
int m = y.Count;
var spreadX = SpreadImpl.Estimate(x, assumeSorted: assumeSorted);
if (spreadX <= 0)
throw AssumptionException.Sparity(Subject.X);
var spreadY = SpreadImpl.Estimate(y, assumeSorted: assumeSorted);
if (spreadY <= 0)
throw AssumptionException.Sparity(Subject.Y);
var shiftVal = ShiftImpl.Estimate(x, y, [0.5], assumeSorted)[0];
var avgSpreadVal = (n * spreadX + m * spreadY) / (n + m);
return shiftVal / avgSpreadVal;
}
}
Tests
The test suite contains 30 test cases (16 original + 12 unsorted + 2 error). Since combines and , unsorted tests verify both components handle sorting correctly.
Demo examples () — from manual introduction, validating properties:
demo-1: , , expected output: (base case: )demo-2: , (= demo-1 + 5), expected output: (location invariance)demo-3: , (= 2 × demo-1), expected output: (scale invariance)demo-4: , (= reversed demo-1), expected output: (anti-symmetry)
Natural sequences () — 4 combinations:
natural-2-2,natural-2-3,natural-3-2,natural-3-3- Minimum size required for meaningful dispersion calculations
Negative values () — end-to-end validation with negative values:
negative-2-2: , , expected output:
Uniform distribution () — 4 combinations with :
uniform-5-5,uniform-5-100,uniform-100-5,uniform-100-100- Random generation: uses seed 0, uses seed 1
The smaller test set for reflects implementation confidence. Since combines and , correct implementation of those components ensures correctness. The test cases validate the division operation and confirm scale-free properties.
Composite estimator stress tests — edge cases for effect size calculation:
composite-small-avgspread: , (tiny spread, large shift)composite-large-avgspread: , (large spread, small shift)composite-extreme-disparity: , (extreme ratio, tests precision)
Unsorted tests — verify both Shift and AvgSpread handle sorting (12 tests):
unsorted-x-natural-{n}-{m}for : X unsorted (reversed), Y sorted (2 tests)unsorted-y-natural-{n}-{m}for : X sorted, Y unsorted (reversed) (2 tests)unsorted-both-natural-{n}-{m}for : both unsorted (reversed) (2 tests)unsorted-demo-unsorted-x: , (demo-1 with X unsorted)unsorted-demo-unsorted-y: , (demo-1 with Y unsorted)unsorted-demo-both-unsorted: , (demo-1 both unsorted)unsorted-location-invariance-unsorted: , (demo-2 unsorted)unsorted-scale-invariance-unsorted: , (demo-3 unsorted)unsorted-anti-symmetry-unsorted: , (demo-4 reversed and unsorted)
As a composite estimator, tests both the numerator () and denominator (). Unsorted variants verify end-to-end correctness including invariance properties.
Error cases — input validation (2 tests):
error-empty-x: , — empty X array violates validityerror-empty-y: , — empty Y array violates validity