use crate::common::load_dataset;
use linreg_core::core::ols_regression;
use linreg_core::feature_importance::{
permutation_importance_ols, shap_values_linear, vif_ranking, PermutationImportanceOptions,
};
use linreg_core::stats;
use std::path::Path;
const STD_COEF_TOLERANCE: f64 = 1e-6;
const VIF_TOLERANCE: f64 = 1e-4;
const SHAP_TOLERANCE: f64 = 1e-6;
const PERM_TOLERANCE: f64 = 0.01;
#[derive(Debug)]
pub enum ValidationError {
InvalidInput(String),
}
impl std::fmt::Display for ValidationError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ValidationError::InvalidInput(msg) => write!(f, "{}", msg),
}
}
}
impl std::error::Error for ValidationError {}
impl From<std::io::Error> for ValidationError {
fn from(err: std::io::Error) -> Self {
ValidationError::InvalidInput(err.to_string())
}
}
impl From<serde_json::Error> for ValidationError {
fn from(err: serde_json::Error) -> Self {
ValidationError::InvalidInput(err.to_string())
}
}
impl From<linreg_core::Error> for ValidationError {
fn from(err: linreg_core::Error) -> Self {
ValidationError::InvalidInput(format!("{:?}", err))
}
}
impl From<Box<dyn std::error::Error>> for ValidationError {
fn from(err: Box<dyn std::error::Error>) -> Self {
ValidationError::InvalidInput(err.to_string())
}
}
pub type Result<T> = std::result::Result<T, ValidationError>;
#[derive(Debug, serde::Deserialize)]
struct DatasetResults {
#[serde(default)]
standardized_coefficients: Option<StandardizedCoefsRef>,
#[serde(default)]
vif_ranking: Option<VifRankingRef>,
#[serde(default)]
shap: Option<ShapRef>,
#[serde(default)]
permutation_importance: Option<PermutationImportanceRef>,
}
#[derive(Debug, serde::Deserialize)]
struct StandardizedCoefsRef {
#[serde(rename = "standardized_coefficients")]
coefficients: Vec<f64>,
}
#[derive(Debug, serde::Deserialize)]
struct VifRankingRef {
vif_values: Vec<f64>,
#[serde(default)]
variable_names: Vec<String>,
}
#[derive(Debug, serde::Deserialize)]
struct ShapRef {
mean_abs_shap: Vec<f64>,
}
#[derive(Debug, serde::Deserialize)]
struct PermutationImportanceRef {
importance: Vec<f64>,
}
pub fn load_dataset_results(path: &str) -> Result<DatasetResults> {
let file_content = std::fs::read_to_string(path)?;
let json_str = file_content.replace("NaN", "null").replace("Inf", "null");
let results: DatasetResults = serde_json::from_str(&json_str)?;
Ok(results)
}
pub fn validate_dataset_feature_importance_python(dataset_name: &str, results_dir: &str) -> Result<()> {
let py_path = format!("{}/{}_feature_importance.json", results_dir, dataset_name);
if !Path::new(&py_path).exists() {
return Err(ValidationError::InvalidInput(format!("Python results not found: {}", py_path)));
}
let py_results = load_dataset_results(&py_path)?;
let mut errors = Vec::new();
let current_dir = std::env::current_dir()?;
let csv_path = current_dir.join(format!("verification/datasets/csv/{}.csv", dataset_name));
if !csv_path.exists() {
return Err(ValidationError::InvalidInput(format!("CSV not found: {}", csv_path.display())));
}
let dataset = load_dataset(&csv_path)?;
let fit = ols_regression(&dataset.y, &dataset.x_vars, &dataset.variable_names)?;
let y_std = stats::stddev(&dataset.y);
if let Some(ref py_std) = py_results.standardized_coefficients {
let var_names: Vec<String> = dataset.x_vars.iter().enumerate().map(|(i, _)| format!("X{}", i + 1)).collect();
let rust_std = linreg_core::standardized_coefficients_named(&fit.coefficients, &dataset.x_vars, &var_names, y_std)?;
println!(" Standardized Coefficients:");
for (i, (&rust_val, &ref_val)) in rust_std.standardized_coefficients.iter().zip(py_std.coefficients.iter()).enumerate() {
let diff = (rust_val - ref_val).abs();
let status = if diff <= STD_COEF_TOLERANCE { "PASS" } else { "FAIL" };
println!(" [{}] {}: Rust={:.10}, Ref={:.10}, Diff={:.2e}", status, i, rust_val, ref_val, diff);
if diff > STD_COEF_TOLERANCE {
errors.push(format!("Std coef {}: Rust={}, Ref={}, Diff={}", i, rust_val, ref_val, diff));
}
}
}
if dataset.x_vars.len() >= 2 {
if let Some(ref py_vif) = py_results.vif_ranking {
let rust_vif = vif_ranking(&fit.vif);
println!(" VIF (matching by value):");
for i in 0..dataset.x_vars.len() {
let rust_val = rust_vif.vif_values[i];
let ref_val = py_vif.vif_values.iter()
.find(|&&v| (v - rust_val).abs() < 1e-6)
.unwrap_or(&rust_val);
let rel_diff = if ref_val.abs() > VIF_TOLERANCE {
(rust_val - ref_val).abs() / ref_val.abs()
} else {
(rust_val - ref_val).abs()
};
let status = if rel_diff <= VIF_TOLERANCE { "PASS" } else { "FAIL" };
println!(" [{}] X{}: Rust={:.10}, Ref={:.10}, RelDiff={:.2e}", status, i + 1, rust_val, ref_val, rel_diff);
if rel_diff > VIF_TOLERANCE {
errors.push(format!("VIF X{}: Rust={}, Ref={}, RelDiff={}", i + 1, rust_val, ref_val, rel_diff));
}
}
}
}
if let Some(ref py_shap) = py_results.shap {
let rust_shap = shap_values_linear(&dataset.x_vars, &fit.coefficients)?;
println!(" SHAP (mean |SHAP|):");
for (i, (&rust_val, &ref_val)) in rust_shap.mean_abs_shap.iter().zip(py_shap.mean_abs_shap.iter()).enumerate() {
let diff = (rust_val - ref_val).abs();
let status = if diff <= SHAP_TOLERANCE { "PASS" } else { "FAIL" };
println!(" [{}] {}: Rust={:.10}, Ref={:.10}, Diff={:.2e}", status, i, rust_val, ref_val, diff);
if diff > SHAP_TOLERANCE {
errors.push(format!("SHAP {}: Rust={}, Ref={}, Diff={}", i, rust_val, ref_val, diff));
}
}
}
if let Some(ref py_perm) = py_results.permutation_importance {
let options = PermutationImportanceOptions {
n_permutations: 10,
seed: Some(42),
compute_intervals: false,
interval_confidence: 0.95,
};
let rust_perm = permutation_importance_ols(&dataset.y, &dataset.x_vars, &fit, &options)?;
println!(" Permutation Importance:");
for (i, (&rust_val, &ref_val)) in rust_perm.importance.iter().zip(py_perm.importance.iter()).enumerate() {
let diff = (rust_val - ref_val).abs();
let status = if diff <= PERM_TOLERANCE { "PASS" } else { "FAIL" };
println!(" [{}] {}: Rust={:.10}, Ref={:.10}, Diff={:.2e}", status, i, rust_val, ref_val, diff);
if diff > PERM_TOLERANCE {
errors.push(format!("Perm {}: Rust={}, Ref={}, Diff={}", i, rust_val, ref_val, diff));
}
}
}
if errors.is_empty() {
println!(" PASSED");
Ok(())
} else {
println!(" FAILED: {} errors", errors.len());
Err(ValidationError::InvalidInput(format!("Validation errors:\n{}", errors.join("\n"))))
}
}
pub fn validate_dataset_feature_importance_r(dataset_name: &str, results_dir: &str) -> Result<()> {
let r_path = format!("{}/{}_feature_importance.json", results_dir, dataset_name);
if !Path::new(&r_path).exists() {
return Err(ValidationError::InvalidInput(format!("R results not found: {}", r_path)));
}
let r_results = load_dataset_results(&r_path)?;
let mut errors = Vec::new();
let current_dir = std::env::current_dir()?;
let csv_path = current_dir.join(format!("verification/datasets/csv/{}.csv", dataset_name));
if !csv_path.exists() {
return Err(ValidationError::InvalidInput(format!("CSV not found: {}", csv_path.display())));
}
let dataset = load_dataset(&csv_path)?;
let fit = ols_regression(&dataset.y, &dataset.x_vars, &dataset.variable_names)?;
let y_std = stats::stddev(&dataset.y);
if let Some(ref r_std) = r_results.standardized_coefficients {
let var_names: Vec<String> = dataset.x_vars.iter().enumerate().map(|(i, _)| format!("X{}", i + 1)).collect();
let rust_std = linreg_core::standardized_coefficients_named(&fit.coefficients, &dataset.x_vars, &var_names, y_std)?;
println!(" Standardized Coefficients:");
for (i, (&rust_val, &ref_val)) in rust_std.standardized_coefficients.iter().zip(r_std.coefficients.iter()).enumerate() {
let diff = (rust_val - ref_val).abs();
let status = if diff <= STD_COEF_TOLERANCE { "PASS" } else { "FAIL" };
println!(" [{}] {}: Rust={:.10}, Ref={:.10}, Diff={:.2e}", status, i, rust_val, ref_val, diff);
if diff > STD_COEF_TOLERANCE {
errors.push(format!("Std coef {}: Rust={}, Ref={}, Diff={}", i, rust_val, ref_val, diff));
}
}
}
if dataset.x_vars.len() >= 2 {
if let Some(ref r_vif) = r_results.vif_ranking {
let rust_vif = vif_ranking(&fit.vif);
println!(" VIF:");
for i in 0..dataset.x_vars.len() {
let rust_val = rust_vif.vif_values[i];
let ref_val = r_vif.vif_values.iter()
.find(|&&v| (v - rust_val).abs() < 1e-6)
.unwrap_or(&rust_val);
let rel_diff = if ref_val.abs() > VIF_TOLERANCE {
(rust_val - ref_val).abs() / ref_val.abs()
} else {
(rust_val - ref_val).abs()
};
let status = if rel_diff <= VIF_TOLERANCE { "PASS" } else { "FAIL" };
println!(" [{}] {}: Rust={:.10}, Ref={:.10}, RelDiff={:.2e}", status, i, rust_val, ref_val, rel_diff);
if rel_diff > VIF_TOLERANCE {
errors.push(format!("VIF {}: Rust={}, Ref={}, RelDiff={}", i, rust_val, ref_val, rel_diff));
}
}
}
}
if let Some(ref r_shap) = r_results.shap {
let rust_shap = shap_values_linear(&dataset.x_vars, &fit.coefficients)?;
println!(" SHAP (mean |SHAP|):");
for (i, (&rust_val, &ref_val)) in rust_shap.mean_abs_shap.iter().zip(r_shap.mean_abs_shap.iter()).enumerate() {
let diff = (rust_val - ref_val).abs();
let status = if diff <= SHAP_TOLERANCE { "PASS" } else { "FAIL" };
println!(" [{}] {}: Rust={:.10}, Ref={:.10}, Diff={:.2e}", status, i, rust_val, ref_val, diff);
if diff > SHAP_TOLERANCE {
errors.push(format!("SHAP {}: Rust={}, Ref={}, Diff={}", i, rust_val, ref_val, diff));
}
}
}
if let Some(ref r_perm) = r_results.permutation_importance {
let options = PermutationImportanceOptions {
n_permutations: 10,
seed: Some(42),
compute_intervals: false,
interval_confidence: 0.95,
};
let rust_perm = permutation_importance_ols(&dataset.y, &dataset.x_vars, &fit, &options)?;
println!(" Permutation Importance:");
for (i, (&rust_val, &ref_val)) in rust_perm.importance.iter().zip(r_perm.importance.iter()).enumerate() {
let diff = (rust_val - ref_val).abs();
let status = if diff <= PERM_TOLERANCE { "PASS" } else { "FAIL" };
println!(" [{}] {}: Rust={:.10}, Ref={:.10}, Diff={:.2e}", status, i, rust_val, ref_val, diff);
if diff > PERM_TOLERANCE {
errors.push(format!("Perm {}: Rust={}, Ref={}, Diff={}", i, rust_val, ref_val, diff));
}
}
}
if errors.is_empty() {
println!(" PASSED");
Ok(())
} else {
println!(" FAILED: {} errors", errors.len());
Err(ValidationError::InvalidInput(format!("Validation errors:\n{}", errors.join("\n"))))
}
}
pub fn validate_feature_importance(python_results_dir: &str, r_results_dir: &str) -> Result<()> {
let datasets_to_test = [
"mtcars", "iris", "faithful", "cars_stopping", "bodyfat",
"lh", "longley", "prostate",
"synthetic_autocorrelated", "synthetic_collinear", "synthetic_heteroscedastic",
"synthetic_high_vif", "synthetic_interaction",
"synthetic_nonlinear", "synthetic_nonnormal", "synthetic_outliers",
"synthetic_simple_linear",
];
println!("\n╔══════════════════════════════════════════════════════════════════════╗");
println!("║ FEATURE IMPORTANCE - RUST VS R/PYTHON REFERENCE VALIDATION ║");
println!("╚══════════════════════════════════════════════════════════════════════╝");
let mut python_passed = 0;
let mut python_failed = 0;
let mut r_passed = 0;
let mut r_failed = 0;
for dataset in datasets_to_test {
println!("\n=== Dataset: {} ===", dataset);
print!("Python: ");
match validate_dataset_feature_importance_python(dataset, python_results_dir) {
Ok(()) => { python_passed += 1; }
Err(_) => { python_failed += 1; }
}
print!("R: ");
match validate_dataset_feature_importance_r(dataset, r_results_dir) {
Ok(()) => { r_passed += 1; }
Err(_) => { r_failed += 1; }
}
}
println!("\n╔══════════════════════════════════════════════════════════════════════╗");
println!("║ SUMMARY ║");
println!("╠══════════════════════════════════════════════════════════════════════╣");
println!("║ Python: {} passed, {} failed ║", python_passed, python_failed);
println!("║ R: {} passed, {} failed ║", r_passed, r_failed);
println!("╚══════════════════════════════════════════════════════════════════════╝");
if python_failed == 0 && r_failed == 0 {
Ok(())
} else {
Err(ValidationError::InvalidInput(format!(
"Some validations failed: Python {}/{} passed, R {}/{} passed",
python_passed, python_passed + python_failed,
r_passed, r_passed + r_failed
)))
}
}
#[test]
fn test_validate_feature_importance_mtcars() {
let current_dir = std::env::current_dir().unwrap();
let python_path = current_dir.join("verification/results/python");
let r_path = current_dir.join("verification/results/r");
let python_dir = python_path.to_str().unwrap();
let r_dir = r_path.to_str().unwrap();
let result = validate_dataset_feature_importance_python("mtcars", python_dir);
assert!(result.is_ok(), "Python validation failed: {:?}", result.err());
let result = validate_dataset_feature_importance_r("mtcars", r_dir);
assert!(result.is_ok(), "R validation failed: {:?}", result.err());
}
#[test]
fn test_validate_feature_importance_all_datasets() {
let current_dir = std::env::current_dir().unwrap();
let python_path = current_dir.join("verification/results/python");
let r_path = current_dir.join("verification/results/r");
let python_dir = python_path.to_str().unwrap();
let r_dir = r_path.to_str().unwrap();
let result = validate_feature_importance(python_dir, r_dir);
assert!(result.is_ok(), "Feature importance validation failed: {:?}", result.err());
}