use crate::config::TrainingParams;
use crate::data::DMatrix;
use crate::error::{HessboostError, Result};
use crate::learner::train::train_with_eval;
use rand::SeedableRng;
use rand::rngs::StdRng;
use rand::seq::SliceRandom;
use std::collections::BTreeMap;
#[derive(Debug, Clone)]
pub struct CvResult {
pub metric: String,
pub test_mean: Vec<f64>,
pub test_std: Vec<f64>,
}
pub fn cv(
params: &TrainingParams,
data: &DMatrix,
num_boost_round: usize,
nfold: usize,
seed: u64,
) -> Result<Vec<CvResult>> {
if nfold < 2 {
return Err(HessboostError::invalid_param("nfold", "must be >= 2"));
}
let n = data.n_rows();
if n < nfold {
return Err(HessboostError::invalid_param(
"nfold",
"more folds than rows",
));
}
let mut order: Vec<usize> = (0..n).collect();
order.shuffle(&mut StdRng::seed_from_u64(seed));
let mut folds: Vec<Vec<usize>> = vec![Vec::new(); nfold];
for (i, &row) in order.iter().enumerate() {
folds[i % nfold].push(row);
}
let mut collected: BTreeMap<String, Vec<Vec<f64>>> = BTreeMap::new();
for f in 0..nfold {
let test_rows = &folds[f];
let train_rows: Vec<usize> = folds
.iter()
.enumerate()
.filter(|(i, _)| *i != f)
.flat_map(|(_, v)| v.iter().copied())
.collect();
let dtrain = data.select_rows(&train_rows)?;
let dtest = data.select_rows(test_rows)?;
let res = train_with_eval(params, &dtrain, num_boost_round, &[(&dtest, "test")], None)?;
for round in &res.history {
for (_ds, metric, value) in &round.scores {
let entry = collected.entry(metric.clone()).or_default();
if entry.len() <= round.iteration {
entry.resize(round.iteration + 1, Vec::new());
}
entry[round.iteration].push(*value);
}
}
}
let mut out = Vec::new();
for (metric, per_round) in collected {
let mut mean = Vec::with_capacity(per_round.len());
let mut std = Vec::with_capacity(per_round.len());
for vals in &per_round {
let m = vals.iter().sum::<f64>() / vals.len().max(1) as f64;
let var =
vals.iter().map(|v| (v - m) * (v - m)).sum::<f64>() / vals.len().max(1) as f64;
mean.push(m);
std.push(var.sqrt());
}
out.push(CvResult {
metric,
test_mean: mean,
test_std: std,
});
}
Ok(out)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn cv_reports_decreasing_rmse() {
let n = 200;
let mut x = Vec::new();
let mut y = Vec::new();
for i in 0..n {
let xi = i as f32 / n as f32;
x.push(xi);
y.push(if xi > 0.5 { 1.0 } else { 0.0 });
}
let d = DMatrix::from_dense(&x, n, 1)
.unwrap()
.with_labels(&y)
.unwrap();
let params = TrainingParams::builder()
.objective("reg:squarederror")
.max_depth(3)
.eta(0.3)
.build()
.unwrap();
let results = cv(¶ms, &d, 30, 5, 42).unwrap();
assert_eq!(results.len(), 1);
let rmse = &results[0];
assert_eq!(rmse.metric, "rmse");
assert_eq!(rmse.test_mean.len(), 30);
assert!(rmse.test_mean[29] < rmse.test_mean[0]);
assert!(rmse.test_std.iter().all(|s| s.is_finite() && *s >= 0.0));
}
}