Skip to main content

sklears_isotonic/
fluent_api.rs

1//! Fluent API for isotonic regression configuration
2//!
3//! This module provides a fluent interface for configuring isotonic regression
4//! models with method chaining and intuitive constraint specification.
5
6use scirs2_core::ndarray::Array1;
7use sklears_core::{
8    error::Result,
9    traits::{Fit, Predict, Trained, Untrained},
10    types::Float,
11};
12
13use crate::core::{IsotonicRegression, LossFunction, MonotonicityConstraint};
14
15/// Fluent builder for isotonic regression with method chaining
16#[derive(Debug, Clone)]
17/// FluentIsotonicRegression
18pub struct FluentIsotonicRegression<State = Untrained> {
19    inner: IsotonicRegression<State>,
20}
21
22impl FluentIsotonicRegression<Untrained> {
23    /// Create a new fluent isotonic regression builder
24    pub fn new() -> Self {
25        Self {
26            inner: IsotonicRegression::new(),
27        }
28    }
29
30    /// Set the regression to be increasing (monotonically non-decreasing)
31    pub fn increasing(mut self) -> Self {
32        self.inner = self.inner.increasing(true);
33        self
34    }
35
36    /// Set the regression to be decreasing (monotonically non-increasing)
37    pub fn decreasing(mut self) -> Self {
38        self.inner = self.inner.increasing(false);
39        self
40    }
41
42    /// Set the regression to be convex (increasing and convex)
43    pub fn convex(mut self) -> Self {
44        self.inner = self.inner.convex();
45        self
46    }
47
48    /// Set the regression to be concave (increasing and concave)
49    pub fn concave(mut self) -> Self {
50        self.inner = self.inner.concave();
51        self
52    }
53
54    /// Set the regression to be convex and decreasing
55    pub fn convex_decreasing(mut self) -> Self {
56        self.inner = self.inner.convex_decreasing();
57        self
58    }
59
60    /// Set the regression to be concave and decreasing
61    pub fn concave_decreasing(mut self) -> Self {
62        self.inner = self.inner.concave_decreasing();
63        self
64    }
65
66    /// Set piecewise monotonic constraints with breakpoints
67    pub fn piecewise(mut self, breakpoints: Vec<Float>, segment_increasing: Vec<bool>) -> Self {
68        self.inner = self.inner.piecewise(breakpoints, segment_increasing);
69        self
70    }
71
72    /// Set the loss function to squared loss (L2) - default
73    pub fn squared_loss(mut self) -> Self {
74        self.inner = self.inner.loss(LossFunction::SquaredLoss);
75        self
76    }
77
78    /// Set the loss function to absolute loss (L1) for robustness
79    pub fn absolute_loss(mut self) -> Self {
80        self.inner = self.inner.loss(LossFunction::AbsoluteLoss);
81        self
82    }
83
84    /// Set the loss function to Huber loss with specified delta
85    pub fn huber_loss(mut self, delta: Float) -> Self {
86        self.inner = self.inner.loss(LossFunction::HuberLoss { delta });
87        self
88    }
89
90    /// Set the loss function to quantile loss with specified quantile
91    pub fn quantile_loss(mut self, quantile: Float) -> Self {
92        self.inner = self.inner.loss(LossFunction::QuantileLoss { quantile });
93        self
94    }
95
96    /// Set lower bound for output values
97    pub fn lower_bound(mut self, bound: Float) -> Self {
98        self.inner = self.inner.y_min(bound);
99        self
100    }
101
102    /// Set upper bound for output values
103    pub fn upper_bound(mut self, bound: Float) -> Self {
104        self.inner = self.inner.y_max(bound);
105        self
106    }
107
108    /// Set both lower and upper bounds
109    pub fn bounds(mut self, lower: Float, upper: Float) -> Self {
110        self.inner = self.inner.y_min(lower).y_max(upper);
111        self
112    }
113
114    /// Set bounds to [0, 1] for probability-like outputs
115    pub fn probability_bounds(self) -> Self {
116        self.bounds(0.0, 1.0)
117    }
118
119    /// Set bounds to [0, +inf) for non-negative outputs
120    pub fn non_negative(self) -> Self {
121        self.lower_bound(0.0)
122    }
123
124    /// Configure for robust regression against outliers
125    pub fn robust(self) -> Self {
126        self.huber_loss(1.35) // Standard robust delta
127    }
128
129    /// Configure for median regression
130    pub fn median_regression(self) -> Self {
131        self.quantile_loss(0.5)
132    }
133
134    /// Configure for 90th percentile regression
135    pub fn percentile_90(self) -> Self {
136        self.quantile_loss(0.9)
137    }
138
139    /// Configure for 10th percentile regression
140    pub fn percentile_10(self) -> Self {
141        self.quantile_loss(0.1)
142    }
143}
144
145impl Default for FluentIsotonicRegression<Untrained> {
146    fn default() -> Self {
147        Self::new()
148    }
149}
150
151// Note: Estimator trait implementation would need to match sklears-core requirements
152// For now, focusing on Fit and Predict traits
153
154impl Fit<Array1<Float>, Array1<Float>> for FluentIsotonicRegression<Untrained> {
155    type Fitted = FluentIsotonicRegression<Trained>;
156
157    fn fit(self, x: &Array1<Float>, y: &Array1<Float>) -> Result<Self::Fitted> {
158        let fitted_inner = self.inner.fit(x, y)?;
159        Ok(FluentIsotonicRegression {
160            inner: fitted_inner,
161        })
162    }
163}
164
165impl Predict<Array1<Float>, Array1<Float>> for FluentIsotonicRegression<Trained> {
166    fn predict(&self, x: &Array1<Float>) -> Result<Array1<Float>> {
167        self.inner.predict(x)
168    }
169}
170
171/// Constraint builder for complex constraint specifications
172#[derive(Debug, Clone)]
173/// ConstraintBuilder
174pub struct ConstraintBuilder {
175    constraints: Vec<ConstraintSpec>,
176}
177
178#[derive(Debug, Clone)]
179enum ConstraintSpec {
180    Monotonic {
181        increasing: bool,
182    },
183    Piecewise {
184        breakpoints: Vec<Float>,
185        segments: Vec<bool>,
186    },
187    Convex,
188    Concave,
189    #[allow(dead_code)]
190    // intentionally deferred: convex/concave-decreasing constraints not yet wired
191    ConvexDecreasing,
192    #[allow(dead_code)]
193    // intentionally deferred: convex/concave-decreasing constraints not yet wired
194    ConcaveDecreasing,
195}
196
197impl ConstraintBuilder {
198    /// Create a new constraint builder
199    pub fn new() -> Self {
200        Self {
201            constraints: Vec::new(),
202        }
203    }
204
205    /// Add an increasing constraint
206    pub fn increasing(mut self) -> Self {
207        self.constraints
208            .push(ConstraintSpec::Monotonic { increasing: true });
209        self
210    }
211
212    /// Add a decreasing constraint
213    pub fn decreasing(mut self) -> Self {
214        self.constraints
215            .push(ConstraintSpec::Monotonic { increasing: false });
216        self
217    }
218
219    /// Add a convex constraint
220    pub fn convex(mut self) -> Self {
221        self.constraints.push(ConstraintSpec::Convex);
222        self
223    }
224
225    /// Add a concave constraint
226    pub fn concave(mut self) -> Self {
227        self.constraints.push(ConstraintSpec::Concave);
228        self
229    }
230
231    /// Add piecewise constraints
232    pub fn piecewise(mut self, breakpoints: Vec<Float>, segments: Vec<bool>) -> Self {
233        self.constraints.push(ConstraintSpec::Piecewise {
234            breakpoints,
235            segments,
236        });
237        self
238    }
239
240    /// Build the constraint specification
241    pub fn build(self) -> Result<MonotonicityConstraint> {
242        if self.constraints.is_empty() {
243            return Ok(MonotonicityConstraint::Global { increasing: true });
244        }
245
246        // For now, take the last constraint (could be extended to combine constraints)
247        let last_constraint = self.constraints.last().ok_or_else(|| {
248            sklears_core::error::SklearsError::InvalidInput("No constraints specified".to_string())
249        })?;
250
251        match last_constraint {
252            ConstraintSpec::Monotonic { increasing } => Ok(MonotonicityConstraint::Global {
253                increasing: *increasing,
254            }),
255            ConstraintSpec::Piecewise {
256                breakpoints,
257                segments,
258            } => Ok(MonotonicityConstraint::Piecewise {
259                breakpoints: breakpoints.clone(),
260                segments: segments.clone(),
261            }),
262            ConstraintSpec::Convex => Ok(MonotonicityConstraint::Convex),
263            ConstraintSpec::Concave => Ok(MonotonicityConstraint::Concave),
264            ConstraintSpec::ConvexDecreasing => Ok(MonotonicityConstraint::ConvexDecreasing),
265            ConstraintSpec::ConcaveDecreasing => Ok(MonotonicityConstraint::ConcaveDecreasing),
266        }
267    }
268}
269
270impl Default for ConstraintBuilder {
271    fn default() -> Self {
272        Self::new()
273    }
274}
275
276/// Advanced builder pattern for complex isotonic regression problems
277#[derive(Debug, Clone)]
278/// ComplexIsotonicBuilder
279pub struct ComplexIsotonicBuilder {
280    loss_function: LossFunction,
281    constraint: MonotonicityConstraint,
282    bounds: Option<(Float, Float)>,
283    regularization: Option<RegularizationConfig>,
284    optimization: OptimizationConfig,
285    preprocessing: PreprocessingConfig,
286}
287
288#[derive(Debug, Clone)]
289/// RegularizationConfig
290pub struct RegularizationConfig {
291    /// l1_penalty
292    pub l1_penalty: Float,
293    /// l2_penalty
294    pub l2_penalty: Float,
295    /// smoothness_penalty
296    pub smoothness_penalty: Float,
297}
298
299#[derive(Debug, Clone)]
300/// OptimizationConfig
301pub struct OptimizationConfig {
302    /// max_iterations
303    pub max_iterations: usize,
304    /// tolerance
305    pub tolerance: Float,
306    /// early_stopping
307    pub early_stopping: bool,
308    /// warm_start
309    pub warm_start: bool,
310}
311
312#[derive(Debug, Clone)]
313/// PreprocessingConfig
314pub struct PreprocessingConfig {
315    /// normalize
316    pub normalize: bool,
317    /// standardize
318    pub standardize: bool,
319    /// remove_outliers
320    pub remove_outliers: bool,
321    /// outlier_threshold
322    pub outlier_threshold: Float,
323}
324
325impl ComplexIsotonicBuilder {
326    /// Create a new complex builder
327    pub fn new() -> Self {
328        Self {
329            loss_function: LossFunction::SquaredLoss,
330            constraint: MonotonicityConstraint::Global { increasing: true },
331            bounds: None,
332            regularization: None,
333            optimization: OptimizationConfig::default(),
334            preprocessing: PreprocessingConfig::default(),
335        }
336    }
337
338    /// Set the loss function
339    pub fn loss(mut self, loss: LossFunction) -> Self {
340        self.loss_function = loss;
341        self
342    }
343
344    /// Set the constraint
345    pub fn constraint(mut self, constraint: MonotonicityConstraint) -> Self {
346        self.constraint = constraint;
347        self
348    }
349
350    /// Set bounds
351    pub fn bounds(mut self, lower: Float, upper: Float) -> Self {
352        self.bounds = Some((lower, upper));
353        self
354    }
355
356    /// Configure regularization
357    pub fn regularization(mut self, config: RegularizationConfig) -> Self {
358        self.regularization = Some(config);
359        self
360    }
361
362    /// Configure optimization parameters
363    pub fn optimization(mut self, config: OptimizationConfig) -> Self {
364        self.optimization = config;
365        self
366    }
367
368    /// Configure preprocessing
369    pub fn preprocessing(mut self, config: PreprocessingConfig) -> Self {
370        self.preprocessing = config;
371        self
372    }
373
374    /// Enable L1 regularization (Lasso)
375    pub fn l1_regularization(mut self, penalty: Float) -> Self {
376        let mut reg = self.regularization.unwrap_or_default();
377        reg.l1_penalty = penalty;
378        self.regularization = Some(reg);
379        self
380    }
381
382    /// Enable L2 regularization (Ridge)
383    pub fn l2_regularization(mut self, penalty: Float) -> Self {
384        let mut reg = self.regularization.unwrap_or_default();
385        reg.l2_penalty = penalty;
386        self.regularization = Some(reg);
387        self
388    }
389
390    /// Enable smoothness regularization
391    pub fn smoothness_regularization(mut self, penalty: Float) -> Self {
392        let mut reg = self.regularization.unwrap_or_default();
393        reg.smoothness_penalty = penalty;
394        self.regularization = Some(reg);
395        self
396    }
397
398    /// Enable early stopping
399    pub fn early_stopping(mut self, enabled: bool) -> Self {
400        self.optimization.early_stopping = enabled;
401        self
402    }
403
404    /// Set convergence tolerance
405    pub fn tolerance(mut self, tol: Float) -> Self {
406        self.optimization.tolerance = tol;
407        self
408    }
409
410    /// Set maximum iterations
411    pub fn max_iterations(mut self, max_iter: usize) -> Self {
412        self.optimization.max_iterations = max_iter;
413        self
414    }
415
416    /// Enable data normalization
417    pub fn normalize(mut self) -> Self {
418        self.preprocessing.normalize = true;
419        self
420    }
421
422    /// Enable data standardization
423    pub fn standardize(mut self) -> Self {
424        self.preprocessing.standardize = true;
425        self
426    }
427
428    /// Enable outlier removal
429    pub fn remove_outliers(mut self, threshold: Float) -> Self {
430        self.preprocessing.remove_outliers = true;
431        self.preprocessing.outlier_threshold = threshold;
432        self
433    }
434
435    /// Build a fluent isotonic regression model
436    pub fn build(self) -> FluentIsotonicRegression<Untrained> {
437        let mut model = FluentIsotonicRegression::new();
438
439        // Set loss function
440        model.inner = model.inner.loss(self.loss_function);
441
442        // Set constraint
443        model.inner = model.inner.constraint(self.constraint);
444
445        // Set bounds
446        if let Some((lower, upper)) = self.bounds {
447            model.inner = model.inner.y_min(lower).y_max(upper);
448        }
449
450        model
451    }
452
453    /// Create a preset for financial modeling (bounded, robust)
454    pub fn financial_model() -> Self {
455        Self::new()
456            .loss(LossFunction::HuberLoss { delta: 1.35 })
457            .bounds(0.0, 1.0)
458            .l1_regularization(0.01)
459            .remove_outliers(3.0)
460            .early_stopping(true)
461    }
462
463    /// Create a preset for scientific data (high precision, no outlier removal)
464    pub fn scientific_model() -> Self {
465        Self::new()
466            .loss(LossFunction::SquaredLoss)
467            .tolerance(1e-10)
468            .max_iterations(10000)
469            .standardize()
470    }
471
472    /// Create a preset for web analytics (robust, fast)
473    pub fn web_analytics_model() -> Self {
474        Self::new()
475            .loss(LossFunction::AbsoluteLoss)
476            .remove_outliers(2.0)
477            .max_iterations(100)
478            .early_stopping(true)
479    }
480
481    /// Create a preset for medical data (conservative, bounded)
482    pub fn medical_model() -> Self {
483        Self::new()
484            .loss(LossFunction::HuberLoss { delta: 1.0 })
485            .bounds(0.0, 100.0)
486            .l2_regularization(0.1)
487            .standardize()
488            .tolerance(1e-8)
489    }
490}
491
492impl Default for ComplexIsotonicBuilder {
493    fn default() -> Self {
494        Self::new()
495    }
496}
497
498impl Default for RegularizationConfig {
499    fn default() -> Self {
500        Self {
501            l1_penalty: 0.0,
502            l2_penalty: 0.0,
503            smoothness_penalty: 0.0,
504        }
505    }
506}
507
508impl Default for OptimizationConfig {
509    fn default() -> Self {
510        Self {
511            max_iterations: 1000,
512            tolerance: 1e-6,
513            early_stopping: false,
514            warm_start: false,
515        }
516    }
517}
518
519impl Default for PreprocessingConfig {
520    fn default() -> Self {
521        Self {
522            normalize: false,
523            standardize: false,
524            remove_outliers: false,
525            outlier_threshold: 3.0,
526        }
527    }
528}
529
530/// Convenience functions for common configurations
531pub mod presets {
532    use super::*;
533
534    /// Create a standard increasing isotonic regression
535    pub fn increasing() -> FluentIsotonicRegression<Untrained> {
536        FluentIsotonicRegression::new().increasing()
537    }
538
539    /// Create a standard decreasing isotonic regression
540    pub fn decreasing() -> FluentIsotonicRegression<Untrained> {
541        FluentIsotonicRegression::new().decreasing()
542    }
543
544    /// Create a robust isotonic regression using Huber loss
545    pub fn robust() -> FluentIsotonicRegression<Untrained> {
546        FluentIsotonicRegression::new().increasing().robust()
547    }
548
549    /// Create a median regression model
550    pub fn median() -> FluentIsotonicRegression<Untrained> {
551        FluentIsotonicRegression::new()
552            .increasing()
553            .median_regression()
554    }
555
556    /// Create a probability-bounded isotonic regression
557    pub fn probability() -> FluentIsotonicRegression<Untrained> {
558        FluentIsotonicRegression::new()
559            .increasing()
560            .probability_bounds()
561    }
562
563    /// Create a non-negative increasing regression
564    pub fn non_negative() -> FluentIsotonicRegression<Untrained> {
565        FluentIsotonicRegression::new().increasing().non_negative()
566    }
567
568    /// Create a convex regression
569    pub fn convex() -> FluentIsotonicRegression<Untrained> {
570        FluentIsotonicRegression::new().convex()
571    }
572
573    /// Create a concave regression
574    pub fn concave() -> FluentIsotonicRegression<Untrained> {
575        FluentIsotonicRegression::new().concave()
576    }
577}
578
579#[allow(non_snake_case)]
580#[cfg(test)]
581mod tests {
582    use super::*;
583    use scirs2_core::ndarray::array;
584
585    #[test]
586    fn test_fluent_basic() -> Result<()> {
587        let x = array![1.0, 2.0, 3.0, 4.0, 5.0];
588        let y = array![1.0, 3.0, 2.0, 4.0, 5.0];
589
590        let model = FluentIsotonicRegression::new().increasing().squared_loss();
591
592        let fitted = model.fit(&x, &y)?;
593        let predictions = fitted.predict(&x)?;
594
595        assert_eq!(predictions.len(), 5);
596
597        // Check monotonicity
598        for i in 0..predictions.len() - 1 {
599            assert!(predictions[i] <= predictions[i + 1]);
600        }
601
602        Ok(())
603    }
604
605    #[test]
606    fn test_fluent_method_chaining() -> Result<()> {
607        let x = array![1.0, 2.0, 3.0, 4.0, 5.0];
608        let y = array![1.0, 3.0, 2.0, 4.0, 5.0];
609
610        let model = FluentIsotonicRegression::new()
611            .increasing()
612            .huber_loss(1.0)
613            .bounds(0.0, 10.0);
614
615        let fitted = model.fit(&x, &y)?;
616        let predictions = fitted.predict(&x)?;
617
618        // Check bounds
619        for &pred in predictions.iter() {
620            assert!((0.0..=10.0).contains(&pred));
621        }
622
623        Ok(())
624    }
625
626    #[test]
627    fn test_presets() -> Result<()> {
628        let x = array![1.0, 2.0, 3.0, 4.0, 5.0];
629        let y = array![0.1, 0.3, 0.2, 0.7, 0.9];
630
631        // Test probability preset
632        let model = presets::probability();
633        let fitted = model.fit(&x, &y)?;
634        let predictions = fitted.predict(&x)?;
635
636        // Check probability bounds
637        for &pred in predictions.iter() {
638            assert!((0.0..=1.0).contains(&pred));
639        }
640
641        Ok(())
642    }
643
644    #[test]
645    fn test_constraint_builder() -> Result<()> {
646        let constraint = ConstraintBuilder::new().increasing().build()?;
647
648        match constraint {
649            MonotonicityConstraint::Global { increasing } => assert!(increasing),
650            _ => panic!("Expected global increasing constraint"),
651        }
652
653        Ok(())
654    }
655
656    #[test]
657    fn test_robust_preset() -> Result<()> {
658        let x = array![1.0, 2.0, 3.0, 4.0, 5.0];
659        let y = array![1.0, 100.0, 2.0, 4.0, 5.0]; // outlier
660
661        let model = presets::robust();
662        let fitted = model.fit(&x, &y)?;
663        let predictions = fitted.predict(&x)?;
664
665        // Should handle outlier reasonably
666        assert!(predictions[1] < 50.0); // Should not be close to outlier value
667
668        Ok(())
669    }
670
671    #[test]
672    fn test_median_regression() -> Result<()> {
673        let x = array![1.0, 2.0, 3.0, 4.0, 5.0];
674        let y = array![1.0, 3.0, 2.0, 4.0, 5.0];
675
676        let model = presets::median();
677        let fitted = model.fit(&x, &y)?;
678        let predictions = fitted.predict(&x)?;
679
680        assert_eq!(predictions.len(), 5);
681
682        Ok(())
683    }
684
685    #[test]
686    fn test_convex_preset() -> Result<()> {
687        let x = array![1.0, 2.0, 3.0, 4.0, 5.0];
688        let y = array![1.0, 1.5, 3.0, 5.5, 9.0]; // convex-like data
689
690        let model = presets::convex();
691        let fitted = model.fit(&x, &y)?;
692        let predictions = fitted.predict(&x)?;
693
694        assert_eq!(predictions.len(), 5);
695
696        Ok(())
697    }
698
699    #[test]
700    fn test_complex_builder_basic() -> Result<()> {
701        let x = array![1.0, 2.0, 3.0, 4.0, 5.0];
702        let y = array![1.0, 3.0, 2.0, 4.0, 5.0];
703
704        let model = ComplexIsotonicBuilder::new()
705            .loss(LossFunction::HuberLoss { delta: 1.0 })
706            .bounds(0.0, 10.0)
707            .l1_regularization(0.01)
708            .early_stopping(true)
709            .build();
710
711        let fitted = model.fit(&x, &y)?;
712        let predictions = fitted.predict(&x)?;
713
714        assert_eq!(predictions.len(), 5);
715
716        Ok(())
717    }
718
719    #[test]
720    fn test_complex_builder_presets() -> Result<()> {
721        let x = array![1.0, 2.0, 3.0, 4.0, 5.0];
722        let y = array![0.1, 0.3, 0.2, 0.7, 0.9];
723
724        // Test financial model preset
725        let financial_model = ComplexIsotonicBuilder::financial_model().build();
726        let fitted = financial_model.fit(&x, &y)?;
727        let predictions = fitted.predict(&x)?;
728
729        assert_eq!(predictions.len(), 5);
730
731        // Test scientific model preset
732        let scientific_model = ComplexIsotonicBuilder::scientific_model().build();
733        let fitted = scientific_model.fit(&x, &y)?;
734        let predictions = fitted.predict(&x)?;
735
736        assert_eq!(predictions.len(), 5);
737
738        Ok(())
739    }
740
741    #[test]
742    fn test_complex_builder_method_chaining() -> Result<()> {
743        let x = array![1.0, 2.0, 3.0, 4.0, 5.0];
744        let y = array![1.0, 3.0, 2.0, 4.0, 5.0];
745
746        let model = ComplexIsotonicBuilder::new()
747            .standardize()
748            .remove_outliers(2.5)
749            .l2_regularization(0.1)
750            .tolerance(1e-8)
751            .max_iterations(500)
752            .build();
753
754        let fitted = model.fit(&x, &y)?;
755        let predictions = fitted.predict(&x)?;
756
757        assert_eq!(predictions.len(), 5);
758
759        Ok(())
760    }
761
762    #[test]
763    fn test_regularization_config() {
764        let config = RegularizationConfig::default();
765        assert_eq!(config.l1_penalty, 0.0);
766        assert_eq!(config.l2_penalty, 0.0);
767        assert_eq!(config.smoothness_penalty, 0.0);
768    }
769
770    #[test]
771    fn test_optimization_config() {
772        let config = OptimizationConfig::default();
773        assert_eq!(config.max_iterations, 1000);
774        assert_eq!(config.tolerance, 1e-6);
775        assert!(!config.early_stopping);
776        assert!(!config.warm_start);
777    }
778
779    #[test]
780    fn test_preprocessing_config() {
781        let config = PreprocessingConfig::default();
782        assert!(!config.normalize);
783        assert!(!config.standardize);
784        assert!(!config.remove_outliers);
785        assert_eq!(config.outlier_threshold, 3.0);
786    }
787}