use scirs2_core::ndarray::{Array1, Array2};
use scirs2_core::random::prelude::*;
use scirs2_core::random::rngs::StdRng;
use scirs2_core::random::{Normal, RngExt};
use sklears_core::error::{Result, SklearsError};
type AbTestingResult = (Array2<f64>, Array1<i32>, Array1<f64>, Array1<i32>);
#[derive(Debug, Clone)]
pub struct ABTestConfig {
pub control_rate: f64,
pub treatment_effect: f64,
pub significance_level: f64,
pub power: f64,
pub minimum_detectable_effect: f64,
}
#[allow(non_snake_case)] pub fn make_ab_testing_simulation(
config: ABTestConfig,
n_samples: usize,
n_features: usize,
random_state: Option<u64>,
) -> Result<AbTestingResult> {
if n_samples == 0 || n_features == 0 {
return Err(SklearsError::InvalidInput(
"n_samples and n_features must be positive".to_string(),
));
}
if config.control_rate < 0.0 || config.control_rate > 1.0 {
return Err(SklearsError::InvalidInput(
"control_rate must be in [0, 1]".to_string(),
));
}
let mut rng = if let Some(seed) = random_state {
StdRng::seed_from_u64(seed)
} else {
StdRng::from_rng(&mut scirs2_core::random::thread_rng())
};
let mut X = Array2::zeros((n_samples, n_features));
let normal = Normal::new(0.0, 1.0).expect("operation should succeed");
for i in 0..n_samples {
for j in 0..n_features {
X[[i, j]] = rng.sample(normal);
}
}
let mut group_assignment = Array1::zeros(n_samples);
let n_treatment = (n_samples as f64 * (1.0 - config.control_rate)) as usize;
let mut indices: Vec<usize> = (0..n_samples).collect();
indices.shuffle(&mut rng);
for (i, &idx) in indices.iter().enumerate() {
group_assignment[idx] = if i < n_treatment { 1 } else { 0 };
}
let mut outcomes = Array1::zeros(n_samples);
let mut conversion = Array1::zeros(n_samples);
for i in 0..n_samples {
let user_propensity =
X.slice(scirs2_core::ndarray::s![i, ..]).iter().sum::<f64>() / n_features as f64;
let base_rate = 0.1 + 0.05 * user_propensity.tanh();
let conversion_probability = if group_assignment[i] == 1 {
base_rate + config.treatment_effect
} else {
base_rate
};
let converted = rng.random::<f64>() < conversion_probability.clamp(0.0, 1.0);
conversion[i] = if converted { 1 } else { 0 };
outcomes[i] = if converted {
let baseline_value = 10.0 + 5.0 * user_propensity;
let treatment_bonus = if group_assignment[i] == 1 {
config.treatment_effect * 20.0
} else {
0.0
};
baseline_value
+ treatment_bonus
+ rng.sample(Normal::new(0.0, 2.0).expect("sampling should succeed"))
} else {
0.0
};
}
Ok((X, group_assignment, outcomes, conversion))
}
#[allow(non_snake_case)]
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_make_ab_testing_simulation() {
let config = ABTestConfig {
control_rate: 0.5,
treatment_effect: 0.1,
significance_level: 0.05,
power: 0.8,
minimum_detectable_effect: 0.05,
};
let (X, groups, outcomes, conversions) =
make_ab_testing_simulation(config, 200, 3, Some(42)).expect("operation should succeed");
assert_eq!(X.shape(), &[200, 3]);
assert_eq!(groups.len(), 200);
assert_eq!(outcomes.len(), 200);
assert_eq!(conversions.len(), 200);
for &group in groups.iter() {
assert!(group == 0 || group == 1, "Groups should be 0 or 1");
}
for &conv in conversions.iter() {
assert!(conv == 0 || conv == 1, "Conversions should be 0 or 1");
}
let treatment_count = groups.iter().filter(|&&g| g == 1).count();
assert!(
treatment_count > 50 && treatment_count < 150,
"Groups should be roughly balanced"
);
}
#[test]
fn test_ab_testing_simulation_invalid_input() {
let config = ABTestConfig {
control_rate: 1.5, treatment_effect: 0.1,
significance_level: 0.05,
power: 0.8,
minimum_detectable_effect: 0.05,
};
assert!(make_ab_testing_simulation(config, 200, 3, Some(42)).is_err());
}
}