pub fn delta_m(m: usize) -> f64 {
let mut s = 0.0;
for n in 0..=m {
let sign = if (m - n) % 2 == 0 { 1.0 } else { -1.0 };
s += sign * binom(m, n) / ((m - n + 2) as f64).powi(2);
}
2.0 / ((m + 2) as f64).powi(2) + 2.0 * s
}
fn binom(n: usize, k: usize) -> f64 {
let k = k.min(n - k.min(n));
let mut c = 1.0;
for i in 0..k {
c = c * (n - i) as f64 / (i + 1) as f64;
}
c
}
pub fn entropy_p(p: f64) -> f64 {
let p = if p == 0.0 {
1e-6
} else if p == 1.0 {
1.0 - 1e-6
} else {
p
};
-(p * p.log2() + (1.0 - p) * (1.0 - p).log2())
}
pub fn theil_p(tt: &[f64], pp: &[f64]) -> f64 {
let t: f64 = tt.iter().sum();
let p_total = tt.iter().zip(pp).map(|(t, p)| t * p).sum::<f64>() / t;
let weighted_e: f64 = tt.iter().zip(pp).map(|(t, &p)| t * entropy_p(p)).sum();
1.0 - weighted_e / (t * entropy_p(p_total))
}
fn weighted_percentiles(values: &[f64], weights: &[f64]) -> Vec<f64> {
let n = values.len();
let mut idx: Vec<usize> = (0..n).collect();
idx.sort_by(|&a, &b| values[a].partial_cmp(&values[b]).expect("NaN in input"));
let mut groups: Vec<(usize, usize, f64)> = Vec::new();
let mut i = 0;
while i < n {
let mut j = i;
let mut group_w = weights[idx[i]];
while j + 1 < n && values[idx[j + 1]] == values[idx[i]] {
j += 1;
group_w += weights[idx[j]];
}
groups.push((i, j, group_w));
i = j + 1;
}
let total: f64 = groups.iter().map(|g| g.2).sum();
let mut out = vec![0.0; n];
let mut w_before = 0.0;
for &(start, end, group_w) in &groups {
let g = (end - start + 1) as f64;
let pct = (w_before + group_w * (g + 1.0) / (2.0 * g)) / total;
for &orig in &idx[start..=end] {
out[orig] = pct;
}
w_before += group_w;
}
out
}
pub fn rank_transform(vv_s: &[Vec<f64>], ww_s: Option<&[Vec<f64>]>) -> Vec<Vec<f64>> {
let flat: Vec<f64> = vv_s.iter().flatten().copied().collect();
let weights: Vec<f64> = match ww_s {
Some(ww_s) => ww_s.iter().flatten().copied().collect(),
None => vec![1.0; flat.len()],
};
let pct = weighted_percentiles(&flat, &weights);
let mut out = Vec::with_capacity(vv_s.len());
let mut offset = 0;
for vv in vv_s {
out.push(pct[offset..offset + vv.len()].to_vec());
offset += vv.len();
}
out
}
fn solve(mut a: Vec<Vec<f64>>, mut b: Vec<f64>) -> Vec<f64> {
let n = b.len();
for col in 0..n {
let pivot = (col..n)
.max_by(|&i, &j| a[i][col].abs().partial_cmp(&a[j][col].abs()).unwrap())
.unwrap();
a.swap(col, pivot);
b.swap(col, pivot);
for row in col + 1..n {
let factor = a[row][col] / a[col][col];
for k in col..n {
a[row][k] -= factor * a[col][k];
}
b[row] -= factor * b[col];
}
}
let mut x = vec![0.0; n];
for row in (0..n).rev() {
let s: f64 = (row + 1..n).map(|k| a[row][k] * x[k]).sum();
x[row] = (b[row] - s) / a[row][row];
}
x
}
pub struct HpDetails {
pub h_r: f64,
pub betas: Vec<f64>,
pub thresholds: Vec<f64>,
pub hp: Vec<f64>,
}
pub fn estimate_hp_details(
vv_s: &[Vec<f64>],
ww_s: Option<&[Vec<f64>]>,
k: usize,
m: usize,
) -> HpDetails {
let rr_s = rank_transform(vv_s, ww_s);
let ww_arr: Vec<Vec<f64>> = match ww_s {
Some(ww_s) => ww_s.to_vec(),
None => vv_s.iter().map(|vv| vec![1.0; vv.len()]).collect(),
};
let thresholds: Vec<f64> = (1..k).map(|i| i as f64 / k as f64).collect();
let w_kk: Vec<f64> = thresholds.iter().map(|&p| entropy_p(p).powi(2)).collect();
let tt: Vec<f64> = ww_arr.iter().map(|ww| ww.iter().sum()).collect();
let sector_cum: Vec<(Vec<f64>, Vec<f64>)> = rr_s
.iter()
.zip(&ww_arr)
.map(|(rr, ww)| {
let mut pairs: Vec<(f64, f64)> =
rr.iter().copied().zip(ww.iter().copied()).collect();
pairs.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap());
let ranks: Vec<f64> = pairs.iter().map(|p| p.0).collect();
let mut cum = 0.0;
let cum_w: Vec<f64> = pairs
.iter()
.map(|p| {
cum += p.1;
cum
})
.collect();
(ranks, cum_w)
})
.collect();
let hp: Vec<f64> = thresholds
.iter()
.map(|&thr| {
let pp: Vec<f64> = sector_cum
.iter()
.map(|(ranks, cum_w)| {
let idx = ranks.partition_point(|&r| r <= thr);
let below = if idx > 0 { cum_w[idx - 1] } else { 0.0 };
below / cum_w[cum_w.len() - 1]
})
.collect();
theil_p(&tt, &pp)
})
.collect();
let dim = m + 1;
let mut xtx = vec![vec![0.0; dim]; dim];
let mut xty = vec![0.0; dim];
for (i, &p) in thresholds.iter().enumerate() {
let mut pow = vec![1.0; dim];
for d in 1..dim {
pow[d] = pow[d - 1] * p;
}
for r in 0..dim {
xty[r] += w_kk[i] * pow[r] * hp[i];
for c in 0..dim {
xtx[r][c] += w_kk[i] * pow[r] * pow[c];
}
}
}
let betas = solve(xtx, xty);
let h_r = betas
.iter()
.enumerate()
.map(|(n, b)| b * delta_m(n))
.sum();
HpDetails { h_r, betas, thresholds, hp }
}
pub fn estimate_hp(vv_s: &[Vec<f64>], ww_s: Option<&[Vec<f64>]>, k: usize, m: usize) -> f64 {
estimate_hp_details(vv_s, ww_s, k, m).h_r
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn percentiles_match_rankdata_for_unit_weights() {
let p = weighted_percentiles(&[10.0, 20.0, 20.0, 30.0], &[1.0; 4]);
assert_eq!(p, vec![0.25, 0.625, 0.625, 1.0]); }
#[test]
fn percentiles_scale_invariant() {
let v = [10.0, 20.0, 20.0, 30.0];
let a = weighted_percentiles(&v, &[1.0, 2.0, 1.0, 1.0]);
let b = weighted_percentiles(&v, &[0.001, 0.002, 0.001, 0.001]);
for (x, y) in a.iter().zip(&b) {
assert!((x - y).abs() < 1e-12);
}
}
#[test]
fn delta_matches_python() {
let expect = [1.0, 0.5, 0.3055555555555556, 0.20833333333333337, 0.15222222222222226];
for (m, e) in expect.iter().enumerate() {
assert!((delta_m(m) - e).abs() < 1e-12, "delta_m({m})");
}
}
#[test]
fn solve_small_system() {
let a = vec![vec![2.0, 1.0], vec![1.0, 3.0]];
let x = solve(a, vec![3.0, 5.0]);
assert!((x[0] - 0.8).abs() < 1e-12);
assert!((x[1] - 1.4).abs() < 1e-12);
}
}