use crate::dataframe::DataFrame;
use crate::error::{Error, Result};
use crate::optimized::OptimizedDataFrame;
use scirs2_core::random::rngs::StdRng;
use scirs2_core::random::Rng;
use scirs2_core::random::SeedableRng;
use scirs2_core::random::SliceRandom;
use std::collections::HashMap;
#[derive(Debug, Clone)]
pub struct ModelMetrics {
pub metrics: HashMap<String, f64>,
pub training_time: f64,
pub prediction_time: Option<f64>,
}
impl ModelMetrics {
pub fn new() -> Self {
ModelMetrics {
metrics: HashMap::new(),
training_time: 0.0,
prediction_time: None,
}
}
pub fn add_metric(&mut self, name: &str, value: f64) {
self.metrics.insert(name.to_string(), value);
}
pub fn get_metric(&self, name: &str) -> Option<&f64> {
self.metrics.get(name)
}
pub fn set_training_time(&mut self, time: f64) {
self.training_time = time;
}
pub fn set_prediction_time(&mut self, time: f64) {
self.prediction_time = Some(time);
}
}
pub trait ModelEvaluator {
fn evaluate(&self, test_data: &DataFrame, test_target: &str) -> Result<ModelMetrics>;
fn cross_validate(
&self,
data: &DataFrame,
target: &str,
folds: usize,
) -> Result<Vec<ModelMetrics>>;
}
pub trait SupervisedModel: ModelEvaluator {
fn fit(&mut self, train_data: &DataFrame, target_column: &str) -> Result<()>;
fn predict(&self, data: &DataFrame) -> Result<Vec<f64>>;
fn feature_importances(&self) -> Option<HashMap<String, f64>>;
}
pub(crate) fn r2_score_guarded(predictions: &[f64], actual: &[f64]) -> f64 {
let y_mean = actual.iter().sum::<f64>() / actual.len() as f64;
let ss_tot: f64 = actual.iter().map(|a| (a - y_mean).powi(2)).sum();
let ss_res: f64 = predictions
.iter()
.zip(actual)
.map(|(p, a)| (a - p).powi(2))
.sum();
if ss_tot == 0.0 {
if ss_res == 0.0 {
1.0
} else {
0.0
}
} else {
1.0 - ss_res / ss_tot
}
}
pub(crate) fn contiguous_kfold_cross_validate<M>(
model: &M,
data: &DataFrame,
target: &str,
folds: usize,
) -> Result<Vec<ModelMetrics>>
where
M: SupervisedModel + Clone,
{
if folds < 2 {
return Err(Error::InvalidInput(
"Number of folds must be at least 2".into(),
));
}
let n = data.nrows();
if n < folds {
return Err(Error::InvalidInput(
"Number of samples must be at least equal to the number of folds".into(),
));
}
let fold_size = n / folds;
let mut all_metrics: Vec<ModelMetrics> = Vec::with_capacity(folds);
for fold_idx in 0..folds {
let test_start = fold_idx * fold_size;
let test_end = if fold_idx == folds - 1 {
n
} else {
(fold_idx + 1) * fold_size
};
let test_indices: Vec<usize> = (test_start..test_end).collect();
let train_indices: Vec<usize> = (0..n)
.filter(|&i| i < test_start || i >= test_end)
.collect();
if train_indices.is_empty() || test_indices.is_empty() {
return Err(Error::InvalidInput(
"A fold resulted in empty train or test set".into(),
));
}
let train_df = data.sample(&train_indices)?;
let test_df = data.sample(&test_indices)?;
let mut fold_model = model.clone();
fold_model.fit(&train_df, target)?;
let fold_metrics = fold_model.evaluate(&test_df, target)?;
all_metrics.push(fold_metrics);
}
Ok(all_metrics)
}
pub trait UnsupervisedModel: ModelEvaluator {
fn fit(&mut self, data: &DataFrame) -> Result<()>;
fn transform(&self, data: &DataFrame) -> Result<DataFrame>;
fn fit_transform(&mut self, data: &DataFrame) -> Result<DataFrame> {
self.fit(data)?;
self.transform(data)
}
}
#[derive(Debug, Clone)]
pub struct CrossValidation {
pub n_folds: usize,
pub shuffle: bool,
pub random_seed: Option<u64>,
}
impl Default for CrossValidation {
fn default() -> Self {
CrossValidation {
n_folds: 5,
shuffle: true,
random_seed: None,
}
}
}
pub fn train_test_split(
data: &DataFrame,
test_size: f64,
shuffle: bool,
random_seed: Option<u64>,
) -> Result<(DataFrame, DataFrame)> {
if test_size <= 0.0 || test_size >= 1.0 {
return Err(Error::InvalidInput(
"test_size must be between 0 and 1".into(),
));
}
let n_rows = data.nrows();
let n_test = (n_rows as f64 * test_size).round() as usize;
if n_test == 0 || n_test == n_rows {
return Err(Error::InvalidInput(format!(
"test_size {} would result in empty training or test set",
test_size
)));
}
let (train_indices, test_indices): (Vec<usize>, Vec<usize>) = if shuffle {
let mut indices: Vec<usize> = (0..n_rows).collect();
indices.shuffle(&mut seeded_rng(random_seed));
let test_indices = indices[..n_test].to_vec();
let train_indices = indices[n_test..].to_vec();
(train_indices, test_indices)
} else {
(
(0..(n_rows - n_test)).collect(),
((n_rows - n_test)..n_rows).collect(),
)
};
let train_data = data.sample(&train_indices)?;
let test_data = data.sample(&test_indices)?;
Ok((train_data, test_data))
}
pub fn train_test_split_opt(
data: &OptimizedDataFrame,
test_size: f64,
random_seed: Option<u64>,
) -> Result<(OptimizedDataFrame, OptimizedDataFrame)> {
if test_size <= 0.0 || test_size >= 1.0 {
return Err(Error::InvalidInput(
"test_size must be between 0 and 1".into(),
));
}
let n_rows = data.row_count();
let n_test = (n_rows as f64 * test_size).round() as usize;
if n_test == 0 || n_test == n_rows {
return Err(Error::InvalidInput(format!(
"test_size {} would result in empty training or test set",
test_size
)));
}
let mut indices: Vec<usize> = (0..n_rows).collect();
indices.shuffle(&mut seeded_rng(random_seed));
let test_indices = indices[..n_test].to_vec();
let train_indices = indices[n_test..].to_vec();
let train_data = data.sample_rows(&train_indices)?;
let test_data = data.sample_rows(&test_indices)?;
Ok((train_data, test_data))
}
fn seeded_rng(seed: Option<u64>) -> StdRng {
match seed {
Some(seed_val) => StdRng::seed_from_u64(seed_val),
None => {
let mut seed_bytes = [0u8; 32];
scirs2_core::random::rng().fill_bytes(&mut seed_bytes);
StdRng::from_seed(seed_bytes)
}
}
}
pub mod ensemble;
pub mod evaluation;
pub mod linear;
pub mod neural;
pub mod selection;
pub mod tree;
pub use ensemble::{
GradientBoostingClassifier, GradientBoostingConfig, GradientBoostingRegressor,
RandomForestClassifier, RandomForestConfig, RandomForestRegressor,
};
pub use evaluation::{cross_val_score, learning_curve, validation_curve};
pub use linear::{LinearRegression, LogisticRegression};
pub use neural::{
Activation, LossFunction, MLPClassifier, MLPConfig, MLPConfigBuilder, MLPRegressor,
};
pub use selection::{GridSearchCV, HyperparameterGrid, RandomizedSearchCV};
pub use tree::{DecisionTreeClassifier, DecisionTreeConfig, DecisionTreeRegressor, SplitCriterion};