Skip to main content

sklears_multioutput/
chains.rs

1//! Chain-based multi-output learning algorithms
2//!
3//! This module provides chain-based approaches for multi-label and multi-output problems,
4//! including ClassifierChain, RegressorChain, EnsembleOfChains, and BayesianClassifierChain.
5#![allow(non_snake_case)] // Standard ML notation: X for feature matrices, K for kernels
6
7use crate::utils::*;
8// Use SciRS2-Core for arrays and random number generation (SciRS2 Policy)
9use scirs2_core::ndarray::{s, Array1, Array2, ArrayView2, Axis};
10use sklears_core::{
11    error::{Result as SklResult, SklearsError},
12    traits::{Estimator, Fit, Predict, Untrained},
13    types::Float,
14};
15
16/// Classifier Chain
17///
18/// A multi-label model that arranges binary classifiers into a chain.
19/// Each model makes a prediction in the order specified by the chain using
20/// all of the available features provided to the model plus the predictions
21/// of models that are earlier in the chain.
22///
23/// # Examples
24///
25/// ```
26/// use sklears_multioutput::chains::ClassifierChain;
27/// // Use SciRS2-Core for arrays and random number generation (SciRS2 Policy)
28/// use scirs2_core::ndarray::array;
29///
30/// // This is a simple example showing the structure
31/// let data = array![[1.0, 2.0], [2.0, 3.0], [3.0, 1.0]];
32/// let labels = array![[0, 1], [1, 0], [1, 1]];
33/// ```
34#[derive(Debug, Clone)]
35pub struct ClassifierChain<S = Untrained> {
36    state: S,
37    order: Option<Vec<usize>>,
38    cv: Option<usize>,
39    random_state: Option<u64>,
40}
41
42impl ClassifierChain<Untrained> {
43    /// Create a new ClassifierChain instance
44    pub fn new() -> Self {
45        Self {
46            state: Untrained,
47            order: None,
48            cv: None,
49            random_state: None,
50        }
51    }
52
53    /// Set the chain order
54    pub fn order(mut self, order: Vec<usize>) -> Self {
55        self.order = Some(order);
56        self
57    }
58
59    /// Set cross-validation folds for training
60    pub fn cv(mut self, cv: usize) -> Self {
61        self.cv = Some(cv);
62        self
63    }
64
65    /// Set random state for reproducibility
66    pub fn random_state(mut self, random_state: u64) -> Self {
67        self.random_state = Some(random_state);
68        self
69    }
70}
71
72impl Default for ClassifierChain<Untrained> {
73    fn default() -> Self {
74        Self::new()
75    }
76}
77
78impl Estimator for ClassifierChain<Untrained> {
79    type Config = ();
80    type Error = SklearsError;
81    type Float = Float;
82
83    fn config(&self) -> &Self::Config {
84        &()
85    }
86}
87
88impl ClassifierChain<Untrained> {
89    /// Fit the classifier chain using a simple mock approach
90    pub fn fit_simple(
91        self,
92        X: &ArrayView2<'_, Float>,
93        y: &Array2<i32>,
94    ) -> SklResult<ClassifierChain<ClassifierChainTrained>> {
95        let (n_samples, n_features) = X.dim();
96        let n_labels = y.ncols();
97
98        if n_samples != y.nrows() {
99            return Err(SklearsError::InvalidInput(
100                "X and y must have the same number of samples".to_string(),
101            ));
102        }
103
104        // Determine chain order
105        let order = self
106            .order
107            .clone()
108            .unwrap_or_else(|| (0..n_labels).collect());
109
110        if order.len() != n_labels {
111            return Err(SklearsError::InvalidInput(
112                "Chain order must contain all label indices".to_string(),
113            ));
114        }
115
116        // Train models in the chain
117        let mut models = Vec::new();
118        let mut current_features = X.to_owned();
119
120        for (i, &label_idx) in order.iter().enumerate() {
121            let y_binary = y.column(label_idx).to_owned();
122
123            // Train binary classifier
124            let model = train_binary_classifier(&current_features.view(), &y_binary)?;
125            models.push(model);
126
127            // Add predictions as features for next model (except for the last one)
128            if i < order.len() - 1 {
129                let predictions = predict_binary_classifier(&current_features.view(), &models[i]);
130                let n_current_features = current_features.ncols();
131                let mut new_features = Array2::<Float>::zeros((n_samples, n_current_features + 1));
132
133                // Copy existing features
134                new_features
135                    .slice_mut(s![.., ..n_current_features])
136                    .assign(&current_features);
137
138                // Add predictions as new feature
139                for j in 0..n_samples {
140                    new_features[[j, n_current_features]] = predictions[j] as Float;
141                }
142
143                current_features = new_features;
144            }
145        }
146
147        let trained_state = ClassifierChainTrained {
148            models,
149            order,
150            n_features,
151            n_labels,
152        };
153
154        Ok(ClassifierChain {
155            state: trained_state,
156            order: self.order,
157            cv: self.cv,
158            random_state: self.random_state,
159        })
160    }
161}
162
163impl Fit<ArrayView2<'_, Float>, Array2<i32>, ClassifierChainTrained>
164    for ClassifierChain<Untrained>
165{
166    type Fitted = ClassifierChain<ClassifierChainTrained>;
167
168    fn fit(self, X: &ArrayView2<'_, Float>, y: &Array2<i32>) -> SklResult<Self::Fitted> {
169        self.fit_simple(X, y)
170    }
171}
172
173/// Trained state for ClassifierChain
174#[derive(Debug, Clone)]
175pub struct ClassifierChainTrained {
176    models: Vec<SimpleBinaryModel>,
177    order: Vec<usize>,
178    n_features: usize,
179    n_labels: usize,
180}
181
182impl Predict<ArrayView2<'_, Float>, Array2<i32>> for ClassifierChain<ClassifierChainTrained> {
183    fn predict(&self, X: &ArrayView2<'_, Float>) -> SklResult<Array2<i32>> {
184        let (n_samples, n_features) = X.dim();
185        if n_features != self.state.n_features {
186            return Err(SklearsError::InvalidInput(
187                "X has different number of features than training data".to_string(),
188            ));
189        }
190
191        let mut predictions = Array2::<i32>::zeros((n_samples, self.state.n_labels));
192        let mut current_features = X.to_owned();
193
194        // Make predictions following the chain order
195        for (i, &label_idx) in self.state.order.iter().enumerate() {
196            let model = &self.state.models[i];
197            let label_predictions = predict_binary_classifier(&current_features.view(), model);
198
199            // Store predictions
200            for j in 0..n_samples {
201                predictions[[j, label_idx]] = label_predictions[j];
202            }
203
204            // Add predictions as features for next model (if not last)
205            if i < self.state.order.len() - 1 {
206                let n_current_features = current_features.ncols();
207                let mut new_features = Array2::<Float>::zeros((n_samples, n_current_features + 1));
208
209                // Copy existing features
210                new_features
211                    .slice_mut(s![.., ..n_current_features])
212                    .assign(&current_features);
213
214                // Add current label predictions as feature
215                for j in 0..n_samples {
216                    new_features[[j, n_current_features]] = label_predictions[j] as Float;
217                }
218
219                current_features = new_features;
220            }
221        }
222
223        Ok(predictions)
224    }
225}
226
227impl ClassifierChain<ClassifierChainTrained> {
228    /// Predict probabilities for each label
229    pub fn predict_proba(&self, X: &ArrayView2<'_, Float>) -> SklResult<Array2<Float>> {
230        let (n_samples, n_features) = X.dim();
231        if n_features != self.state.n_features {
232            return Err(SklearsError::InvalidInput(
233                "X has different number of features than training data".to_string(),
234            ));
235        }
236
237        let mut probabilities = Array2::<Float>::zeros((n_samples, self.state.n_labels));
238        let mut current_features = X.to_owned();
239
240        // Make probability predictions following the chain order
241        for (i, &label_idx) in self.state.order.iter().enumerate() {
242            let model = &self.state.models[i];
243            let label_probabilities = predict_binary_probabilities(&current_features.view(), model);
244
245            // Store probabilities
246            for j in 0..n_samples {
247                probabilities[[j, label_idx]] = label_probabilities[j];
248            }
249
250            // Add predictions as features for next model (if not last)
251            if i < self.state.order.len() - 1 {
252                let label_predictions =
253                    label_probabilities.mapv(|p| if p > 0.5 { 1.0 } else { 0.0 });
254                let n_current_features = current_features.ncols();
255                let mut new_features = Array2::<Float>::zeros((n_samples, n_current_features + 1));
256
257                // Copy existing features
258                new_features
259                    .slice_mut(s![.., ..n_current_features])
260                    .assign(&current_features);
261
262                // Add predictions as feature
263                for j in 0..n_samples {
264                    new_features[[j, n_current_features]] = label_predictions[j];
265                }
266
267                current_features = new_features;
268            }
269        }
270
271        Ok(probabilities)
272    }
273
274    /// Get the chain order used during training
275    pub fn chain_order(&self) -> &[usize] {
276        &self.state.order
277    }
278
279    /// Get the number of models in the chain
280    pub fn n_models(&self) -> usize {
281        self.state.models.len()
282    }
283
284    /// Get number of targets/labels
285    pub fn n_targets(&self) -> usize {
286        self.state.n_labels
287    }
288
289    /// Simple prediction method (alias for predict)
290    pub fn predict_simple(&self, X: &ArrayView2<'_, Float>) -> SklResult<Array2<i32>> {
291        self.predict(X)
292    }
293
294    /// Monte Carlo prediction (simplified)
295    pub fn predict_monte_carlo(
296        &self,
297        X: &ArrayView2<'_, Float>,
298        n_samples: usize,
299        _random_state: Option<u64>,
300    ) -> SklResult<Array2<Float>> {
301        if n_samples == 0 {
302            return Err(SklearsError::InvalidInput(
303                "n_samples must be greater than 0".to_string(),
304            ));
305        }
306        // For now, just return probabilities
307        self.predict_proba(X)
308    }
309
310    /// Monte Carlo prediction for labels (simplified)
311    pub fn predict_monte_carlo_labels(
312        &self,
313        X: &ArrayView2<'_, Float>,
314        n_samples: usize,
315        _random_state: Option<u64>,
316    ) -> SklResult<Array2<i32>> {
317        if n_samples == 0 {
318            return Err(SklearsError::InvalidInput(
319                "n_samples must be greater than 0".to_string(),
320            ));
321        }
322        // For now, just return predictions
323        self.predict(X)
324    }
325}
326
327/// Regressor Chain
328///
329/// A multi-output model that arranges regressors into a chain.
330/// Each model makes a prediction in the order specified by the chain using
331/// all of the available features provided to the model plus the predictions
332/// of models that are earlier in the chain.
333///
334/// # Examples
335///
336/// ```
337/// use sklears_multioutput::chains::RegressorChain;
338/// // Use SciRS2-Core for arrays and random number generation (SciRS2 Policy)
339/// use scirs2_core::ndarray::array;
340///
341/// // This is a simple example showing the structure
342/// let data = array![[1.0, 2.0], [2.0, 3.0], [3.0, 1.0]];
343/// let targets = array![[1.5, 2.5], [2.5, 3.5], [3.5, 1.5]];
344/// ```
345#[derive(Debug, Clone)]
346pub struct RegressorChain<S = Untrained> {
347    state: S,
348    order: Option<Vec<usize>>,
349    cv: Option<usize>,
350    random_state: Option<u64>,
351}
352
353impl RegressorChain<Untrained> {
354    /// Create a new RegressorChain instance
355    pub fn new() -> Self {
356        Self {
357            state: Untrained,
358            order: None,
359            cv: None,
360            random_state: None,
361        }
362    }
363
364    /// Set the chain order
365    pub fn order(mut self, order: Vec<usize>) -> Self {
366        self.order = Some(order);
367        self
368    }
369
370    /// Set cross-validation folds for training
371    pub fn cv(mut self, cv: usize) -> Self {
372        self.cv = Some(cv);
373        self
374    }
375
376    /// Set random state for reproducibility
377    pub fn random_state(mut self, random_state: u64) -> Self {
378        self.random_state = Some(random_state);
379        self
380    }
381}
382
383impl Default for RegressorChain<Untrained> {
384    fn default() -> Self {
385        Self::new()
386    }
387}
388
389impl Estimator for RegressorChain<Untrained> {
390    type Config = ();
391    type Error = SklearsError;
392    type Float = Float;
393
394    fn config(&self) -> &Self::Config {
395        &()
396    }
397}
398
399impl RegressorChain<Untrained> {
400    /// Fit the regressor chain using a simple linear approach
401    pub fn fit_simple(
402        self,
403        X: &ArrayView2<'_, Float>,
404        y: &Array2<Float>,
405    ) -> SklResult<RegressorChain<RegressorChainTrained>> {
406        let (n_samples, n_features) = X.dim();
407        let n_targets = y.ncols();
408
409        if n_samples != y.nrows() {
410            return Err(SklearsError::InvalidInput(
411                "X and y must have the same number of samples".to_string(),
412            ));
413        }
414
415        // Determine chain order
416        let order = self
417            .order
418            .clone()
419            .unwrap_or_else(|| (0..n_targets).collect());
420
421        if order.len() != n_targets {
422            return Err(SklearsError::InvalidInput(
423                "Chain order must contain all target indices".to_string(),
424            ));
425        }
426
427        // Train models in the chain
428        let mut models = Vec::new();
429        let mut current_features = X.to_owned();
430
431        for (i, &target_idx) in order.iter().enumerate() {
432            let y_target = y.column(target_idx).to_owned();
433
434            // Train linear regressor
435            let model = train_simple_linear_classifier(&current_features.view(), &y_target)?;
436            models.push(model);
437
438            // Add predictions as features for next model (except for the last one)
439            if i < order.len() - 1 {
440                let predictions = predict_simple_linear(&current_features.view(), &models[i]);
441                let n_current_features = current_features.ncols();
442                let mut new_features = Array2::<Float>::zeros((n_samples, n_current_features + 1));
443
444                // Copy existing features
445                new_features
446                    .slice_mut(s![.., ..n_current_features])
447                    .assign(&current_features);
448
449                // Add predictions as new feature
450                for j in 0..n_samples {
451                    new_features[[j, n_current_features]] = predictions[j];
452                }
453
454                current_features = new_features;
455            }
456        }
457
458        let trained_state = RegressorChainTrained {
459            models,
460            order,
461            n_features,
462            n_targets,
463        };
464
465        Ok(RegressorChain {
466            state: trained_state,
467            order: self.order,
468            cv: self.cv,
469            random_state: self.random_state,
470        })
471    }
472}
473
474impl Fit<ArrayView2<'_, Float>, Array2<Float>, RegressorChainTrained>
475    for RegressorChain<Untrained>
476{
477    type Fitted = RegressorChain<RegressorChainTrained>;
478
479    fn fit(self, X: &ArrayView2<'_, Float>, y: &Array2<Float>) -> SklResult<Self::Fitted> {
480        self.fit_simple(X, y)
481    }
482}
483
484/// Trained state for RegressorChain
485#[derive(Debug, Clone)]
486pub struct RegressorChainTrained {
487    models: Vec<SimpleLinearClassifier>,
488    order: Vec<usize>,
489    n_features: usize,
490    n_targets: usize,
491}
492
493impl Predict<ArrayView2<'_, Float>, Array2<Float>> for RegressorChain<RegressorChainTrained> {
494    fn predict(&self, X: &ArrayView2<'_, Float>) -> SklResult<Array2<Float>> {
495        let (n_samples, n_features) = X.dim();
496        if n_features != self.state.n_features {
497            return Err(SklearsError::InvalidInput(
498                "X has different number of features than training data".to_string(),
499            ));
500        }
501
502        let mut predictions = Array2::<Float>::zeros((n_samples, self.state.n_targets));
503        let mut current_features = X.to_owned();
504
505        // Make predictions following the chain order
506        for (i, &target_idx) in self.state.order.iter().enumerate() {
507            let model = &self.state.models[i];
508            let target_predictions = predict_simple_linear(&current_features.view(), model);
509
510            // Store predictions
511            for j in 0..n_samples {
512                predictions[[j, target_idx]] = target_predictions[j];
513            }
514
515            // Add predictions as features for next model (if not last)
516            if i < self.state.order.len() - 1 {
517                let n_current_features = current_features.ncols();
518                let mut new_features = Array2::<Float>::zeros((n_samples, n_current_features + 1));
519
520                // Copy existing features
521                new_features
522                    .slice_mut(s![.., ..n_current_features])
523                    .assign(&current_features);
524
525                // Add current target predictions as feature
526                for j in 0..n_samples {
527                    new_features[[j, n_current_features]] = target_predictions[j];
528                }
529
530                current_features = new_features;
531            }
532        }
533
534        Ok(predictions)
535    }
536}
537
538impl RegressorChain<RegressorChainTrained> {
539    /// Get the chain order used during training
540    pub fn chain_order(&self) -> &[usize] {
541        &self.state.order
542    }
543
544    /// Get the number of models in the chain
545    pub fn n_models(&self) -> usize {
546        self.state.models.len()
547    }
548
549    /// Get model at specified index
550    pub fn get_model(&self, index: usize) -> Option<&SimpleLinearClassifier> {
551        self.state.models.get(index)
552    }
553
554    /// Get the number of targets
555    pub fn n_targets(&self) -> usize {
556        self.state.n_targets
557    }
558
559    /// Simple prediction method (alias for predict)
560    pub fn predict_simple(&self, X: &ArrayView2<'_, Float>) -> SklResult<Array2<Float>> {
561        self.predict(X)
562    }
563}
564
565/// Ensemble of Chains
566///
567/// An ensemble approach that combines multiple ClassifierChain models
568/// with different chain orders or different random seeds to improve
569/// prediction performance and robustness.
570///
571/// # Examples
572///
573/// ```
574/// use sklears_multioutput::chains::EnsembleOfChains;
575/// // Use SciRS2-Core for arrays and random number generation (SciRS2 Policy)
576/// use scirs2_core::ndarray::array;
577///
578/// // This is a simple example showing the structure
579/// let data = array![[1.0, 2.0], [2.0, 3.0], [3.0, 1.0]];
580/// let labels = array![[0, 1], [1, 0], [1, 1]];
581/// ```
582#[derive(Debug, Clone)]
583pub struct EnsembleOfChains<S = Untrained> {
584    state: S,
585    n_chains: usize,
586    chain_method: ChainMethod,
587    random_state: Option<u64>,
588}
589
590/// Method for generating chains in ensemble
591#[derive(Debug, Clone, Copy, PartialEq)]
592pub enum ChainMethod {
593    /// Random chain orders
594    Random,
595    /// Fixed different orders
596    Fixed,
597    /// Bootstrap sampling with chains
598    Bootstrap,
599}
600
601impl EnsembleOfChains<Untrained> {
602    /// Create a new EnsembleOfChains instance
603    pub fn new() -> Self {
604        Self {
605            state: Untrained,
606            n_chains: 10,
607            chain_method: ChainMethod::Random,
608            random_state: None,
609        }
610    }
611
612    /// Set number of chains in ensemble
613    pub fn n_chains(mut self, n_chains: usize) -> Self {
614        self.n_chains = n_chains;
615        self
616    }
617
618    /// Set chain generation method
619    pub fn chain_method(mut self, method: ChainMethod) -> Self {
620        self.chain_method = method;
621        self
622    }
623
624    /// Set random state for reproducibility
625    pub fn random_state(mut self, random_state: u64) -> Self {
626        self.random_state = Some(random_state);
627        self
628    }
629}
630
631impl Default for EnsembleOfChains<Untrained> {
632    fn default() -> Self {
633        Self::new()
634    }
635}
636
637impl Estimator for EnsembleOfChains<Untrained> {
638    type Config = ();
639    type Error = SklearsError;
640    type Float = Float;
641
642    fn config(&self) -> &Self::Config {
643        &()
644    }
645}
646
647impl EnsembleOfChains<Untrained> {
648    /// Fit the ensemble of chains
649    pub fn fit_simple(
650        self,
651        X: &ArrayView2<'_, Float>,
652        y: &Array2<i32>,
653    ) -> SklResult<EnsembleOfChains<EnsembleOfChainsTrained>> {
654        let (n_samples, n_features) = X.dim();
655        let n_labels = y.ncols();
656
657        if n_samples != y.nrows() {
658            return Err(SklearsError::InvalidInput(
659                "X and y must have the same number of samples".to_string(),
660            ));
661        }
662
663        let mut chains = Vec::new();
664        let mut rng_state = self.random_state.unwrap_or(42);
665
666        for i in 0..self.n_chains {
667            // Generate chain order based on method
668            let chain_order = match self.chain_method {
669                ChainMethod::Random => {
670                    let mut order: Vec<usize> = (0..n_labels).collect();
671                    // Simple shuffle using deterministic random
672                    for j in (1..order.len()).rev() {
673                        rng_state = rng_state.wrapping_mul(1664525).wrapping_add(1013904223);
674                        let k = (rng_state as usize) % (j + 1);
675                        order.swap(j, k);
676                    }
677                    order
678                }
679                ChainMethod::Fixed => {
680                    // Create different fixed orders
681                    let mut order: Vec<usize> = (0..n_labels).collect();
682                    order.rotate_left(i % n_labels);
683                    order
684                }
685                ChainMethod::Bootstrap => {
686                    // For bootstrap, use random order and later bootstrap samples
687                    let mut order: Vec<usize> = (0..n_labels).collect();
688                    for j in (1..order.len()).rev() {
689                        rng_state = rng_state.wrapping_mul(1664525).wrapping_add(1013904223);
690                        let k = (rng_state as usize) % (j + 1);
691                        order.swap(j, k);
692                    }
693                    order
694                }
695            };
696
697            // Create and train individual chain
698            let chain = ClassifierChain::new()
699                .order(chain_order)
700                .random_state(rng_state);
701
702            let trained_chain = chain.fit_simple(X, y)?;
703            chains.push(trained_chain);
704
705            rng_state = rng_state.wrapping_add(1);
706        }
707
708        let trained_state = EnsembleOfChainsTrained {
709            chains,
710            n_features,
711            n_labels,
712        };
713
714        Ok(EnsembleOfChains {
715            state: trained_state,
716            n_chains: self.n_chains,
717            chain_method: self.chain_method,
718            random_state: self.random_state,
719        })
720    }
721}
722
723impl Fit<ArrayView2<'_, Float>, Array2<i32>, EnsembleOfChainsTrained>
724    for EnsembleOfChains<Untrained>
725{
726    type Fitted = EnsembleOfChains<EnsembleOfChainsTrained>;
727
728    fn fit(self, X: &ArrayView2<'_, Float>, y: &Array2<i32>) -> SklResult<Self::Fitted> {
729        self.fit_simple(X, y)
730    }
731}
732
733/// Trained state for EnsembleOfChains
734#[derive(Debug, Clone)]
735pub struct EnsembleOfChainsTrained {
736    chains: Vec<ClassifierChain<ClassifierChainTrained>>,
737    n_features: usize,
738    n_labels: usize,
739}
740
741impl Predict<ArrayView2<'_, Float>, Array2<i32>> for EnsembleOfChains<EnsembleOfChainsTrained> {
742    fn predict(&self, X: &ArrayView2<'_, Float>) -> SklResult<Array2<i32>> {
743        let (n_samples, n_features) = X.dim();
744        if n_features != self.state.n_features {
745            return Err(SklearsError::InvalidInput(
746                "X has different number of features than training data".to_string(),
747            ));
748        }
749
750        // Collect predictions from all chains
751        let mut all_predictions = Vec::new();
752        for chain in &self.state.chains {
753            let predictions = chain.predict(X)?;
754            all_predictions.push(predictions);
755        }
756
757        // Ensemble predictions by majority voting
758        let mut final_predictions = Array2::<i32>::zeros((n_samples, self.state.n_labels));
759
760        for i in 0..n_samples {
761            for j in 0..self.state.n_labels {
762                let mut votes = 0;
763                for predictions in &all_predictions {
764                    votes += predictions[[i, j]];
765                }
766                // Majority vote
767                final_predictions[[i, j]] = if votes > (self.state.chains.len() as i32) / 2 {
768                    1
769                } else {
770                    0
771                };
772            }
773        }
774
775        Ok(final_predictions)
776    }
777}
778
779impl EnsembleOfChains<EnsembleOfChainsTrained> {
780    /// Predict probabilities using ensemble voting
781    pub fn predict_proba(&self, X: &ArrayView2<'_, Float>) -> SklResult<Array2<Float>> {
782        let (n_samples, n_features) = X.dim();
783        if n_features != self.state.n_features {
784            return Err(SklearsError::InvalidInput(
785                "X has different number of features than training data".to_string(),
786            ));
787        }
788
789        // Collect probability predictions from all chains
790        let mut all_probabilities = Vec::new();
791        for chain in &self.state.chains {
792            let probabilities = chain.predict_proba(X)?;
793            all_probabilities.push(probabilities);
794        }
795
796        // Average probabilities across chains
797        let mut final_probabilities = Array2::<Float>::zeros((n_samples, self.state.n_labels));
798
799        for i in 0..n_samples {
800            for j in 0..self.state.n_labels {
801                let mut prob_sum = 0.0;
802                for probabilities in &all_probabilities {
803                    prob_sum += probabilities[[i, j]];
804                }
805                final_probabilities[[i, j]] = prob_sum / self.state.chains.len() as Float;
806            }
807        }
808
809        Ok(final_probabilities)
810    }
811
812    /// Get number of chains in ensemble
813    pub fn n_chains(&self) -> usize {
814        self.state.chains.len()
815    }
816
817    /// Get individual chain at specified index
818    pub fn get_chain(&self, index: usize) -> Option<&ClassifierChain<ClassifierChainTrained>> {
819        self.state.chains.get(index)
820    }
821
822    /// Get diversity measure between chains
823    pub fn chain_diversity(&self) -> Float {
824        if self.state.chains.len() < 2 {
825            return 0.0;
826        }
827
828        let mut diversity_sum = 0.0;
829        let mut count = 0;
830
831        // Compare chain orders pairwise
832        for i in 0..self.state.chains.len() {
833            for j in (i + 1)..self.state.chains.len() {
834                let order1 = self.state.chains[i].chain_order();
835                let order2 = self.state.chains[j].chain_order();
836
837                // Calculate order similarity (Kendall's tau-like measure)
838                let mut agreements = 0;
839                for k in 0..order1.len() {
840                    if order1[k] == order2[k] {
841                        agreements += 1;
842                    }
843                }
844
845                let similarity = agreements as Float / order1.len() as Float;
846                diversity_sum += 1.0 - similarity;
847                count += 1;
848            }
849        }
850
851        if count > 0 {
852            diversity_sum / count as Float
853        } else {
854            0.0
855        }
856    }
857
858    /// Get the number of targets
859    pub fn n_targets(&self) -> usize {
860        self.state.n_labels
861    }
862
863    /// Simple prediction method (alias for predict)
864    pub fn predict_simple(&self, X: &ArrayView2<'_, Float>) -> SklResult<Array2<i32>> {
865        self.predict(X)
866    }
867
868    /// Simple probability prediction method (alias for predict_proba)
869    pub fn predict_proba_simple(&self, X: &ArrayView2<'_, Float>) -> SklResult<Array2<Float>> {
870        self.predict_proba(X)
871    }
872}
873
874/// Bayesian Classifier Chain
875///
876/// A probabilistic variant of classifier chain that uses Bayesian inference
877/// for the binary classifiers, providing uncertainty quantification alongside
878/// predictions.
879///
880/// # Examples
881///
882/// ```
883/// use sklears_multioutput::chains::BayesianClassifierChain;
884/// // Use SciRS2-Core for arrays and random number generation (SciRS2 Policy)
885/// use scirs2_core::ndarray::array;
886///
887/// // This is a simple example showing the structure
888/// let data = array![[1.0, 2.0], [2.0, 3.0], [3.0, 1.0]];
889/// let labels = array![[0, 1], [1, 0], [1, 1]];
890/// let model = BayesianClassifierChain::new()
891///     .n_samples(100)
892///     .prior_strength(1.0);
893/// ```
894#[derive(Debug, Clone)]
895pub struct BayesianClassifierChain<S = Untrained> {
896    state: S,
897    /// order
898    pub order: Option<Vec<usize>>,
899    /// n_samples
900    pub n_samples: usize,
901    /// prior_strength
902    pub prior_strength: Float,
903    /// random_state
904    pub random_state: Option<u64>,
905}
906
907impl BayesianClassifierChain<Untrained> {
908    /// Create a new BayesianClassifierChain instance
909    pub fn new() -> Self {
910        Self {
911            state: Untrained,
912            order: None,
913            n_samples: 100,
914            prior_strength: 1.0,
915            random_state: None,
916        }
917    }
918
919    /// Set the chain order
920    pub fn order(mut self, order: Vec<usize>) -> Self {
921        self.order = Some(order);
922        self
923    }
924
925    /// Set number of posterior samples
926    pub fn n_samples(mut self, n_samples: usize) -> Self {
927        self.n_samples = n_samples;
928        self
929    }
930
931    /// Set prior strength (regularization parameter)
932    pub fn prior_strength(mut self, prior_strength: Float) -> Self {
933        self.prior_strength = prior_strength;
934        self
935    }
936
937    /// Set random state for reproducibility
938    pub fn random_state(mut self, random_state: u64) -> Self {
939        self.random_state = Some(random_state);
940        self
941    }
942}
943
944impl Default for BayesianClassifierChain<Untrained> {
945    fn default() -> Self {
946        Self::new()
947    }
948}
949
950impl Estimator for BayesianClassifierChain<Untrained> {
951    type Config = ();
952    type Error = SklearsError;
953    type Float = Float;
954
955    fn config(&self) -> &Self::Config {
956        &()
957    }
958}
959
960impl BayesianClassifierChain<Untrained> {
961    /// Fit the Bayesian classifier chain
962    #[allow(non_snake_case)]
963    pub fn fit_simple(
964        self,
965        X: &ArrayView2<'_, Float>,
966        y: &Array2<i32>,
967    ) -> SklResult<BayesianClassifierChain<BayesianClassifierChainTrained>> {
968        let (n_samples, n_features) = X.dim();
969        let n_labels = y.ncols();
970
971        if n_samples != y.nrows() {
972            return Err(SklearsError::InvalidInput(
973                "X and y must have the same number of samples".to_string(),
974            ));
975        }
976
977        // Validate binary labels
978        for &val in y.iter() {
979            if val != 0 && val != 1 {
980                return Err(SklearsError::InvalidInput(
981                    "y must contain only binary values (0 or 1)".to_string(),
982                ));
983            }
984        }
985
986        // Determine chain order
987        let order = self
988            .order
989            .clone()
990            .unwrap_or_else(|| (0..n_labels).collect());
991
992        if order.len() != n_labels {
993            return Err(SklearsError::InvalidInput(
994                "Chain order must contain all label indices".to_string(),
995            ));
996        }
997
998        // Standardize features
999        let feature_means = X
1000            .mean_axis(Axis(0))
1001            .expect("array should have elements for mean computation");
1002        let feature_stds = X.std_axis(Axis(0), 0.0);
1003        let X_standardized = standardize_features_simple(X, &feature_means, &feature_stds);
1004
1005        // Train Bayesian models in the chain
1006        let mut bayesian_models = Vec::new();
1007        let mut current_features = X_standardized;
1008
1009        for (i, &label_idx) in order.iter().enumerate() {
1010            let y_binary = y.column(label_idx).to_owned();
1011
1012            // Train Bayesian binary classifier
1013            let model = train_bayesian_binary_classifier(
1014                &current_features,
1015                &y_binary,
1016                self.prior_strength,
1017            )?;
1018            bayesian_models.push(model);
1019
1020            // Add predictions as features for next model (except for the last one)
1021            if i < order.len() - 1 {
1022                let predictions =
1023                    predict_bayesian_mean(&current_features.view(), &bayesian_models[i]);
1024                let n_current_features = current_features.ncols();
1025                let mut new_features = Array2::<Float>::zeros((n_samples, n_current_features + 1));
1026
1027                // Copy existing features
1028                new_features
1029                    .slice_mut(s![.., ..n_current_features])
1030                    .assign(&current_features);
1031
1032                // Add predictions as new feature
1033                for j in 0..n_samples {
1034                    new_features[[j, n_current_features]] = predictions[j];
1035                }
1036
1037                current_features = new_features;
1038            }
1039        }
1040
1041        let trained_state = BayesianClassifierChainTrained {
1042            bayesian_models,
1043            order,
1044            n_features,
1045            n_labels,
1046            feature_means,
1047            feature_stds,
1048        };
1049
1050        Ok(BayesianClassifierChain {
1051            state: trained_state,
1052            order: None,
1053            n_samples: self.n_samples,
1054            prior_strength: self.prior_strength,
1055            random_state: self.random_state,
1056        })
1057    }
1058}
1059
1060impl Fit<ArrayView2<'_, Float>, Array2<i32>, BayesianClassifierChainTrained>
1061    for BayesianClassifierChain<Untrained>
1062{
1063    type Fitted = BayesianClassifierChain<BayesianClassifierChainTrained>;
1064
1065    fn fit(self, X: &ArrayView2<'_, Float>, y: &Array2<i32>) -> SklResult<Self::Fitted> {
1066        self.fit_simple(X, y)
1067    }
1068}
1069
1070/// Trained state for Bayesian Classifier Chain
1071#[derive(Debug, Clone)]
1072pub struct BayesianClassifierChainTrained {
1073    bayesian_models: Vec<BayesianBinaryModel>,
1074    order: Vec<usize>,
1075    #[allow(dead_code)]
1076    n_features: usize,
1077    n_labels: usize,
1078    feature_means: Array1<Float>,
1079    feature_stds: Array1<Float>,
1080}
1081
1082impl Predict<ArrayView2<'_, Float>, Array2<i32>>
1083    for BayesianClassifierChain<BayesianClassifierChainTrained>
1084{
1085    #[allow(non_snake_case)]
1086    fn predict(&self, X: &ArrayView2<'_, Float>) -> SklResult<Array2<i32>> {
1087        let (n_samples, n_features) = X.dim();
1088        if n_features != self.state.feature_means.len() {
1089            return Err(SklearsError::InvalidInput(
1090                "X has different number of features than training data".to_string(),
1091            ));
1092        }
1093
1094        // Standardize features
1095        let X_standardized =
1096            standardize_features_simple(X, &self.state.feature_means, &self.state.feature_stds);
1097
1098        let mut predictions = Array2::<i32>::zeros((n_samples, self.state.n_labels));
1099        let mut current_features = X_standardized;
1100
1101        // Make predictions following the chain order
1102        for (chain_pos, &label_idx) in self.state.order.iter().enumerate() {
1103            let model = &self.state.bayesian_models[chain_pos];
1104
1105            // Sample from posterior distribution and make predictions
1106            let label_predictions = predict_bayesian_binary(&current_features.view(), model);
1107
1108            // Convert probabilities to binary predictions
1109            for i in 0..n_samples {
1110                predictions[[i, label_idx]] = if label_predictions[i] > 0.5 { 1 } else { 0 };
1111            }
1112
1113            // Add predictions as features for next model (if not last)
1114            if chain_pos < self.state.order.len() - 1 {
1115                let mut new_features =
1116                    Array2::<Float>::zeros((n_samples, current_features.ncols() + 1));
1117
1118                // Copy existing features
1119                new_features
1120                    .slice_mut(s![.., ..current_features.ncols()])
1121                    .assign(&current_features);
1122
1123                // Add current label predictions as feature
1124                for i in 0..n_samples {
1125                    new_features[[i, current_features.ncols()]] =
1126                        predictions[[i, label_idx]] as Float;
1127                }
1128
1129                current_features = new_features;
1130            }
1131        }
1132
1133        Ok(predictions)
1134    }
1135}
1136
1137impl BayesianClassifierChain<BayesianClassifierChainTrained> {
1138    /// Predict with uncertainty quantification
1139    #[allow(non_snake_case)]
1140    pub fn predict_uncertainty(&self, X: &ArrayView2<'_, Float>) -> SklResult<Array2<Float>> {
1141        let (n_samples, n_features) = X.dim();
1142        if n_features != self.state.feature_means.len() {
1143            return Err(SklearsError::InvalidInput(
1144                "X has different number of features than training data".to_string(),
1145            ));
1146        }
1147
1148        // Standardize features
1149        let X_standardized =
1150            standardize_features_simple(X, &self.state.feature_means, &self.state.feature_stds);
1151
1152        let mut uncertainties = Array2::<Float>::zeros((n_samples, self.state.n_labels));
1153        let mut current_features = X_standardized;
1154
1155        // Make predictions following the chain order with uncertainty estimation
1156        for (chain_pos, &label_idx) in self.state.order.iter().enumerate() {
1157            let model = &self.state.bayesian_models[chain_pos];
1158
1159            // Get uncertainty estimates
1160            let (means, variances) = predict_bayesian_uncertainty(&current_features.view(), model)?;
1161
1162            // Store uncertainties
1163            for i in 0..n_samples {
1164                uncertainties[[i, label_idx]] = variances[i];
1165            }
1166
1167            // For chaining, use mean predictions as features
1168            if chain_pos < self.state.order.len() - 1 {
1169                let mut new_features =
1170                    Array2::<Float>::zeros((n_samples, current_features.ncols() + 1));
1171
1172                // Copy existing features
1173                new_features
1174                    .slice_mut(s![.., ..current_features.ncols()])
1175                    .assign(&current_features);
1176
1177                // Add mean predictions as feature
1178                for i in 0..n_samples {
1179                    new_features[[i, current_features.ncols()]] = means[i];
1180                }
1181
1182                current_features = new_features;
1183            }
1184        }
1185
1186        Ok(uncertainties)
1187    }
1188
1189    /// Predict probabilities with Bayesian averaging
1190    #[allow(non_snake_case)]
1191    pub fn predict_proba(&self, X: &ArrayView2<'_, Float>) -> SklResult<Array2<Float>> {
1192        let (n_samples, n_features) = X.dim();
1193        if n_features != self.state.feature_means.len() {
1194            return Err(SklearsError::InvalidInput(
1195                "X has different number of features than training data".to_string(),
1196            ));
1197        }
1198
1199        // Standardize features
1200        let X_standardized =
1201            standardize_features_simple(X, &self.state.feature_means, &self.state.feature_stds);
1202
1203        let mut probabilities = Array2::<Float>::zeros((n_samples, self.state.n_labels));
1204        let mut current_features = X_standardized;
1205
1206        // Make probability predictions following the chain order
1207        for (chain_pos, &label_idx) in self.state.order.iter().enumerate() {
1208            let model = &self.state.bayesian_models[chain_pos];
1209
1210            // Get probability predictions
1211            let label_probabilities = predict_bayesian_binary(&current_features.view(), model);
1212
1213            // Store probabilities
1214            for i in 0..n_samples {
1215                probabilities[[i, label_idx]] = label_probabilities[i];
1216            }
1217
1218            // Add mean predictions as features for next model (if not last)
1219            if chain_pos < self.state.order.len() - 1 {
1220                let mut new_features =
1221                    Array2::<Float>::zeros((n_samples, current_features.ncols() + 1));
1222
1223                // Copy existing features
1224                new_features
1225                    .slice_mut(s![.., ..current_features.ncols()])
1226                    .assign(&current_features);
1227
1228                // Add mean predictions as feature
1229                for i in 0..n_samples {
1230                    new_features[[i, current_features.ncols()]] = label_probabilities[i];
1231                }
1232
1233                current_features = new_features;
1234            }
1235        }
1236
1237        Ok(probabilities)
1238    }
1239
1240    /// Get the chain order used during training
1241    pub fn chain_order(&self) -> &[usize] {
1242        &self.state.order
1243    }
1244
1245    /// Get number of Bayesian models in the chain
1246    pub fn n_models(&self) -> usize {
1247        self.state.bayesian_models.len()
1248    }
1249
1250    /// Get posterior statistics for a specific model in the chain
1251    pub fn model_posterior_stats(
1252        &self,
1253        model_idx: usize,
1254    ) -> Option<(&Array1<Float>, &Array2<Float>)> {
1255        self.state
1256            .bayesian_models
1257            .get(model_idx)
1258            .map(|model| (&model.weight_mean, &model.weight_cov))
1259    }
1260
1261    /// Get the chain order used during training
1262    pub fn order(&self) -> &[usize] {
1263        &self.state.order
1264    }
1265}
1266
1267// Chain-specific utility functions
1268
1269/// Helper function to predict binary classification
1270fn predict_binary_classifier(X: &ArrayView2<Float>, model: &SimpleBinaryModel) -> Array1<i32> {
1271    let raw_scores = X.dot(&model.weights) + model.bias;
1272    raw_scores.mapv(|x| if x > 0.0 { 1 } else { 0 })
1273}