1use 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#[derive(Debug, Clone)]
17pub struct FluentIsotonicRegression<State = Untrained> {
19 inner: IsotonicRegression<State>,
20}
21
22impl FluentIsotonicRegression<Untrained> {
23 pub fn new() -> Self {
25 Self {
26 inner: IsotonicRegression::new(),
27 }
28 }
29
30 pub fn increasing(mut self) -> Self {
32 self.inner = self.inner.increasing(true);
33 self
34 }
35
36 pub fn decreasing(mut self) -> Self {
38 self.inner = self.inner.increasing(false);
39 self
40 }
41
42 pub fn convex(mut self) -> Self {
44 self.inner = self.inner.convex();
45 self
46 }
47
48 pub fn concave(mut self) -> Self {
50 self.inner = self.inner.concave();
51 self
52 }
53
54 pub fn convex_decreasing(mut self) -> Self {
56 self.inner = self.inner.convex_decreasing();
57 self
58 }
59
60 pub fn concave_decreasing(mut self) -> Self {
62 self.inner = self.inner.concave_decreasing();
63 self
64 }
65
66 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 pub fn squared_loss(mut self) -> Self {
74 self.inner = self.inner.loss(LossFunction::SquaredLoss);
75 self
76 }
77
78 pub fn absolute_loss(mut self) -> Self {
80 self.inner = self.inner.loss(LossFunction::AbsoluteLoss);
81 self
82 }
83
84 pub fn huber_loss(mut self, delta: Float) -> Self {
86 self.inner = self.inner.loss(LossFunction::HuberLoss { delta });
87 self
88 }
89
90 pub fn quantile_loss(mut self, quantile: Float) -> Self {
92 self.inner = self.inner.loss(LossFunction::QuantileLoss { quantile });
93 self
94 }
95
96 pub fn lower_bound(mut self, bound: Float) -> Self {
98 self.inner = self.inner.y_min(bound);
99 self
100 }
101
102 pub fn upper_bound(mut self, bound: Float) -> Self {
104 self.inner = self.inner.y_max(bound);
105 self
106 }
107
108 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 pub fn probability_bounds(self) -> Self {
116 self.bounds(0.0, 1.0)
117 }
118
119 pub fn non_negative(self) -> Self {
121 self.lower_bound(0.0)
122 }
123
124 pub fn robust(self) -> Self {
126 self.huber_loss(1.35) }
128
129 pub fn median_regression(self) -> Self {
131 self.quantile_loss(0.5)
132 }
133
134 pub fn percentile_90(self) -> Self {
136 self.quantile_loss(0.9)
137 }
138
139 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
151impl 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#[derive(Debug, Clone)]
173pub 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 ConvexDecreasing,
192 #[allow(dead_code)]
193 ConcaveDecreasing,
195}
196
197impl ConstraintBuilder {
198 pub fn new() -> Self {
200 Self {
201 constraints: Vec::new(),
202 }
203 }
204
205 pub fn increasing(mut self) -> Self {
207 self.constraints
208 .push(ConstraintSpec::Monotonic { increasing: true });
209 self
210 }
211
212 pub fn decreasing(mut self) -> Self {
214 self.constraints
215 .push(ConstraintSpec::Monotonic { increasing: false });
216 self
217 }
218
219 pub fn convex(mut self) -> Self {
221 self.constraints.push(ConstraintSpec::Convex);
222 self
223 }
224
225 pub fn concave(mut self) -> Self {
227 self.constraints.push(ConstraintSpec::Concave);
228 self
229 }
230
231 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 pub fn build(self) -> Result<MonotonicityConstraint> {
242 if self.constraints.is_empty() {
243 return Ok(MonotonicityConstraint::Global { increasing: true });
244 }
245
246 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#[derive(Debug, Clone)]
278pub 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)]
289pub struct RegularizationConfig {
291 pub l1_penalty: Float,
293 pub l2_penalty: Float,
295 pub smoothness_penalty: Float,
297}
298
299#[derive(Debug, Clone)]
300pub struct OptimizationConfig {
302 pub max_iterations: usize,
304 pub tolerance: Float,
306 pub early_stopping: bool,
308 pub warm_start: bool,
310}
311
312#[derive(Debug, Clone)]
313pub struct PreprocessingConfig {
315 pub normalize: bool,
317 pub standardize: bool,
319 pub remove_outliers: bool,
321 pub outlier_threshold: Float,
323}
324
325impl ComplexIsotonicBuilder {
326 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 pub fn loss(mut self, loss: LossFunction) -> Self {
340 self.loss_function = loss;
341 self
342 }
343
344 pub fn constraint(mut self, constraint: MonotonicityConstraint) -> Self {
346 self.constraint = constraint;
347 self
348 }
349
350 pub fn bounds(mut self, lower: Float, upper: Float) -> Self {
352 self.bounds = Some((lower, upper));
353 self
354 }
355
356 pub fn regularization(mut self, config: RegularizationConfig) -> Self {
358 self.regularization = Some(config);
359 self
360 }
361
362 pub fn optimization(mut self, config: OptimizationConfig) -> Self {
364 self.optimization = config;
365 self
366 }
367
368 pub fn preprocessing(mut self, config: PreprocessingConfig) -> Self {
370 self.preprocessing = config;
371 self
372 }
373
374 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 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 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 pub fn early_stopping(mut self, enabled: bool) -> Self {
400 self.optimization.early_stopping = enabled;
401 self
402 }
403
404 pub fn tolerance(mut self, tol: Float) -> Self {
406 self.optimization.tolerance = tol;
407 self
408 }
409
410 pub fn max_iterations(mut self, max_iter: usize) -> Self {
412 self.optimization.max_iterations = max_iter;
413 self
414 }
415
416 pub fn normalize(mut self) -> Self {
418 self.preprocessing.normalize = true;
419 self
420 }
421
422 pub fn standardize(mut self) -> Self {
424 self.preprocessing.standardize = true;
425 self
426 }
427
428 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 pub fn build(self) -> FluentIsotonicRegression<Untrained> {
437 let mut model = FluentIsotonicRegression::new();
438
439 model.inner = model.inner.loss(self.loss_function);
441
442 model.inner = model.inner.constraint(self.constraint);
444
445 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 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 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 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 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
530pub mod presets {
532 use super::*;
533
534 pub fn increasing() -> FluentIsotonicRegression<Untrained> {
536 FluentIsotonicRegression::new().increasing()
537 }
538
539 pub fn decreasing() -> FluentIsotonicRegression<Untrained> {
541 FluentIsotonicRegression::new().decreasing()
542 }
543
544 pub fn robust() -> FluentIsotonicRegression<Untrained> {
546 FluentIsotonicRegression::new().increasing().robust()
547 }
548
549 pub fn median() -> FluentIsotonicRegression<Untrained> {
551 FluentIsotonicRegression::new()
552 .increasing()
553 .median_regression()
554 }
555
556 pub fn probability() -> FluentIsotonicRegression<Untrained> {
558 FluentIsotonicRegression::new()
559 .increasing()
560 .probability_bounds()
561 }
562
563 pub fn non_negative() -> FluentIsotonicRegression<Untrained> {
565 FluentIsotonicRegression::new().increasing().non_negative()
566 }
567
568 pub fn convex() -> FluentIsotonicRegression<Untrained> {
570 FluentIsotonicRegression::new().convex()
571 }
572
573 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 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 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 let model = presets::probability();
633 let fitted = model.fit(&x, &y)?;
634 let predictions = fitted.predict(&x)?;
635
636 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]; let model = presets::robust();
662 let fitted = model.fit(&x, &y)?;
663 let predictions = fitted.predict(&x)?;
664
665 assert!(predictions[1] < 50.0); 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]; 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 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 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}