use crate::GreenersError;
use ndarray::Array1;
use std::fmt;
#[derive(Debug)]
pub struct IsotonicResult {
pub fitted: Array1<f64>,
pub x: Array1<f64>,
pub y: Array1<f64>,
pub weights: Array1<f64>,
pub increasing: bool,
pub x_steps: Vec<f64>,
pub y_steps: Vec<f64>,
pub r_squared: f64,
pub mse: f64,
pub n_blocks: usize,
pub n_obs: usize,
}
impl fmt::Display for IsotonicResult {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let order = if self.increasing {
"non-decreasing"
} else {
"non-increasing"
};
writeln!(f, "\n{:=^78}", " Isotonic Regression ")?;
writeln!(f, "Barlow, Bartholomew, Bremner & Brunk (1972)")?;
writeln!(f, "Pool Adjacent Violators Algorithm (PAVA)")?;
writeln!(f, "{:<20} {:>12}", "Observations:", self.n_obs)?;
writeln!(f, "{:<20} {:>12}", "Order:", order)?;
writeln!(f, "{:<20} {:>12}", "Unique blocks:", self.n_blocks)?;
writeln!(f, "{:<20} {:>12.6}", "R²:", self.r_squared)?;
writeln!(f, "{:<20} {:>12.6}", "MSE:", self.mse)?;
writeln!(f, "\n{:-^78}", "")?;
writeln!(f, " Step function:")?;
writeln!(f, " {:<10} {:>14}", "x", "y_hat")?;
writeln!(f, "{:-^78}", "")?;
let n_show = self.x_steps.len().min(15);
for i in 0..n_show {
writeln!(f, " {:<10.4} {:>14.6}", self.x_steps[i], self.y_steps[i])?;
}
if self.x_steps.len() > 15 {
writeln!(f, " ... ({} steps total)", self.x_steps.len())?;
}
write!(f, "{:=^78}", "")
}
}
pub struct IsotonicRegression;
impl IsotonicRegression {
pub fn fit(
x: &Array1<f64>,
y: &Array1<f64>,
increasing: bool,
weights: Option<&Array1<f64>>,
) -> Result<IsotonicResult, GreenersError> {
let n = x.len();
if y.len() != n {
return Err(GreenersError::ShapeMismatch(
"IsotonicRegression: x and y must have same length".into(),
));
}
if n < 2 {
return Err(GreenersError::InvalidOperation(
"IsotonicRegression: need at least 2 observations".into(),
));
}
let w = weights.cloned().unwrap_or_else(|| Array1::ones(n));
if w.len() != n {
return Err(GreenersError::ShapeMismatch(
"IsotonicRegression: weights must have same length as x".into(),
));
}
let mut indices: Vec<usize> = (0..n).collect();
indices.sort_by(|&a, &b| x[a].total_cmp(&x[b]));
let x_sorted: Array1<f64> = indices.iter().map(|&i| x[i]).collect();
let y_sorted: Array1<f64> = indices.iter().map(|&i| y[i]).collect();
let w_sorted: Array1<f64> = indices.iter().map(|&i| w[i]).collect();
let y_work: Array1<f64> = if increasing {
y_sorted.clone()
} else {
y_sorted.mapv(|v| -v)
};
let fitted = Self::pava(&y_work, &w_sorted, n);
let fitted_final: Array1<f64> = if increasing {
fitted
} else {
fitted.mapv(|v| -v)
};
let (x_steps, y_steps) = Self::extract_steps(&x_sorted, &fitted_final, n);
let y_mean = y_sorted.mean().unwrap_or(0.0);
let tss: f64 = y_sorted.mapv(|v| (v - y_mean).powi(2)).sum();
let sse: f64 = y_sorted
.iter()
.zip(fitted_final.iter())
.map(|(a, &b)| (a - b).powi(2))
.sum::<f64>();
let r_squared = if tss > 1e-15 { 1.0 - sse / tss } else { 0.0 };
let mse = sse / n as f64;
Ok(IsotonicResult {
fitted: fitted_final,
x: x_sorted,
y: y_sorted,
weights: w_sorted,
increasing,
x_steps,
y_steps,
r_squared,
mse,
n_blocks: 0, n_obs: n,
})
}
fn pava(y: &Array1<f64>, w: &Array1<f64>, n: usize) -> Array1<f64> {
let mut block_values: Vec<f64> = y.to_vec();
let mut block_weights: Vec<f64> = w.to_vec();
let mut block_counts: Vec<usize> = vec![1; n];
let mut block_starts: Vec<usize> = (0..n).collect();
let mut n_blocks = n;
let mut i = 0;
while i < n_blocks - 1 {
if block_values[i] > block_values[i + 1] {
let w_i = block_weights[i];
let w_j = block_weights[i + 1];
let pooled_value =
(block_values[i] * w_i + block_values[i + 1] * w_j) / (w_i + w_j);
let pooled_weight = w_i + w_j;
let pooled_count = block_counts[i] + block_counts[i + 1];
block_values[i] = pooled_value;
block_weights[i] = pooled_weight;
block_counts[i] = pooled_count;
block_values.remove(i + 1);
block_weights.remove(i + 1);
block_counts.remove(i + 1);
block_starts.remove(i + 1);
n_blocks -= 1;
if i > 0 {
i = i.saturating_sub(1);
}
} else {
i += 1;
}
}
let mut fitted = Array1::zeros(n);
let mut idx = 0;
for b in 0..n_blocks {
for _ in 0..block_counts[b] {
fitted[idx] = block_values[b];
idx += 1;
}
}
fitted
}
fn extract_steps(x: &Array1<f64>, fitted: &Array1<f64>, n: usize) -> (Vec<f64>, Vec<f64>) {
let mut x_steps = Vec::new();
let mut y_steps = Vec::new();
if n == 0 {
return (x_steps, y_steps);
}
x_steps.push(x[0]);
y_steps.push(fitted[0]);
for i in 1..n {
if (fitted[i] - fitted[i - 1]).abs() > 1e-12 {
x_steps.push(x[i]);
y_steps.push(fitted[i]);
}
}
(x_steps, y_steps)
}
pub fn predict(result: &IsotonicResult, x_new: &Array1<f64>) -> Array1<f64> {
let n = x_new.len();
let mut pred = Array1::zeros(n);
for i in 0..n {
let x = x_new[i];
if x <= result.x_steps[0] {
pred[i] = result.y_steps[0];
} else if x >= result.x_steps.last().copied().unwrap_or(f64::INFINITY) {
pred[i] = result.y_steps.last().copied().unwrap_or(f64::NAN);
} else {
let mut found = false;
for j in 1..result.x_steps.len() {
if x < result.x_steps[j] {
pred[i] = result.y_steps[j - 1];
found = true;
break;
}
}
if !found {
pred[i] = result.y_steps.last().copied().unwrap_or(f64::NAN);
}
}
}
pred
}
}