use crate::SklResult;
use scirs2_core::ndarray::{Array1, Array2, ArrayView1, ArrayView2, Axis};
use sklears_core::prelude::{Float, SklearsError};
#[derive(Debug, Clone)]
pub struct StreamingFeatureExtractor {
chunk_size: usize,
buffer_size: usize,
include_mean: bool,
include_std: bool,
include_min_max: bool,
include_quantiles: bool,
include_moments: bool,
running_mean: Option<Array1<Float>>,
running_variance: Option<Array1<Float>>,
running_count: usize,
running_min: Option<Array1<Float>>,
running_max: Option<Array1<Float>>,
}
#[allow(non_snake_case)] impl StreamingFeatureExtractor {
pub fn new() -> Self {
Self {
chunk_size: 1000,
buffer_size: 100,
include_mean: true,
include_std: true,
include_min_max: true,
include_quantiles: false,
include_moments: false,
running_mean: None,
running_variance: None,
running_count: 0,
running_min: None,
running_max: None,
}
}
pub fn chunk_size(mut self, chunk_size: usize) -> Self {
self.chunk_size = chunk_size;
self
}
pub fn buffer_size(mut self, buffer_size: usize) -> Self {
self.buffer_size = buffer_size;
self
}
pub fn include_mean(mut self, include: bool) -> Self {
self.include_mean = include;
self
}
pub fn include_std(mut self, include: bool) -> Self {
self.include_std = include;
self
}
pub fn include_min_max(mut self, include: bool) -> Self {
self.include_min_max = include;
self
}
pub fn include_quantiles(mut self, include: bool) -> Self {
self.include_quantiles = include;
self
}
pub fn include_moments(mut self, include: bool) -> Self {
self.include_moments = include;
self
}
pub fn extract_chunk(&mut self, data: &ArrayView2<Float>) -> SklResult<Array1<Float>> {
if data.nrows() == 0 {
return Err(SklearsError::InvalidInput("Empty data chunk".to_string()));
}
let (_n_samples, n_features) = data.dim();
self.update_running_stats(data)?;
let mut features = Vec::new();
if self.include_mean {
if let Some(ref mean) = self.running_mean {
features.extend(mean.iter().cloned());
}
}
if self.include_std {
if let Some(ref variance) = self.running_variance {
let std_dev: Vec<Float> = variance.iter().map(|&v| v.sqrt()).collect();
features.extend(std_dev);
}
}
if self.include_min_max {
if let Some(ref min_vals) = self.running_min {
features.extend(min_vals.iter().cloned());
}
if let Some(ref max_vals) = self.running_max {
features.extend(max_vals.iter().cloned());
}
}
if self.include_quantiles {
for feature_idx in 0..n_features {
let column = data.column(feature_idx);
let quantiles = self.compute_approximate_quantiles(&column)?;
features.extend(quantiles);
}
}
if self.include_moments {
for feature_idx in 0..n_features {
let column = data.column(feature_idx);
let moments = self.compute_moments(&column)?;
features.extend(moments);
}
}
Ok(Array1::from_vec(features))
}
pub fn finalize(&self) -> SklResult<Array1<Float>> {
if self.running_count == 0 {
return Err(SklearsError::InvalidInput("No data processed".to_string()));
}
let mut features = Vec::new();
if self.include_mean {
if let Some(ref mean) = self.running_mean {
features.extend(mean.iter().cloned());
}
}
if self.include_std {
if let Some(ref variance) = self.running_variance {
let std_dev: Vec<Float> = variance.iter().map(|&v| v.sqrt()).collect();
features.extend(std_dev);
}
}
if self.include_min_max {
if let Some(ref min_vals) = self.running_min {
features.extend(min_vals.iter().cloned());
}
if let Some(ref max_vals) = self.running_max {
features.extend(max_vals.iter().cloned());
}
}
Ok(Array1::from_vec(features))
}
pub fn reset(&mut self) {
self.running_mean = None;
self.running_variance = None;
self.running_count = 0;
self.running_min = None;
self.running_max = None;
}
fn update_running_stats(&mut self, data: &ArrayView2<Float>) -> SklResult<()> {
let (_n_samples, n_features) = data.dim();
if self.running_count == 0 {
self.running_mean = Some(Array1::zeros(n_features));
self.running_variance = Some(Array1::zeros(n_features));
self.running_min = Some(Array1::from_elem(n_features, Float::INFINITY));
self.running_max = Some(Array1::from_elem(n_features, Float::NEG_INFINITY));
}
for sample in data.axis_iter(Axis(0)) {
self.running_count += 1;
let count = self.running_count as Float;
for (feature_idx, &value) in sample.iter().enumerate() {
if let (Some(ref mut mean), Some(ref mut variance)) =
(&mut self.running_mean, &mut self.running_variance)
{
let delta = value - mean[feature_idx];
mean[feature_idx] += delta / count;
let delta2 = value - mean[feature_idx];
variance[feature_idx] += delta * delta2;
}
if let Some(ref mut min_vals) = self.running_min {
if value < min_vals[feature_idx] {
min_vals[feature_idx] = value;
}
}
if let Some(ref mut max_vals) = self.running_max {
if value > max_vals[feature_idx] {
max_vals[feature_idx] = value;
}
}
}
}
if let Some(ref mut variance) = self.running_variance {
let count = self.running_count as Float;
if count > 1.0 {
variance.mapv_inplace(|v| v / (count - 1.0));
}
}
Ok(())
}
fn compute_approximate_quantiles(&self, data: &ArrayView1<Float>) -> SklResult<Vec<Float>> {
let mut sorted_data: Vec<Float> = data.iter().cloned().collect();
sorted_data.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
let n = sorted_data.len();
let quantiles = vec![0.25, 0.5, 0.75]; let mut result = Vec::new();
for &q in &quantiles {
let index = (q * (n - 1) as Float).round() as usize;
let index = index.min(n - 1);
result.push(sorted_data[index]);
}
Ok(result)
}
fn compute_moments(&self, data: &ArrayView1<Float>) -> SklResult<Vec<Float>> {
let n = data.len() as Float;
if n == 0.0 {
return Ok(vec![0.0, 0.0, 0.0]); }
let mean = data.sum() / n;
let variance = data.mapv(|x| (x - mean).powi(2)).sum() / n;
let std_dev = variance.sqrt();
let (skewness, kurtosis) = if std_dev < 1e-10 {
(0.0, 0.0)
} else {
let skewness = data.mapv(|x| ((x - mean) / std_dev).powi(3)).sum() / n;
let kurtosis = data.mapv(|x| ((x - mean) / std_dev).powi(4)).sum() / n - 3.0;
(skewness, kurtosis)
};
Ok(vec![mean, skewness, kurtosis])
}
}
impl Default for StreamingFeatureExtractor {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone)]
pub struct RandomProjectionFeatures {
n_components: Option<usize>,
random_state: Option<u64>,
density: Option<Float>,
eps: Float,
projection_matrix: Option<Array2<Float>>,
n_features: Option<usize>,
}
#[allow(non_snake_case)] impl RandomProjectionFeatures {
pub fn new() -> Self {
Self {
n_components: None,
random_state: None,
density: None,
eps: 0.1,
projection_matrix: None,
n_features: None,
}
}
pub fn n_components(mut self, n_components: usize) -> Self {
self.n_components = Some(n_components);
self
}
pub fn random_state(mut self, random_state: u64) -> Self {
self.random_state = Some(random_state);
self
}
pub fn density(mut self, density: Float) -> Self {
self.density = Some(density);
self
}
pub fn eps(mut self, eps: Float) -> Self {
self.eps = eps;
self
}
pub fn fit(&mut self, X: &ArrayView2<Float>) -> SklResult<()> {
let (n_samples, n_features) = X.dim();
self.n_features = Some(n_features);
let n_components = if let Some(n_comp) = self.n_components {
n_comp
} else {
self.johnson_lindenstrauss_min_dim(n_samples, self.eps)?
};
self.projection_matrix = Some(self.create_projection_matrix(n_features, n_components)?);
Ok(())
}
pub fn transform(&self, X: &ArrayView2<Float>) -> SklResult<Array2<Float>> {
if let Some(ref proj_matrix) = self.projection_matrix {
let (_n_samples, n_features) = X.dim();
if Some(n_features) != self.n_features {
return Err(SklearsError::InvalidInput(
"Number of features does not match fitted data".to_string(),
));
}
let result = X.dot(proj_matrix);
Ok(result)
} else {
Err(SklearsError::InvalidInput(
"Random projection not fitted yet".to_string(),
))
}
}
pub fn fit_transform(&mut self, X: &ArrayView2<Float>) -> SklResult<Array2<Float>> {
self.fit(X)?;
self.transform(X)
}
fn create_projection_matrix(
&self,
n_features: usize,
n_components: usize,
) -> SklResult<Array2<Float>> {
use scirs2_core::random::thread_rng;
let mut rng = thread_rng();
let mut matrix = Array2::zeros((n_features, n_components));
if let Some(density) = self.density {
let threshold = density;
for i in 0..n_features {
for j in 0..n_components {
let random_val: Float = rng.random();
if random_val < threshold {
matrix[[i, j]] = if rng.random_bool(0.5) { 1.0 } else { -1.0 };
matrix[[i, j]] /= (density * n_components as Float).sqrt();
}
}
}
} else {
for i in 0..n_features {
for j in 0..n_components {
matrix[[i, j]] = rng.random_range(-1.0..1.0);
}
}
let norm_factor = 1.0 / (n_components as Float).sqrt();
matrix.mapv_inplace(|x| x * norm_factor);
}
Ok(matrix)
}
fn johnson_lindenstrauss_min_dim(&self, n_samples: usize, eps: Float) -> SklResult<usize> {
if eps <= 0.0 || eps >= 1.0 {
return Err(SklearsError::InvalidInput(
"eps must be between 0 and 1".to_string(),
));
}
if n_samples <= 1 {
return Ok(1);
}
let ln_n = (n_samples as Float).ln();
let denominator = eps * eps / 2.0;
if denominator <= 0.0 {
return Err(SklearsError::InvalidInput(
"Invalid eps value for Johnson-Lindenstrauss bound".to_string(),
));
}
let min_dim = (4.0 * ln_n / denominator).ceil() as usize;
Ok(min_dim.max(1))
}
}
impl Default for RandomProjectionFeatures {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone)]
pub struct LocalitySensitiveHashing {
n_hash_functions: usize,
hash_table_size: usize,
random_state: Option<u64>,
distance_metric: String,
hash_matrices: Option<Vec<Array2<Float>>>,
n_features: Option<usize>,
}
#[allow(non_snake_case)] impl LocalitySensitiveHashing {
pub fn new() -> Self {
Self {
n_hash_functions: 10,
hash_table_size: 1000,
random_state: None,
distance_metric: "cosine".to_string(),
hash_matrices: None,
n_features: None,
}
}
pub fn n_hash_functions(mut self, n_hash_functions: usize) -> Self {
self.n_hash_functions = n_hash_functions;
self
}
pub fn hash_table_size(mut self, hash_table_size: usize) -> Self {
self.hash_table_size = hash_table_size;
self
}
pub fn random_state(mut self, random_state: u64) -> Self {
self.random_state = Some(random_state);
self
}
pub fn distance_metric(mut self, metric: &str) -> Self {
self.distance_metric = metric.to_string();
self
}
pub fn fit(&mut self, X: &ArrayView2<Float>) -> SklResult<()> {
let (_, n_features) = X.dim();
self.n_features = Some(n_features);
self.hash_matrices = Some(self.create_hash_matrices(n_features)?);
Ok(())
}
pub fn transform(&self, X: &ArrayView2<Float>) -> SklResult<Array2<Float>> {
if let Some(ref hash_matrices) = self.hash_matrices {
let (n_samples, n_features) = X.dim();
if Some(n_features) != self.n_features {
return Err(SklearsError::InvalidInput(
"Number of features does not match fitted data".to_string(),
));
}
let mut hash_codes = Array2::zeros((n_samples, self.n_hash_functions));
for (hash_idx, hash_matrix) in hash_matrices.iter().enumerate() {
let hash_values = X.dot(hash_matrix);
for (sample_idx, &value) in hash_values.iter().enumerate() {
hash_codes[[sample_idx, hash_idx]] = if value >= 0.0 { 1.0 } else { 0.0 };
}
}
Ok(hash_codes)
} else {
Err(SklearsError::InvalidInput("LSH not fitted yet".to_string()))
}
}
pub fn fit_transform(&mut self, X: &ArrayView2<Float>) -> SklResult<Array2<Float>> {
self.fit(X)?;
self.transform(X)
}
pub fn find_neighbors(
&self,
X: &ArrayView2<Float>,
query: &ArrayView1<Float>,
n_neighbors: usize,
) -> SklResult<Vec<usize>> {
if let Some(ref hash_matrices) = self.hash_matrices {
let (n_samples, n_features) = X.dim();
if query.len() != n_features {
return Err(SklearsError::InvalidInput(
"Query dimension does not match fitted data".to_string(),
));
}
let mut query_hash = vec![0.0; self.n_hash_functions];
for (hash_idx, hash_matrix) in hash_matrices.iter().enumerate() {
let hash_value = query.dot(&hash_matrix.column(0));
query_hash[hash_idx] = if hash_value >= 0.0 { 1.0 } else { 0.0 };
}
let data_hashes = self.transform(X)?;
let mut candidates: Vec<(usize, usize)> = Vec::new();
for sample_idx in 0..n_samples {
let mut matching_hashes = 0;
for hash_idx in 0..self.n_hash_functions {
if (data_hashes[[sample_idx, hash_idx]] - query_hash[hash_idx]).abs() < 1e-10 {
matching_hashes += 1;
}
}
if matching_hashes > 0 {
candidates.push((sample_idx, matching_hashes));
}
}
candidates.sort_by_key(|b| std::cmp::Reverse(b.1));
let result: Vec<usize> = candidates
.into_iter()
.take(n_neighbors)
.map(|(idx, _)| idx)
.collect();
Ok(result)
} else {
Err(SklearsError::InvalidInput("LSH not fitted yet".to_string()))
}
}
fn create_hash_matrices(&self, n_features: usize) -> SklResult<Vec<Array2<Float>>> {
use scirs2_core::random::thread_rng;
let mut rng = thread_rng();
let mut hash_matrices = Vec::new();
for _ in 0..self.n_hash_functions {
let mut matrix = Array2::zeros((n_features, 1));
match self.distance_metric.as_str() {
"cosine" => {
for i in 0..n_features {
matrix[[i, 0]] = rng.random_range(-1.0..1.0);
}
}
"euclidean" => {
for i in 0..n_features {
matrix[[i, 0]] = rng.random_range(-1.0..1.0);
}
let norm = (matrix.mapv(|x| x * x).sum() as Float).sqrt();
matrix.mapv_inplace(|x| x / norm);
}
_ => {
return Err(SklearsError::InvalidInput(
"Unsupported distance metric".to_string(),
));
}
}
hash_matrices.push(matrix);
}
Ok(hash_matrices)
}
}
impl Default for LocalitySensitiveHashing {
fn default() -> Self {
Self::new()
}
}