use perpetual::booster::config::BoosterIO;
use perpetual::objective::Objective;
use perpetual::{Matrix, PerpetualBooster};
use std::error::Error;
const ABALONE_URL: &str = "https://archive.ics.uci.edu/ml/machine-learning-databases/abalone/abalone.data";
const FEATURE_NAMES: [&str; 8] = [
"Sex(encoded)",
"Length",
"Diameter",
"Height",
"Whole_weight",
"Shucked_weight",
"Viscera_weight",
"Shell_weight",
];
fn rmse(y: &[f64], yhat: &[f64]) -> f64 {
let mse: f64 = y.iter().zip(yhat).map(|(a, b)| (a - b).powi(2)).sum::<f64>() / y.len() as f64;
mse.sqrt()
}
fn main() -> Result<(), Box<dyn Error>> {
println!("Downloading Abalone dataset from UCI …");
let body = reqwest::blocking::get(ABALONE_URL)?.text()?;
let n_features = 8;
let mut data_columns: Vec<Vec<f64>> = vec![Vec::new(); n_features];
let mut y = Vec::new();
for line in body.lines() {
let line = line.trim();
if line.is_empty() {
continue;
}
let fields: Vec<&str> = line.split(',').collect();
if fields.len() < 9 {
continue;
}
let sex = match fields[0] {
"M" => 0.0,
"F" => 1.0,
"I" => 2.0,
_ => f64::NAN,
};
data_columns[0].push(sex);
for col in 1..n_features {
let val = fields[col].parse::<f64>().unwrap_or(f64::NAN);
data_columns[col].push(val);
}
let rings = fields[8].parse::<f64>().unwrap_or(f64::NAN);
y.push(rings);
}
let n_rows = y.len();
println!("Loaded {n_rows} samples, {n_features} features ({FEATURE_NAMES:?}).");
let split = (n_rows as f64 * 0.8) as usize;
let train_cols: Vec<Vec<f64>> = data_columns.iter().map(|c| c[..split].to_vec()).collect();
let test_cols: Vec<Vec<f64>> = data_columns.iter().map(|c| c[split..].to_vec()).collect();
let y_train = &y[..split];
let y_test = &y[split..];
let flat_train: Vec<f64> = train_cols.into_iter().flatten().collect();
let flat_test: Vec<f64> = test_cols.into_iter().flatten().collect();
let matrix_train = Matrix::new(&flat_train, split, n_features);
let matrix_test = Matrix::new(&flat_test, n_rows - split, n_features);
let mut model = PerpetualBooster::default()
.set_objective(Objective::SquaredLoss)
.set_budget(0.5);
model.fit(&matrix_train, y_train, None, None)?;
let n_trees = model.get_prediction_trees().len();
println!("Trained {n_trees} trees.");
let pred_test = model.predict(&matrix_test, true);
println!("RMSE test (before save): {:.4}", rmse(y_test, &pred_test));
let model_path = "abalone_model.json";
model.save_booster(model_path)?;
println!("Model saved to {model_path}");
let loaded = PerpetualBooster::load_booster(model_path)?;
let pred_loaded = loaded.predict(&matrix_test, true);
println!("RMSE test (after load): {:.4}", rmse(y_test, &pred_loaded));
assert_eq!(pred_test, pred_loaded, "Predictions differ after round-trip!");
println!("Round-trip save/load verified ✓");
std::fs::remove_file(model_path)?;
Ok(())
}