use scirs2_core::ndarray::{Array1, Array2, Axis};
use scirs2_core::random::{Random, rng};
use scirs2_core::random::distributions::{Normal, StandardNormal};
use sklears_core::error::{Result, SklearsError};
pub fn make_missing_completely_at_random(
data: &Array2<f64>,
missing_rate: f64,
random_state: Option<u64>,
) -> Result<Array2<f64>> {
if missing_rate < 0.0 || missing_rate > 1.0 {
return Err(SklearsError::InvalidInput(
"missing_rate must be between 0.0 and 1.0".to_string(),
));
}
let mut rng = Random::from_seed(random_state.unwrap_or_else(|| rng().gen()));
let mut result = data.clone();
let (n_rows, n_cols) = data.dim();
for i in 0..n_rows {
for j in 0..n_cols {
if rng.gen() < missing_rate {
result[[i, j]] = f64::NAN;
}
}
}
Ok(result)
}
pub fn make_missing_at_random(
data: &Array2<f64>,
missing_rate: f64,
predictor_column: usize,
threshold_percentile: f64,
random_state: Option<u64>,
) -> Result<Array2<f64>> {
if missing_rate < 0.0 || missing_rate > 1.0 {
return Err(SklearsError::InvalidInput(
"missing_rate must be between 0.0 and 1.0".to_string(),
));
}
if threshold_percentile < 0.0 || threshold_percentile > 100.0 {
return Err(SklearsError::InvalidInput(
"threshold_percentile must be between 0.0 and 100.0".to_string(),
));
}
let (n_rows, n_cols) = data.dim();
if predictor_column >= n_cols {
return Err(SklearsError::InvalidInput(
"predictor_column must be less than the number of columns".to_string(),
));
}
let mut rng = Random::from_seed(random_state.unwrap_or_else(|| rng().gen()));
let mut result = data.clone();
let mut predictor_values: Vec<f64> = data.column(predictor_column).to_vec();
predictor_values.sort_by(|a, b| a.partial_cmp(b).expect("operation should succeed"));
let threshold_index = (threshold_percentile / 100.0 * n_rows as f64) as usize;
let threshold = predictor_values[threshold_index.min(n_rows - 1)];
for i in 0..n_rows {
let predictor_value = data[[i, predictor_column]];
let missing_prob = if predictor_value > threshold {
missing_rate * 2.0
} else {
missing_rate * 0.5
};
for j in 0..n_cols {
if j != predictor_column && rng.gen() < missing_prob {
result[[i, j]] = f64::NAN;
}
}
}
Ok(result)
}
pub fn make_missing_not_at_random(
data: &Array2<f64>,
missing_rate: f64,
target_column: usize,
threshold_percentile: f64,
random_state: Option<u64>,
) -> Result<Array2<f64>> {
if missing_rate < 0.0 || missing_rate > 1.0 {
return Err(SklearsError::InvalidInput(
"missing_rate must be between 0.0 and 1.0".to_string(),
));
}
if threshold_percentile < 0.0 || threshold_percentile > 100.0 {
return Err(SklearsError::InvalidInput(
"threshold_percentile must be between 0.0 and 100.0".to_string(),
));
}
let (n_rows, n_cols) = data.dim();
if target_column >= n_cols {
return Err(SklearsError::InvalidInput(
"target_column must be less than the number of columns".to_string(),
));
}
let mut rng = Random::from_seed(random_state.unwrap_or_else(|| rng().gen()));
let mut result = data.clone();
let mut target_values: Vec<f64> = data.column(target_column).to_vec();
target_values.sort_by(|a, b| a.partial_cmp(b).expect("operation should succeed"));
let threshold_index = (threshold_percentile / 100.0 * n_rows as f64) as usize;
let threshold = target_values[threshold_index.min(n_rows - 1)];
for i in 0..n_rows {
let target_value = data[[i, target_column]];
let missing_prob = if target_value > threshold {
missing_rate * 3.0
} else {
missing_rate * 0.2
};
if rng.gen() < missing_prob {
result[[i, target_column]] = f64::NAN;
}
}
Ok(result)
}
pub fn make_outliers(
data: &Array2<f64>,
outlier_fraction: f64,
outlier_magnitude: f64,
random_state: Option<u64>,
) -> Result<Array2<f64>> {
if outlier_fraction < 0.0 || outlier_fraction > 1.0 {
return Err(SklearsError::InvalidInput(
"outlier_fraction must be between 0.0 and 1.0".to_string(),
));
}
if outlier_magnitude <= 0.0 {
return Err(SklearsError::InvalidInput(
"outlier_magnitude must be positive".to_string(),
));
}
let mut rng = Random::from_seed(random_state.unwrap_or_else(|| rng().gen()));
let mut result = data.clone();
let (n_rows, n_cols) = data.dim();
let n_outliers = (outlier_fraction * n_rows as f64) as usize;
let means = data.mean_axis(Axis(0)).expect("array should have elements for mean computation");
let stds = data.std_axis(Axis(0), 0.0);
let mut outlier_indices: Vec<usize> = (0..n_rows).collect();
for i in (1..outlier_indices.len()).rev() {
let j = rng.gen_range(0..i + 1);
outlier_indices.swap(i, j);
}
outlier_indices.truncate(n_outliers);
for &sample_idx in &outlier_indices {
for feature_idx in 0..n_cols {
let mean = means[feature_idx];
let std = stds[feature_idx];
let sign = if rng.gen() < 0.5 { -1.0 } else { 1.0 };
let deviation = sign * outlier_magnitude * std;
result[[sample_idx, feature_idx]] = mean + deviation;
}
}
Ok(result)
}
pub fn make_imbalanced_classification(
n_samples: usize,
n_features: usize,
n_classes: usize,
class_weights: &Array1<f64>,
cluster_std: f64,
random_state: Option<u64>,
) -> Result<(Array2<f64>, Array1<i32>)> {
if n_samples == 0 || n_features == 0 || n_classes == 0 {
return Err(SklearsError::InvalidInput(
"n_samples, n_features, and n_classes must be positive".to_string(),
));
}
if class_weights.len() != n_classes {
return Err(SklearsError::InvalidInput(
"class_weights must have same length as n_classes".to_string(),
));
}
let weight_sum = class_weights.sum();
if (weight_sum - 1.0).abs() > 1e-10 {
return Err(SklearsError::InvalidInput(
"class_weights must sum to 1.0".to_string(),
));
}
let mut rng = Random::from_seed(random_state.unwrap_or_else(|| rng().gen()));
let mut centers = Array2::zeros((n_classes, n_features));
for i in 0..n_classes {
for j in 0..n_features {
centers[[i, j]] = rng.random_range(-5.0..5.0);
}
}
let mut class_sizes = Array1::zeros(n_classes);
let mut remaining_samples = n_samples;
for i in 0..(n_classes - 1) {
let class_size = (class_weights[i] * n_samples as f64) as usize;
class_sizes[i] = class_size as f64;
remaining_samples -= class_size;
}
class_sizes[n_classes - 1] = remaining_samples as f64;
let mut data = Array2::zeros((n_samples, n_features));
let mut labels = Array1::zeros(n_samples);
let mut sample_idx = 0;
for class_idx in 0..n_classes {
let n_class_samples = class_sizes[class_idx] as usize;
for _ in 0..n_class_samples {
labels[sample_idx] = class_idx as i32;
for feature_idx in 0..n_features {
let center = centers[[class_idx, feature_idx]];
let normal = Normal::new(center, cluster_std).expect("operation should succeed");
data[[sample_idx, feature_idx]] = rng.sample(normal);
}
sample_idx += 1;
}
}
Ok((data, labels))
}
pub fn make_anomalies(
data: &Array2<f64>,
anomaly_fraction: f64,
anomaly_type: &str,
severity: f64,
random_state: Option<u64>,
) -> Result<(Array2<f64>, Array1<i32>)> {
if anomaly_fraction < 0.0 || anomaly_fraction > 1.0 {
return Err(SklearsError::InvalidInput(
"anomaly_fraction must be between 0.0 and 1.0".to_string(),
));
}
if severity <= 0.0 {
return Err(SklearsError::InvalidInput(
"severity must be positive".to_string(),
));
}
let mut rng = Random::from_seed(random_state.unwrap_or_else(|| rng().gen()));
let mut result = data.clone();
let (n_rows, n_cols) = data.dim();
let n_anomalies = (anomaly_fraction * n_rows as f64) as usize;
let mut labels = Array1::zeros(n_rows);
let means = data.mean_axis(Axis(0)).expect("array should have elements for mean computation");
let stds = data.std_axis(Axis(0), 0.0);
let mut anomaly_indices: Vec<usize> = (0..n_rows).collect();
for i in (1..anomaly_indices.len()).rev() {
let j = rng.gen_range(0..i + 1);
anomaly_indices.swap(i, j);
}
anomaly_indices.truncate(n_anomalies);
match anomaly_type {
"point" => {
for &idx in &anomaly_indices {
labels[idx] = 1;
for j in 0..n_cols {
let sign = if rng.gen() < 0.5 { -1.0 } else { 1.0 };
result[[idx, j]] = means[j] + sign * severity * stds[j];
}
}
}
"contextual" => {
for &idx in &anomaly_indices {
labels[idx] = 1;
let n_features_to_modify = (n_cols / 2).max(1);
for j in 0..n_features_to_modify {
let sign = if rng.gen() < 0.5 { -1.0 } else { 1.0 };
result[[idx, j]] = means[j] + sign * severity * stds[j];
}
}
}
"collective" => {
let group_size = (n_anomalies / 3).max(1);
for chunk in anomaly_indices.chunks(group_size) {
let pattern_feature = rng.gen_range(0..n_cols);
let pattern_magnitude = severity * stds[pattern_feature];
for &idx in chunk {
labels[idx] = 1;
result[[idx, pattern_feature]] += pattern_magnitude;
}
}
}
_ => {
return Err(SklearsError::InvalidInput(format!(
"Unknown anomaly_type: {}. Use 'point', 'contextual', or 'collective'",
anomaly_type
)));
}
}
Ok((result, labels))
}