use faer::{Col, Mat};
fn build_design_matrix(x: &Mat<f64>, with_intercept: bool) -> Mat<f64> {
let n = x.nrows();
let p = x.ncols();
if with_intercept {
Mat::from_fn(n, p + 1, |i, j| if j == 0 { 1.0 } else { x[(i, j - 1)] })
} else {
x.to_owned()
}
}
fn build_design_matrix_reduced(x: &Mat<f64>, aliased: &[bool], with_intercept: bool) -> Mat<f64> {
let n = x.nrows();
let non_aliased: Vec<usize> = aliased
.iter()
.enumerate()
.filter(|(_, &is_aliased)| !is_aliased)
.map(|(j, _)| j)
.collect();
let p_reduced = non_aliased.len();
let total_cols = if with_intercept {
p_reduced + 1
} else {
p_reduced
};
Mat::from_fn(n, total_cols, |i, j| {
if with_intercept {
if j == 0 {
1.0
} else {
x[(i, non_aliased[j - 1])]
}
} else {
x[(i, non_aliased[j])]
}
})
}
fn compute_xtx(design: &Mat<f64>) -> Mat<f64> {
design.transpose() * design
}
fn compute_xtx_inverse(xtx: &Mat<f64>) -> Mat<f64> {
let p = xtx.nrows();
let qr = xtx.qr();
let q = qr.compute_Q();
let r = qr.R().to_owned();
let qt = q.transpose().to_owned();
let mut inv = Mat::zeros(p, p);
for col in 0..p {
let solution = solve_triangular_column(&r, &qt, col, p);
for row in 0..p {
inv[(row, col)] = solution[row];
}
}
inv
}
fn solve_triangular_column(r: &Mat<f64>, qt: &Mat<f64>, col: usize, p: usize) -> Vec<f64> {
let mut solution = vec![0.0; p];
for i in (0..p).rev() {
if r[(i, i)].abs() < 1e-14 {
continue;
}
let mut sum = qt[(i, col)];
for j in (i + 1)..p {
sum -= r[(i, j)] * solution[j];
}
solution[i] = sum / r[(i, i)];
}
solution
}
fn compute_single_leverage(design_row: &[f64], xtx_inv: &Mat<f64>) -> f64 {
let p = design_row.len();
let mut h_ii = 0.0;
for j in 0..p {
for k in 0..p {
h_ii += design_row[j] * xtx_inv[(j, k)] * design_row[k];
}
}
h_ii.clamp(0.0, 1.0)
}
pub fn compute_leverage(x: &Mat<f64>, with_intercept: bool) -> Col<f64> {
let n = x.nrows();
let design = build_design_matrix(x, with_intercept);
let p = design.ncols();
let xtx = compute_xtx(&design);
let xtx_inv = compute_xtx_inverse(&xtx);
Col::from_fn(n, |i| {
let row: Vec<f64> = (0..p).map(|j| design[(i, j)]).collect();
compute_single_leverage(&row, &xtx_inv)
})
}
pub fn compute_leverage_with_aliased(
x: &Mat<f64>,
aliased: &[bool],
with_intercept: bool,
) -> Col<f64> {
let n = x.nrows();
let has_aliased = aliased.iter().any(|&a| a);
if !has_aliased {
return compute_leverage(x, with_intercept);
}
let design = build_design_matrix_reduced(x, aliased, with_intercept);
let p = design.ncols();
if p == 0 {
return Col::from_fn(n, |_| f64::NAN);
}
let xtx = compute_xtx(&design);
let xtx_inv = compute_xtx_inverse(&xtx);
let mut inverse_valid = true;
for i in 0..p {
if xtx_inv[(i, i)].abs() < 1e-14 {
inverse_valid = false;
break;
}
}
if !inverse_valid {
return Col::from_fn(n, |_| f64::NAN);
}
Col::from_fn(n, |i| {
let row: Vec<f64> = (0..p).map(|j| design[(i, j)]).collect();
compute_single_leverage(&row, &xtx_inv)
})
}
pub fn high_leverage_points(
leverage: &Col<f64>,
n_params: usize,
threshold: Option<f64>,
) -> Vec<usize> {
let n = leverage.nrows();
let cutoff = threshold.unwrap_or(2.0 * n_params as f64 / n as f64);
leverage
.iter()
.enumerate()
.filter(|(_, &h)| h > cutoff)
.map(|(i, _)| i)
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_leverage_bounds() {
let x = Mat::from_fn(20, 2, |i, j| ((i + j) as f64) * 0.1);
let leverage = compute_leverage(&x, true);
for i in 0..leverage.nrows() {
assert!(
leverage[i] >= 0.0,
"Leverage[{}] = {} should be >= 0",
i,
leverage[i]
);
assert!(
leverage[i] <= 1.0,
"Leverage[{}] = {} should be <= 1",
i,
leverage[i]
);
}
}
#[test]
fn test_leverage_sum() {
let x = Mat::from_fn(
30,
2,
|i, j| {
if j == 0 {
i as f64
} else {
(i as f64).sin()
}
},
);
let leverage = compute_leverage(&x, true);
let sum: f64 = leverage.iter().sum();
let n_params = 3;
assert!(
(sum - n_params as f64).abs() < 0.5,
"Sum of leverage {} should be close to {}",
sum,
n_params
);
}
#[test]
fn test_high_leverage_detection() {
let mut x = Mat::from_fn(20, 1, |i, _| i as f64);
x[(19, 0)] = 100.0;
let leverage = compute_leverage(&x, true);
let high = high_leverage_points(&leverage, 2, None);
assert!(
high.contains(&19),
"Point 19 should be flagged as high leverage"
);
}
#[test]
fn test_build_design_matrix_no_intercept() {
let x = Mat::from_fn(10, 2, |i, j| (i + j) as f64);
let design = build_design_matrix(&x, false);
assert_eq!(design.nrows(), 10);
assert_eq!(design.ncols(), 2);
for i in 0..10 {
for j in 0..2 {
assert_eq!(design[(i, j)], x[(i, j)]);
}
}
}
#[test]
fn test_build_design_matrix_with_intercept() {
let x = Mat::from_fn(10, 2, |i, j| (i + j + 1) as f64);
let design = build_design_matrix(&x, true);
assert_eq!(design.nrows(), 10);
assert_eq!(design.ncols(), 3);
for i in 0..10 {
assert_eq!(design[(i, 0)], 1.0);
assert_eq!(design[(i, 1)], x[(i, 0)]);
assert_eq!(design[(i, 2)], x[(i, 1)]);
}
}
#[test]
fn test_build_design_matrix_reduced_with_aliased() {
let x = Mat::from_fn(10, 3, |i, j| (i * 10 + j) as f64);
let aliased = vec![false, true, false];
let design = build_design_matrix_reduced(&x, &aliased, true);
assert_eq!(design.nrows(), 10);
assert_eq!(design.ncols(), 3);
for i in 0..10 {
assert_eq!(design[(i, 0)], 1.0);
}
for i in 0..10 {
assert_eq!(design[(i, 1)], x[(i, 0)]);
assert_eq!(design[(i, 2)], x[(i, 2)]);
}
}
#[test]
fn test_build_design_matrix_reduced_no_intercept() {
let x = Mat::from_fn(10, 3, |i, j| (i * 10 + j) as f64);
let aliased = vec![true, false, false];
let design = build_design_matrix_reduced(&x, &aliased, false);
assert_eq!(design.nrows(), 10);
assert_eq!(design.ncols(), 2);
for i in 0..10 {
assert_eq!(design[(i, 0)], x[(i, 1)]);
assert_eq!(design[(i, 1)], x[(i, 2)]);
}
}
#[test]
fn test_compute_xtx_inverse_basic() {
let x = Mat::from_fn(
20,
2,
|i, j| if j == 0 { i as f64 } else { (i as f64).sin() },
);
let design = build_design_matrix(&x, true);
let xtx = compute_xtx(&design);
let inv = compute_xtx_inverse(&xtx);
let p = xtx.nrows();
let product = &xtx * &inv;
for i in 0..p {
for j in 0..p {
let expected = if i == j { 1.0 } else { 0.0 };
assert!(
(product[(i, j)] - expected).abs() < 1e-6,
"Product[{},{}] = {}, expected {}",
i,
j,
product[(i, j)],
expected
);
}
}
}
#[test]
fn test_compute_xtx_inverse_1x1() {
let x = Mat::from_fn(10, 1, |i, _| (i + 1) as f64);
let design = build_design_matrix(&x, false);
let xtx = compute_xtx(&design);
let inv = compute_xtx_inverse(&xtx);
let expected_xtx: f64 = (1..=10).map(|i| (i * i) as f64).sum();
assert!((xtx[(0, 0)] - expected_xtx).abs() < 1e-10);
assert!((inv[(0, 0)] - 1.0 / expected_xtx).abs() < 1e-10);
}
#[test]
fn test_solve_triangular_column_near_singular() {
let mut x = Mat::zeros(10, 3);
for i in 0..10 {
x[(i, 0)] = i as f64;
x[(i, 1)] = i as f64 * 2.0; x[(i, 2)] = (i as f64).sin();
}
let leverage = compute_leverage(&x, true);
for i in 0..10 {
assert!(
leverage[i].is_finite() || leverage[i].is_nan(),
"Leverage[{}] should be finite or NaN, got {}",
i,
leverage[i]
);
}
}
#[test]
fn test_compute_single_leverage_basic() {
let xtx_inv = Mat::identity(3, 3);
let row = vec![1.0, 2.0, 3.0];
let h = compute_single_leverage(&row, &xtx_inv);
assert!((h - 1.0).abs() < 1e-10); }
#[test]
fn test_compute_single_leverage_small_value() {
let mut xtx_inv = Mat::zeros(2, 2);
xtx_inv[(0, 0)] = 0.1;
xtx_inv[(1, 1)] = 0.1;
let row = vec![1.0, 1.0];
let h = compute_single_leverage(&row, &xtx_inv);
assert!((h - 0.2).abs() < 1e-10);
}
#[test]
fn test_compute_single_leverage_clamping() {
let mut xtx_inv = Mat::zeros(2, 2);
xtx_inv[(0, 0)] = 2.0;
xtx_inv[(1, 1)] = 2.0;
let row = vec![1.0, 1.0];
let h = compute_single_leverage(&row, &xtx_inv);
assert!(
(h - 1.0).abs() < 1e-10,
"Leverage should be clamped to at most 1.0, got {}",
h
);
}
#[test]
fn test_compute_leverage_with_aliased_no_aliased() {
let x = Mat::from_fn(
20,
2,
|i, j| if j == 0 { i as f64 } else { (i as f64).sin() },
);
let aliased = vec![false, false];
let lev_aliased = compute_leverage_with_aliased(&x, &aliased, true);
let lev_regular = compute_leverage(&x, true);
for i in 0..20 {
assert!(
(lev_aliased[i] - lev_regular[i]).abs() < 1e-10,
"Leverage[{}] mismatch: aliased={}, regular={}",
i,
lev_aliased[i],
lev_regular[i]
);
}
}
#[test]
fn test_compute_leverage_with_aliased_one_aliased() {
let x = Mat::from_fn(20, 3, |i, j| match j {
0 => i as f64,
1 => i as f64 * 2.0, 2 => (i as f64).sin() * 10.0,
_ => 0.0,
});
let aliased = vec![false, true, false];
let lev = compute_leverage_with_aliased(&x, &aliased, true);
for i in 0..20 {
assert!(lev[i].is_finite(), "Leverage[{}] should be finite", i);
assert!(
lev[i] >= 0.0 && lev[i] <= 1.0,
"Leverage[{}] = {} should be in [0, 1]",
i,
lev[i]
);
}
let sum: f64 = lev.iter().sum();
let expected_params = 3; assert!(
(sum - expected_params as f64).abs() < 0.5,
"Sum {} should be close to {}",
sum,
expected_params
);
}
#[test]
fn test_compute_leverage_with_aliased_all_aliased_returns_nan() {
let x = Mat::from_fn(10, 2, |i, _| i as f64);
let aliased = vec![true, true];
let lev = compute_leverage_with_aliased(&x, &aliased, false);
for i in 0..10 {
assert!(
lev[i].is_nan(),
"Leverage[{}] should be NaN when all columns aliased, got {}",
i,
lev[i]
);
}
}
#[test]
fn test_compute_leverage_with_aliased_with_intercept_only() {
let x = Mat::from_fn(10, 2, |i, _| i as f64);
let aliased = vec![true, true];
let lev = compute_leverage_with_aliased(&x, &aliased, true);
for i in 0..10 {
assert!(
(lev[i] - 0.1).abs() < 1e-10,
"Leverage[{}] should be 0.1 (1/n), got {}",
i,
lev[i]
);
}
}
#[test]
fn test_high_leverage_points_default_threshold() {
let mut leverage = Col::zeros(20);
for i in 0..20 {
leverage[i] = 0.1;
}
leverage[15] = 0.5;
let n_params = 3;
let high = high_leverage_points(&leverage, n_params, None);
assert!(high.contains(&15));
assert_eq!(high.len(), 1);
}
#[test]
fn test_high_leverage_points_custom_threshold() {
let mut leverage = Col::zeros(20);
for i in 0..20 {
leverage[i] = 0.1;
}
leverage[18] = 0.3;
leverage[19] = 0.3;
let high = high_leverage_points(&leverage, 3, Some(0.25));
assert!(high.contains(&18));
assert!(high.contains(&19));
assert_eq!(high.len(), 2);
}
#[test]
fn test_high_leverage_points_no_high_leverage() {
let leverage = Col::from_fn(100, |_| 0.01);
let high = high_leverage_points(&leverage, 3, None);
assert!(high.is_empty());
}
#[test]
fn test_high_leverage_points_all_high() {
let leverage = Col::from_fn(10, |_| 0.5);
let high = high_leverage_points(&leverage, 2, Some(0.2));
assert_eq!(high.len(), 10);
}
#[test]
fn test_leverage_n_less_than_p() {
let x = Mat::from_fn(3, 5, |i, j| (i * j + 1) as f64);
let leverage = compute_leverage(&x, true);
for i in 0..3 {
assert!(
leverage[i].is_nan() || (leverage[i] >= 0.0 && leverage[i] <= 1.0),
"Leverage[{}] = {} should be NaN or in [0, 1]",
i,
leverage[i]
);
}
}
#[test]
fn test_leverage_n_equals_p() {
let x = Mat::from_fn(
3,
2,
|i, j| if j == 0 { i as f64 } else { (i as f64).cos() },
);
let leverage = compute_leverage(&x, true);
for i in 0..3 {
assert!(
(leverage[i] - 1.0).abs() < 1e-6 || leverage[i].is_nan(),
"Leverage[{}] should be 1.0 or NaN when n=p, got {}",
i,
leverage[i]
);
}
}
#[test]
fn test_leverage_single_observation() {
let x = Mat::from_fn(1, 2, |_, j| (j + 1) as f64);
let leverage = compute_leverage(&x, true);
assert!(
(leverage[0] - 1.0).abs() < 1e-10 || leverage[0].is_nan(),
"Single observation leverage should be 1.0 or NaN, got {}",
leverage[0]
);
}
#[test]
fn test_leverage_without_intercept() {
let x = Mat::from_fn(20, 2, |i, j| {
if j == 0 {
(i + 1) as f64
} else {
(i as f64).sin()
}
});
let leverage = compute_leverage(&x, false);
let sum: f64 = leverage.iter().sum();
let n_params = 2;
assert!(
(sum - n_params as f64).abs() < 0.5,
"Sum {} should be close to {}",
sum,
n_params
);
}
}