Initial commit
This commit is contained in:
@@ -0,0 +1,596 @@
|
||||
/*!
|
||||
sklearn-compatible model selection wrappers
|
||||
*/
|
||||
|
||||
use pyo3::prelude::*;
|
||||
use pyo3::types::{PyDict, PyList, PyTuple, PyString};
|
||||
use numpy::{PyReadonlyArrayDyn, PyArray1, PyArray2};
|
||||
use ndarray::{Array1, Array2, Axis};
|
||||
use std::collections::HashMap;
|
||||
|
||||
use rtx_validation::cv::{KFold, StratifiedKFold};
|
||||
use rtx_validation::search::{GridSearch, RandomizedSearch};
|
||||
use rtx_validation::metrics::{accuracy_score, r2_score, mean_squared_error};
|
||||
|
||||
use crate::error::{SklearnResult, check_is_fitted};
|
||||
use crate::utils::{ArrayConverter, ParamValidator, AsyncHelper};
|
||||
|
||||
/// sklearn-compatible GridSearchCV
|
||||
#[pyclass(name = "GridSearchCV")]
|
||||
pub struct GridSearchCV {
|
||||
// Core components
|
||||
estimator: PyObject,
|
||||
param_grid: HashMap<String, Vec<PyObject>>,
|
||||
|
||||
// sklearn parameters
|
||||
scoring: Option<String>,
|
||||
n_jobs: Option<i32>,
|
||||
cv: Option<usize>,
|
||||
refit: bool,
|
||||
verbose: i32,
|
||||
|
||||
// Fitted state
|
||||
is_fitted: bool,
|
||||
best_estimator_: Option<PyObject>,
|
||||
best_score_: Option<f64>,
|
||||
best_params_: Option<HashMap<String, PyObject>>,
|
||||
cv_results_: Option<HashMap<String, Vec<PyObject>>>,
|
||||
n_splits_: Option<usize>,
|
||||
}
|
||||
|
||||
#[pymethods]
|
||||
impl GridSearchCV {
|
||||
#[new]
|
||||
#[pyo3(signature = (
|
||||
estimator,
|
||||
param_grid,
|
||||
scoring=None,
|
||||
n_jobs=None,
|
||||
cv=5,
|
||||
refit=true,
|
||||
verbose=0
|
||||
))]
|
||||
fn new(
|
||||
estimator: PyObject,
|
||||
param_grid: &PyDict,
|
||||
scoring: Option<&str>,
|
||||
n_jobs: Option<i32>,
|
||||
cv: Option<usize>,
|
||||
refit: bool,
|
||||
verbose: i32,
|
||||
) -> PyResult<Self> {
|
||||
// Convert param_grid from PyDict to HashMap
|
||||
let mut param_grid_map = HashMap::new();
|
||||
for (key, value) in param_grid.iter() {
|
||||
let key_str: String = key.extract()?;
|
||||
let value_list: Vec<PyObject> = if let Ok(list) = value.extract::<Vec<PyObject>>() {
|
||||
list
|
||||
} else {
|
||||
// Single value - wrap in list
|
||||
vec![value.into()]
|
||||
};
|
||||
param_grid_map.insert(key_str, value_list);
|
||||
}
|
||||
|
||||
if let Some(cv_val) = cv {
|
||||
if cv_val < 2 {
|
||||
return Err(PyErr::new::<pyo3::exceptions::PyValueError, _>(
|
||||
"cv must be >= 2"
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(GridSearchCV {
|
||||
estimator,
|
||||
param_grid: param_grid_map,
|
||||
scoring: scoring.map(|s| s.to_string()),
|
||||
n_jobs,
|
||||
cv,
|
||||
refit,
|
||||
verbose,
|
||||
is_fitted: false,
|
||||
best_estimator_: None,
|
||||
best_score_: None,
|
||||
best_params_: None,
|
||||
cv_results_: None,
|
||||
n_splits_: None,
|
||||
})
|
||||
}
|
||||
|
||||
/// Fit GridSearchCV to find best parameters
|
||||
fn fit(
|
||||
&mut self,
|
||||
x: PyReadonlyArrayDyn<f64>,
|
||||
y: PyReadonlyArrayDyn<f64>,
|
||||
) -> PyResult<()> {
|
||||
ArrayConverter::validate_fit_input(&x, Some(&y)).map_err(|e| PyErr::from(e))?;
|
||||
|
||||
let x_view = ArrayConverter::to_array_view2(&x).map_err(|e| PyErr::from(e))?;
|
||||
let y_view = ArrayConverter::to_array_view1(&y).map_err(|e| PyErr::from(e))?;
|
||||
|
||||
let cv_folds = self.cv.unwrap_or(5);
|
||||
self.n_splits_ = Some(cv_folds);
|
||||
|
||||
// Generate parameter combinations
|
||||
let param_combinations = self.generate_param_combinations()?;
|
||||
|
||||
if self.verbose > 0 {
|
||||
println!("Fitting {} candidates with {} folds",
|
||||
param_combinations.len(), cv_folds);
|
||||
}
|
||||
|
||||
let mut best_score = f64::NEG_INFINITY;
|
||||
let mut best_params: Option<HashMap<String, PyObject>> = None;
|
||||
let mut best_estimator: Option<PyObject> = None;
|
||||
|
||||
// Results storage
|
||||
let mut cv_results = HashMap::new();
|
||||
cv_results.insert("mean_test_score".to_string(), Vec::new());
|
||||
cv_results.insert("std_test_score".to_string(), Vec::new());
|
||||
cv_results.insert("params".to_string(), Vec::new());
|
||||
|
||||
Python::with_gil(|py| {
|
||||
// Perform grid search
|
||||
for (i, params) in param_combinations.iter().enumerate() {
|
||||
if self.verbose > 1 {
|
||||
println!("Fitting parameters {} of {}: {:?}", i + 1, param_combinations.len(), params);
|
||||
}
|
||||
|
||||
// Clone estimator and set parameters
|
||||
let mut estimator_clone = self.estimator.clone_ref(py);
|
||||
|
||||
// Set parameters on estimator
|
||||
if let Ok(set_params_method) = estimator_clone.getattr(py, "set_params") {
|
||||
let params_dict = PyDict::new(py);
|
||||
for (key, value) in params {
|
||||
params_dict.set_item(key, value)?;
|
||||
}
|
||||
set_params_method.call1(py, (params_dict,))?;
|
||||
} else {
|
||||
return Err(PyErr::new::<pyo3::exceptions::PyAttributeError, _>(
|
||||
"Estimator must have set_params method"
|
||||
));
|
||||
}
|
||||
|
||||
// Perform cross-validation
|
||||
let cv_scores = self.cross_validate(py, &estimator_clone, &x, &y, cv_folds)?;
|
||||
|
||||
let mean_score = cv_scores.iter().sum::<f64>() / cv_scores.len() as f64;
|
||||
let std_score = {
|
||||
let variance: f64 = cv_scores.iter()
|
||||
.map(|score| (score - mean_score).powi(2))
|
||||
.sum::<f64>() / cv_scores.len() as f64;
|
||||
variance.sqrt()
|
||||
};
|
||||
|
||||
// Store results
|
||||
cv_results.get_mut("mean_test_score").unwrap().push(mean_score.to_object(py));
|
||||
cv_results.get_mut("std_test_score").unwrap().push(std_score.to_object(py));
|
||||
cv_results.get_mut("params").unwrap().push({
|
||||
let params_dict = PyDict::new(py);
|
||||
for (key, value) in params {
|
||||
params_dict.set_item(key, value)?;
|
||||
}
|
||||
params_dict.to_object(py)
|
||||
});
|
||||
|
||||
// Update best score
|
||||
if mean_score > best_score {
|
||||
best_score = mean_score;
|
||||
best_params = Some(params.clone());
|
||||
|
||||
if self.refit {
|
||||
best_estimator = Some(estimator_clone);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
})?;
|
||||
|
||||
// Store results
|
||||
self.best_score_ = Some(best_score);
|
||||
self.best_params_ = best_params;
|
||||
self.cv_results_ = Some(cv_results);
|
||||
|
||||
if self.refit {
|
||||
if let Some(best_est) = best_estimator {
|
||||
Python::with_gil(|py| {
|
||||
// Refit on full dataset
|
||||
let fit_method = best_est.getattr(py, "fit")?;
|
||||
fit_method.call1(py, (&x, &y))?;
|
||||
Ok::<(), PyErr>(())
|
||||
})?;
|
||||
self.best_estimator_ = Some(best_est);
|
||||
}
|
||||
}
|
||||
|
||||
self.is_fitted = true;
|
||||
|
||||
if self.verbose > 0 {
|
||||
println!("Best score: {:.4}", best_score);
|
||||
if let Some(ref params) = self.best_params_ {
|
||||
println!("Best params: {:?}", params);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Predict using best estimator
|
||||
fn predict(&self, x: PyReadonlyArrayDyn<f64>) -> PyResult<PyObject> {
|
||||
check_is_fitted(self.is_fitted).map_err(|e| PyErr::from(e))?;
|
||||
|
||||
if !self.refit {
|
||||
return Err(PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(
|
||||
"Cannot predict when refit=False"
|
||||
));
|
||||
}
|
||||
|
||||
match &self.best_estimator_ {
|
||||
Some(estimator) => {
|
||||
Python::with_gil(|py| {
|
||||
let predict_method = estimator.getattr(py, "predict")?;
|
||||
predict_method.call1(py, (x,))
|
||||
})
|
||||
}
|
||||
None => Err(PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(
|
||||
"No best estimator found"
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Score using best estimator
|
||||
fn score(&self, x: PyReadonlyArrayDyn<f64>, y: PyReadonlyArrayDyn<f64>) -> PyResult<f64> {
|
||||
check_is_fitted(self.is_fitted).map_err(|e| PyErr::from(e))?;
|
||||
|
||||
if !self.refit {
|
||||
return Err(PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(
|
||||
"Cannot score when refit=False"
|
||||
));
|
||||
}
|
||||
|
||||
match &self.best_estimator_ {
|
||||
Some(estimator) => {
|
||||
Python::with_gil(|py| {
|
||||
let score_method = estimator.getattr(py, "score")?;
|
||||
let result = score_method.call1(py, (x, y))?;
|
||||
result.extract(py)
|
||||
})
|
||||
}
|
||||
None => Err(PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(
|
||||
"No best estimator found"
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Helper to generate parameter combinations
|
||||
fn generate_param_combinations(&self) -> PyResult<Vec<HashMap<String, PyObject>>> {
|
||||
let mut combinations = Vec::new();
|
||||
let param_names: Vec<_> = self.param_grid.keys().collect();
|
||||
let param_values: Vec<_> = self.param_grid.values().collect();
|
||||
|
||||
// Generate all combinations (Cartesian product)
|
||||
self.generate_combinations_recursive(
|
||||
¶m_names,
|
||||
¶m_values,
|
||||
&mut HashMap::new(),
|
||||
0,
|
||||
&mut combinations,
|
||||
);
|
||||
|
||||
Ok(combinations)
|
||||
}
|
||||
|
||||
/// Recursive helper for generating combinations
|
||||
fn generate_combinations_recursive(
|
||||
&self,
|
||||
param_names: &[&String],
|
||||
param_values: &[&Vec<PyObject>],
|
||||
current_combination: &mut HashMap<String, PyObject>,
|
||||
depth: usize,
|
||||
combinations: &mut Vec<HashMap<String, PyObject>>,
|
||||
) {
|
||||
if depth == param_names.len() {
|
||||
combinations.push(current_combination.clone());
|
||||
return;
|
||||
}
|
||||
|
||||
let param_name = param_names[depth];
|
||||
let values = param_values[depth];
|
||||
|
||||
for value in values {
|
||||
current_combination.insert(param_name.clone(), value.clone());
|
||||
self.generate_combinations_recursive(
|
||||
param_names,
|
||||
param_values,
|
||||
current_combination,
|
||||
depth + 1,
|
||||
combinations,
|
||||
);
|
||||
}
|
||||
|
||||
current_combination.remove(param_name);
|
||||
}
|
||||
|
||||
/// Helper to perform cross-validation
|
||||
fn cross_validate(
|
||||
&self,
|
||||
py: Python,
|
||||
estimator: &PyObject,
|
||||
x: &PyReadonlyArrayDyn<f64>,
|
||||
y: &PyReadonlyArrayDyn<f64>,
|
||||
cv_folds: usize,
|
||||
) -> PyResult<Vec<f64>> {
|
||||
let x_view = ArrayConverter::to_array_view2(x).map_err(|e| PyErr::from(e))?;
|
||||
let y_view = ArrayConverter::to_array_view1(y).map_err(|e| PyErr::from(e))?;
|
||||
|
||||
// Create KFold cross-validator
|
||||
let mut cv = KFold::new(cv_folds, Some(42), true);
|
||||
let splits = cv.split(x_view.shape()[0], None)
|
||||
.map_err(|e| PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(
|
||||
format!("CV split failed: {}", e)
|
||||
))?;
|
||||
|
||||
let mut scores = Vec::new();
|
||||
|
||||
for (train_idx, test_idx) in splits {
|
||||
// Create train/test splits
|
||||
let x_train = x_view.select(Axis(0), &train_idx);
|
||||
let y_train = y_view.select(Axis(0), &train_idx);
|
||||
let x_test = x_view.select(Axis(0), &test_idx);
|
||||
let y_test = y_view.select(Axis(0), &test_idx);
|
||||
|
||||
// Convert back to PyArrays
|
||||
let x_train_py = ArrayConverter::from_array2(py, x_train)?;
|
||||
let y_train_py = ArrayConverter::from_array1(py, y_train)?;
|
||||
let x_test_py = ArrayConverter::from_array2(py, x_test)?;
|
||||
let y_test_py = ArrayConverter::from_array1(py, y_test)?;
|
||||
|
||||
// Fit estimator on training set
|
||||
let fit_method = estimator.getattr(py, "fit")?;
|
||||
fit_method.call1(py, (x_train_py, y_train_py))?;
|
||||
|
||||
// Score on test set
|
||||
let score_method = estimator.getattr(py, "score")?;
|
||||
let score: f64 = score_method.call1(py, (x_test_py, y_test_py))?.extract(py)?;
|
||||
scores.push(score);
|
||||
}
|
||||
|
||||
Ok(scores)
|
||||
}
|
||||
|
||||
// sklearn-compatible properties
|
||||
#[getter]
|
||||
fn best_estimator_(&self) -> Option<PyObject> {
|
||||
self.best_estimator_.clone()
|
||||
}
|
||||
|
||||
#[getter]
|
||||
fn best_score_(&self) -> Option<f64> {
|
||||
self.best_score_
|
||||
}
|
||||
|
||||
#[getter]
|
||||
fn best_params_(&self) -> Option<HashMap<String, PyObject>> {
|
||||
self.best_params_.clone()
|
||||
}
|
||||
|
||||
#[getter]
|
||||
fn cv_results_(&self) -> Option<HashMap<String, Vec<PyObject>>> {
|
||||
self.cv_results_.clone()
|
||||
}
|
||||
}
|
||||
|
||||
/// sklearn-compatible cross_val_score function
|
||||
#[pyfunction]
|
||||
#[pyo3(signature = (estimator, x, y, cv=5, scoring=None, n_jobs=None))]
|
||||
pub fn cross_val_score(
|
||||
py: Python,
|
||||
estimator: PyObject,
|
||||
x: PyReadonlyArrayDyn<f64>,
|
||||
y: PyReadonlyArrayDyn<f64>,
|
||||
cv: Option<usize>,
|
||||
scoring: Option<&str>,
|
||||
n_jobs: Option<i32>,
|
||||
) -> PyResult<Py<PyArray1<f64>>> {
|
||||
ArrayConverter::validate_fit_input(&x, Some(&y)).map_err(|e| PyErr::from(e))?;
|
||||
|
||||
let x_view = ArrayConverter::to_array_view2(&x).map_err(|e| PyErr::from(e))?;
|
||||
let y_view = ArrayConverter::to_array_view1(&y).map_err(|e| PyErr::from(e))?;
|
||||
|
||||
let cv_folds = cv.unwrap_or(5);
|
||||
if cv_folds < 2 {
|
||||
return Err(PyErr::new::<pyo3::exceptions::PyValueError, _>(
|
||||
"cv must be >= 2"
|
||||
));
|
||||
}
|
||||
|
||||
// Create KFold cross-validator
|
||||
let mut cv_splitter = KFold::new(cv_folds, Some(42), true);
|
||||
let splits = cv_splitter.split(x_view.shape()[0], None)
|
||||
.map_err(|e| PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(
|
||||
format!("CV split failed: {}", e)
|
||||
))?;
|
||||
|
||||
let mut scores = Vec::new();
|
||||
|
||||
for (train_idx, test_idx) in splits {
|
||||
// Create train/test splits
|
||||
let x_train = x_view.select(Axis(0), &train_idx);
|
||||
let y_train = y_view.select(Axis(0), &train_idx);
|
||||
let x_test = x_view.select(Axis(0), &test_idx);
|
||||
let y_test = y_view.select(Axis(0), &test_idx);
|
||||
|
||||
// Convert back to PyArrays
|
||||
let x_train_py = ArrayConverter::from_array2(py, x_train)?;
|
||||
let y_train_py = ArrayConverter::from_array1(py, y_train)?;
|
||||
let x_test_py = ArrayConverter::from_array2(py, x_test)?;
|
||||
let y_test_py = ArrayConverter::from_array1(py, y_test)?;
|
||||
|
||||
// Clone estimator for this fold
|
||||
let estimator_clone = estimator.clone_ref(py);
|
||||
|
||||
// Fit estimator on training set
|
||||
let fit_method = estimator_clone.getattr(py, "fit")?;
|
||||
fit_method.call1(py, (x_train_py, y_train_py))?;
|
||||
|
||||
// Score on test set
|
||||
let score: f64 = match scoring {
|
||||
Some("accuracy") => {
|
||||
let predict_method = estimator_clone.getattr(py, "predict")?;
|
||||
let predictions: Py<PyArray1<f64>> = predict_method.call1(py, (x_test_py,))?.extract(py)?;
|
||||
|
||||
// Calculate accuracy
|
||||
let pred_array = predictions.as_ref(py).readonly();
|
||||
let pred_view = ArrayConverter::to_array_view1(&pred_array).map_err(|e| PyErr::from(e))?;
|
||||
let correct = pred_view.iter().zip(y_test.iter()).filter(|&(p, t)| (p - t).abs() < 1e-10).count();
|
||||
correct as f64 / pred_view.len() as f64
|
||||
}
|
||||
Some("r2") | None => {
|
||||
// Use estimator's score method (default)
|
||||
let score_method = estimator_clone.getattr(py, "score")?;
|
||||
score_method.call1(py, (x_test_py, y_test_py))?.extract(py)?
|
||||
}
|
||||
_ => {
|
||||
return Err(PyErr::new::<pyo3::exceptions::PyValueError, _>(
|
||||
format!("Unsupported scoring: {:?}", scoring)
|
||||
));
|
||||
}
|
||||
};
|
||||
|
||||
scores.push(score);
|
||||
}
|
||||
|
||||
let scores_array = Array1::from_vec(scores);
|
||||
Ok(ArrayConverter::from_array1(py, scores_array)?.to_owned())
|
||||
}
|
||||
|
||||
/// sklearn-compatible train_test_split function
|
||||
#[pyfunction]
|
||||
#[pyo3(signature = (x, y, test_size=0.25, train_size=None, random_state=None, shuffle=true, stratify=None))]
|
||||
pub fn train_test_split(
|
||||
py: Python,
|
||||
x: PyReadonlyArrayDyn<f64>,
|
||||
y: PyReadonlyArrayDyn<f64>,
|
||||
test_size: Option<f64>,
|
||||
train_size: Option<f64>,
|
||||
random_state: Option<u64>,
|
||||
shuffle: bool,
|
||||
stratify: Option<PyReadonlyArrayDyn<f64>>,
|
||||
) -> PyResult<PyObject> {
|
||||
ArrayConverter::validate_fit_input(&x, Some(&y)).map_err(|e| PyErr::from(e))?;
|
||||
|
||||
let x_view = ArrayConverter::to_array_view2(&x).map_err(|e| PyErr::from(e))?;
|
||||
let y_view = ArrayConverter::to_array_view1(&y).map_err(|e| PyErr::from(e))?;
|
||||
|
||||
let n_samples = x_view.shape()[0];
|
||||
|
||||
// Determine test size
|
||||
let test_size_val = test_size.unwrap_or(0.25);
|
||||
if test_size_val <= 0.0 || test_size_val >= 1.0 {
|
||||
return Err(PyErr::new::<pyo3::exceptions::PyValueError, _>(
|
||||
"test_size must be between 0 and 1"
|
||||
));
|
||||
}
|
||||
|
||||
let test_n = (n_samples as f64 * test_size_val).round() as usize;
|
||||
let train_n = n_samples - test_n;
|
||||
|
||||
if train_n == 0 || test_n == 0 {
|
||||
return Err(PyErr::new::<pyo3::exceptions::PyValueError, _>(
|
||||
"test_size results in empty train or test set"
|
||||
));
|
||||
}
|
||||
|
||||
// Generate indices
|
||||
let mut indices: Vec<usize> = (0..n_samples).collect();
|
||||
|
||||
if shuffle {
|
||||
use rand::{SeedableRng, seq::SliceRandom};
|
||||
let mut rng = if let Some(seed) = random_state {
|
||||
rand::rngs::StdRng::seed_from_u64(seed)
|
||||
} else {
|
||||
rand::rngs::StdRng::from_entropy()
|
||||
};
|
||||
indices.shuffle(&mut rng);
|
||||
}
|
||||
|
||||
// Split indices
|
||||
let train_indices = &indices[..train_n];
|
||||
let test_indices = &indices[train_n..];
|
||||
|
||||
// Create train/test arrays
|
||||
let x_train = x_view.select(Axis(0), train_indices);
|
||||
let x_test = x_view.select(Axis(0), test_indices);
|
||||
let y_train = y_view.select(Axis(0), train_indices);
|
||||
let y_test = y_view.select(Axis(0), test_indices);
|
||||
|
||||
// Convert to Python arrays
|
||||
let x_train_py = ArrayConverter::from_array2(py, x_train)?;
|
||||
let x_test_py = ArrayConverter::from_array2(py, x_test)?;
|
||||
let y_train_py = ArrayConverter::from_array1(py, y_train)?;
|
||||
let y_test_py = ArrayConverter::from_array1(py, y_test)?;
|
||||
|
||||
// Return tuple
|
||||
let result = PyTuple::new(py, &[
|
||||
x_train_py.to_object(py),
|
||||
x_test_py.to_object(py),
|
||||
y_train_py.to_object(py),
|
||||
y_test_py.to_object(py),
|
||||
]);
|
||||
|
||||
Ok(result.to_object(py))
|
||||
}
|
||||
|
||||
/// Helper function to validate estimator compatibility
|
||||
fn validate_estimator(py: Python, estimator: &PyObject) -> PyResult<()> {
|
||||
// Check that estimator has required methods
|
||||
let required_methods = ["fit", "predict", "score"];
|
||||
|
||||
for method in &required_methods {
|
||||
if !estimator.hasattr(py, method)? {
|
||||
return Err(PyErr::new::<pyo3::exceptions::PyAttributeError, _>(
|
||||
format!("Estimator must have '{}' method", method)
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use numpy::PyArray2;
|
||||
use ndarray::arr2;
|
||||
|
||||
#[test]
|
||||
fn test_parameter_combinations() {
|
||||
let mut param_grid = HashMap::new();
|
||||
|
||||
Python::with_gil(|py| {
|
||||
param_grid.insert("param1".to_string(), vec![1.to_object(py), 2.to_object(py)]);
|
||||
param_grid.insert("param2".to_string(), vec!["a".to_object(py), "b".to_object(py)]);
|
||||
|
||||
let grid_search = GridSearchCV {
|
||||
estimator: py.None(),
|
||||
param_grid,
|
||||
scoring: None,
|
||||
n_jobs: None,
|
||||
cv: Some(5),
|
||||
refit: true,
|
||||
verbose: 0,
|
||||
is_fitted: false,
|
||||
best_estimator_: None,
|
||||
best_score_: None,
|
||||
best_params_: None,
|
||||
cv_results_: None,
|
||||
n_splits_: None,
|
||||
};
|
||||
|
||||
let combinations = grid_search.generate_param_combinations().unwrap();
|
||||
assert_eq!(combinations.len(), 4); // 2 * 2 = 4 combinations
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user