use anofox_regression::solvers::{BinomialRegressor, FittedRegressor, Regressor};
use faer::{Col, Mat};
fn main() {
println!("=== Binomial/Logistic Regression ===\n");
logistic_regression();
link_function_comparison();
predict_probabilities();
classification_example();
}
fn logistic_regression() {
println!("--- Logistic Regression ---\n");
let n = 100;
let x = Mat::from_fn(n, 2, |i, j| {
if j == 0 {
(i as f64 - 50.0) * 0.1 } else {
((i as f64) * 0.15).sin()
}
});
let y = Col::from_fn(n, |i| {
let eta = -0.5 + 1.0 * x[(i, 0)] + 0.5 * x[(i, 1)];
let p = 1.0 / (1.0 + (-eta).exp());
let threshold = 0.5 + ((i as f64 * 0.7).sin()) * 0.2;
if p > threshold {
1.0
} else {
0.0
}
});
let model = BinomialRegressor::logistic().build();
let fitted = model.fit(&x, &y).expect("fit should succeed");
let result = fitted.result();
println!("True model: logit(p) = -0.5 + 1.0*x1 + 0.5*x2\n");
println!(
"Intercept: {:.4} (true: -0.5)",
fitted.intercept().unwrap_or(0.0)
);
println!(
"Coefficient x1: {:.4} (true: 1.0)",
fitted.coefficients()[0]
);
println!(
"Coefficient x2: {:.4} (true: 0.5)",
fitted.coefficients()[1]
);
println!("\nDeviance: {:.4}", result.log_likelihood * -2.0);
println!("AIC: {:.4}", result.aic);
println!("\nOdds Ratio interpretation:");
let or_x1 = fitted.coefficients()[0].exp();
println!(
" OR for x1: {:.4} (1 unit increase multiplies odds by {:.2}x)",
or_x1, or_x1
);
println!();
}
fn link_function_comparison() {
println!("--- Link Function Comparison ---\n");
let n = 120;
let x = Mat::from_fn(n, 1, |i, _| (i as f64 - 60.0) * 0.05);
let y = Col::from_fn(n, |i| {
let p = 1.0 / (1.0 + (-0.3 - 0.8 * x[(i, 0)]).exp());
let threshold = 0.5 + ((i as f64 * 0.9).sin()) * 0.15;
if p > threshold {
1.0
} else {
0.0
}
});
let logit_model = BinomialRegressor::logistic().build();
let logit_fit = logit_model.fit(&x, &y).expect("logit fit");
let probit_model = BinomialRegressor::probit().build();
let probit_fit = probit_model.fit(&x, &y).expect("probit fit");
let cloglog_model = BinomialRegressor::cloglog().build();
let cloglog_fit = cloglog_model.fit(&x, &y).expect("cloglog fit");
println!(
"{:<12} {:>12} {:>12} {:>12}",
"Link", "Intercept", "Slope", "AIC"
);
println!("{}", "-".repeat(52));
println!(
"{:<12} {:>12.4} {:>12.4} {:>12.4}",
"Logit",
logit_fit.intercept().unwrap_or(0.0),
logit_fit.coefficients()[0],
logit_fit.result().aic
);
println!(
"{:<12} {:>12.4} {:>12.4} {:>12.4}",
"Probit",
probit_fit.intercept().unwrap_or(0.0),
probit_fit.coefficients()[0],
probit_fit.result().aic
);
println!(
"{:<12} {:>12.4} {:>12.4} {:>12.4}",
"CLogLog",
cloglog_fit.intercept().unwrap_or(0.0),
cloglog_fit.coefficients()[0],
cloglog_fit.result().aic
);
println!("\nNote: Coefficients differ across link functions but give similar predictions.");
println!(" Probit coefficients are roughly logit/1.7 due to scaling.");
println!();
}
fn predict_probabilities() {
println!("--- Predicting Probabilities ---\n");
use anofox_regression::core::IntervalType;
let n = 80;
let x = Mat::from_fn(n, 1, |i, _| (i as f64 - 40.0) * 0.1);
let y = Col::from_fn(n, |i| {
let p = 1.0 / (1.0 + (-(0.2 + 0.5 * x[(i, 0)])).exp());
let threshold = 0.5 + ((i as f64 * 1.1).sin()) * 0.2;
if p > threshold {
1.0
} else {
0.0
}
});
let model = BinomialRegressor::logistic().build();
let fitted = model.fit(&x, &y).expect("fit should succeed");
let x_new = Mat::from_fn(7, 1, |i, _| -3.0 + (i as f64));
let probabilities = fitted.predict_probability(&x_new);
let pred_ci = fitted.predict_with_interval(&x_new, Some(IntervalType::Confidence), 0.95);
println!("Predicted probabilities for new x values:\n");
println!(
"{:>8} {:>12} {:>12} {:>12}",
"x", "P(Y=1)", "Lower 95%", "Upper 95%"
);
println!("{}", "-".repeat(48));
for i in 0..7 {
println!(
"{:>8.1} {:>12.4} {:>12.4} {:>12.4}",
x_new[(i, 0)],
probabilities[i],
pred_ci.lower[i],
pred_ci.upper[i]
);
}
println!("\nNote: Probabilities are bounded between 0 and 1.");
println!();
}
fn classification_example() {
println!("--- Classification Example ---\n");
let n = 150;
let x = Mat::from_fn(n, 2, |i, j| {
let base = if j == 0 {
(i as f64 - 75.0) * 0.08
} else {
((i as f64) * 0.1).sin() * 2.0
};
base + ((i as f64 * (j as f64 + 1.0) * 0.5).sin()) * 0.3
});
let y = Col::from_fn(n, |i| {
let score = 0.5 * x[(i, 0)] + 0.3 * x[(i, 1)];
let noise = ((i as f64 * 0.8).sin()) * 0.5;
if score + noise > 0.0 {
1.0
} else {
0.0
}
});
let n_train = 100;
let x_train = Mat::from_fn(n_train, 2, |i, j| x[(i, j)]);
let y_train = Col::from_fn(n_train, |i| y[i]);
let x_test = Mat::from_fn(n - n_train, 2, |i, j| x[(i + n_train, j)]);
let y_test = Col::from_fn(n - n_train, |i| y[i + n_train]);
let model = BinomialRegressor::logistic().build();
let fitted = model.fit(&x_train, &y_train).expect("fit should succeed");
let test_probs = fitted.predict_probability(&x_test);
let threshold = 0.5;
let mut tp = 0;
let mut tn = 0;
let mut fp = 0;
let mut fn_ = 0;
for i in 0..y_test.nrows() {
let predicted = if test_probs[i] > threshold { 1.0 } else { 0.0 };
let actual = y_test[i];
if predicted == 1.0 && actual == 1.0 {
tp += 1;
} else if predicted == 0.0 && actual == 0.0 {
tn += 1;
} else if predicted == 1.0 && actual == 0.0 {
fp += 1;
} else {
fn_ += 1;
}
}
let accuracy = (tp + tn) as f64 / y_test.nrows() as f64;
let precision = if tp + fp > 0 {
tp as f64 / (tp + fp) as f64
} else {
0.0
};
let recall = if tp + fn_ > 0 {
tp as f64 / (tp + fn_) as f64
} else {
0.0
};
println!("Classification on test set (threshold = {}):\n", threshold);
println!("Confusion Matrix:");
println!(" Predicted");
println!(" 0 1");
println!("Actual 0 {:>3} {:>3}", tn, fp);
println!(" 1 {:>3} {:>3}", fn_, tp);
println!("\nMetrics:");
println!(" Accuracy: {:.2}%", accuracy * 100.0);
println!(" Precision: {:.2}%", precision * 100.0);
println!(" Recall: {:.2}%", recall * 100.0);
println!("\nAccuracy at different thresholds:");
for &t in &[0.3, 0.4, 0.5, 0.6, 0.7] {
let correct: usize = (0..y_test.nrows())
.filter(|&i| {
let pred = if test_probs[i] > t { 1.0 } else { 0.0 };
(pred - y_test[i]).abs() < 0.01
})
.count();
println!(
" Threshold {:.1}: {:.2}%",
t,
100.0 * correct as f64 / y_test.nrows() as f64
);
}
}