Verified. All five implementations give the same answers on 430 data sets: examples from the original paper, degenerate and non-converging cases, data on scales from 10−34 to 109. The preferential median was checked against the authors' reference program. Besides it, the code implements nine common estimators — from the mean and median to DerSimonian–Laird and the mixture model mode (table with the original sources); they were checked across languages and against independent computations by the formulas of the original sources. The data sets with answers can be downloaded to check your own implementation (see “Reference values”).
Python 3.8+, standard library only. File: ifpa.py.
from ifpa import preferential_median, compare_estimators
x = [10.012, 10.018, 10.009, 10.015, 10.021, 10.011, 10.064, 10.016]
u = [0.010, 0.012, 0.015, 0.008, 0.014, 0.010, 0.004, 0.020]
r = preferential_median(x, u)
print(f"PM = {r['pm']:.4f} ± {r['u_pm']:.4f}") # PM = 10.0125 ± 0.0040
for e in compare_estimators(x, u): # PM and nine other estimators
print(e["name"], e["value"], e["u"])Full code of ifpa.py
"""
IF&PA — interval fusion with preference aggregation.
Preferential median (PM) of a set of intervals [x_k - u_k, x_k + u_k].
Pure Python, no dependencies. Python 3.8+.
References:
Muravyov S.V., Khudonogova L.I., Emelyanova E.Yu. Interval data fusion with
preference aggregation. Measurement, 2018, 116, 621-630. doi:10.1016/j.measurement.2017.08.045
Muravyov S.V., Khudonogova L.I., Shabramov V.S., Ignatyev V.D., Andreyev D.I.
Kemeny and Borda rules in constructing central tendency estimators based on the
preferential median. Proc. AISP'25, 2025. doi:10.1109/AISP68263.2025.11396113
(the Borda rule gives the same preferential median as the Kemeny rule).
Muravyov S.V., Khudonogova L.I., Shabramov V.S., Ignatyev V.D. Comparative studies
of preferential and weighted medians accuracy in robust estimation of central
tendency. Int. J. Data Sci. Anal., 2026, 22(1), 262. doi:10.1007/s41060-026-01222-6
(standard uncertainty of the PM due to the RAV partition: 0.41 h).
Version of the method: IF&PA-L — Borda rule, no self-refinement.
Uncertainty: u_pm returned here is the combined standard uncertainty
sqrt(u_grid^2 + u_sampling^2), written u(x_pm) on the portal's Method page;
u_grid = 0.41 h is what the papers call u_pm; u_sampling = 1.2533 * 1.4826 * MAD / sqrt(m)
is an addition of this implementation, not part of the published method.
Usage:
>>> from ifpa import preferential_median
>>> r = preferential_median([4.375, 4.9], [0.175, 0.2], n=3)
>>> round(r["pm"], 3), r["n"]
(4.65, 4)
"""
import math
PM_UNCERTAINTY_FACTOR = 0.41 # u_grid = 0.41 h (h / sqrt(6), triangular distribution)
MEDIAN_SE_FACTOR = 1.2533 # sqrt(pi/2): standard error of the median
MAD_TO_SD = 1.4826 # MAD -> standard deviation for a normal sample
N_MIN_DEFAULT = 11
N_MAX_DEFAULT = 201
FLOAT_EPS = 2.220446049250313e-16 # machine epsilon of double
def _median(values):
v = sorted(values)
p = len(v)
if p == 0:
raise ValueError("empty sample")
if p % 2 == 1:
return v[(p - 1) // 2]
return (v[p // 2 - 1] + v[p // 2]) / 2.0
def _sampling_uncertainty(centres):
"""Standard error of the median of interval centres, robust scale (MAD)."""
m = len(centres)
if m < 2:
return 0.0
med = _median(centres)
scale = MAD_TO_SD * _median([abs(c - med) for c in centres])
return MEDIAN_SE_FACTOR * scale / math.sqrt(m)
def _coverage(grid, lo, hi, eps):
"""c_i = number of k with lo_k - eps <= a_i <= hi_k + eps, for an ascending grid.
Two pointers over the sorted bounds: O(n + m log m) instead of O(n m)."""
left = sorted(l - eps for l in lo)
right = sorted(r + eps for r in hi)
m, i, j, out = len(left), 0, 0, []
for a in grid:
while i < m and left[i] <= a:
i += 1
while j < m and right[j] < a:
j += 1
out.append(i - j) # opened up to a minus closed before a
return out
def _widest_block(idx, grid, centre):
"""Non-adjacent winners after n_max: the widest block of consecutive indices;
on a tie — the block whose median is closest to the median of centres."""
blocks, cur = [], [idx[0]]
for i in idx[1:]:
if i == cur[-1] + 1:
cur.append(i)
else:
blocks.append(cur)
cur = [i]
blocks.append(cur)
best, best_key = None, None
for b in blocks:
key = (len(b), -abs(_median([grid[i] for i in b]) - centre))
if best_key is None or key > best_key:
best, best_key = b, key
return best
def preferential_median(x, u, n=None, n_min=N_MIN_DEFAULT, n_max=N_MAX_DEFAULT):
"""
Preferential median of intervals [x_k - u_k, x_k + u_k].
x : interval centres
u : half-widths (standard uncertainties); a single number means equal u_k
n : number of discrete values of the range of actual values (RAV);
None -> n = max(ceil((a_n - a_1) / min 2u_k), n_min)
Returns a dict: pm, u_pm (total), u_grid (0.41 h), u_sampling, n, h,
winners (0-based indices), converged, grid, coverage.
"""
x = [float(v) for v in x]
if isinstance(u, (int, float)):
u = [float(u)] * len(x)
u = [float(v) for v in u]
if len(x) != len(u):
raise ValueError("x and u have different lengths")
if not x:
raise ValueError("empty set of intervals")
if any(v < 0 for v in u):
raise ValueError("uncertainties must be non-negative")
if not all(math.isfinite(v) for v in x + u):
raise ValueError("NaN or infinity in data")
lo = [a - b for a, b in zip(x, u)]
hi = [a + b for a, b in zip(x, u)]
a1, an = min(lo), max(hi)
centres = [(a + b) / 2.0 for a, b in zip(lo, hi)]
if an - a1 <= 0: # all intervals collapse to one point
return dict(pm=a1, u_pm=0.0, u_grid=0.0, u_sampling=0.0, n=1, h=0.0,
winners=[0], converged=True, grid=[a1], coverage=[len(lo)])
if n is None:
width_min = min(b - a for a, b in zip(lo, hi)) # the narrowest interval, 2 min u_k
n = math.ceil((an - a1) / width_min) if width_min > 0 else n_min
n = max(n, n_min)
n = max(int(n), 2)
while True:
h = (an - a1) / (n - 1)
grid = [a1 + h * i for i in range(n)]
# tolerance in the units of the data: a share of the grid step and a few ulps of the values
eps = max(1e-9 * h, 8 * FLOAT_EPS * max(abs(a1), abs(an)))
# coverage c_i: how many intervals contain a_i. The Borda score is
# z_i = sum_k (n - |A_k| - 1) + n c_i, so Borda winners are the values
# with the largest coverage.
coverage = _coverage(grid, lo, hi, eps)
best = max(coverage)
idx = [i for i, c in enumerate(coverage) if c == best]
adjacent = all(j - i == 1 for i, j in zip(idx, idx[1:]))
if adjacent or n >= n_max:
if not adjacent:
idx = _widest_block(idx, grid, _median(centres))
u_grid = PM_UNCERTAINTY_FACTOR * h
u_samp = _sampling_uncertainty(centres)
return dict(pm=_median([grid[i] for i in idx]),
u_pm=math.hypot(u_grid, u_samp),
u_grid=u_grid, u_sampling=u_samp, n=n, h=h,
winners=idx, converged=adjacent,
grid=grid, coverage=coverage)
n += 1
# ---------------------------------------------------------------------------
# Alternatives for comparison: common practice for combining x_k +/- u_k
# ---------------------------------------------------------------------------
CHI2_Z95 = 1.6448536269514722 # standard normal 0.95 quantile
def chi2_quantile_95(df):
"""0.95 quantile of chi-square, Wilson-Hilferty approximation (error < 1 %)."""
a = 2.0 / (9.0 * df)
return df * (1.0 - a + CHI2_Z95 * math.sqrt(a)) ** 3
def weighted_median(x, w):
"""Weighted median: the smallest x with cumulative weight >= half of the total;
exactly half -> midpoint with the next value."""
pairs = sorted(zip(x, w), key=lambda p: p[0])
total = sum(w)
cum = 0.0
for j, (v, wj) in enumerate(pairs):
cum += wj
if abs(cum - total / 2) <= 1e-12 * total and j + 1 < len(pairs):
return (v + pairs[j + 1][0]) / 2.0
if cum >= total / 2:
return v
return pairs[-1][0]
def hodges_lehmann(x):
"""Hodges-Lehmann estimate: median of all pairwise (Walsh) averages, i <= j."""
m = len(x)
return _median([(x[i] + x[j]) / 2.0 for i in range(m) for j in range(i, m)])
def algorithm_a(x, tol=1e-10, max_iter=1000):
"""Robust mean and SD by ISO 13528 Algorithm A (Huber-type, cut-off 1.5 s*).
Returns (x*, s*, u) with u = 1.25 s* / sqrt(m)."""
m = len(x)
xs = _median(x)
ss = 1.483 * _median([abs(v - xs) for v in x])
if ss == 0 or m < 2:
return xs, ss, 0.0
for _ in range(max_iter):
d = 1.5 * ss
clipped = [min(max(v, xs - d), xs + d) for v in x]
new_x = sum(clipped) / m
new_s = 1.134 * math.sqrt(sum((v - new_x) ** 2 for v in clipped) / (m - 1))
done = abs(new_x - xs) <= tol * max(abs(xs), 1e-300) and abs(new_s - ss) <= tol * ss
xs, ss = new_x, new_s
if done or ss == 0:
break
return xs, ss, 1.25 * ss / math.sqrt(m)
def dersimonian_laird(x, u):
"""Random-effects consensus (DerSimonian-Laird), as in NIST Consensus Builder.
Returns (value, standard uncertainty, tau) where tau is the dark uncertainty."""
m = len(x)
w = [1.0 / (v * v) for v in u]
sw = sum(w)
mean_w = sum(a * b for a, b in zip(w, x)) / sw
q = sum(a * (b - mean_w) ** 2 for a, b in zip(w, x))
denom = sw - sum(a * a for a in w) / sw
tau2 = max(0.0, (q - (m - 1)) / denom) if denom > 0 else 0.0
ws = [1.0 / (v * v + tau2) for v in u]
value = sum(a * b for a, b in zip(ws, x)) / sum(ws)
return value, 1.0 / math.sqrt(sum(ws)), math.sqrt(tau2)
def largest_consistent_subset(x, u):
"""Weighted mean of the largest consistent subset (after Cox, 2007), sequential
form: while the chi-square test fails at 0.05, exclude the result with the
largest |x_k - y| / sqrt(u_k^2 - u(y)^2). Returns (value, u, number excluded)."""
keep = list(range(len(x)))
while True:
w = [1.0 / (u[k] ** 2) for k in keep]
sw = sum(w)
y = sum(wk * x[k] for wk, k in zip(w, keep)) / sw
chi2 = sum(wk * (x[k] - y) ** 2 for wk, k in zip(w, keep))
if len(keep) <= 2 or chi2 <= chi2_quantile_95(len(keep) - 1):
return y, 1.0 / math.sqrt(sw), len(x) - len(keep)
uy2 = 1.0 / sw
worst = max(keep, key=lambda k: abs(x[k] - y) / math.sqrt(max(u[k] ** 2 - uy2, 1e-300)))
keep.remove(worst)
def mixture_mode(x, u, points=2001, refine=100):
"""Mode of the mixture of normal densities N(x_k, u_k^2) with equal weights
(Ciarlini, Cox, Pavese, Regoliosi, 2004; Duewer, 2004): grid search over [min(x - 3u), max(x + 3u)], then
golden-section refinement around the best grid point."""
def f(t):
return sum(math.exp(-0.5 * ((t - a) / b) ** 2) / b for a, b in zip(x, u))
lo = min(a - 3 * b for a, b in zip(x, u))
hi = max(a + 3 * b for a, b in zip(x, u))
step = (hi - lo) / (points - 1)
best_i, best_f = 0, -1.0
for i in range(points):
fi = f(lo + step * i)
if fi > best_f:
best_i, best_f = i, fi
a, b = lo + step * max(best_i - 1, 0), lo + step * min(best_i + 1, points - 1)
g = (math.sqrt(5.0) - 1.0) / 2.0
c, d = b - g * (b - a), a + g * (b - a)
for _ in range(refine):
if f(c) >= f(d):
b = d
else:
a = c
c, d = b - g * (b - a), a + g * (b - a)
return (a + b) / 2.0
# Names are shared by all implementations; the site translates them.
ESTIMATORS = (
"Preferential median (IF&PA)", "Arithmetic mean", "Median", "Hodges-Lehmann",
"ISO 13528 Algorithm A", "Weighted mean (1/u^2)", "Weighted median (1/u^2)",
"DerSimonian-Laird", "Largest consistent subset (Cox)", "Mixture model mode",
)
def compare_estimators(x, u, n=None):
"""
The preferential median next to common alternatives on the same data.
Returns a list of dicts {name, value, u}; u is None where the estimator
has no standard uncertainty formula. Estimators that use u_k (weighted,
DerSimonian-Laird, consistent subset, mixture mode) need all u_k > 0.
"""
x = [float(v) for v in x]
if isinstance(u, (int, float)):
u = [float(u)] * len(x)
u = [float(v) for v in u]
m = len(x)
r = preferential_median(x, u, n=n)
out = [dict(name=ESTIMATORS[0], value=r["pm"], u=r["u_pm"])]
mean = sum(x) / m
sd = math.sqrt(sum((v - mean) ** 2 for v in x) / (m - 1)) if m > 1 else float("nan")
out.append(dict(name=ESTIMATORS[1], value=mean, u=sd / math.sqrt(m)))
out.append(dict(name=ESTIMATORS[2], value=_median(x), u=_sampling_uncertainty(x)))
out.append(dict(name=ESTIMATORS[3], value=hodges_lehmann(x), u=None))
xa, _sa, ua = algorithm_a(x)
out.append(dict(name=ESTIMATORS[4], value=xa, u=ua))
if all(v > 0 for v in u):
w = [1.0 / (v * v) for v in u]
out.append(dict(name=ESTIMATORS[5], value=sum(a * b for a, b in zip(w, x)) / sum(w),
u=1.0 / math.sqrt(sum(w))))
out.append(dict(name=ESTIMATORS[6], value=weighted_median(x, w), u=None))
if m >= 2:
value, unc, _tau = dersimonian_laird(x, u)
out.append(dict(name=ESTIMATORS[7], value=value, u=unc))
value, unc, _excl = largest_consistent_subset(x, u)
out.append(dict(name=ESTIMATORS[8], value=value, u=unc))
out.append(dict(name=ESTIMATORS[9], value=mixture_mode(x, u), u=None))
return out
if __name__ == "__main__":
r = preferential_median([4.375, 4.9], [0.175, 0.2], n=3)
print(f"PM = {r['pm']:.3f} +/- {r['u_pm']:.3f} (n = {r['n']})") # PM = 4.650
# interlaboratory comparison where Lab-7 understated its uncertainty
x = [10.012, 10.018, 10.009, 10.015, 10.021, 10.011, 10.064, 10.016]
u = [0.010, 0.012, 0.015, 0.008, 0.014, 0.010, 0.004, 0.020]
for e in compare_estimators(x, u):
unc = "" if e["u"] is None else f" +/- {e['u']:.4f}"
print(f"{e['name']:30s} {e['value']:.4f}{unc}")Modern JavaScript for the browser and Node.js. File: ifpa.js. The same file powers the calculator.
// browser: <script src="ifpa.js"></script>, then the global IFPA object
// Node.js:
const IFPA = require("./ifpa.js");
const x = [10.012, 10.018, 10.009, 10.015, 10.021, 10.011, 10.064, 10.016];
const u = [0.010, 0.012, 0.015, 0.008, 0.014, 0.010, 0.004, 0.020];
const r = IFPA.preferentialMedian(x, u);
console.log(r.pm.toFixed(4), r.uPm.toFixed(4)); // 10.0125 0.0040
console.table(IFPA.compareEstimators(x, u));Full code of ifpa.js
/*
* IF&PA — interval fusion with preference aggregation.
* Preferential median (PM) of a set of intervals [x_k - u_k, x_k + u_k].
*
* Plain JavaScript, no dependencies. Works in the browser (global IFPA)
* and in Node.js (require('./ifpa.js')).
*
* References:
* Muravyov S.V., Khudonogova L.I., Emelyanova E.Yu. Interval data fusion with
* preference aggregation. Measurement, 2018, 116, 621-630. doi:10.1016/j.measurement.2017.08.045
* Muravyov S.V., Khudonogova L.I., Shabramov V.S., Ignatyev V.D., Andreyev D.I.
* Kemeny and Borda rules in constructing central tendency estimators based on the
* preferential median. Proc. AISP'25, 2025. doi:10.1109/AISP68263.2025.11396113
* (the Borda rule gives the same preferential median as the Kemeny rule).
* Muravyov S.V., Khudonogova L.I., Shabramov V.S., Ignatyev V.D. Comparative studies
* of preferential and weighted medians accuracy in robust estimation of central
* tendency. Int. J. Data Sci. Anal., 2026, 22(1), 262. doi:10.1007/s41060-026-01222-6
* (standard uncertainty of the PM due to the RAV partition: 0.41 h).
*
* Version of the method: IF&PA-L — Borda rule, no self-refinement.
* Uncertainty: u_pm returned here is the combined standard uncertainty
* sqrt(u_grid^2 + u_sampling^2), written u(x_pm) on the portal's Method page;
* u_grid = 0.41 h is what the papers call u_pm; u_sampling = 1.2533 * 1.4826 * MAD / sqrt(m)
* is an addition of this implementation, not part of the published method.
*
* Usage:
* const r = IFPA.preferentialMedian([4.375, 4.9], [0.175, 0.2], { n: 3 });
* r.pm // 4.65
*/
(function (root, factory) {
if (typeof module === "object" && module.exports) module.exports = factory();
else root.IFPA = factory();
})(typeof self !== "undefined" ? self : this, function () {
"use strict";
const PM_UNCERTAINTY_FACTOR = 0.41; // u_grid = 0.41 h
const MEDIAN_SE_FACTOR = 1.2533; // sqrt(pi/2)
const MAD_TO_SD = 1.4826;
const N_MIN_DEFAULT = 11;
const N_MAX_DEFAULT = 201;
function median(values) {
const v = values.slice().sort((a, b) => a - b);
const p = v.length;
if (p === 0) throw new Error("empty sample");
return p % 2 === 1 ? v[(p - 1) / 2] : (v[p / 2 - 1] + v[p / 2]) / 2;
}
function samplingUncertainty(centres) {
const m = centres.length;
if (m < 2) return 0;
const med = median(centres);
const scale = MAD_TO_SD * median(centres.map((c) => Math.abs(c - med)));
return (MEDIAN_SE_FACTOR * scale) / Math.sqrt(m);
}
// c_i = number of k with lo_k - eps <= a_i <= hi_k + eps, for an ascending grid;
// two pointers over the sorted bounds: O(n + m log m) instead of O(n m)
function coverageOf(grid, lo, hi, eps) {
const left = lo.map((l) => l - eps).sort((p, q) => p - q);
const right = hi.map((r) => r + eps).sort((p, q) => p - q);
const m = left.length, out = new Array(grid.length);
let i = 0, j = 0;
for (let g = 0; g < grid.length; g++) {
const a = grid[g];
while (i < m && left[i] <= a) i++;
while (j < m && right[j] < a) j++;
out[g] = i - j; // opened up to a minus closed before a
}
return out;
}
function widestBlock(idx, grid, centre) {
const blocks = [];
let cur = [idx[0]];
for (let j = 1; j < idx.length; j++) {
if (idx[j] === cur[cur.length - 1] + 1) cur.push(idx[j]);
else { blocks.push(cur); cur = [idx[j]]; }
}
blocks.push(cur);
let best = null, bestSize = -1, bestDist = Infinity;
for (const b of blocks) {
const dist = Math.abs(median(b.map((i) => grid[i])) - centre);
if (b.length > bestSize || (b.length === bestSize && dist < bestDist)) {
best = b; bestSize = b.length; bestDist = dist;
}
}
return best;
}
/**
* Preferential median of intervals [x_k - u_k, x_k + u_k].
* @param {number[]} x interval centres
* @param {number[]|number} u half-widths; a number means equal u_k
* @param {{n?: number, nMin?: number, nMax?: number}} [opts]
* @returns {{pm, uPm, uGrid, uSampling, n, h, winners, converged, grid, coverage}}
*/
function preferentialMedian(x, u, opts) {
opts = opts || {};
const nMin = opts.nMin ?? N_MIN_DEFAULT;
const nMax = opts.nMax ?? N_MAX_DEFAULT;
x = Array.from(x, Number);
u = typeof u === "number" ? x.map(() => u) : Array.from(u, Number);
if (x.length !== u.length) throw new Error("x and u have different lengths");
if (x.length === 0) throw new Error("empty set of intervals");
if (u.some((v) => v < 0)) throw new Error("uncertainties must be non-negative");
if (!x.concat(u).every(Number.isFinite)) throw new Error("NaN or infinity in data");
const lo = x.map((v, k) => v - u[k]);
const hi = x.map((v, k) => v + u[k]);
const a1 = Math.min(...lo), an = Math.max(...hi);
const centres = lo.map((l, k) => (l + hi[k]) / 2);
if (an - a1 <= 0) {
return { pm: a1, uPm: 0, uGrid: 0, uSampling: 0, n: 1, h: 0,
winners: [0], converged: true, grid: [a1], coverage: [lo.length] };
}
let n = opts.n;
if (n == null) {
const widthMin = Math.min(...lo.map((l, k) => hi[k] - l));
n = widthMin > 0 ? Math.ceil((an - a1) / widthMin) : nMin;
n = Math.max(n, nMin);
}
n = Math.max(Math.trunc(n), 2);
for (;;) {
const h = (an - a1) / (n - 1);
const grid = Array.from({ length: n }, (_, i) => a1 + h * i);
// tolerance in the units of the data: a share of the grid step and a few ulps of the values
const eps = Math.max(1e-9 * h, 8 * Number.EPSILON * Math.max(Math.abs(a1), Math.abs(an)));
// coverage c_i; Borda score z_i = const + n c_i, so winners = max coverage
const coverage = coverageOf(grid, lo, hi, eps);
let best = 0;
for (const c of coverage) if (c > best) best = c; // no Math.max(...): n can be millions
let idx = [];
coverage.forEach((c, i) => { if (c === best) idx.push(i); });
const adjacent = idx.every((v, j) => j === 0 || v - idx[j - 1] === 1);
if (adjacent || n >= nMax) {
if (!adjacent) idx = widestBlock(idx, grid, median(centres));
const uGrid = PM_UNCERTAINTY_FACTOR * h;
const uSampling = samplingUncertainty(centres);
return {
pm: median(idx.map((i) => grid[i])),
uPm: Math.hypot(uGrid, uSampling),
uGrid, uSampling, n, h, winners: idx, converged: adjacent, grid, coverage,
};
}
n += 1;
}
}
// -------------------------------------------------------------------------
// Alternatives for comparison: common practice for combining x_k ± u_k
// -------------------------------------------------------------------------
const sum = (a) => a.reduce((s, v) => s + v, 0);
/** 0.95 quantile of chi-square, Wilson–Hilferty approximation (error < 1 %). */
function chi2Quantile95(df) {
const a = 2 / (9 * df);
return df * Math.pow(1 - a + 1.6448536269514722 * Math.sqrt(a), 3);
}
/** Weighted median: smallest x with cumulative weight >= half; exactly half -> midpoint. */
function weightedMedian(x, w) {
const pairs = x.map((v, k) => [v, w[k]]).sort((a, b) => a[0] - b[0]);
const total = sum(w);
let cum = 0;
for (let j = 0; j < pairs.length; j++) {
cum += pairs[j][1];
if (Math.abs(cum - total / 2) <= 1e-12 * total && j + 1 < pairs.length) return (pairs[j][0] + pairs[j + 1][0]) / 2;
if (cum >= total / 2) return pairs[j][0];
}
return pairs[pairs.length - 1][0];
}
/** Hodges–Lehmann estimate: median of all pairwise (Walsh) averages, i <= j. */
function hodgesLehmann(x) {
const w = [];
for (let i = 0; i < x.length; i++) for (let j = i; j < x.length; j++) w.push((x[i] + x[j]) / 2);
return median(w);
}
/** Robust mean by ISO 13528 Algorithm A; u = 1.25 s* / sqrt(m). */
function algorithmA(x, tol = 1e-10, maxIter = 1000) {
const m = x.length;
let xs = median(x);
let ss = 1.483 * median(x.map((v) => Math.abs(v - xs)));
if (ss === 0 || m < 2) return { value: xs, s: ss, u: 0 };
for (let it = 0; it < maxIter; it++) {
const d = 1.5 * ss;
const c = x.map((v) => Math.min(Math.max(v, xs - d), xs + d));
const nx = sum(c) / m;
const ns = 1.134 * Math.sqrt(sum(c.map((v) => (v - nx) ** 2)) / (m - 1));
const done = Math.abs(nx - xs) <= tol * Math.max(Math.abs(xs), 1e-300) && Math.abs(ns - ss) <= tol * ss;
xs = nx; ss = ns;
if (done || ss === 0) break;
}
return { value: xs, s: ss, u: (1.25 * ss) / Math.sqrt(m) };
}
/** Random-effects consensus (DerSimonian–Laird), as in NIST Consensus Builder. */
function dersimonianLaird(x, u) {
const m = x.length;
const w = u.map((v) => 1 / (v * v));
const sw = sum(w);
const meanW = sum(w.map((v, k) => v * x[k])) / sw;
const q = sum(w.map((v, k) => v * (x[k] - meanW) ** 2));
const denom = sw - sum(w.map((v) => v * v)) / sw;
const tau2 = denom > 0 ? Math.max(0, (q - (m - 1)) / denom) : 0;
const ws = u.map((v) => 1 / (v * v + tau2));
return { value: sum(ws.map((v, k) => v * x[k])) / sum(ws), u: 1 / Math.sqrt(sum(ws)), tau: Math.sqrt(tau2) };
}
/** Largest consistent subset (after Cox, 2007), sequential exclusion by chi-square at 0.05. */
function largestConsistentSubset(x, u) {
let keep = x.map((_, k) => k);
for (;;) {
const w = keep.map((k) => 1 / (u[k] * u[k]));
const sw = sum(w);
const y = sum(keep.map((k, j) => w[j] * x[k])) / sw;
const chi2 = sum(keep.map((k, j) => w[j] * (x[k] - y) ** 2));
if (keep.length <= 2 || chi2 <= chi2Quantile95(keep.length - 1)) {
return { value: y, u: 1 / Math.sqrt(sw), excluded: x.length - keep.length };
}
const uy2 = 1 / sw;
let worst = keep[0], worstD = -1;
for (const k of keep) {
const dk = Math.abs(x[k] - y) / Math.sqrt(Math.max(u[k] * u[k] - uy2, 1e-300));
if (dk > worstD) { worst = k; worstD = dk; }
}
keep = keep.filter((k) => k !== worst);
}
}
/** Mode of the mixture of N(x_k, u_k^2) with equal weights (Ciarlini, Cox, Pavese, Regoliosi, 2004; Duewer, 2004). */
function mixtureMode(x, u, points = 2001, refine = 100) {
const f = (t) => { let s = 0; for (let k = 0; k < x.length; k++) s += Math.exp(-0.5 * ((t - x[k]) / u[k]) ** 2) / u[k]; return s; };
const lo = Math.min(...x.map((v, k) => v - 3 * u[k]));
const hi = Math.max(...x.map((v, k) => v + 3 * u[k]));
const step = (hi - lo) / (points - 1);
let bestI = 0, bestF = -1;
for (let i = 0; i < points; i++) { const fi = f(lo + step * i); if (fi > bestF) { bestI = i; bestF = fi; } }
let a = lo + step * Math.max(bestI - 1, 0), b = lo + step * Math.min(bestI + 1, points - 1);
const g = (Math.sqrt(5) - 1) / 2;
let c = b - g * (b - a), d = a + g * (b - a);
for (let it = 0; it < refine; it++) {
if (f(c) >= f(d)) b = d; else a = c;
c = b - g * (b - a); d = a + g * (b - a);
}
return (a + b) / 2;
}
// Names are shared by all implementations; the site translates them.
const ESTIMATORS = [
"Preferential median (IF&PA)", "Arithmetic mean", "Median", "Hodges-Lehmann",
"ISO 13528 Algorithm A", "Weighted mean (1/u^2)", "Weighted median (1/u^2)",
"DerSimonian-Laird", "Largest consistent subset (Cox)", "Mixture model mode",
];
/**
* The preferential median next to common alternatives on the same data.
* Returns [{name, value, u}]; u is null where no standard formula exists.
* Estimators that use u_k need all u_k > 0.
*/
function compareEstimators(x, u, opts) {
x = Array.from(x, Number);
u = typeof u === "number" ? x.map(() => u) : Array.from(u, Number);
const m = x.length;
const r = preferentialMedian(x, u, opts);
const out = [{ name: ESTIMATORS[0], value: r.pm, u: r.uPm }];
const mean = sum(x) / m;
const sd = m > 1 ? Math.sqrt(sum(x.map((v) => (v - mean) ** 2)) / (m - 1)) : NaN;
out.push({ name: ESTIMATORS[1], value: mean, u: sd / Math.sqrt(m) });
out.push({ name: ESTIMATORS[2], value: median(x), u: samplingUncertainty(x) });
out.push({ name: ESTIMATORS[3], value: hodgesLehmann(x), u: null });
const aa = algorithmA(x);
out.push({ name: ESTIMATORS[4], value: aa.value, u: aa.u });
if (u.every((v) => v > 0)) {
const w = u.map((v) => 1 / (v * v));
const sw = sum(w);
out.push({ name: ESTIMATORS[5], value: sum(w.map((v, k) => v * x[k])) / sw, u: 1 / Math.sqrt(sw) });
out.push({ name: ESTIMATORS[6], value: weightedMedian(x, w), u: null });
if (m >= 2) {
const dl = dersimonianLaird(x, u);
out.push({ name: ESTIMATORS[7], value: dl.value, u: dl.u });
const lcs = largestConsistentSubset(x, u);
out.push({ name: ESTIMATORS[8], value: lcs.value, u: lcs.u });
}
out.push({ name: ESTIMATORS[9], value: mixtureMode(x, u), u: null });
}
return out;
}
return {
preferentialMedian, compareEstimators, ESTIMATORS, median,
weightedMedian, hodgesLehmann, algorithmA, dersimonianLaird, largestConsistentSubset, mixtureMode,
};
});Base R, no packages. File: ifpa.R.
source("ifpa.R")
x <- c(10.012, 10.018, 10.009, 10.015, 10.021, 10.011, 10.064, 10.016)
u <- c(0.010, 0.012, 0.015, 0.008, 0.014, 0.010, 0.004, 0.020)
r <- preferential_median(x, u)
sprintf("PM = %.4f ± %.4f", r$pm, r$u_pm) # PM = 10.0125 ± 0.0040
compare_estimators(x, u) # data.frame: name, value, uFull code of ifpa.R
# IF&PA — interval fusion with preference aggregation.
# Preferential median (PM) of a set of intervals [x_k - u_k, x_k + u_k].
#
# Base R, no packages.
#
# References:
# Muravyov S.V., Khudonogova L.I., Emelyanova E.Yu. Interval data fusion with
# preference aggregation. Measurement, 2018, 116, 621-630. doi:10.1016/j.measurement.2017.08.045
# Muravyov S.V., Khudonogova L.I., Shabramov V.S., Ignatyev V.D., Andreyev D.I.
# Kemeny and Borda rules in constructing central tendency estimators based on the
# preferential median. Proc. AISP'25, 2025. doi:10.1109/AISP68263.2025.11396113
# (the Borda rule gives the same preferential median as the Kemeny rule).
# Muravyov S.V., Khudonogova L.I., Shabramov V.S., Ignatyev V.D. Comparative studies
# of preferential and weighted medians accuracy in robust estimation of central
# tendency. Int. J. Data Sci. Anal., 2026, 22(1), 262. doi:10.1007/s41060-026-01222-6
# (standard uncertainty of the PM due to the RAV partition: 0.41 h).
#
# Version of the method: IF&PA-L — Borda rule, no self-refinement.
# Uncertainty: u_pm returned here is the combined standard uncertainty
# sqrt(u_grid^2 + u_sampling^2), written u(x_pm) on the portal's Method page;
# u_grid = 0.41 h is what the papers call u_pm; u_sampling = 1.2533 * 1.4826 * MAD / sqrt(m)
# is an addition of this implementation, not part of the published method.
#
# Usage:
# source("ifpa.R")
# r <- preferential_median(c(4.375, 4.9), c(0.175, 0.2), n = 3)
# r$pm # 4.65
preferential_median <- function(x, u, n = NULL, n_min = 11, n_max = 201) {
x <- as.numeric(x)
u <- as.numeric(u)
if (length(u) == 1) u <- rep(u, length(x))
if (length(x) != length(u)) stop("x and u have different lengths")
if (length(x) == 0) stop("empty set of intervals")
if (any(u < 0)) stop("uncertainties must be non-negative")
if (!all(is.finite(c(x, u)))) stop("NaN or infinity in data")
lo <- x - u
hi <- x + u
a1 <- min(lo)
an <- max(hi)
centres <- (lo + hi) / 2
if (an - a1 <= 0) {
return(list(pm = a1, u_pm = 0, u_grid = 0, u_sampling = 0, n = 1, h = 0,
winners = 1, converged = TRUE, grid = a1, coverage = length(lo)))
}
if (is.null(n)) {
width_min <- min(hi - lo) # the narrowest interval, 2 min u_k
n <- if (width_min > 0) ceiling((an - a1) / width_min) else n_min
n <- max(n, n_min)
}
n <- max(as.integer(n), 2L)
# median() in R averages the two middle values, as required
sampling <- function(c) {
m <- length(c)
if (m < 2) return(0)
1.2533 * 1.4826 * median(abs(c - median(c))) / sqrt(m)
}
repeat {
h <- (an - a1) / (n - 1)
grid <- a1 + h * (0:(n - 1))
# tolerance in the units of the data: a share of the grid step and a few ulps of the values
eps <- max(1e-9 * h, 8 * .Machine$double.eps * max(abs(a1), abs(an)))
# coverage c_i; Borda score z_i = const + n c_i, so winners = max coverage
# c_i = #{lo_k - eps <= a_i} - #{hi_k + eps < a_i}: O(n log m) instead of O(n m)
coverage <- findInterval(grid, sort(lo - eps)) - findInterval(grid, sort(hi + eps), left.open = TRUE)
idx <- which(coverage == max(coverage)) # 1-based indices
adjacent <- length(idx) <= 1 || all(diff(idx) == 1)
if (adjacent || n >= n_max) {
if (!adjacent) {
# widest block of consecutive winners; tie -> median closest to centres
blocks <- split(idx, cumsum(c(1, diff(idx) != 1)))
centre <- median(centres)
best <- NULL
for (b in blocks) {
d <- abs(median(grid[b]) - centre)
if (is.null(best) || length(b) > length(best$b) ||
(length(b) == length(best$b) && d < best$d)) best <- list(b = b, d = d)
}
idx <- best$b
}
u_grid <- 0.41 * h
u_samp <- sampling(centres)
return(list(pm = median(grid[idx]), u_pm = sqrt(u_grid^2 + u_samp^2),
u_grid = u_grid, u_sampling = u_samp, n = n, h = h,
winners = idx, converged = adjacent,
grid = grid, coverage = coverage))
}
n <- n + 1L
}
}
# ---------------------------------------------------------------------------
# Alternatives for comparison: common practice for combining x_k +/- u_k
# ---------------------------------------------------------------------------
# 0.95 quantile of chi-square, Wilson-Hilferty approximation (error < 1 %);
# kept instead of qchisq() so that all implementations give the same result
chi2_quantile_95 <- function(df) {
a <- 2 / (9 * df)
df * (1 - a + 1.6448536269514722 * sqrt(a))^3
}
# Weighted median: smallest x with cumulative weight >= half; exactly half -> midpoint
weighted_median <- function(x, w) {
o <- order(x)
x <- x[o]
w <- w[o]
total <- sum(w)
cum <- cumsum(w)
j <- which(cum >= total / 2 | abs(cum - total / 2) <= 1e-12 * total)[1]
if (abs(cum[j] - total / 2) <= 1e-12 * total && j < length(x)) return((x[j] + x[j + 1]) / 2)
x[j]
}
# Hodges-Lehmann estimate: median of all pairwise (Walsh) averages, i <= j
hodges_lehmann <- function(x) {
s <- outer(x, x, "+") / 2
median(s[upper.tri(s, diag = TRUE)])
}
# Robust mean by ISO 13528 Algorithm A; u = 1.25 s* / sqrt(m)
algorithm_a <- function(x, tol = 1e-10, max_iter = 1000) {
m <- length(x)
xs <- median(x)
ss <- 1.483 * median(abs(x - xs))
if (ss == 0 || m < 2) return(list(value = xs, s = ss, u = 0))
for (it in seq_len(max_iter)) {
d <- 1.5 * ss
cl <- pmin(pmax(x, xs - d), xs + d)
nx <- mean(cl)
ns <- 1.134 * sqrt(sum((cl - nx)^2) / (m - 1))
done <- abs(nx - xs) <= tol * max(abs(xs), 1e-300) && abs(ns - ss) <= tol * ss
xs <- nx
ss <- ns
if (done || ss == 0) break
}
list(value = xs, s = ss, u = 1.25 * ss / sqrt(m))
}
# Random-effects consensus (DerSimonian-Laird), as in NIST Consensus Builder
dersimonian_laird <- function(x, u) {
m <- length(x)
w <- 1 / u^2
sw <- sum(w)
mean_w <- sum(w * x) / sw
q <- sum(w * (x - mean_w)^2)
denom <- sw - sum(w^2) / sw
tau2 <- if (denom > 0) max(0, (q - (m - 1)) / denom) else 0
ws <- 1 / (u^2 + tau2)
list(value = sum(ws * x) / sum(ws), u = 1 / sqrt(sum(ws)), tau = sqrt(tau2))
}
# Largest consistent subset (after Cox, 2007), sequential exclusion by chi-square at 0.05
largest_consistent_subset <- function(x, u) {
keep <- seq_along(x)
repeat {
w <- 1 / u[keep]^2
sw <- sum(w)
y <- sum(w * x[keep]) / sw
chi2 <- sum(w * (x[keep] - y)^2)
if (length(keep) <= 2 || chi2 <= chi2_quantile_95(length(keep) - 1)) {
return(list(value = y, u = 1 / sqrt(sw), excluded = length(x) - length(keep)))
}
d <- abs(x[keep] - y) / sqrt(pmax(u[keep]^2 - 1 / sw, 1e-300))
keep <- keep[-which.max(d)]
}
}
# Mode of the mixture of N(x_k, u_k^2) with equal weights (Ciarlini, Cox, Pavese, Regoliosi, 2004; Duewer, 2004)
mixture_mode <- function(x, u, points = 2001, refine = 100) {
f <- function(t) sum(exp(-0.5 * ((t - x) / u)^2) / u)
lo <- min(x - 3 * u)
hi <- max(x + 3 * u)
step <- (hi - lo) / (points - 1)
fv <- vapply(lo + step * (0:(points - 1)), f, numeric(1))
i <- which.max(fv) - 1 # 0-based, first maximum
a <- lo + step * max(i - 1, 0)
b <- lo + step * min(i + 1, points - 1)
g <- (sqrt(5) - 1) / 2
cc <- b - g * (b - a)
dd <- a + g * (b - a)
for (it in seq_len(refine)) {
if (f(cc) >= f(dd)) b <- dd else a <- cc
cc <- b - g * (b - a)
dd <- a + g * (b - a)
}
(a + b) / 2
}
# The preferential median next to common alternatives on the same data.
# Returns a data.frame (name, value, u); u is NA where no standard formula exists.
# Estimators that use u_k need all u_k > 0.
compare_estimators <- function(x, u, n = NULL) {
x <- as.numeric(x)
u <- as.numeric(u)
if (length(u) == 1) u <- rep(u, length(x))
m <- length(x)
r <- preferential_median(x, u, n = n)
med <- median(x)
u_med <- if (m < 2) 0 else 1.2533 * 1.4826 * median(abs(x - med)) / sqrt(m)
aa <- algorithm_a(x)
out <- data.frame(
name = c("Preferential median (IF&PA)", "Arithmetic mean", "Median", "Hodges-Lehmann",
"ISO 13528 Algorithm A"),
value = c(r$pm, mean(x), med, hodges_lehmann(x), aa$value),
u = c(r$u_pm, if (m > 1) sd(x) / sqrt(m) else NA, u_med, NA, aa$u),
stringsAsFactors = FALSE)
add <- function(df, name, value, u) rbind(df, data.frame(name = name, value = value, u = u))
if (all(u > 0)) {
w <- 1 / u^2
out <- add(out, "Weighted mean (1/u^2)", sum(w * x) / sum(w), 1 / sqrt(sum(w)))
out <- add(out, "Weighted median (1/u^2)", weighted_median(x, w), NA)
if (m >= 2) {
dl <- dersimonian_laird(x, u)
out <- add(out, "DerSimonian-Laird", dl$value, dl$u)
lcs <- largest_consistent_subset(x, u)
out <- add(out, "Largest consistent subset (Cox)", lcs$value, lcs$u)
}
out <- add(out, "Mixture model mode", mixture_mode(x, u), NA)
}
out
}MATLAB R2016b+ and GNU Octave 6+, no toolboxes. Files: preferential_median.m, compare_estimators.m — put them in one folder.
x = [10.012 10.018 10.009 10.015 10.021 10.011 10.064 10.016];
u = [0.010 0.012 0.015 0.008 0.014 0.010 0.004 0.020];
r = preferential_median(x, u);
fprintf('PM = %.4f ± %.4f\n', r.pm, r.u_pm); % PM = 10.0125 ± 0.0040
T = compare_estimators(x, u);
for k = 1:numel(T), fprintf('%-32s %.4f\n', T(k).name, T(k).value); endFull code of preferential_median.m
function r = preferential_median(x, u, n, n_min, n_max)
% PREFERENTIAL_MEDIAN IF&PA — interval fusion with preference aggregation.
% r = preferential_median(x, u) returns the preferential median of the
% intervals [x(k) - u(k), x(k) + u(k)]. u may be a scalar.
% r = preferential_median(x, u, n) sets the number of discrete values of the
% range of actual values (RAV); pass [] for the default.
%
% Fields of r: pm, u_pm, u_grid, u_sampling, n, h, winners (1-based),
% converged, grid, coverage.
%
% MATLAB R2016b+ and GNU Octave 6+, no toolboxes.
%
% References:
% Muravyov S.V., Khudonogova L.I., Emelyanova E.Yu. Interval data fusion with
% preference aggregation. Measurement, 2018, 116, 621-630. doi:10.1016/j.measurement.2017.08.045
% Muravyov S.V., Khudonogova L.I., Shabramov V.S., Ignatyev V.D., Andreyev D.I.
% Kemeny and Borda rules in constructing central tendency estimators based on the
% preferential median. Proc. AISP'25, 2025. doi:10.1109/AISP68263.2025.11396113
% (the Borda rule gives the same preferential median as the Kemeny rule).
% Muravyov S.V., Khudonogova L.I., Shabramov V.S., Ignatyev V.D. Comparative studies
% of preferential and weighted medians accuracy in robust estimation of central
% tendency. Int. J. Data Sci. Anal., 2026, 22(1), 262. doi:10.1007/s41060-026-01222-6
% (standard uncertainty of the PM due to the RAV partition: 0.41 h).
%
% Version of the method: IF&PA-L — Borda rule, no self-refinement.
% Uncertainty: u_pm returned here is the combined standard uncertainty
% sqrt(u_grid^2 + u_sampling^2), written u(x_pm) on the portal's Method page;
% u_grid = 0.41 h is what the papers call u_pm; u_sampling = 1.2533 * 1.4826 * MAD / sqrt(m)
% is an addition of this implementation, not part of the published method.
%
% Example:
% r = preferential_median([4.375 4.9], [0.175 0.2], 3);
% r.pm % 4.65
if nargin < 3, n = []; end
if nargin < 4 || isempty(n_min), n_min = 11; end
if nargin < 5 || isempty(n_max), n_max = 201; end
x = double(x(:)');
u = double(u(:)');
if isscalar(u), u = repmat(u, size(x)); end
if numel(x) ~= numel(u), error('x and u have different lengths'); end
if isempty(x), error('empty set of intervals'); end
if any(u < 0), error('uncertainties must be non-negative'); end
if ~all(isfinite([x u])), error('NaN or infinity in data'); end
lo = x - u;
hi = x + u;
a1 = min(lo);
an = max(hi);
centres = (lo + hi) / 2;
if an - a1 <= 0
r = struct('pm', a1, 'u_pm', 0, 'u_grid', 0, 'u_sampling', 0, 'n', 1, ...
'h', 0, 'winners', 1, 'converged', true, 'grid', a1, ...
'coverage', numel(lo));
return
end
if isempty(n)
width_min = min(hi - lo); % the narrowest interval, 2 min u_k
if width_min > 0
n = ceil((an - a1) / width_min);
else
n = n_min;
end
n = max(n, n_min);
end
n = max(floor(n), 2);
while true
h = (an - a1) / (n - 1);
grid = a1 + h * (0:n-1);
% tolerance in the units of the data: a share of the grid step and a few ulps of the values
epsv = max(1e-9 * h, 8 * eps * max(abs(a1), abs(an)));
% coverage c_i; Borda score z_i = const + n c_i, so winners = max coverage
% c_i = #{lo_k - eps <= a_i} - #{hi_k + eps < a_i}, by a stable sort of bounds together with the grid:
% O((n + m) log(n + m)) memory O(n + m) instead of an m-by-n matrix
coverage = (count_before(lo(:) - epsv, grid(:), true) - count_before(hi(:) + epsv, grid(:), false)).';
idx = find(coverage == max(coverage));
adjacent = numel(idx) <= 1 || all(diff(idx) == 1);
if adjacent || n >= n_max
if ~adjacent
% widest block of consecutive winners; tie -> median closest to centres
starts = [1, find(diff(idx) ~= 1) + 1];
stops = [starts(2:end) - 1, numel(idx)];
centre = median(centres);
best = []; best_d = inf;
for j = 1:numel(starts)
b = idx(starts(j):stops(j));
d = abs(median(grid(b)) - centre);
if numel(b) > numel(best) || (numel(b) == numel(best) && d < best_d)
best = b; best_d = d;
end
end
idx = best;
end
u_grid = 0.41 * h;
m = numel(centres);
if m < 2
u_samp = 0;
else
u_samp = 1.2533 * 1.4826 * median(abs(centres - median(centres))) / sqrt(m);
end
r = struct('pm', median(grid(idx)), 'u_pm', hypot(u_grid, u_samp), ...
'u_grid', u_grid, 'u_sampling', u_samp, 'n', n, 'h', h, ...
'winners', idx, 'converged', adjacent, 'grid', grid, ...
'coverage', coverage);
return
end
n = n + 1;
end
end
function c = count_before(b, g, inclusive)
% For each g(i) of an ascending grid: how many b are <= g(i) (inclusive) or < g(i) (strict).
% sort is stable, so on ties the order of concatenation decides which side counts.
if inclusive
v = [b; g]; isb = [true(numel(b), 1); false(numel(g), 1)];
else
v = [g; b]; isb = [false(numel(g), 1); true(numel(b), 1)];
end
[~, o] = sort(v);
cnt = cumsum(isb(o));
c = cnt(~isb(o));
endFull code of compare_estimators.m
function T = compare_estimators(x, u, n)
% COMPARE_ESTIMATORS The preferential median next to common alternatives.
% T = compare_estimators(x, u) returns a struct array with fields name,
% value, u (NaN where no standard uncertainty formula exists):
% preferential median (IF&PA), arithmetic mean, median, Hodges-Lehmann,
% ISO 13528 Algorithm A, and — when all u(k) > 0 — weighted mean (1/u^2),
% weighted median (1/u^2), DerSimonian-Laird (as in NIST Consensus Builder),
% largest consistent subset (Cox, 2007) and mixture model mode.
%
% Needs preferential_median.m in the same folder. MATLAB and GNU Octave,
% no toolboxes.
%
% The first row is the preferential median (IF&PA-L); its u is the combined
% uncertainty — see preferential_median.m for the references and notation.
%
% Example:
% x = [10.012 10.018 10.009 10.015 10.021 10.011 10.064 10.016];
% u = [0.010 0.012 0.015 0.008 0.014 0.010 0.004 0.020];
% T = compare_estimators(x, u);
% for k = 1:numel(T), fprintf('%-32s %.4f\n', T(k).name, T(k).value); end
if nargin < 3, n = []; end
x = double(x(:)');
u = double(u(:)');
if isscalar(u), u = repmat(u, size(x)); end
m = numel(x);
r = preferential_median(x, u, n);
T = struct('name', 'Preferential median (IF&PA)', 'value', r.pm, 'u', r.u_pm);
if m > 1, u_mean = std(x) / sqrt(m); else, u_mean = NaN; end
T(end+1) = struct('name', 'Arithmetic mean', 'value', mean(x), 'u', u_mean);
med = median(x);
if m < 2, u_med = 0; else, u_med = 1.2533 * 1.4826 * median(abs(x - med)) / sqrt(m); end
T(end+1) = struct('name', 'Median', 'value', med, 'u', u_med);
% Hodges-Lehmann: median of all pairwise (Walsh) averages, i <= j
[I, J] = ndgrid(1:m, 1:m);
walsh = (x(I(I <= J)) + x(J(I <= J))) / 2;
T(end+1) = struct('name', 'Hodges-Lehmann', 'value', median(walsh), 'u', NaN);
[xa, ua] = algorithm_a(x);
T(end+1) = struct('name', 'ISO 13528 Algorithm A', 'value', xa, 'u', ua);
if all(u > 0)
w = 1 ./ u.^2;
sw = sum(w);
mean_w = sum(w .* x) / sw;
T(end+1) = struct('name', 'Weighted mean (1/u^2)', 'value', mean_w, 'u', 1 / sqrt(sw));
T(end+1) = struct('name', 'Weighted median (1/u^2)', 'value', weighted_median(x, w), 'u', NaN);
if m >= 2
% random-effects consensus (DerSimonian-Laird)
q = sum(w .* (x - mean_w).^2);
denom = sw - sum(w.^2) / sw;
if denom > 0, tau2 = max(0, (q - (m - 1)) / denom); else, tau2 = 0; end
wr = 1 ./ (u.^2 + tau2);
T(end+1) = struct('name', 'DerSimonian-Laird', 'value', sum(wr .* x) / sum(wr), ...
'u', 1 / sqrt(sum(wr)));
[yl, ul] = largest_consistent_subset(x, u);
T(end+1) = struct('name', 'Largest consistent subset (Cox)', 'value', yl, 'u', ul);
end
T(end+1) = struct('name', 'Mixture model mode', 'value', mixture_mode(x, u), 'u', NaN);
end
end
function v = weighted_median(x, w)
% smallest x with cumulative weight >= half; exactly half -> midpoint
[xs, o] = sort(x);
ws = w(o);
total = sum(ws);
cum = cumsum(ws);
j = find(cum >= total / 2 | abs(cum - total / 2) <= 1e-12 * total, 1);
if abs(cum(j) - total / 2) <= 1e-12 * total && j < numel(xs)
v = (xs(j) + xs(j + 1)) / 2;
else
v = xs(j);
end
end
function [xs, u] = algorithm_a(x)
% robust mean by ISO 13528 Algorithm A; u = 1.25 s* / sqrt(m)
m = numel(x);
xs = median(x);
ss = 1.483 * median(abs(x - xs));
if ss == 0 || m < 2, u = 0; return; end
for it = 1:1000
d = 1.5 * ss;
c = min(max(x, xs - d), xs + d);
nx = mean(c);
ns = 1.134 * sqrt(sum((c - nx).^2) / (m - 1));
done = abs(nx - xs) <= 1e-10 * max(abs(xs), 1e-300) && abs(ns - ss) <= 1e-10 * ss;
xs = nx; ss = ns;
if done || ss == 0, break; end
end
u = 1.25 * ss / sqrt(m);
end
function [y, uy] = largest_consistent_subset(x, u)
% sequential exclusion by chi-square at 0.05 (after Cox, 2007)
keep = 1:numel(x);
while true
w = 1 ./ u(keep).^2;
sw = sum(w);
y = sum(w .* x(keep)) / sw;
chi2 = sum(w .* (x(keep) - y).^2);
df = numel(keep) - 1;
a = 2 / (9 * df);
q95 = df * (1 - a + 1.6448536269514722 * sqrt(a))^3; % Wilson-Hilferty
if numel(keep) <= 2 || chi2 <= q95
uy = 1 / sqrt(sw);
return
end
d = abs(x(keep) - y) ./ sqrt(max(u(keep).^2 - 1 / sw, 1e-300));
[~, worst] = max(d);
keep(worst) = [];
end
end
function t = mixture_mode(x, u)
% mode of the mixture of N(x_k, u_k^2) with equal weights (Ciarlini, Cox, Pavese, Regoliosi, 2004; Duewer, 2004)
f = @(t) sum(exp(-0.5 * ((t - x) ./ u).^2) ./ u);
points = 2001;
lo = min(x - 3 * u);
hi = max(x + 3 * u);
step = (hi - lo) / (points - 1);
fv = arrayfun(f, lo + step * (0:points-1));
[~, i] = max(fv);
i = i - 1; % 0-based, first maximum
a = lo + step * max(i - 1, 0);
b = lo + step * min(i + 1, points - 1);
g = (sqrt(5) - 1) / 2;
c = b - g * (b - a);
d = a + g * (b - a);
for it = 1:100
if f(c) >= f(d), b = d; else, a = c; end
c = b - g * (b - a);
d = a + g * (b - a);
end
t = (a + b) / 2;
endC++17, header-only, standard library only. File: ifpa.hpp.
#include "ifpa.hpp"
#include <cstdio>
int main() {
std::vector<double> x{10.012, 10.018, 10.009, 10.015, 10.021, 10.011, 10.064, 10.016};
std::vector<double> u{0.010, 0.012, 0.015, 0.008, 0.014, 0.010, 0.004, 0.020};
auto r = ifpa::preferential_median(x, u);
std::printf("PM = %.4f +/- %.4f\n", r.pm, r.u_pm); // PM = 10.0125 +/- 0.0040
for (const auto& e : ifpa::compare_estimators(x, u))
std::printf("%-32s %.4f\n", e.name, e.value);
}Full code of ifpa.hpp
// IF&PA — interval fusion with preference aggregation.
// Preferential median (PM) of a set of intervals [x_k - u_k, x_k + u_k].
//
// Header-only, C++17, standard library only.
//
// References:
// Muravyov S.V., Khudonogova L.I., Emelyanova E.Yu. Interval data fusion with
// preference aggregation. Measurement, 2018, 116, 621-630. doi:10.1016/j.measurement.2017.08.045
// Muravyov S.V., Khudonogova L.I., Shabramov V.S., Ignatyev V.D., Andreyev D.I.
// Kemeny and Borda rules in constructing central tendency estimators based on the
// preferential median. Proc. AISP'25, 2025. doi:10.1109/AISP68263.2025.11396113
// (the Borda rule gives the same preferential median as the Kemeny rule).
// Muravyov S.V., Khudonogova L.I., Shabramov V.S., Ignatyev V.D. Comparative studies
// of preferential and weighted medians accuracy in robust estimation of central
// tendency. Int. J. Data Sci. Anal., 2026, 22(1), 262. doi:10.1007/s41060-026-01222-6
// (standard uncertainty of the PM due to the RAV partition: 0.41 h).
//
// Version of the method: IF&PA-L — Borda rule, no self-refinement.
// Uncertainty: u_pm returned here is the combined standard uncertainty
// sqrt(u_grid^2 + u_sampling^2), written u(x_pm) on the portal's Method page;
// u_grid = 0.41 h is what the papers call u_pm; u_sampling = 1.2533 * 1.4826 * MAD / sqrt(m)
// is an addition of this implementation, not part of the published method.
//
// Usage:
// #include "ifpa.hpp"
// auto r = ifpa::preferential_median({4.375, 4.9}, {0.175, 0.2}, 3);
// r.pm; // 4.65
#pragma once
#include <algorithm>
#include <cmath>
#include <cstdlib>
#include <limits>
#include <stdexcept>
#include <utility>
#include <vector>
namespace ifpa {
struct Result {
double pm = 0, u_pm = 0, u_grid = 0, u_sampling = 0, h = 0;
int n = 0;
std::vector<int> winners; // 0-based indices into grid
bool converged = true;
std::vector<double> grid;
std::vector<int> coverage;
};
inline double median(std::vector<double> v) {
if (v.empty()) throw std::invalid_argument("empty sample");
std::sort(v.begin(), v.end());
const std::size_t p = v.size();
return p % 2 ? v[(p - 1) / 2] : (v[p / 2 - 1] + v[p / 2]) / 2.0;
}
// n <= 0 means "choose automatically"
inline Result preferential_median(const std::vector<double>& x, const std::vector<double>& u,
int n = 0, int n_min = 11, int n_max = 201) {
if (x.size() != u.size()) throw std::invalid_argument("x and u have different lengths");
if (x.empty()) throw std::invalid_argument("empty set of intervals");
const std::size_t m = x.size();
std::vector<double> lo(m), hi(m), centres(m);
for (std::size_t k = 0; k < m; ++k) {
if (!(u[k] >= 0)) throw std::invalid_argument("uncertainties must be non-negative");
if (!std::isfinite(x[k]) || !std::isfinite(u[k])) throw std::invalid_argument("NaN or infinity in data");
lo[k] = x[k] - u[k];
hi[k] = x[k] + u[k];
centres[k] = (lo[k] + hi[k]) / 2.0;
}
const double a1 = *std::min_element(lo.begin(), lo.end());
const double an = *std::max_element(hi.begin(), hi.end());
Result r;
if (an - a1 <= 0) {
r.pm = a1; r.n = 1; r.winners = {0}; r.grid = {a1}; r.coverage = {int(m)};
return r;
}
if (n <= 0) {
double width_min = std::numeric_limits<double>::infinity();
for (std::size_t k = 0; k < m; ++k) width_min = std::min(width_min, hi[k] - lo[k]);
n = width_min > 0 ? int(std::ceil((an - a1) / width_min)) : n_min;
n = std::max(n, n_min);
}
n = std::max(n, 2);
const double centre = median(centres);
for (;;) {
const double h = (an - a1) / (n - 1);
std::vector<double> grid(n);
for (int i = 0; i < n; ++i) grid[i] = a1 + h * i;
// tolerance in the units of the data: a share of the grid step and a few ulps of the values
const double eps = std::max(1e-9 * h, 8 * std::numeric_limits<double>::epsilon() * std::max(std::fabs(a1), std::fabs(an)));
// coverage c_i; Borda score z_i = const + n c_i, so winners = max coverage
// c_i = #{lo_k - eps <= a_i} - #{hi_k + eps < a_i}; two pointers: O(n + m log m) instead of O(n m)
std::vector<double> left(m), right(m);
for (std::size_t k = 0; k < m; ++k) { left[k] = lo[k] - eps; right[k] = hi[k] + eps; }
std::sort(left.begin(), left.end());
std::sort(right.begin(), right.end());
std::vector<int> coverage(n, 0);
for (std::size_t i = 0, p = 0, q = 0; i < static_cast<std::size_t>(n); ++i) {
while (p < m && left[p] <= grid[i]) ++p;
while (q < m && right[q] < grid[i]) ++q;
coverage[i] = static_cast<int>(p - q);
}
const int best = *std::max_element(coverage.begin(), coverage.end());
std::vector<int> idx;
for (int i = 0; i < n; ++i) if (coverage[i] == best) idx.push_back(i);
bool adjacent = true;
for (std::size_t j = 1; j < idx.size(); ++j) if (idx[j] - idx[j - 1] != 1) adjacent = false;
if (adjacent || n >= n_max) {
if (!adjacent) {
// widest block of consecutive winners; tie -> median closest to centres
std::vector<int> bestBlock, cur{idx[0]};
double bestDist = std::numeric_limits<double>::infinity();
auto consider = [&](const std::vector<int>& b) {
std::vector<double> vals;
for (int i : b) vals.push_back(grid[i]);
const double d = std::fabs(median(vals) - centre);
if (b.size() > bestBlock.size() || (b.size() == bestBlock.size() && d < bestDist)) {
bestBlock = b; bestDist = d;
}
};
for (std::size_t j = 1; j < idx.size(); ++j) {
if (idx[j] == cur.back() + 1) cur.push_back(idx[j]);
else { consider(cur); cur = {idx[j]}; }
}
consider(cur);
idx = bestBlock;
}
std::vector<double> win;
for (int i : idx) win.push_back(grid[i]);
r.pm = median(win);
r.u_grid = 0.41 * h;
if (m >= 2) {
std::vector<double> dev(m);
for (std::size_t k = 0; k < m; ++k) dev[k] = std::fabs(centres[k] - centre);
r.u_sampling = 1.2533 * 1.4826 * median(dev) / std::sqrt(double(m));
}
r.u_pm = std::hypot(r.u_grid, r.u_sampling);
r.n = n; r.h = h; r.winners = idx; r.converged = adjacent;
r.grid = grid; r.coverage = coverage;
return r;
}
++n;
}
}
// ---------------------------------------------------------------------------
// Alternatives for comparison: common practice for combining x_k +/- u_k
// ---------------------------------------------------------------------------
struct Estimate {
const char* name;
double value;
double u; // NaN where no standard uncertainty formula exists
};
// 0.95 quantile of chi-square, Wilson-Hilferty approximation (error < 1 %)
inline double chi2_quantile_95(int df) {
const double a = 2.0 / (9.0 * df);
return df * std::pow(1.0 - a + 1.6448536269514722 * std::sqrt(a), 3);
}
// Weighted median: smallest x with cumulative weight >= half; exactly half -> midpoint
inline double weighted_median(const std::vector<double>& x, const std::vector<double>& w) {
std::vector<std::size_t> o(x.size());
for (std::size_t k = 0; k < o.size(); ++k) o[k] = k;
std::stable_sort(o.begin(), o.end(), [&](std::size_t a, std::size_t b) { return x[a] < x[b]; });
double total = 0;
for (double v : w) total += v;
double cum = 0;
for (std::size_t j = 0; j < o.size(); ++j) {
cum += w[o[j]];
if (std::fabs(cum - total / 2) <= 1e-12 * total && j + 1 < o.size()) return (x[o[j]] + x[o[j + 1]]) / 2.0;
if (cum >= total / 2) return x[o[j]];
}
return x[o.back()];
}
// Hodges-Lehmann estimate: median of all pairwise (Walsh) averages, i <= j
inline double hodges_lehmann(const std::vector<double>& x) {
std::vector<double> w;
for (std::size_t i = 0; i < x.size(); ++i)
for (std::size_t j = i; j < x.size(); ++j) w.push_back((x[i] + x[j]) / 2.0);
return median(w);
}
// Robust mean by ISO 13528 Algorithm A; returns {x*, u = 1.25 s*/sqrt(m)}
inline std::pair<double, double> algorithm_a(const std::vector<double>& x, double tol = 1e-10, int max_iter = 1000) {
const std::size_t m = x.size();
double xs = median(x);
std::vector<double> dev(m);
for (std::size_t k = 0; k < m; ++k) dev[k] = std::fabs(x[k] - xs);
double ss = 1.483 * median(dev);
if (ss == 0 || m < 2) return {xs, 0.0};
for (int it = 0; it < max_iter; ++it) {
const double d = 1.5 * ss;
std::vector<double> c(m);
double s = 0;
for (std::size_t k = 0; k < m; ++k) { c[k] = std::min(std::max(x[k], xs - d), xs + d); s += c[k]; }
const double nx = s / double(m);
double q = 0;
for (double v : c) q += (v - nx) * (v - nx);
const double ns = 1.134 * std::sqrt(q / double(m - 1));
const bool done = std::fabs(nx - xs) <= tol * std::max(std::fabs(xs), 1e-300) && std::fabs(ns - ss) <= tol * ss;
xs = nx; ss = ns;
if (done || ss == 0) break;
}
return {xs, 1.25 * ss / std::sqrt(double(m))};
}
// Largest consistent subset (after Cox, 2007), sequential exclusion by chi-square at 0.05
inline std::pair<double, double> largest_consistent_subset(const std::vector<double>& x, const std::vector<double>& u) {
std::vector<std::size_t> keep(x.size());
for (std::size_t k = 0; k < keep.size(); ++k) keep[k] = k;
for (;;) {
double sw = 0, swx = 0;
for (std::size_t k : keep) { const double w = 1.0 / (u[k] * u[k]); sw += w; swx += w * x[k]; }
const double y = swx / sw;
double chi2 = 0;
for (std::size_t k : keep) chi2 += (x[k] - y) * (x[k] - y) / (u[k] * u[k]);
if (keep.size() <= 2 || chi2 <= chi2_quantile_95(int(keep.size()) - 1)) return {y, 1.0 / std::sqrt(sw)};
const double uy2 = 1.0 / sw;
std::size_t worst = 0;
double worstD = -1;
for (std::size_t j = 0; j < keep.size(); ++j) {
const std::size_t k = keep[j];
const double dk = std::fabs(x[k] - y) / std::sqrt(std::max(u[k] * u[k] - uy2, 1e-300));
if (dk > worstD) { worst = j; worstD = dk; }
}
keep.erase(keep.begin() + long(worst));
}
}
// Mode of the mixture of N(x_k, u_k^2) with equal weights (Ciarlini, Cox, Pavese, Regoliosi, 2004; Duewer, 2004)
inline double mixture_mode(const std::vector<double>& x, const std::vector<double>& u, int points = 2001, int refine = 100) {
auto f = [&](double t) {
double s = 0;
for (std::size_t k = 0; k < x.size(); ++k) { const double z = (t - x[k]) / u[k]; s += std::exp(-0.5 * z * z) / u[k]; }
return s;
};
double lo = std::numeric_limits<double>::infinity(), hi = -lo;
for (std::size_t k = 0; k < x.size(); ++k) { lo = std::min(lo, x[k] - 3 * u[k]); hi = std::max(hi, x[k] + 3 * u[k]); }
const double step = (hi - lo) / (points - 1);
int best_i = 0;
double best_f = -1;
for (int i = 0; i < points; ++i) { const double fi = f(lo + step * i); if (fi > best_f) { best_i = i; best_f = fi; } }
double a = lo + step * std::max(best_i - 1, 0), b = lo + step * std::min(best_i + 1, points - 1);
const double g = (std::sqrt(5.0) - 1.0) / 2.0;
double c = b - g * (b - a), d = a + g * (b - a);
for (int it = 0; it < refine; ++it) {
if (f(c) >= f(d)) b = d; else a = c;
c = b - g * (b - a); d = a + g * (b - a);
}
return (a + b) / 2.0;
}
// The preferential median next to common alternatives on the same data.
// Estimators that use u_k need all u_k > 0.
inline std::vector<Estimate> compare_estimators(const std::vector<double>& x, const std::vector<double>& u,
int n = 0) {
const double nan = std::numeric_limits<double>::quiet_NaN();
const std::size_t m = x.size();
const Result r = preferential_median(x, u, n);
std::vector<Estimate> out{{"Preferential median (IF&PA)", r.pm, r.u_pm}};
double mean = 0;
for (double v : x) mean += v;
mean /= double(m);
double ss = 0;
for (double v : x) ss += (v - mean) * (v - mean);
out.push_back({"Arithmetic mean", mean, m > 1 ? std::sqrt(ss / double(m - 1)) / std::sqrt(double(m)) : nan});
const double med = median(x);
double u_med = 0;
if (m >= 2) {
std::vector<double> dev(m);
for (std::size_t k = 0; k < m; ++k) dev[k] = std::fabs(x[k] - med);
u_med = 1.2533 * 1.4826 * median(dev) / std::sqrt(double(m));
}
out.push_back({"Median", med, u_med});
out.push_back({"Hodges-Lehmann", hodges_lehmann(x), nan});
const auto aa = algorithm_a(x);
out.push_back({"ISO 13528 Algorithm A", aa.first, aa.second});
bool positive = true;
for (double v : u) if (!(v > 0)) positive = false;
if (positive) {
std::vector<double> w(m);
double sw = 0, swx = 0, sw2 = 0;
for (std::size_t k = 0; k < m; ++k) {
w[k] = 1.0 / (u[k] * u[k]);
sw += w[k]; swx += w[k] * x[k]; sw2 += w[k] * w[k];
}
const double mean_w = swx / sw;
out.push_back({"Weighted mean (1/u^2)", mean_w, 1.0 / std::sqrt(sw)});
out.push_back({"Weighted median (1/u^2)", weighted_median(x, w), nan});
if (m >= 2) {
// random-effects consensus (DerSimonian-Laird), as in NIST Consensus Builder
double q = 0;
for (std::size_t k = 0; k < m; ++k) q += w[k] * (x[k] - mean_w) * (x[k] - mean_w);
const double denom = sw - sw2 / sw;
const double tau2 = denom > 0 ? std::max(0.0, (q - double(m - 1)) / denom) : 0.0;
double sr = 0, srx = 0;
for (std::size_t k = 0; k < m; ++k) {
const double wr = 1.0 / (u[k] * u[k] + tau2);
sr += wr; srx += wr * x[k];
}
out.push_back({"DerSimonian-Laird", srx / sr, 1.0 / std::sqrt(sr)});
const auto lcs = largest_consistent_subset(x, u);
out.push_back({"Largest consistent subset (Cox)", lcs.first, lcs.second});
}
out.push_back({"Mixture model mode", mixture_mode(x, u), nan});
}
return out;
}
} // namespace ifpaNotation in the code
Names in the code relate to the notation on the Method page and in the papers as follows:
| In the code | On the Method page | Meaning |
|---|---|---|
pm | xpm | preferential median |
u_pm (uPm in JavaScript) | u(xpm) | combined standard uncertainty √(u_grid² + u_sampling²) |
u_grid | upm = 0.41h | component due to the RAV partition; this is what the papers call upm |
u_sampling | us | sampling component 1.2533·1.4826·MAD/√m — an addition of the site's implementations, not in the papers |
n, h | n, h | final number of RAV values and the partition norm |
winners | Abest | indices of the Borda winners |
coverage | ci | coverage — how many intervals contain each RAV value |
converged | — | false if the winners never became adjacent and the widest block was taken (n reached n_max) |
Reference values
Check your copy or your own implementation on these data sets:
| Data set | Parameters | PM | u | final n |
|---|---|---|---|---|
| Five instruments, bounds [22.009, 24.991], [25.417, 25.843], [24.565, 24.991], [25.843, 26.269], [24.991, 24.991] | n = 11 | 24.991 | 0.559 | 11 |
| Non-adjacent winners: [4.2, 4.55], [4.7, 5.1] | n = 3 | 4.65 | 0.366 | 4 |
| Interlaboratory comparison (example 1) | auto | 10.0125 | 0.0040 | 11 |
All ten estimators of compare_estimators on the comparison example — a check of the other estimators:
| Estimator | Value | u |
|---|---|---|
| Preferential median (IF&PA) | 10.0125 | 0.00401 |
| Arithmetic mean | 10.02075 | 0.00633 |
| Median | 10.0155 | 0.00263 |
| Hodges–Lehmann estimate | 10.01575 | — |
| Robust mean, Algorithm A | 10.01587 | 0.00268 |
| Weighted mean, weights 1/u² | 10.04078 | 0.00292 |
| Weighted median, weights 1/u² | 10.064 | — |
| DerSimonian–Laird | 10.02163 | 0.01105 |
| Largest consistent subset | 10.01422 | 0.00428 |
| Mixture model mode | 10.01408 | — |
All 430 check data sets with answers — ifpa_test_vectors.json (486 KB):
examples from the paper, degenerate, non-converging and random sets, and the same data in other units (×10−34, ×109, a shift by 109): the answer must not depend on the units. Each has inputs x, u, n
(null — automatic) and answers: pm, u_pm, u_grid, u_sampling,
the final n_final, converged and the values of all ten estimators. Tolerance — relative 10−9,
10−6 for the mixture model mode (it is found numerically).
What compare_estimators returns
| Estimator | Uncertainty | Source |
|---|---|---|
| Preferential median (IF&PA) | √[(0.41h)² + (1.2533·MAD′/√m)²] | method — Muravyov et al., Measurement, 2018; Borda rule — AISP'25, 2025; 0.41h — IJDSA, 2026; sampling component — a site addition |
| Arithmetic mean | s/√m | uncertainty — GUM, JCGM 100:2008, clause 4.2 · open text |
| Median | 1.2533·MAD′/√m | asymptotic standard error of the median for a normal distribution |
| Hodges–Lehmann estimate | — | Hodges, Lehmann, Ann. Math. Statist., 1963 · open text |
| Robust mean, Algorithm A | 1.25·s*/√m | ISO 13528:2022, Annex C (paid standard) |
| Weighted mean, weights 1/u² | 1/√Σw | Cox, Metrologia, 2002 (procedure A) |
| Weighted median, weights 1/u² | — | Gurwitz, BIT, 1990 |
| DerSimonian–Laird | 1/√Σw* | DerSimonian, Laird, Control. Clin. Trials, 1986; Koepke et al., Metrologia, 2017; NIST Consensus Builder |
| Largest consistent subset | 1/√Σw over the subset | Cox, Metrologia, 2007 · open text; sequential exclusion, χ² by Wilson, Hilferty, PNAS, 1931 · open text |
| Mixture model mode | — | Ciarlini, Cox, Pavese, Regoliosi, Metrologia, 2004 · open text; Duewer, CCQM document CCQM/04-15, 2004 |
MAD′ = 1.4826 × median absolute deviation from the median. Estimators that use the uncertainties are returned only if all uk > 0.
This code does not reproduce the configurator's numbers directly. compare_estimators
computes the PM from the intervals x ± u with the RAV partition of IF&PA-L. By default the configurator takes
the intervals x ± 2u (expanded uncertainty) and a fine partition — a norm of at most 0.05 of the median
u. To get the PM as in the configurator, use pm_estimate from benchmark.py
(or pmEstimate from benchmark.js):
from benchmark import pm_estimate
pm, u = pm_estimate(x, u, {"pm_k": 2, "pm_grid": "fine"})Comparison on simulated data
benchmark.py reproduces the applicability
configurator: same conditions, same random generator, same numbers. Needs ifpa.py in the same folder.
python benchmark.py --list # ready situations python benchmark.py --preset overconfident # outliers with understated uncertainty python benchmark.py --m 12 --contamination 0.2 --shift 8 --outlier-u 0.3 python benchmark.py --preset bounded --kernel triangular # another error distribution python benchmark.py --preset clean --pm-k 1 --pm-grid auto # PM with x ± 1u, IF&PA-L partition
Distributions (--kernel): normal, uniform, triangular, laplace, t3, skewed. PM settings: --pm-k is the
interval x ± k·u (default 2), --pm-grid is fine (norm h ≤ 0.05 of the median u, default), medium (0.2) or auto (as in IF&PA-L).
For JavaScript — benchmark.js (needs ifpa.js next to it); in Node.js:
const B = require("./benchmark.js");
const res = B.run(Object.assign({}, B.PRESETS.overconfident, { replicates: 500, seed: 1 }));
console.table(res.rows.map((r) => ({ name: r.name, rmse: r.rmse, mae: r.mae, p90: r.p90 })));
// PM as in the configurator: intervals x ± 2u, fine RAV partition
const pm = B.pmEstimate(x, u, { pmK: 2, pmGrid: "fine" }); // { value, u }In a browser the files are included with <script> tags (ifpa.js first); the object is window.IFPABench.
Terms of use
The code is provided for research and teaching. If the method or the code helped your work, please cite the paper describing the method. Open licence terms will be announced separately.