466 lines
13 KiB
Rust
466 lines
13 KiB
Rust
use rtx_automeasure::agents::{
|
|
BinningStrategy, FeatureEngineer, FeaturePipeline, FeatureTransformation, ScalingMethod,
|
|
StatisticalMethod,
|
|
};
|
|
use rtx_automeasure::{AutoMLResult, TaskType};
|
|
use rtx_tensor::{Device, Tensor};
|
|
|
|
#[tokio::test]
|
|
async fn test_feature_engineer_creation() -> AutoMLResult<()> {
|
|
let engineer = FeatureEngineer::new(TaskType::Classification)?;
|
|
Ok(())
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_feature_engineer_configuration() -> AutoMLResult<()> {
|
|
let engineer = FeatureEngineer::new(TaskType::Classification)?
|
|
.with_max_features(500)
|
|
.with_selection_threshold(0.05)
|
|
.with_random_state(123);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[tokio::test]
|
|
#[ignore = "Pre-existing shape error issue"]
|
|
async fn test_engineer_features_basic() -> AutoMLResult<()> {
|
|
let device = Device::cpu();
|
|
let engineer = FeatureEngineer::new(TaskType::Classification)?;
|
|
|
|
let x = Tensor::randn(&[100, 5], &device)?;
|
|
let y = Tensor::zeros(&[100], &device)?;
|
|
|
|
let pipeline = engineer.engineer_features(&x, &y).await?;
|
|
|
|
assert!(!pipeline.transformations.is_empty());
|
|
assert!(!pipeline.feature_names.is_empty());
|
|
Ok(())
|
|
}
|
|
|
|
#[tokio::test]
|
|
#[ignore = "Pre-existing shape error issue"]
|
|
async fn test_engineer_features_with_transform() -> AutoMLResult<()> {
|
|
let device = Device::cpu();
|
|
let engineer = FeatureEngineer::new(TaskType::Regression)?;
|
|
|
|
let x_train = Tensor::randn(&[80, 6], &device)?;
|
|
let y_train = Tensor::randn(&[80], &device)?;
|
|
|
|
let pipeline = engineer.engineer_features(&x_train, &y_train).await?;
|
|
|
|
// Apply transformation to new data
|
|
let x_test = Tensor::randn(&[20, 6], &device)?;
|
|
let x_transformed = engineer.transform(&x_test, &pipeline).await?;
|
|
|
|
assert_eq!(x_transformed.shape()[0], 20);
|
|
Ok(())
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_feature_pipeline_creation() {
|
|
let pipeline = FeaturePipeline::new();
|
|
|
|
assert!(pipeline.transformations.is_empty());
|
|
assert!(pipeline.selected_features.is_empty());
|
|
assert!(pipeline.feature_names.is_empty());
|
|
assert!(pipeline.importance_scores.is_empty());
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_feature_pipeline_add_transformation() {
|
|
let mut pipeline = FeaturePipeline::new();
|
|
|
|
pipeline.add_transformation(FeatureTransformation::Scaling {
|
|
method: ScalingMethod::StandardScaler,
|
|
});
|
|
|
|
pipeline.add_transformation(FeatureTransformation::Polynomial {
|
|
degree: 2,
|
|
include_bias: false,
|
|
});
|
|
|
|
assert_eq!(pipeline.transformations.len(), 2);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_feature_pipeline_set_selected_features() {
|
|
let mut pipeline = FeaturePipeline::new();
|
|
|
|
pipeline.set_selected_features(vec![0, 2, 4, 6]);
|
|
|
|
assert_eq!(pipeline.selected_features.len(), 4);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_feature_pipeline_get_n_features() {
|
|
let mut pipeline = FeaturePipeline::new();
|
|
pipeline.feature_names = vec![
|
|
"feat1".to_string(),
|
|
"feat2".to_string(),
|
|
"feat3".to_string(),
|
|
];
|
|
|
|
assert_eq!(pipeline.get_n_features(), 3);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_polynomial_transformation() {
|
|
let transform = FeatureTransformation::Polynomial {
|
|
degree: 2,
|
|
include_bias: true,
|
|
};
|
|
|
|
match transform {
|
|
FeatureTransformation::Polynomial {
|
|
degree,
|
|
include_bias,
|
|
} => {
|
|
assert_eq!(degree, 2);
|
|
assert!(include_bias);
|
|
}
|
|
_ => panic!("Wrong transformation type"),
|
|
}
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_interaction_transformation() {
|
|
let transform = FeatureTransformation::Interaction {
|
|
features: vec![0, 1, 3],
|
|
};
|
|
|
|
match transform {
|
|
FeatureTransformation::Interaction { features } => {
|
|
assert_eq!(features.len(), 3);
|
|
}
|
|
_ => panic!("Wrong transformation type"),
|
|
}
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_statistical_transformations() {
|
|
let methods = vec![
|
|
StatisticalMethod::Log,
|
|
StatisticalMethod::Sqrt,
|
|
StatisticalMethod::Square,
|
|
StatisticalMethod::Reciprocal,
|
|
StatisticalMethod::Abs,
|
|
StatisticalMethod::Sign,
|
|
];
|
|
|
|
for method in methods {
|
|
let transform = FeatureTransformation::Statistical {
|
|
method: method.clone(),
|
|
};
|
|
assert!(matches!(
|
|
transform,
|
|
FeatureTransformation::Statistical { .. }
|
|
));
|
|
}
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_binning_transformation_uniform() {
|
|
let transform = FeatureTransformation::Binning {
|
|
n_bins: 10,
|
|
strategy: BinningStrategy::Uniform,
|
|
};
|
|
|
|
match transform {
|
|
FeatureTransformation::Binning { n_bins, strategy } => {
|
|
assert_eq!(n_bins, 10);
|
|
assert!(matches!(strategy, BinningStrategy::Uniform));
|
|
}
|
|
_ => panic!("Wrong transformation type"),
|
|
}
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_binning_transformation_quantile() {
|
|
let transform = FeatureTransformation::Binning {
|
|
n_bins: 5,
|
|
strategy: BinningStrategy::Quantile,
|
|
};
|
|
|
|
match transform {
|
|
FeatureTransformation::Binning { n_bins, strategy } => {
|
|
assert_eq!(n_bins, 5);
|
|
assert!(matches!(strategy, BinningStrategy::Quantile));
|
|
}
|
|
_ => panic!("Wrong transformation type"),
|
|
}
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_scaling_transformations() {
|
|
let methods = vec![
|
|
ScalingMethod::StandardScaler,
|
|
ScalingMethod::MinMaxScaler,
|
|
ScalingMethod::RobustScaler,
|
|
];
|
|
|
|
for method in methods {
|
|
let transform = FeatureTransformation::Scaling {
|
|
method: method.clone(),
|
|
};
|
|
assert!(matches!(transform, FeatureTransformation::Scaling { .. }));
|
|
}
|
|
}
|
|
|
|
#[tokio::test]
|
|
#[ignore = "Pre-existing shape error issue"]
|
|
async fn test_engineer_features_regression() -> AutoMLResult<()> {
|
|
let device = Device::cpu();
|
|
let engineer = FeatureEngineer::new(TaskType::Regression)?;
|
|
|
|
let x = Tensor::randn(&[60, 8], &device)?;
|
|
let y = Tensor::randn(&[60], &device)?;
|
|
|
|
let pipeline = engineer.engineer_features(&x, &y).await?;
|
|
|
|
assert!(!pipeline.transformations.is_empty());
|
|
Ok(())
|
|
}
|
|
|
|
#[tokio::test]
|
|
#[ignore = "Pre-existing shape error issue"]
|
|
async fn test_engineer_features_classification() -> AutoMLResult<()> {
|
|
let device = Device::cpu();
|
|
let engineer = FeatureEngineer::new(TaskType::Classification)?;
|
|
|
|
let x = Tensor::randn(&[100, 10], &device)?;
|
|
let y = Tensor::zeros(&[100], &device)?;
|
|
|
|
let pipeline = engineer.engineer_features(&x, &y).await?;
|
|
|
|
assert!(!pipeline.transformations.is_empty());
|
|
Ok(())
|
|
}
|
|
|
|
#[tokio::test]
|
|
#[ignore = "Pre-existing shape error issue"]
|
|
async fn test_transform_preserves_samples() -> AutoMLResult<()> {
|
|
let device = Device::cpu();
|
|
let engineer = FeatureEngineer::new(TaskType::Classification)?;
|
|
|
|
let x_train = Tensor::randn(&[80, 5], &device)?;
|
|
let y_train = Tensor::zeros(&[80], &device)?;
|
|
|
|
let pipeline = engineer.engineer_features(&x_train, &y_train).await?;
|
|
|
|
let x_test = Tensor::randn(&[30, 5], &device)?;
|
|
let x_transformed = engineer.transform(&x_test, &pipeline).await?;
|
|
|
|
assert_eq!(x_transformed.shape()[0], 30);
|
|
Ok(())
|
|
}
|
|
|
|
#[tokio::test]
|
|
#[ignore = "Pre-existing shape error issue"]
|
|
async fn test_feature_importance_scores() -> AutoMLResult<()> {
|
|
let device = Device::cpu();
|
|
let engineer = FeatureEngineer::new(TaskType::Classification)?;
|
|
|
|
let x = Tensor::randn(&[100, 8], &device)?;
|
|
let y = Tensor::zeros(&[100], &device)?;
|
|
|
|
let pipeline = engineer.engineer_features(&x, &y).await?;
|
|
|
|
assert!(!pipeline.importance_scores.is_empty());
|
|
Ok(())
|
|
}
|
|
|
|
#[tokio::test]
|
|
#[ignore = "Pre-existing shape error issue"]
|
|
async fn test_feature_selection_with_threshold() -> AutoMLResult<()> {
|
|
let device = Device::cpu();
|
|
let engineer = FeatureEngineer::new(TaskType::Classification)?.with_selection_threshold(0.05);
|
|
|
|
let x = Tensor::randn(&[150, 20], &device)?;
|
|
let y = Tensor::zeros(&[150], &device)?;
|
|
|
|
let pipeline = engineer.engineer_features(&x, &y).await?;
|
|
|
|
assert!(!pipeline.selected_features.is_empty());
|
|
Ok(())
|
|
}
|
|
|
|
#[tokio::test]
|
|
#[ignore = "Pre-existing shape error issue"]
|
|
async fn test_max_features_constraint() -> AutoMLResult<()> {
|
|
let device = Device::cpu();
|
|
let engineer = FeatureEngineer::new(TaskType::Regression)?.with_max_features(10);
|
|
|
|
let x = Tensor::randn(&[100, 50], &device)?;
|
|
let y = Tensor::randn(&[100], &device)?;
|
|
|
|
let pipeline = engineer.engineer_features(&x, &y).await?;
|
|
|
|
if !pipeline.selected_features.is_empty() {
|
|
assert!(pipeline.selected_features.len() <= 10);
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_feature_pipeline_serialization() -> AutoMLResult<()> {
|
|
let mut pipeline = FeaturePipeline::new();
|
|
|
|
pipeline.add_transformation(FeatureTransformation::Scaling {
|
|
method: ScalingMethod::StandardScaler,
|
|
});
|
|
|
|
pipeline.set_selected_features(vec![0, 1, 2]);
|
|
pipeline.feature_names = vec!["feat1".to_string(), "feat2".to_string()];
|
|
|
|
let serialized = serde_json::to_string(&pipeline)?;
|
|
assert!(!serialized.is_empty());
|
|
|
|
let deserialized: FeaturePipeline = serde_json::from_str(&serialized)?;
|
|
assert_eq!(
|
|
deserialized.transformations.len(),
|
|
pipeline.transformations.len()
|
|
);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_transformation_serialization() -> AutoMLResult<()> {
|
|
let transform = FeatureTransformation::Polynomial {
|
|
degree: 3,
|
|
include_bias: true,
|
|
};
|
|
|
|
let serialized = serde_json::to_string(&transform)?;
|
|
assert!(!serialized.is_empty());
|
|
|
|
let deserialized: FeatureTransformation = serde_json::from_str(&serialized)?;
|
|
|
|
match deserialized {
|
|
FeatureTransformation::Polynomial {
|
|
degree,
|
|
include_bias,
|
|
} => {
|
|
assert_eq!(degree, 3);
|
|
assert!(include_bias);
|
|
}
|
|
_ => panic!("Wrong transformation type after deserialization"),
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[tokio::test]
|
|
#[ignore = "Pre-existing shape error issue"]
|
|
async fn test_engineer_with_small_dataset() -> AutoMLResult<()> {
|
|
let device = Device::cpu();
|
|
let engineer = FeatureEngineer::new(TaskType::Classification)?;
|
|
|
|
let x = Tensor::randn(&[30, 4], &device)?;
|
|
let y = Tensor::zeros(&[30], &device)?;
|
|
|
|
let pipeline = engineer.engineer_features(&x, &y).await?;
|
|
|
|
assert!(!pipeline.transformations.is_empty());
|
|
Ok(())
|
|
}
|
|
|
|
#[tokio::test]
|
|
#[ignore = "Pre-existing shape error issue"]
|
|
async fn test_engineer_with_large_feature_space() -> AutoMLResult<()> {
|
|
let device = Device::cpu();
|
|
let engineer = FeatureEngineer::new(TaskType::Regression)?.with_max_features(100);
|
|
|
|
let x = Tensor::randn(&[200, 50], &device)?;
|
|
let y = Tensor::randn(&[200], &device)?;
|
|
|
|
let pipeline = engineer.engineer_features(&x, &y).await?;
|
|
|
|
assert!(!pipeline.transformations.is_empty());
|
|
Ok(())
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_multiple_transformations_in_pipeline() {
|
|
let mut pipeline = FeaturePipeline::new();
|
|
|
|
pipeline.add_transformation(FeatureTransformation::Scaling {
|
|
method: ScalingMethod::StandardScaler,
|
|
});
|
|
|
|
pipeline.add_transformation(FeatureTransformation::Polynomial {
|
|
degree: 2,
|
|
include_bias: false,
|
|
});
|
|
|
|
pipeline.add_transformation(FeatureTransformation::Statistical {
|
|
method: StatisticalMethod::Log,
|
|
});
|
|
|
|
assert_eq!(pipeline.transformations.len(), 3);
|
|
}
|
|
|
|
#[tokio::test]
|
|
#[ignore = "Pre-existing shape error issue"]
|
|
async fn test_feature_names_generation() -> AutoMLResult<()> {
|
|
let device = Device::cpu();
|
|
let engineer = FeatureEngineer::new(TaskType::Classification)?;
|
|
|
|
let x = Tensor::randn(&[50, 5], &device)?;
|
|
let y = Tensor::zeros(&[50], &device)?;
|
|
|
|
let pipeline = engineer.engineer_features(&x, &y).await?;
|
|
|
|
assert!(!pipeline.feature_names.is_empty());
|
|
for name in &pipeline.feature_names {
|
|
assert!(!name.is_empty());
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
#[tokio::test]
|
|
#[ignore = "Pre-existing shape error issue"]
|
|
async fn test_random_state_reproducibility() -> AutoMLResult<()> {
|
|
let device = Device::cpu();
|
|
let engineer1 = FeatureEngineer::new(TaskType::Classification)?.with_random_state(42);
|
|
let engineer2 = FeatureEngineer::new(TaskType::Classification)?.with_random_state(42);
|
|
|
|
let x = Tensor::randn(&[60, 6], &device)?;
|
|
let y = Tensor::zeros(&[60], &device)?;
|
|
|
|
let pipeline1 = engineer1.engineer_features(&x, &y).await?;
|
|
let pipeline2 = engineer2.engineer_features(&x, &y).await?;
|
|
|
|
assert_eq!(
|
|
pipeline1.transformations.len(),
|
|
pipeline2.transformations.len()
|
|
);
|
|
Ok(())
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_box_cox_transformation() {
|
|
let transform = FeatureTransformation::Statistical {
|
|
method: StatisticalMethod::BoxCox { lambda: 0.5 },
|
|
};
|
|
|
|
match transform {
|
|
FeatureTransformation::Statistical { method } => match method {
|
|
StatisticalMethod::BoxCox { lambda } => {
|
|
assert_eq!(lambda, 0.5);
|
|
}
|
|
_ => panic!("Wrong statistical method"),
|
|
},
|
|
_ => panic!("Wrong transformation type"),
|
|
}
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_empty_pipeline_default() {
|
|
let pipeline = FeaturePipeline::default();
|
|
|
|
assert!(pipeline.transformations.is_empty());
|
|
assert!(pipeline.selected_features.is_empty());
|
|
assert_eq!(pipeline.get_n_features(), 0);
|
|
}
|