Files
rustytorch/crates/training/rtx-automeasure/tests/feature_engineer_tests.rs
T
2026-03-04 00:08:42 +00:00

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);
}