Skip to main content

sklears_multioutput/
utils.rs

1//! Utility functions and shared types for multi-output learning
2//!
3//! This module contains common functionality used across multiple algorithms in the
4//! multi-output learning suite, including mathematical operations, binary classifier
5//! training, feature processing, and label reconstruction methods.
6
7// Use SciRS2-Core for arrays and random number generation (SciRS2 Policy)
8use scirs2_core::ndarray::{s, Array1, Array2, ArrayView1, ArrayView2, Axis};
9use sklears_core::{
10    error::{Result as SklResult, SklearsError},
11    types::Float,
12};
13
14/// Methods for label reconstruction in compressed sensing approaches
15#[derive(Debug, Clone, Copy, PartialEq)]
16pub enum ReconstructionMethod {
17    /// Linear reconstruction using least squares
18    Linear,
19    /// Iterative soft thresholding
20    IterativeThresholding,
21    /// Orthogonal matching pursuit
22    OrthogonalMatchingPursuit,
23}
24
25/// Strategies for pruning label combinations in label powerset methods
26#[derive(Debug, Clone, Copy, PartialEq)]
27pub enum PruningStrategy {
28    /// Remove rare combinations and map to default (e.g., all zeros)
29    Default,
30    /// Map rare combinations to most similar frequent combination
31    Similarity,
32}
33
34/// Classification criteria for decision trees
35#[derive(Debug, Clone, Copy, PartialEq)]
36pub enum ClassificationCriterion {
37    /// Gini impurity
38    Gini,
39    /// Information gain (entropy)
40    Entropy,
41}
42
43/// Threshold strategies for binary relevance methods
44#[derive(Debug, Clone, Copy, PartialEq)]
45pub enum ThresholdStrategy {
46    /// Fixed threshold for all labels
47    Fixed,
48    /// Per-label optimal thresholds
49    PerLabel,
50    /// Optimal thresholds based on validation data
51    Optimal,
52    /// F-score based thresholds
53    FScore,
54}
55
56/// Calibration methods for probability calibration
57#[derive(Debug, Clone, Copy, PartialEq)]
58pub enum CalibrationMethod {
59    /// Sigmoid calibration (Platt scaling)
60    Sigmoid,
61    /// Isotonic regression calibration
62    Isotonic,
63}
64
65/// Simple linear classifier for binary classification
66#[derive(Debug, Clone)]
67pub struct SimpleLinearClassifier {
68    /// Weight vector
69    pub weights: Array1<Float>,
70    /// Bias term
71    pub bias: Float,
72}
73
74/// Simple binary classification model
75#[derive(Debug, Clone)]
76pub struct SimpleBinaryModel {
77    /// Feature weights
78    pub weights: Array1<Float>,
79    /// Bias term
80    pub bias: Float,
81    /// Training accuracy
82    pub accuracy: Float,
83}
84
85/// Bayesian binary classification model
86#[derive(Debug, Clone)]
87pub struct BayesianBinaryModel {
88    /// Posterior mean of weights
89    pub weight_mean: Array1<Float>,
90    /// Posterior covariance of weights
91    pub weight_cov: Array2<Float>,
92    /// Bias parameter
93    pub bias_mean: Float,
94    /// Bias variance
95    pub bias_var: Float,
96    /// Noise precision
97    pub noise_precision: Float,
98}
99
100/// Cost matrix for cost-sensitive learning
101#[derive(Debug, Clone)]
102pub struct CostMatrix {
103    /// False positive costs for each label
104    pub fp_costs: Vec<Float>,
105    /// False negative costs for each label
106    pub fn_costs: Vec<Float>,
107}
108
109impl CostMatrix {
110    /// Create cost matrix from false positive and false negative costs
111    pub fn from_fp_fn_costs(fp_costs: Vec<Float>, fn_costs: Vec<Float>) -> SklResult<Self> {
112        if fp_costs.len() != fn_costs.len() {
113            return Err(SklearsError::InvalidInput(
114                "False positive and false negative cost vectors must have the same length"
115                    .to_string(),
116            ));
117        }
118
119        if fp_costs.is_empty() {
120            return Err(SklearsError::InvalidInput(
121                "Cost vectors cannot be empty".to_string(),
122            ));
123        }
124
125        // Check for non-negative costs
126        for &cost in fp_costs.iter().chain(fn_costs.iter()) {
127            if cost < 0.0 {
128                return Err(SklearsError::InvalidInput(
129                    "All costs must be non-negative".to_string(),
130                ));
131            }
132        }
133
134        Ok(Self { fp_costs, fn_costs })
135    }
136
137    /// Create balanced cost matrix (all costs = 1.0)
138    pub fn balanced(n_labels: usize) -> Self {
139        Self {
140            fp_costs: vec![1.0; n_labels],
141            fn_costs: vec![1.0; n_labels],
142        }
143    }
144
145    /// Get optimal threshold for a given label based on cost ratio
146    pub fn get_threshold(&self, label_idx: usize) -> Float {
147        if label_idx >= self.fp_costs.len() {
148            return 0.5; // Default threshold
149        }
150
151        let fp_cost = self.fp_costs[label_idx];
152        let fn_cost = self.fn_costs[label_idx];
153
154        // Optimal threshold = FP_cost / (FP_cost + FN_cost)
155        if fp_cost + fn_cost > 0.0 {
156            fp_cost / (fp_cost + fn_cost)
157        } else {
158            0.5
159        }
160    }
161}
162
163/// Calculate Euclidean distance between two points
164pub fn euclidean_distance(x1: &ArrayView1<Float>, x2: &ArrayView1<Float>) -> Float {
165    x1.iter()
166        .zip(x2.iter())
167        .map(|(a, b)| (a - b).powi(2))
168        .sum::<Float>()
169        .sqrt()
170}
171
172/// Standardize features using provided means and standard deviations
173#[allow(non_snake_case)] // standard ML notation
174pub fn standardize_features_simple(
175    X: &ArrayView2<Float>,
176    means: &Array1<Float>,
177    stds: &Array1<Float>,
178) -> Array2<Float> {
179    let mut X_standardized = X.to_owned();
180
181    for (mut col, (&mean, &std)) in X_standardized
182        .axis_iter_mut(Axis(1))
183        .zip(means.iter().zip(stds.iter()))
184    {
185        col.mapv_inplace(|x| (x - mean) / std);
186    }
187
188    X_standardized
189}
190
191/// Train a simple binary classifier using correlation-based approach
192#[allow(non_snake_case)] // standard ML notation
193pub fn train_binary_classifier(
194    X: &ArrayView2<Float>,
195    y: &Array1<i32>,
196) -> SklResult<SimpleBinaryModel> {
197    let (n_samples, n_features) = X.dim();
198
199    if n_samples != y.len() {
200        return Err(SklearsError::InvalidInput(
201            "X and y must have the same number of samples".to_string(),
202        ));
203    }
204
205    // Convert labels to Float for computation
206    let y_float: Array1<Float> = y.mapv(|x| x as Float);
207
208    // Calculate means
209    let x_means = X
210        .mean_axis(Axis(0))
211        .expect("array should have elements for mean computation");
212    let y_mean = y_float
213        .mean()
214        .expect("array should have elements for mean computation");
215
216    // Calculate weights using correlation
217    let mut weights = Array1::<Float>::zeros(n_features);
218
219    for (i, weight) in weights.iter_mut().enumerate() {
220        let x_col = X.column(i);
221        let x_mean = x_means[i];
222
223        let numerator: Float = x_col
224            .iter()
225            .zip(y_float.iter())
226            .map(|(&x, &y)| (x - x_mean) * (y - y_mean))
227            .sum();
228
229        let denominator: Float = x_col
230            .iter()
231            .map(|&x| (x - x_mean).powi(2))
232            .sum::<Float>()
233            .sqrt()
234            * y_float
235                .iter()
236                .map(|&y| (y - y_mean).powi(2))
237                .sum::<Float>()
238                .sqrt();
239
240        *weight = if denominator > 1e-10 {
241            numerator / denominator
242        } else {
243            0.0
244        };
245    }
246
247    // Calculate bias
248    let bias = y_mean - weights.dot(&x_means);
249
250    // Calculate training accuracy
251    let mut correct = 0;
252    for i in 0..n_samples {
253        let prediction = if weights.dot(&X.row(i)) + bias > 0.0 {
254            1
255        } else {
256            0
257        };
258        if prediction == y[i] {
259            correct += 1;
260        }
261    }
262    let accuracy = correct as Float / n_samples as Float;
263
264    Ok(SimpleBinaryModel {
265        weights,
266        bias,
267        accuracy,
268    })
269}
270
271/// Train a simple linear classifier using least squares
272#[allow(non_snake_case)] // standard ML notation
273pub fn train_simple_linear_classifier(
274    X: &ArrayView2<Float>,
275    y: &Array1<Float>,
276) -> SklResult<SimpleLinearClassifier> {
277    let (n_samples, n_features) = X.dim();
278
279    if n_samples != y.len() {
280        return Err(SklearsError::InvalidInput(
281            "X and y must have the same number of samples".to_string(),
282        ));
283    }
284
285    // Add bias column to X
286    let mut X_with_bias = Array2::ones((n_samples, n_features + 1));
287    X_with_bias.slice_mut(s![.., ..n_features]).assign(X);
288
289    // Solve normal equations: (X^T X) w = X^T y
290    let xtx = X_with_bias.t().dot(&X_with_bias);
291    let xty = X_with_bias.t().dot(y);
292
293    let weights_with_bias = solve_linear_system(&xtx, &xty)?;
294
295    let weights = weights_with_bias.slice(s![..n_features]).to_owned();
296    let bias = weights_with_bias[n_features];
297
298    Ok(SimpleLinearClassifier { weights, bias })
299}
300
301/// Predict using a simple linear classifier
302#[allow(non_snake_case)] // standard ML notation
303pub fn predict_simple_linear(
304    X: &ArrayView2<Float>,
305    classifier: &SimpleLinearClassifier,
306) -> Array1<Float> {
307    X.dot(&classifier.weights) + classifier.bias
308}
309
310/// Solve linear system Ax = b using Gaussian elimination
311#[allow(non_snake_case)] // standard ML notation
312pub fn solve_linear_system(A: &Array2<Float>, b: &Array1<Float>) -> SklResult<Array1<Float>> {
313    let n = A.nrows();
314    if A.ncols() != n || b.len() != n {
315        return Err(SklearsError::InvalidInput(
316            "Matrix must be square and vector must match matrix size".to_string(),
317        ));
318    }
319
320    let mut aug = Array2::<Float>::zeros((n, n + 1));
321    aug.slice_mut(s![.., ..n]).assign(A);
322    aug.slice_mut(s![.., n]).assign(b);
323
324    // Forward elimination
325    for i in 0..n {
326        // Find pivot
327        let mut max_row = i;
328        for k in (i + 1)..n {
329            if aug[[k, i]].abs() > aug[[max_row, i]].abs() {
330                max_row = k;
331            }
332        }
333
334        // Swap rows
335        if max_row != i {
336            for j in 0..=n {
337                let temp = aug[[i, j]];
338                aug[[i, j]] = aug[[max_row, j]];
339                aug[[max_row, j]] = temp;
340            }
341        }
342
343        // Check for singular matrix
344        if aug[[i, i]].abs() < 1e-10 {
345            // Add small regularization
346            aug[[i, i]] += 1e-8;
347        }
348
349        // Eliminate
350        for k in (i + 1)..n {
351            let factor = aug[[k, i]] / aug[[i, i]];
352            for j in i..=n {
353                aug[[k, j]] -= factor * aug[[i, j]];
354            }
355        }
356    }
357
358    // Back substitution
359    let mut x = Array1::<Float>::zeros(n);
360    for i in (0..n).rev() {
361        x[i] = aug[[i, n]];
362        for j in (i + 1)..n {
363            x[i] -= aug[[i, j]] * x[j];
364        }
365        x[i] /= aug[[i, i]];
366    }
367
368    Ok(x)
369}
370
371/// Generate random projection matrix for compressed sensing
372pub fn generate_random_projection_matrix(
373    n_compressed: usize,
374    n_labels: usize,
375    random_state: Option<u64>,
376) -> Array2<Float> {
377    // Use deterministic random generation based on seed
378    let mut rng_state = random_state.unwrap_or(42);
379
380    let mut matrix = Array2::<Float>::zeros((n_compressed, n_labels));
381
382    for i in 0..n_compressed {
383        for j in 0..n_labels {
384            // Simple LCG for reproducible random numbers
385            rng_state = rng_state.wrapping_mul(1664525).wrapping_add(1013904223);
386            let random_val = (rng_state as Float) / (u64::MAX as Float);
387            matrix[[i, j]] = (random_val - 0.5) * 2.0; // Range [-1, 1]
388        }
389    }
390
391    // Normalize rows
392    for mut row in matrix.rows_mut() {
393        let norm = row.iter().map(|x| x * x).sum::<Float>().sqrt();
394        if norm > 1e-10 {
395            row /= norm;
396        }
397    }
398
399    matrix
400}
401
402/// Reconstruct labels using specified method
403pub fn reconstruct_labels(
404    compressed_labels: &Array1<Float>,
405    projection_matrix: &Array2<Float>,
406    method: ReconstructionMethod,
407) -> SklResult<Array1<Float>> {
408    match method {
409        ReconstructionMethod::Linear => {
410            // Pseudo-inverse reconstruction
411            let pinv = compute_pseudoinverse(projection_matrix)?;
412            Ok(pinv.dot(compressed_labels))
413        }
414        ReconstructionMethod::IterativeThresholding => {
415            iterative_thresholding_reconstruction(compressed_labels, projection_matrix)
416        }
417        ReconstructionMethod::OrthogonalMatchingPursuit => {
418            omp_reconstruction(compressed_labels, projection_matrix)
419        }
420    }
421}
422
423/// Compute pseudo-inverse of a matrix
424fn compute_pseudoinverse(matrix: &Array2<Float>) -> SklResult<Array2<Float>> {
425    let (m, n) = matrix.dim();
426
427    if m >= n {
428        // More rows than columns: (A^T A)^-1 A^T
429        let ata = matrix.t().dot(matrix);
430        let ata_inv = matrix_inverse(&ata)?;
431        Ok(ata_inv.dot(&matrix.t()))
432    } else {
433        // More columns than rows: A^T (A A^T)^-1
434        let aat = matrix.dot(&matrix.t());
435        let aat_inv = matrix_inverse(&aat)?;
436        Ok(matrix.t().dot(&aat_inv))
437    }
438}
439
440/// Compute matrix inverse using Gaussian elimination
441fn matrix_inverse(matrix: &Array2<Float>) -> SklResult<Array2<Float>> {
442    let n = matrix.nrows();
443    if matrix.ncols() != n {
444        return Err(SklearsError::InvalidInput(
445            "Matrix must be square".to_string(),
446        ));
447    }
448
449    let mut aug = Array2::<Float>::zeros((n, 2 * n));
450    aug.slice_mut(s![.., ..n]).assign(matrix);
451
452    // Set up identity on the right side
453    for i in 0..n {
454        aug[[i, n + i]] = 1.0;
455    }
456
457    // Gaussian elimination
458    for i in 0..n {
459        // Find pivot
460        let mut max_row = i;
461        for k in (i + 1)..n {
462            if aug[[k, i]].abs() > aug[[max_row, i]].abs() {
463                max_row = k;
464            }
465        }
466
467        // Swap rows
468        if max_row != i {
469            for j in 0..(2 * n) {
470                let temp = aug[[i, j]];
471                aug[[i, j]] = aug[[max_row, j]];
472                aug[[max_row, j]] = temp;
473            }
474        }
475
476        // Check for singular matrix
477        if aug[[i, i]].abs() < 1e-10 {
478            return Err(SklearsError::InvalidInput("Matrix is singular".to_string()));
479        }
480
481        // Scale pivot row
482        let pivot = aug[[i, i]];
483        for j in 0..(2 * n) {
484            aug[[i, j]] /= pivot;
485        }
486
487        // Eliminate column
488        for k in 0..n {
489            if k != i {
490                let factor = aug[[k, i]];
491                for j in 0..(2 * n) {
492                    aug[[k, j]] -= factor * aug[[i, j]];
493                }
494            }
495        }
496    }
497
498    Ok(aug.slice(s![.., n..]).to_owned())
499}
500
501/// Iterative thresholding reconstruction
502pub fn iterative_thresholding_reconstruction(
503    compressed_labels: &Array1<Float>,
504    projection_matrix: &Array2<Float>,
505) -> SklResult<Array1<Float>> {
506    let n_labels = projection_matrix.ncols();
507    let mut x = Array1::<Float>::zeros(n_labels);
508    let step_size = 0.1;
509    let threshold = 0.1;
510    let max_iterations = 100;
511
512    for _ in 0..max_iterations {
513        // Gradient step
514        let residual = projection_matrix.dot(&x) - compressed_labels;
515        let gradient = projection_matrix.t().dot(&residual);
516        x = &x - step_size * &gradient;
517
518        // Soft thresholding
519        x.mapv_inplace(|xi| {
520            if xi > threshold {
521                xi - threshold
522            } else if xi < -threshold {
523                xi + threshold
524            } else {
525                0.0
526            }
527        });
528    }
529
530    Ok(x)
531}
532
533/// Orthogonal Matching Pursuit reconstruction
534pub fn omp_reconstruction(
535    compressed_labels: &Array1<Float>,
536    projection_matrix: &Array2<Float>,
537) -> SklResult<Array1<Float>> {
538    let n_labels = projection_matrix.ncols();
539    let mut selected_indices = Vec::new();
540    let mut residual = compressed_labels.clone();
541    let max_iterations = std::cmp::min(10, n_labels); // Sparsity constraint
542
543    for _ in 0..max_iterations {
544        // Find column with maximum correlation to residual
545        let mut max_corr = 0.0;
546        let mut best_idx = 0;
547
548        for j in 0..n_labels {
549            if !selected_indices.contains(&j) {
550                let column = projection_matrix.column(j);
551                let corr = column.dot(&residual).abs();
552                if corr > max_corr {
553                    max_corr = corr;
554                    best_idx = j;
555                }
556            }
557        }
558
559        if max_corr < 1e-6 {
560            break;
561        }
562
563        selected_indices.push(best_idx);
564
565        // Solve least squares on selected columns
566        if let Ok(coeffs) =
567            solve_least_squares_subset(compressed_labels, projection_matrix, &selected_indices)
568        {
569            // Update residual
570            let mut reconstruction = Array1::<Float>::zeros(projection_matrix.nrows());
571            for (i, &idx) in selected_indices.iter().enumerate() {
572                let column = projection_matrix.column(idx);
573                reconstruction = reconstruction + coeffs[i] * &column;
574            }
575            residual = compressed_labels - &reconstruction;
576        }
577    }
578
579    // Construct final solution
580    let mut x = Array1::<Float>::zeros(n_labels);
581    if let Ok(coeffs) =
582        solve_least_squares_subset(compressed_labels, projection_matrix, &selected_indices)
583    {
584        for (i, &idx) in selected_indices.iter().enumerate() {
585            x[idx] = coeffs[i];
586        }
587    }
588
589    Ok(x)
590}
591
592/// Solve least squares problem for a subset of columns
593#[allow(non_snake_case)] // standard ML notation
594pub fn solve_least_squares_subset(
595    y: &Array1<Float>,
596    A: &Array2<Float>,
597    indices: &[usize],
598) -> SklResult<Array1<Float>> {
599    if indices.is_empty() {
600        return Err(SklearsError::InvalidInput(
601            "No indices provided".to_string(),
602        ));
603    }
604
605    let n_rows = A.nrows();
606    let n_selected = indices.len();
607    let mut A_subset = Array2::<Float>::zeros((n_rows, n_selected));
608
609    for (j, &idx) in indices.iter().enumerate() {
610        A_subset.column_mut(j).assign(&A.column(idx));
611    }
612
613    // Solve normal equations
614    let ata = A_subset.t().dot(&A_subset);
615    let aty = A_subset.t().dot(y);
616
617    solve_linear_system(&ata, &aty)
618}
619
620/// Train a weighted binary classifier
621#[allow(non_snake_case)] // standard ML notation
622pub fn train_weighted_binary_classifier_simple(
623    X: &ArrayView2<Float>,
624    y: &Array1<i32>,
625    sample_weights: &Array1<Float>,
626) -> SklResult<SimpleBinaryModel> {
627    let (n_samples, n_features) = X.dim();
628
629    if n_samples != y.len() || n_samples != sample_weights.len() {
630        return Err(SklearsError::InvalidInput(
631            "X, y, and sample_weights must have the same number of samples".to_string(),
632        ));
633    }
634
635    // Weighted means
636    let total_weight = sample_weights.sum();
637    let mut x_means = Array1::<Float>::zeros(n_features);
638    let mut y_mean = 0.0;
639
640    for i in 0..n_samples {
641        let weight = sample_weights[i];
642        y_mean += weight * y[i] as Float;
643        for j in 0..n_features {
644            x_means[j] += weight * X[[i, j]];
645        }
646    }
647
648    x_means /= total_weight;
649    y_mean /= total_weight;
650
651    // Weighted covariance computation
652    let mut weights = Array1::<Float>::zeros(n_features);
653
654    for j in 0..n_features {
655        let mut numerator = 0.0;
656        let mut x_var = 0.0;
657        let mut y_var = 0.0;
658
659        for i in 0..n_samples {
660            let weight = sample_weights[i];
661            let x_diff = X[[i, j]] - x_means[j];
662            let y_diff = y[i] as Float - y_mean;
663
664            numerator += weight * x_diff * y_diff;
665            x_var += weight * x_diff * x_diff;
666            y_var += weight * y_diff * y_diff;
667        }
668
669        let denominator = (x_var * y_var).sqrt();
670        weights[j] = if denominator > 1e-10 {
671            numerator / denominator
672        } else {
673            0.0
674        };
675    }
676
677    let bias = y_mean - weights.dot(&x_means);
678
679    // Calculate weighted accuracy
680    let mut correct_weight = 0.0;
681    for i in 0..n_samples {
682        let prediction = if weights.dot(&X.row(i)) + bias > 0.0 {
683            1
684        } else {
685            0
686        };
687        if prediction == y[i] {
688            correct_weight += sample_weights[i];
689        }
690    }
691    let accuracy = correct_weight / total_weight;
692
693    Ok(SimpleBinaryModel {
694        weights,
695        bias,
696        accuracy,
697    })
698}
699
700/// Predict binary probabilities using sigmoid function
701#[allow(non_snake_case)] // standard ML notation
702pub fn predict_binary_probabilities(
703    X: &ArrayView2<Float>,
704    model: &SimpleBinaryModel,
705) -> Array1<Float> {
706    let raw_scores = X.dot(&model.weights) + model.bias;
707    raw_scores.mapv(|x| 1.0 / (1.0 + (-x).exp()))
708}
709
710/// Compute cost-sensitive sample weights
711pub fn compute_cost_sensitive_weights(y: &Array2<i32>, cost_matrix: &CostMatrix) -> Array1<Float> {
712    let n_samples = y.nrows();
713    let mut weights = Array1::ones(n_samples);
714
715    for i in 0..n_samples {
716        let mut sample_weight = 1.0;
717
718        for (j, &label) in y.row(i).iter().enumerate() {
719            if j < cost_matrix.fp_costs.len() {
720                // Weight based on label imbalance and costs
721                if label == 1 {
722                    sample_weight *= cost_matrix.fn_costs[j];
723                } else {
724                    sample_weight *= cost_matrix.fp_costs[j];
725                }
726            }
727        }
728
729        weights[i] = sample_weight;
730    }
731
732    // Normalize weights to sum to n_samples
733    let total_weight = weights.sum();
734    if total_weight > 0.0 {
735        weights *= n_samples as Float / total_weight;
736    }
737
738    weights
739}
740
741/// Generate random normal distribution using Box-Muller transform
742pub fn random_normal() -> Float {
743    use std::cell::RefCell;
744
745    thread_local! {
746        static SAVED: RefCell<Option<Float>> = const { RefCell::new(None) };
747    }
748
749    SAVED.with(|saved| {
750        if let Some(value) = saved.borrow_mut().take() {
751            return value;
752        }
753
754        // Generate two uniform random numbers
755        let u1 = (rand_u32() as Float) / (u32::MAX as Float);
756        let u2 = (rand_u32() as Float) / (u32::MAX as Float);
757
758        // Box-Muller transform
759        let mag = (-2.0 * u1.ln()).sqrt();
760        let z0 = mag * (2.0 * std::f64::consts::PI * u2).cos();
761        let z1 = mag * (2.0 * std::f64::consts::PI * u2).sin();
762
763        *saved.borrow_mut() = Some(z1);
764        z0
765    })
766}
767
768/// Simple random number generator for reproducible results
769fn rand_u32() -> u32 {
770    use std::cell::RefCell;
771
772    thread_local! {
773        static STATE: RefCell<u32> = const { RefCell::new(42) };
774    }
775
776    STATE.with(|state| {
777        let mut s = state.borrow_mut();
778        *s = s.wrapping_mul(1664525).wrapping_add(1013904223);
779        *s
780    })
781}
782
783/// Train a Bayesian binary classifier
784#[allow(non_snake_case)] // standard ML notation
785pub fn train_bayesian_binary_classifier(
786    X: &Array2<Float>,
787    y: &Array1<i32>,
788    alpha: Float,
789) -> SklResult<BayesianBinaryModel> {
790    let (n_samples, n_features) = X.dim();
791
792    if n_samples != y.len() {
793        return Err(SklearsError::InvalidInput(
794            "X and y must have the same number of samples".to_string(),
795        ));
796    }
797
798    // Convert labels to Float and to {-1, 1}
799    let y_float: Array1<Float> = y.mapv(|x| if x == 1 { 1.0 } else { -1.0 });
800
801    // Prior precision matrix (alpha * I)
802    let prior_precision = Array2::<Float>::eye(n_features) * alpha;
803
804    // Likelihood precision (simplified)
805    let noise_precision = 1.0;
806
807    // Posterior precision = prior_precision + X^T X * noise_precision
808    let xtx = X.t().dot(X);
809    let posterior_precision = &prior_precision + &xtx * noise_precision;
810
811    // Posterior covariance = inv(posterior_precision)
812    let weight_cov = matrix_inverse(&posterior_precision)?;
813
814    // Posterior mean = posterior_cov * X^T * y * noise_precision
815    let xty = X.t().dot(&y_float);
816    let weight_mean = weight_cov.dot(&(&xty * noise_precision));
817
818    // Bias parameters (simplified)
819    let bias_mean = 0.0;
820    let bias_var = 1.0 / alpha;
821
822    Ok(BayesianBinaryModel {
823        weight_mean,
824        weight_cov,
825        bias_mean,
826        bias_var,
827        noise_precision,
828    })
829}
830
831/// Predict with Bayesian binary classifier (mean prediction)
832#[allow(non_snake_case)] // standard ML notation
833pub fn predict_bayesian_binary(
834    X: &ArrayView2<Float>,
835    model: &BayesianBinaryModel,
836) -> Array1<Float> {
837    let raw_scores = X.dot(&model.weight_mean) + model.bias_mean;
838    raw_scores.mapv(|x| 1.0 / (1.0 + (-x).exp()))
839}
840
841/// Predict with uncertainty quantification
842#[allow(non_snake_case)] // standard ML notation
843pub fn predict_bayesian_uncertainty(
844    X: &ArrayView2<Float>,
845    model: &BayesianBinaryModel,
846) -> SklResult<(Array1<Float>, Array1<Float>)> {
847    let n_samples = X.nrows();
848    let mut means = Array1::<Float>::zeros(n_samples);
849    let mut variances = Array1::<Float>::zeros(n_samples);
850
851    for i in 0..n_samples {
852        let x = X.row(i);
853
854        // Mean prediction
855        let mean_score = x.dot(&model.weight_mean) + model.bias_mean;
856        means[i] = 1.0 / (1.0 + (-mean_score).exp());
857
858        // Variance calculation
859        let score_var =
860            x.dot(&model.weight_cov.dot(&x)) + model.bias_var + 1.0 / model.noise_precision;
861        variances[i] = score_var;
862    }
863
864    Ok((means, variances))
865}
866
867/// Predict mean of Bayesian model
868#[allow(non_snake_case)] // standard ML notation
869pub fn predict_bayesian_mean(X: &ArrayView2<Float>, model: &BayesianBinaryModel) -> Array1<Float> {
870    X.dot(&model.weight_mean) + model.bias_mean
871}