Проверено. Все пять реализаций дают одинаковые ответы на 430 наборах данных: примеры из статьи-первоисточника, вырожденные и несходящиеся случаи, данные в масштабах от 10−34 до 109. Преференциальная медиана сверена с эталонной программой авторов. Кроме неё, в коде реализованы девять распространённых оценок — от среднего и медианы до DerSimonian–Laird и моды смеси (таблица с первоисточниками); они сверены между языками и с независимыми расчётами по формулам первоисточников. Наборы с ответами можно скачать и проверить ими свою реализацию (см. «Контрольные значения»).
Python 3.8+, только стандартная библиотека. Файл: 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 и девять других оценок
print(e["name"], e["value"], e["u"])Весь код 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}")Современный JavaScript, работает в браузере и в Node.js. Файл: ifpa.js. Этот же файл считает в калькуляторе.
// браузер: <script src="ifpa.js"></script>, затем глобальный объект IFPA
// 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));Весь код 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,
};
});Базовый R, без пакетов. Файл: 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, uВесь код 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+ и GNU Octave 6+, без тулбоксов. Файлы: preferential_median.m, compare_estimators.m — положите их в одну папку.
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); endВесь код 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));
endВесь код 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, заголовочный файл, только стандартная библиотека. Файл: 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);
}Весь код 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 ifpaОбозначения в коде
Имена в коде и обозначения на странице «Метод» и в статьях соотносятся так:
| В коде | На странице «Метод» | Смысл |
|---|---|---|
pm | xpm | преференциальная медиана |
u_pm (uPm в JavaScript) | u(xpm) | суммарная стандартная неопределённость √(u_grid² + u_sampling²) |
u_grid | upm = 0,41h | составляющая от разбиения ДАЗ; в статьях именно она называется upm |
u_sampling | us | выборочная составляющая 1,2533·1,4826·MAD/√m — дополнение реализаций сайта, в статьях её нет |
n, h | n, h | итоговое число значений ДАЗ и норма разбиения |
winners | Abest | номера победителей Борда |
coverage | ci | сводные индикаторы — сколько интервалов накрывает каждое значение ДАЗ |
converged | — | false, если победители так и не пошли подряд и взят самый широкий блок (n достигло n_max) |
Контрольные значения
Проверьте свою копию или собственную реализацию на этих наборах:
| Набор | Параметры | ПМ | u | итоговое n |
|---|---|---|---|---|
| Пять средств измерений, границы [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 |
| Несмежные победители: [4,2; 4,55], [4,7; 5,1] | n = 3 | 4,65 | 0,366 | 4 |
| Межлабораторное сличение (пример 1) | авто | 10,0125 | 0,0040 | 11 |
Все десять оценок compare_estimators на примере сличения — проверка остальных оценок:
| Оценка | Значение | u |
|---|---|---|
| Преференциальная медиана (IF&PA) | 10,0125 | 0,00401 |
| Среднее арифметическое | 10,02075 | 0,00633 |
| Медиана | 10,0155 | 0,00263 |
| Оценка Ходжеса–Лемана | 10,01575 | — |
| Робастное среднее, алгоритм A | 10,01587 | 0,00268 |
| Взвешенное среднее, веса 1/u² | 10,04078 | 0,00292 |
| Взвешенная медиана, веса 1/u² | 10,064 | — |
| DerSimonian–Laird | 10,02163 | 0,01105 |
| Наибольшее согласованное подмножество | 10,01422 | 0,00428 |
| Мода смеси распределений | 10,01408 | — |
Все 430 наборов сверки с ответами — ifpa_test_vectors.json (486 КБ):
примеры статьи, вырожденные, несходящиеся и случайные наборы, а также те же данные в других единицах (×10−34, ×109, сдвиг на 109): ответ не должен зависеть от единиц измерения. Для каждого — входы x, u, n
(null — автоматически) и ответы: pm, u_pm, u_grid, u_sampling,
итоговое n_final, converged и значения всех десяти оценок. Допуск — относительный 10−9,
для моды смеси 10−6 (она ищется численно).
Что возвращает compare_estimators
| Оценка | Неопределённость | Источник |
|---|---|---|
| Преференциальная медиана (IF&PA) | √[(0,41h)² + (1,2533·MAD′/√m)²] | метод — Muravyov et al., Measurement, 2018; правило Борда — AISP'25, 2025; 0,41h — IJDSA, 2026; выборочная составляющая — дополнение сайта |
| Среднее арифметическое | s/√m | неопределённость — GUM, JCGM 100:2008, п. 4.2 · открытый текст |
| Медиана | 1,2533·MAD′/√m | асимптотическая стандартная ошибка медианы для нормального распределения |
| Оценка Ходжеса–Лемана | — | Hodges, Lehmann, Ann. Math. Statist., 1963 · открытый текст |
| Робастное среднее, алгоритм A | 1,25·s*/√m | ISO 13528:2022, прил. C (стандарт, платный) |
| Взвешенное среднее, веса 1/u² | 1/√Σw | Cox, Metrologia, 2002 (процедура A) |
| Взвешенная медиана, веса 1/u² | — | Gurwitz, BIT, 1990 |
| DerSimonian–Laird | 1/√Σw* | DerSimonian, Laird, Control. Clin. Trials, 1986; Koepke et al., Metrologia, 2017; NIST Consensus Builder |
| Наибольшее согласованное подмножество | 1/√Σw по подмножеству | Cox, Metrologia, 2007 · открытый текст; последовательное исключение, χ² по Wilson, Hilferty, PNAS, 1931 · открытый текст |
| Мода смеси распределений | — | Ciarlini, Cox, Pavese, Regoliosi, Metrologia, 2004 · открытый текст; Duewer, документ CCQM/04-15, 2004 |
MAD′ = 1,4826 · медиана абсолютных отклонений от медианы. Оценки, которые используют неопределённости, возвращаются, только если все uk > 0.
Числа конфигуратора этим кодом напрямую не повторяются. compare_estimators считает ПМ
по интервалам x ± u с разбиением ДАЗ, как в IF&PA-L. Конфигуратор по умолчанию берёт интервалы
x ± 2u (расширенная неопределённость) и мелкое разбиение — норма не больше 0,05 медианы u.
Чтобы получить ПМ, как в конфигураторе, используйте pm_estimate из benchmark.py
(или pmEstimate из benchmark.js):
from benchmark import pm_estimate
pm, u = pm_estimate(x, u, {"pm_k": 2, "pm_grid": "fine"})Сравнение на имитированных данных
benchmark.py повторяет конфигуратор
применимости: те же условия, тот же генератор случайных чисел, те же числа. Нужен ifpa.py в той же папке.
python benchmark.py --list # готовые ситуации python benchmark.py --preset overconfident # выбросы с заниженной неопределённостью python benchmark.py --m 12 --contamination 0.2 --shift 8 --outlier-u 0.3 python benchmark.py --preset bounded --kernel triangular # другое распределение погрешности python benchmark.py --preset clean --pm-k 1 --pm-grid auto # ПМ по x ± 1u, разбиение IF&PA-L
Распределения (--kernel): normal, uniform, triangular, laplace, t3, skewed. Настройки ПМ: --pm-k —
интервал x ± k·u (по умолчанию 2), --pm-grid — fine (норма h ≤ 0,05 медианы u, по умолчанию), medium (0,2) или auto (как в IF&PA-L).
Для JavaScript — benchmark.js (рядом нужен ifpa.js); в 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 })));
// ПМ, как в конфигураторе: интервалы x ± 2u, мелкое разбиение ДАЗ
const pm = B.pmEstimate(x, u, { pmK: 2, pmGrid: "fine" }); // { value, u }В браузере файлы подключаются тегами <script> (сначала ifpa.js), объект — window.IFPABench.
Условия использования
Код предоставляется для исследовательских и учебных целей. Если метод или код помогли в вашей работе, пожалуйста, сошлитесь на статью о методе. Условия открытой лицензии будут объявлены дополнительно.