1use crate::error::{StatsError, StatsResult};
4use crate::regression::stat_tests::{f_test_p_value, t_test_p_value};
5use crate::regression::utils::*;
6use crate::regression::RegressionResults;
7use scirs2_core::ndarray::{s, Array1, Array2, ArrayView1, ArrayView2};
8use scirs2_core::numeric::Float;
9use scirs2_linalg::{inv, lstsq};
10use std::collections::HashSet;
11
12type PreprocessingResult<F> = (Array2<F>, F, Array1<F>, Array1<F>);
14
15#[allow(clippy::too_many_arguments)]
60#[allow(dead_code)]
61pub fn ridge_regression<F>(
62 x: &ArrayView2<F>,
63 y: &ArrayView1<F>,
64 alpha: Option<F>,
65 fit_intercept: Option<bool>,
66 normalize: Option<bool>,
67 tol: Option<F>,
68 max_iter: Option<usize>,
69 conf_level: Option<F>,
70) -> StatsResult<RegressionResults<F>>
71where
72 F: Float
73 + std::iter::Sum<F>
74 + std::ops::Div<Output = F>
75 + std::fmt::Debug
76 + std::fmt::Display
77 + 'static
78 + scirs2_core::numeric::NumAssign
79 + scirs2_core::numeric::One
80 + scirs2_core::ndarray::ScalarOperand
81 + Send
82 + Sync,
83{
84 if x.nrows() != y.len() {
86 return Err(StatsError::DimensionMismatch(format!(
87 "Input x has {} rows but y has length {}",
88 x.nrows(),
89 y.len()
90 )));
91 }
92
93 let n = x.nrows();
94 let p_features = x.ncols();
95
96 let alpha = alpha.unwrap_or_else(|| F::from(1.0).expect("Failed to convert constant to float"));
98 let fit_intercept = fit_intercept.unwrap_or(true);
99 let normalize = normalize.unwrap_or(false);
100 let tol = tol.unwrap_or_else(|| F::from(1e-4).expect("Failed to convert constant to float"));
101 let max_iter = max_iter.unwrap_or(1000);
102 let conf_level =
103 conf_level.unwrap_or_else(|| F::from(0.95).expect("Failed to convert constant to float"));
104
105 if alpha < F::zero() {
106 return Err(StatsError::InvalidArgument(
107 "alpha must be non-negative".to_string(),
108 ));
109 }
110
111 let (x_processed, _y_mean, x_mean, x_std) = preprocessdata(x, y, fit_intercept, normalize)?;
118
119 let p = if fit_intercept {
121 p_features + 1
122 } else {
123 p_features
124 };
125
126 if n < 2 {
128 return Err(StatsError::InvalidArgument(
129 "At least 2 observations required for ridge regression".to_string(),
130 ));
131 }
132
133 let ridgesize = if fit_intercept { p_features } else { p };
138 let mut x_ridge = Array2::zeros((n + ridgesize, p));
139
140 for i in 0..n {
142 for j in 0..p {
143 x_ridge[[i, j]] = x_processed[[i, j]];
144 }
145 }
146
147 let sqrt_alpha = scirs2_core::numeric::Float::sqrt(alpha);
149 for i in 0..ridgesize {
150 let j = if fit_intercept { i + 1 } else { i }; x_ridge[[n + i, j]] = sqrt_alpha;
152 }
153
154 let mut y_ridge = Array1::zeros(n + ridgesize);
156 for i in 0..n {
157 y_ridge[i] = y[i];
158 }
159
160 let coefficients = solve_ridge_system(&x_ridge.view(), &y_ridge.view(), tol, max_iter)?;
162
163 let transformed_coefficients = if normalize || fit_intercept {
165 transform_coefficients(&coefficients, &x_mean, &x_std, fit_intercept)
166 } else {
167 coefficients.clone()
168 };
169
170 let x_design = if fit_intercept {
172 add_intercept(x)
173 } else {
174 x.to_owned()
175 };
176
177 let fitted_values = x_design.dot(&transformed_coefficients);
178 let residuals = y.to_owned() - &fitted_values;
179
180 let df_model = p - 1; let df_residuals = n - p;
183
184 let (_y_mean, ss_total, ss_residual, ss_explained) =
186 calculate_sum_of_squares(y, &residuals.view());
187
188 let r_squared = ss_explained / ss_total;
190 let adj_r_squared = F::one()
191 - (F::one() - r_squared) * F::from(n - 1).expect("Failed to convert to float")
192 / F::from(df_residuals).expect("Failed to convert to float");
193
194 let mse = ss_residual / F::from(df_residuals).expect("Failed to convert to float");
196 let residual_std_error = scirs2_core::numeric::Float::sqrt(mse);
197
198 let std_errors = match calculate_ridge_std_errors(
200 &x_design.view(),
201 &residuals.view(),
202 alpha,
203 df_residuals,
204 ) {
205 Ok(se) => se,
206 Err(_) => Array1::<F>::zeros(p),
207 };
208
209 let t_values = calculate_t_values(&transformed_coefficients, &std_errors);
211
212 let p_values = t_values.mapv(|t| t_test_p_value(t, df_residuals));
215
216 let mut conf_intervals = Array2::<F>::zeros((p, 2));
218 let z = norm_ppf(
219 F::from(0.5).expect("Failed to convert constant to float") * (F::one() + conf_level),
220 );
221
222 for i in 0..p {
223 let margin = std_errors[i] * z;
224 conf_intervals[[i, 0]] = transformed_coefficients[i] - margin;
225 conf_intervals[[i, 1]] = transformed_coefficients[i] + margin;
226 }
227
228 let f_statistic = if df_model > 0 && df_residuals > 0 {
230 (ss_explained / F::from(df_model).expect("Failed to convert to float"))
231 / (ss_residual / F::from(df_residuals).expect("Failed to convert to float"))
232 } else {
233 F::infinity()
234 };
235
236 let f_p_value = f_test_p_value(f_statistic, df_model, df_residuals);
239
240 Ok(RegressionResults {
242 coefficients: transformed_coefficients,
243 std_errors,
244 t_values,
245 p_values,
246 conf_intervals,
247 r_squared,
248 adj_r_squared,
249 f_statistic,
250 f_p_value,
251 residual_std_error,
252 df_residuals,
253 residuals,
254 fitted_values,
255 inlier_mask: vec![true; n], })
257}
258
259#[allow(dead_code)]
261fn solve_ridge_system<F>(
262 x_ridge: &ArrayView2<F>,
263 y_ridge: &ArrayView1<F>,
264 _tol: F,
265 _max_iter: usize,
266) -> StatsResult<Array1<F>>
267where
268 F: Float
269 + std::iter::Sum<F>
270 + std::ops::Div<Output = F>
271 + 'static
272 + scirs2_core::numeric::NumAssign
273 + scirs2_core::numeric::One
274 + scirs2_core::ndarray::ScalarOperand
275 + std::fmt::Display
276 + Send
277 + Sync,
278{
279 match lstsq(x_ridge, y_ridge, None) {
280 Ok(result) => Ok(result.x),
281 Err(e) => Err(StatsError::ComputationError(format!(
282 "Least squares computation failed: {:?}",
283 e
284 ))),
285 }
286}
287
288#[allow(dead_code)]
290fn preprocessdata<F>(
291 x: &ArrayView2<F>,
292 y: &ArrayView1<F>,
293 fit_intercept: bool,
294 normalize: bool,
295) -> StatsResult<PreprocessingResult<F>>
296where
297 F: Float + std::iter::Sum<F> + 'static + std::fmt::Display,
298{
299 let n = x.nrows();
300 let p = x.ncols();
301
302 let y_mean = if fit_intercept {
304 y.iter().cloned().sum::<F>() / F::from(n).expect("Failed to convert to float")
305 } else {
306 F::zero()
307 };
308
309 let mut x_mean = Array1::<F>::zeros(p);
311 let mut x_std = Array1::<F>::ones(p);
312
313 if fit_intercept || normalize {
314 for j in 0..p {
315 let col = x.column(j);
316 let mean =
317 col.iter().cloned().sum::<F>() / F::from(n).expect("Failed to convert to float");
318 x_mean[j] = mean;
319
320 if normalize {
321 let mut ss = F::zero();
322 for &val in col {
323 ss = ss + scirs2_core::numeric::Float::powi(val - mean, 2);
324 }
325 let std_dev = scirs2_core::numeric::Float::sqrt(
326 ss / F::from(n).expect("Failed to convert to float"),
327 );
328 x_std[j] = if std_dev > F::epsilon() {
329 std_dev
330 } else {
331 F::one()
332 };
333 }
334 }
335 }
336
337 let mut x_processed = if fit_intercept {
339 Array2::<F>::zeros((n, p + 1))
340 } else {
341 Array2::<F>::zeros((n, p))
342 };
343
344 if fit_intercept {
346 for i in 0..n {
347 x_processed[[i, 0]] = F::one();
348 }
349 }
350
351 let offset = if fit_intercept { 1 } else { 0 };
353 for i in 0..n {
354 for j in 0..p {
355 let val = if normalize || fit_intercept {
356 (x[[i, j]] - x_mean[j]) / x_std[j]
357 } else {
358 x[[i, j]]
359 };
360 x_processed[[i, j + offset]] = val;
361 }
362 }
363
364 Ok((x_processed, y_mean, x_mean, x_std))
365}
366
367#[allow(dead_code)]
382fn transform_coefficients<F>(
383 coefficients: &Array1<F>,
384 x_mean: &Array1<F>,
385 x_std: &Array1<F>,
386 fit_intercept: bool,
387) -> Array1<F>
388where
389 F: Float + 'static + std::fmt::Display,
390{
391 let _p = coefficients.len();
392 let p_features = x_mean.len();
393
394 let mut transformed = coefficients.clone();
395
396 if fit_intercept {
397 let mut _intercept = coefficients[0];
398
399 for j in 0..p_features {
401 _intercept = _intercept - coefficients[j + 1] * x_mean[j] / x_std[j];
402 }
403
404 transformed[0] = _intercept;
405
406 for j in 0..p_features {
408 transformed[j + 1] = coefficients[j + 1] / x_std[j];
409 }
410 } else {
411 for j in 0..p_features {
413 transformed[j] = coefficients[j] / x_std[j];
414 }
415 }
416
417 transformed
418}
419
420#[allow(dead_code)]
422fn calculate_ridge_std_errors<F>(
423 x: &ArrayView2<F>,
424 residuals: &ArrayView1<F>,
425 alpha: F,
426 df: usize,
427) -> StatsResult<Array1<F>>
428where
429 F: Float
430 + std::iter::Sum<F>
431 + std::ops::Div<Output = F>
432 + 'static
433 + scirs2_core::numeric::NumAssign
434 + scirs2_core::numeric::One
435 + scirs2_core::ndarray::ScalarOperand
436 + std::fmt::Display
437 + Send
438 + Sync,
439{
440 let mse = residuals
442 .iter()
443 .map(|&r| scirs2_core::numeric::Float::powi(r, 2))
444 .sum::<F>()
445 / F::from(df).expect("Failed to convert to float");
446
447 let xtx = x.t().dot(x);
449
450 let p = x.ncols();
452 let mut xtx_reg = xtx.clone();
453
454 for i in 0..p {
455 xtx_reg[[i, i]] += alpha;
456 }
457
458 let xtx_reg_inv = match inv(&xtx_reg.view(), None) {
460 Ok(inv_result) => inv_result,
461 Err(_) => {
462 return Ok(Array1::<F>::zeros(p));
464 }
465 };
466
467 let std_errors = (xtx_reg_inv.dot(&xtx).dot(&xtx_reg_inv))
470 .diag()
471 .mapv(|v| scirs2_core::numeric::Float::sqrt(v * mse));
472
473 Ok(std_errors)
474}
475
476#[allow(clippy::too_many_arguments)]
528#[allow(dead_code)]
529pub fn lasso_regression<F>(
530 x: &ArrayView2<F>,
531 y: &ArrayView1<F>,
532 alpha: Option<F>,
533 fit_intercept: Option<bool>,
534 normalize: Option<bool>,
535 tol: Option<F>,
536 max_iter: Option<usize>,
537 conf_level: Option<F>,
538) -> StatsResult<RegressionResults<F>>
539where
540 F: Float
541 + std::iter::Sum<F>
542 + std::ops::Div<Output = F>
543 + std::fmt::Debug
544 + std::fmt::Display
545 + 'static
546 + scirs2_core::numeric::NumAssign
547 + scirs2_core::numeric::One
548 + scirs2_core::ndarray::ScalarOperand
549 + Send
550 + Sync,
551{
552 if x.nrows() != y.len() {
554 return Err(StatsError::DimensionMismatch(format!(
555 "Input x has {} rows but y has length {}",
556 x.nrows(),
557 y.len()
558 )));
559 }
560
561 let n = x.nrows();
562 let p_features = x.ncols();
563
564 let alpha = alpha.unwrap_or_else(|| F::from(1.0).expect("Failed to convert constant to float"));
566 let fit_intercept = fit_intercept.unwrap_or(true);
567 let normalize = normalize.unwrap_or(false);
568 let tol = tol.unwrap_or_else(|| F::from(1e-4).expect("Failed to convert constant to float"));
569 let max_iter = max_iter.unwrap_or(1000);
570 let conf_level =
571 conf_level.unwrap_or_else(|| F::from(0.95).expect("Failed to convert constant to float"));
572
573 if alpha < F::zero() {
574 return Err(StatsError::InvalidArgument(
575 "alpha must be non-negative".to_string(),
576 ));
577 }
578
579 let (x_processed, _y_mean, x_mean, x_std) = preprocessdata(x, y, fit_intercept, normalize)?;
586
587 let p = if fit_intercept {
589 p_features + 1
590 } else {
591 p_features
592 };
593
594 if n < 2 {
596 return Err(StatsError::InvalidArgument(
597 "At least 2 observations required for lasso regression".to_string(),
598 ));
599 }
600
601 let mut coefficients = Array1::<F>::zeros(p);
603
604 let xtx = x_processed.t().dot(&x_processed);
606 let xty = x_processed.t().dot(y);
607
608 let mut converged = false;
610 let mut _iter = 0;
611
612 while !converged && _iter < max_iter {
613 converged = true;
614
615 let old_coefs = coefficients.clone();
617
618 for j in 0..p {
620 let r_partial = xty[j]
622 - xtx
623 .row(j)
624 .iter()
625 .zip(coefficients.iter())
626 .enumerate()
627 .filter(|&(i_, _)| i_ != j)
628 .map(|(_, (&xtx_ij, &coef_i))| xtx_ij * coef_i)
629 .sum::<F>();
630
631 let xtx_jj = xtx[[j, j]];
633 if xtx_jj < F::epsilon() {
634 coefficients[j] = F::zero();
635 continue;
636 }
637
638 if j == 0 && fit_intercept {
639 coefficients[j] = r_partial / xtx_jj;
641 } else {
642 if crate::regression::utils::float_abs(r_partial) <= alpha {
644 coefficients[j] = F::zero();
645 } else if r_partial > F::zero() {
646 coefficients[j] = (r_partial - alpha) / xtx_jj;
647 } else {
648 coefficients[j] = (r_partial + alpha) / xtx_jj;
649 }
650 }
651 }
652
653 let coef_diff = (&coefficients - &old_coefs)
655 .mapv(|x| scirs2_core::numeric::Float::abs(x))
656 .sum();
657 let coef_norm = old_coefs
658 .mapv(|x| scirs2_core::numeric::Float::abs(x))
659 .sum()
660 .max(F::epsilon());
661
662 if coef_diff / coef_norm < tol {
663 converged = true;
664 }
665
666 _iter += 1;
667 }
668
669 let transformed_coefficients = if normalize || fit_intercept {
671 transform_coefficients(&coefficients, &x_mean, &x_std, fit_intercept)
672 } else {
673 coefficients.clone()
674 };
675
676 let x_design = if fit_intercept {
678 add_intercept(x)
679 } else {
680 x.to_owned()
681 };
682
683 let fitted_values = x_design.dot(&transformed_coefficients);
684 let residuals = y.to_owned() - &fitted_values;
685
686 let nonzero_coefs = transformed_coefficients
689 .iter()
690 .filter(|&&x| crate::regression::utils::float_abs(x) > F::epsilon())
691 .count();
692 let df_model = nonzero_coefs - if fit_intercept { 1 } else { 0 };
693 let df_residuals = n - nonzero_coefs;
694
695 let (_y_mean, ss_total, ss_residual, ss_explained) =
697 calculate_sum_of_squares(y, &residuals.view());
698
699 let r_squared = ss_explained / ss_total;
701 let adj_r_squared = F::one()
702 - (F::one() - r_squared) * F::from(n - 1).expect("Failed to convert to float")
703 / F::from(df_residuals).expect("Failed to convert to float");
704
705 let mse = ss_residual / F::from(df_residuals).expect("Failed to convert to float");
707 let residual_std_error = scirs2_core::numeric::Float::sqrt(mse);
708
709 let std_errors = match calculate_lasso_std_errors(
711 &x_design.view(),
712 &residuals.view(),
713 &transformed_coefficients,
714 df_residuals,
715 ) {
716 Ok(se) => se,
717 Err(_) => Array1::<F>::zeros(p),
718 };
719
720 let t_values = calculate_t_values(&transformed_coefficients, &std_errors);
722
723 let p_values = t_values.mapv(|t| t_test_p_value(t, df_residuals));
726
727 let mut conf_intervals = Array2::<F>::zeros((p, 2));
729 let z = norm_ppf(
730 F::from(0.5).expect("Failed to convert constant to float") * (F::one() + conf_level),
731 );
732
733 for i in 0..p {
734 let margin = std_errors[i] * z;
735 conf_intervals[[i, 0]] = transformed_coefficients[i] - margin;
736 conf_intervals[[i, 1]] = transformed_coefficients[i] + margin;
737 }
738
739 let f_statistic = if df_model > 0 && df_residuals > 0 {
741 (ss_explained / F::from(df_model).expect("Failed to convert to float"))
742 / (ss_residual / F::from(df_residuals).expect("Failed to convert to float"))
743 } else {
744 F::infinity()
745 };
746
747 let f_p_value = f_test_p_value(f_statistic, df_model, df_residuals);
750
751 Ok(RegressionResults {
753 coefficients: transformed_coefficients,
754 std_errors,
755 t_values,
756 p_values,
757 conf_intervals,
758 r_squared,
759 adj_r_squared,
760 f_statistic,
761 f_p_value,
762 residual_std_error,
763 df_residuals,
764 residuals,
765 fitted_values,
766 inlier_mask: vec![true; n], })
768}
769
770#[allow(dead_code)]
772fn calculate_lasso_std_errors<F>(
773 x: &ArrayView2<F>,
774 residuals: &ArrayView1<F>,
775 coefficients: &Array1<F>,
776 df: usize,
777) -> StatsResult<Array1<F>>
778where
779 F: Float
780 + std::iter::Sum<F>
781 + std::ops::Div<Output = F>
782 + 'static
783 + scirs2_core::numeric::NumAssign
784 + scirs2_core::numeric::One
785 + scirs2_core::ndarray::ScalarOperand
786 + std::fmt::Display
787 + Send
788 + Sync,
789{
790 let mse = residuals
792 .iter()
793 .map(|&r| scirs2_core::numeric::Float::powi(r, 2))
794 .sum::<F>()
795 / F::from(df).expect("Failed to convert to float");
796
797 let p = coefficients.len();
799 let mut active_set = Vec::new();
800
801 for j in 0..p {
802 if crate::regression::utils::float_abs(coefficients[j]) > F::epsilon() {
803 active_set.push(j);
804 }
805 }
806
807 if active_set.is_empty() {
809 return Ok(Array1::<F>::zeros(p));
810 }
811
812 let n_active = active_set.len();
814 let mut xtx_active = Array2::<F>::zeros((n_active, n_active));
815
816 for (i, &idx_i) in active_set.iter().enumerate() {
817 for (j, &idx_j) in active_set.iter().enumerate() {
818 let x_i = x.column(idx_i);
819 let x_j = x.column(idx_j);
820
821 xtx_active[[i, j]] = x_i.iter().zip(x_j.iter()).map(|(&xi, &xj)| xi * xj).sum();
822 }
823 }
824
825 let xtx_active_inv = match inv(&xtx_active.view(), None) {
827 Ok(inv_result) => inv_result,
828 Err(_) => {
829 return Ok(Array1::<F>::zeros(p));
831 }
832 };
833
834 let mut std_errors = Array1::<F>::zeros(p);
836
837 for (i, &idx) in active_set.iter().enumerate() {
838 std_errors[idx] = scirs2_core::numeric::Float::sqrt(xtx_active_inv[[i, i]] * mse);
839 }
840
841 Ok(std_errors)
842}
843
844#[allow(clippy::too_many_arguments)]
895#[allow(dead_code)]
896pub fn elastic_net<F>(
897 x: &ArrayView2<F>,
898 y: &ArrayView1<F>,
899 alpha: Option<F>,
900 l1_ratio: Option<F>,
901 fit_intercept: Option<bool>,
902 normalize: Option<bool>,
903 tol: Option<F>,
904 max_iter: Option<usize>,
905 conf_level: Option<F>,
906) -> StatsResult<RegressionResults<F>>
907where
908 F: Float
909 + std::iter::Sum<F>
910 + std::ops::Div<Output = F>
911 + std::fmt::Debug
912 + std::fmt::Display
913 + 'static
914 + scirs2_core::numeric::NumAssign
915 + scirs2_core::numeric::One
916 + scirs2_core::ndarray::ScalarOperand
917 + Send
918 + Sync,
919{
920 if x.nrows() != y.len() {
922 return Err(StatsError::DimensionMismatch(format!(
923 "Input x has {} rows but y has length {}",
924 x.nrows(),
925 y.len()
926 )));
927 }
928
929 let n = x.nrows();
930 let p_features = x.ncols();
931
932 let alpha = alpha.unwrap_or_else(|| F::from(1.0).expect("Failed to convert constant to float"));
934 let l1_ratio =
935 l1_ratio.unwrap_or_else(|| F::from(0.5).expect("Failed to convert constant to float"));
936 let fit_intercept = fit_intercept.unwrap_or(true);
937 let normalize = normalize.unwrap_or(false);
938 let tol = tol.unwrap_or_else(|| F::from(1e-4).expect("Failed to convert constant to float"));
939 let max_iter = max_iter.unwrap_or(1000);
940 let conf_level =
941 conf_level.unwrap_or_else(|| F::from(0.95).expect("Failed to convert constant to float"));
942
943 if alpha < F::zero() {
944 return Err(StatsError::InvalidArgument(
945 "alpha must be non-negative".to_string(),
946 ));
947 }
948
949 if l1_ratio < F::zero() || l1_ratio > F::one() {
950 return Err(StatsError::InvalidArgument(
951 "l1_ratio must be between 0 and 1".to_string(),
952 ));
953 }
954
955 if l1_ratio < F::epsilon() {
957 return ridge_regression(
958 x,
959 y,
960 Some(alpha),
961 Some(fit_intercept),
962 Some(normalize),
963 Some(tol),
964 Some(max_iter),
965 Some(conf_level),
966 );
967 }
968
969 if crate::regression::utils::float_abs(l1_ratio - F::one()) < F::epsilon() {
971 return lasso_regression(
972 x,
973 y,
974 Some(alpha),
975 Some(fit_intercept),
976 Some(normalize),
977 Some(tol),
978 Some(max_iter),
979 Some(conf_level),
980 );
981 }
982
983 let (x_processed, _y_mean, x_mean, x_std) = preprocessdata(x, y, fit_intercept, normalize)?;
990
991 let p = if fit_intercept {
993 p_features + 1
994 } else {
995 p_features
996 };
997
998 if n < 2 {
1000 return Err(StatsError::InvalidArgument(
1001 "At least 2 observations required for elastic net regression".to_string(),
1002 ));
1003 }
1004
1005 let mut coefficients = Array1::<F>::zeros(p);
1007
1008 let xtx = x_processed.t().dot(&x_processed);
1010 let xty = x_processed.t().dot(y);
1011
1012 let alpha_l1 = alpha * l1_ratio;
1014 let one_minus_l1_ratio = F::one() - l1_ratio;
1015 let alpha_l2 = alpha * one_minus_l1_ratio;
1016
1017 let mut converged = false;
1019 let mut _iter = 0;
1020
1021 while !converged && _iter < max_iter {
1022 converged = true;
1023
1024 let old_coefs = coefficients.clone();
1026
1027 for j in 0..p {
1029 let r_partial = xty[j]
1031 - xtx
1032 .row(j)
1033 .iter()
1034 .zip(coefficients.iter())
1035 .enumerate()
1036 .filter(|&(i_, _)| i_ != j)
1037 .map(|(_, (&xtx_ij, &coef_i))| xtx_ij * coef_i)
1038 .sum::<F>();
1039
1040 let xtx_jj = xtx[[j, j]] + alpha_l2;
1042 if xtx_jj < F::epsilon() {
1043 coefficients[j] = F::zero();
1044 continue;
1045 }
1046
1047 if j == 0 && fit_intercept {
1048 coefficients[j] = r_partial / xtx_jj;
1050 } else {
1051 if crate::regression::utils::float_abs(r_partial) <= alpha_l1 {
1053 coefficients[j] = F::zero();
1054 } else if r_partial > F::zero() {
1055 coefficients[j] = (r_partial - alpha_l1) / xtx_jj;
1056 } else {
1057 coefficients[j] = (r_partial + alpha_l1) / xtx_jj;
1058 }
1059 }
1060 }
1061
1062 let coef_diff = (&coefficients - &old_coefs)
1064 .mapv(|x| scirs2_core::numeric::Float::abs(x))
1065 .sum();
1066 let coef_norm = old_coefs
1067 .mapv(|x| scirs2_core::numeric::Float::abs(x))
1068 .sum()
1069 .max(F::epsilon());
1070
1071 if coef_diff / coef_norm < tol {
1072 converged = true;
1073 }
1074
1075 _iter += 1;
1076 }
1077
1078 let transformed_coefficients = if normalize || fit_intercept {
1080 transform_coefficients(&coefficients, &x_mean, &x_std, fit_intercept)
1081 } else {
1082 coefficients.clone()
1083 };
1084
1085 let x_design = if fit_intercept {
1087 add_intercept(x)
1088 } else {
1089 x.to_owned()
1090 };
1091
1092 let fitted_values = x_design.dot(&transformed_coefficients);
1093 let residuals = y.to_owned() - &fitted_values;
1094
1095 let nonzero_coefs = transformed_coefficients
1098 .iter()
1099 .filter(|&&x| crate::regression::utils::float_abs(x) > F::epsilon())
1100 .count();
1101 let df_model = nonzero_coefs - if fit_intercept { 1 } else { 0 };
1102 let df_residuals = n - nonzero_coefs;
1103
1104 let (_y_mean, ss_total, ss_residual, ss_explained) =
1106 calculate_sum_of_squares(y, &residuals.view());
1107
1108 let r_squared = ss_explained / ss_total;
1110 let adj_r_squared = F::one()
1111 - (F::one() - r_squared) * F::from(n - 1).expect("Failed to convert to float")
1112 / F::from(df_residuals).expect("Failed to convert to float");
1113
1114 let mse = ss_residual / F::from(df_residuals).expect("Failed to convert to float");
1116 let residual_std_error = scirs2_core::numeric::Float::sqrt(mse);
1117
1118 let std_errors = match calculate_elastic_net_std_errors(
1120 &x_design.view(),
1121 &residuals.view(),
1122 &transformed_coefficients,
1123 alpha_l2,
1124 df_residuals,
1125 ) {
1126 Ok(se) => se,
1127 Err(_) => Array1::<F>::zeros(p),
1128 };
1129
1130 let t_values = calculate_t_values(&transformed_coefficients, &std_errors);
1132
1133 let p_values = t_values.mapv(|t| t_test_p_value(t, df_residuals));
1136
1137 let mut conf_intervals = Array2::<F>::zeros((p, 2));
1139 let z = norm_ppf(
1140 F::from(0.5).expect("Failed to convert constant to float") * (F::one() + conf_level),
1141 );
1142
1143 for i in 0..p {
1144 let margin = std_errors[i] * z;
1145 conf_intervals[[i, 0]] = transformed_coefficients[i] - margin;
1146 conf_intervals[[i, 1]] = transformed_coefficients[i] + margin;
1147 }
1148
1149 let f_statistic = if df_model > 0 && df_residuals > 0 {
1151 (ss_explained / F::from(df_model).expect("Failed to convert to float"))
1152 / (ss_residual / F::from(df_residuals).expect("Failed to convert to float"))
1153 } else {
1154 F::infinity()
1155 };
1156
1157 let f_p_value = f_test_p_value(f_statistic, df_model, df_residuals);
1160
1161 Ok(RegressionResults {
1163 coefficients: transformed_coefficients,
1164 std_errors,
1165 t_values,
1166 p_values,
1167 conf_intervals,
1168 r_squared,
1169 adj_r_squared,
1170 f_statistic,
1171 f_p_value,
1172 residual_std_error,
1173 df_residuals,
1174 residuals,
1175 fitted_values,
1176 inlier_mask: vec![true; n], })
1178}
1179
1180#[allow(dead_code)]
1182fn calculate_elastic_net_std_errors<F>(
1183 x: &ArrayView2<F>,
1184 residuals: &ArrayView1<F>,
1185 coefficients: &Array1<F>,
1186 alpha_l2: F,
1187 df: usize,
1188) -> StatsResult<Array1<F>>
1189where
1190 F: Float
1191 + std::iter::Sum<F>
1192 + std::ops::Div<Output = F>
1193 + 'static
1194 + scirs2_core::numeric::NumAssign
1195 + scirs2_core::numeric::One
1196 + scirs2_core::ndarray::ScalarOperand
1197 + std::fmt::Display
1198 + Send
1199 + Sync,
1200{
1201 let mse = residuals
1203 .iter()
1204 .map(|&r| scirs2_core::numeric::Float::powi(r, 2))
1205 .sum::<F>()
1206 / F::from(df).expect("Failed to convert to float");
1207
1208 let p = coefficients.len();
1210 let mut active_set = Vec::new();
1211
1212 for j in 0..p {
1213 if crate::regression::utils::float_abs(coefficients[j]) > F::epsilon() {
1214 active_set.push(j);
1215 }
1216 }
1217
1218 if active_set.is_empty() {
1220 return Ok(Array1::<F>::zeros(p));
1221 }
1222
1223 let n_active = active_set.len();
1225 let mut xtx_active = Array2::<F>::zeros((n_active, n_active));
1226
1227 for (i, &idx_i) in active_set.iter().enumerate() {
1228 for (j, &idx_j) in active_set.iter().enumerate() {
1229 let x_i = x.column(idx_i);
1230 let x_j = x.column(idx_j);
1231
1232 xtx_active[[i, j]] = x_i.iter().zip(x_j.iter()).map(|(&xi, &xj)| xi * xj).sum();
1233
1234 if i == j {
1236 xtx_active[[i, j]] += alpha_l2;
1237 }
1238 }
1239 }
1240
1241 let xtx_active_inv = match inv(&xtx_active.view(), None) {
1243 Ok(inv_result) => inv_result,
1244 Err(_) => {
1245 return Ok(Array1::<F>::zeros(p));
1247 }
1248 };
1249
1250 let mut std_errors = Array1::<F>::zeros(p);
1252
1253 for (i, &idx) in active_set.iter().enumerate() {
1254 std_errors[idx] = scirs2_core::numeric::Float::sqrt(xtx_active_inv[[i, i]] * mse);
1255 }
1256
1257 Ok(std_errors)
1258}
1259
1260#[allow(clippy::too_many_arguments)]
1316#[allow(dead_code)]
1317pub fn group_lasso<F>(
1318 x: &ArrayView2<F>,
1319 y: &ArrayView1<F>,
1320 groups: &[usize],
1321 alpha: Option<F>,
1322 fit_intercept: Option<bool>,
1323 normalize: Option<bool>,
1324 tol: Option<F>,
1325 max_iter: Option<usize>,
1326 conf_level: Option<F>,
1327) -> StatsResult<RegressionResults<F>>
1328where
1329 F: Float
1330 + std::iter::Sum<F>
1331 + std::ops::Div<Output = F>
1332 + std::fmt::Debug
1333 + std::fmt::Display
1334 + 'static
1335 + scirs2_core::numeric::NumAssign
1336 + scirs2_core::numeric::One
1337 + scirs2_core::ndarray::ScalarOperand
1338 + Send
1339 + Sync,
1340{
1341 if x.nrows() != y.len() {
1343 return Err(StatsError::DimensionMismatch(format!(
1344 "Input x has {} rows but y has length {}",
1345 x.nrows(),
1346 y.len()
1347 )));
1348 }
1349
1350 if x.ncols() != groups.len() {
1351 return Err(StatsError::DimensionMismatch(format!(
1352 "Number of columns in x ({}) must match length of groups ({})",
1353 x.ncols(),
1354 groups.len()
1355 )));
1356 }
1357
1358 let n = x.nrows();
1359 let p_features = x.ncols();
1360
1361 let alpha = alpha.unwrap_or_else(|| F::from(1.0).expect("Failed to convert constant to float"));
1363 let fit_intercept = fit_intercept.unwrap_or(true);
1364 let normalize = normalize.unwrap_or(false);
1365 let tol = tol.unwrap_or_else(|| F::from(1e-4).expect("Failed to convert constant to float"));
1366 let max_iter = max_iter.unwrap_or(1000);
1367 let conf_level =
1368 conf_level.unwrap_or_else(|| F::from(0.95).expect("Failed to convert constant to float"));
1369
1370 if alpha < F::zero() {
1371 return Err(StatsError::InvalidArgument(
1372 "alpha must be non-negative".to_string(),
1373 ));
1374 }
1375
1376 let (x_processed, _y_mean, x_mean, x_std) = preprocessdata(x, y, fit_intercept, normalize)?;
1383
1384 let p = if fit_intercept {
1386 p_features + 1
1387 } else {
1388 p_features
1389 };
1390
1391 if n < 2 {
1393 return Err(StatsError::InvalidArgument(
1394 "At least 2 observations required for group lasso regression".to_string(),
1395 ));
1396 }
1397
1398 let mut unique_groups = HashSet::new();
1400 for &g in groups {
1401 unique_groups.insert(g);
1402 }
1403
1404 let mut group_indices = Vec::new();
1405 for &g in &unique_groups {
1406 let mut indices = Vec::new();
1407 for (i, &group) in groups.iter().enumerate() {
1408 if group == g {
1409 indices.push(if fit_intercept { i + 1 } else { i });
1410 }
1411 }
1412 group_indices.push(indices);
1413 }
1414
1415 let mut coefficients = Array1::<F>::zeros(p);
1417
1418 let mut converged = false;
1420 let mut _iter = 0;
1421
1422 while !converged && _iter < max_iter {
1423 converged = true;
1424
1425 let old_coefs = coefficients.clone();
1427
1428 if fit_intercept {
1430 let r = y - &x_processed
1431 .slice(s![.., 1..])
1432 .dot(&coefficients.slice(s![1..]));
1433 let r_sum: F = r.iter().cloned().sum();
1434 coefficients[0] = r_sum / F::from(r.len()).expect("Operation failed");
1435 }
1436
1437 for group in &group_indices {
1439 if group.is_empty() {
1441 continue;
1442 }
1443
1444 let mut r = y.to_owned();
1446
1447 for j in 0..p {
1449 if !group.contains(&j) {
1450 let x_j = x_processed.column(j);
1451 let beta_j = coefficients[j];
1452
1453 for i in 0..n {
1454 r[i] -= x_j[i] * beta_j;
1455 }
1456 }
1457 }
1458
1459 let mut x_group = Array2::<F>::zeros((n, group.len()));
1461 for (i, &idx) in group.iter().enumerate() {
1462 x_group.column_mut(i).assign(&x_processed.column(idx));
1463 }
1464
1465 let xtr = x_group.t().dot(&r);
1467
1468 let xtx = x_group.t().dot(&x_group);
1470
1471 let xtr_norm = scirs2_core::numeric::Float::sqrt(
1473 xtr.iter()
1474 .map(|&x| scirs2_core::numeric::Float::powi(x, 2))
1475 .sum::<F>(),
1476 );
1477
1478 if xtr_norm < alpha {
1480 for &idx in group {
1481 coefficients[idx] = F::zero();
1482 }
1483 continue;
1484 }
1485
1486 let mut beta_group = match solve_group(xtr, xtx, alpha, tol, max_iter) {
1488 Ok(beta) => beta,
1489 Err(_) => Array1::<F>::zeros(group.len()),
1490 };
1491
1492 let beta_norm = scirs2_core::numeric::Float::sqrt(
1494 beta_group
1495 .iter()
1496 .map(|&x| scirs2_core::numeric::Float::powi(x, 2))
1497 .sum::<F>(),
1498 );
1499 if beta_norm > F::epsilon() {
1500 let shrinkage = F::one().max((beta_norm - alpha) / beta_norm);
1501 beta_group = beta_group.mapv(|x| x * shrinkage);
1502 } else {
1503 beta_group.fill(F::zero());
1504 }
1505
1506 for (i, &idx) in group.iter().enumerate() {
1508 coefficients[idx] = beta_group[i];
1509 }
1510 }
1511
1512 let coef_diff = (&coefficients - &old_coefs)
1514 .mapv(|x| scirs2_core::numeric::Float::abs(x))
1515 .sum();
1516 let coef_norm = old_coefs
1517 .mapv(|x| scirs2_core::numeric::Float::abs(x))
1518 .sum()
1519 .max(F::epsilon());
1520
1521 if coef_diff / coef_norm < tol {
1522 converged = true;
1523 }
1524
1525 _iter += 1;
1526 }
1527
1528 let transformed_coefficients = if normalize || fit_intercept {
1530 transform_coefficients(&coefficients, &x_mean, &x_std, fit_intercept)
1531 } else {
1532 coefficients.clone()
1533 };
1534
1535 let x_design = if fit_intercept {
1537 add_intercept(x)
1538 } else {
1539 x.to_owned()
1540 };
1541
1542 let fitted_values = x_design.dot(&transformed_coefficients);
1543 let residuals = y.to_owned() - &fitted_values;
1544
1545 let mut nonzero_coefs = 0;
1548 let mut nonzero_groups = HashSet::new();
1549
1550 for (i, &g) in groups.iter().enumerate() {
1551 let idx = if fit_intercept { i + 1 } else { i };
1552 if crate::regression::utils::float_abs(transformed_coefficients[idx]) > F::epsilon() {
1553 nonzero_groups.insert(g);
1554 }
1555 }
1556
1557 for &g in &nonzero_groups {
1558 let groupsize = groups.iter().filter(|&&group| group == g).count();
1559 nonzero_coefs += groupsize;
1560 }
1561
1562 if fit_intercept
1563 && crate::regression::utils::float_abs(transformed_coefficients[0]) > F::epsilon()
1564 {
1565 nonzero_coefs += 1;
1566 }
1567
1568 let df_model = nonzero_coefs - if fit_intercept { 1 } else { 0 };
1569 let df_residuals = n - nonzero_coefs;
1570
1571 let (_y_mean, ss_total, ss_residual, ss_explained) =
1573 calculate_sum_of_squares(y, &residuals.view());
1574
1575 let r_squared = ss_explained / ss_total;
1577 let adj_r_squared = F::one()
1578 - (F::one() - r_squared) * F::from(n - 1).expect("Failed to convert to float")
1579 / F::from(df_residuals).expect("Failed to convert to float");
1580
1581 let mse = ss_residual / F::from(df_residuals).expect("Failed to convert to float");
1583 let residual_std_error = scirs2_core::numeric::Float::sqrt(mse);
1584
1585 let std_errors = match calculate_group_lasso_std_errors(
1587 &x_design.view(),
1588 &residuals.view(),
1589 &transformed_coefficients,
1590 groups,
1591 fit_intercept,
1592 df_residuals,
1593 ) {
1594 Ok(se) => se,
1595 Err(_) => Array1::<F>::zeros(p),
1596 };
1597
1598 let t_values = calculate_t_values(&transformed_coefficients, &std_errors);
1600
1601 let p_values = t_values.mapv(|t| t_test_p_value(t, df_residuals));
1604
1605 let mut conf_intervals = Array2::<F>::zeros((p, 2));
1607 let z = norm_ppf(
1608 F::from(0.5).expect("Failed to convert constant to float") * (F::one() + conf_level),
1609 );
1610
1611 for i in 0..p {
1612 let margin = std_errors[i] * z;
1613 conf_intervals[[i, 0]] = transformed_coefficients[i] - margin;
1614 conf_intervals[[i, 1]] = transformed_coefficients[i] + margin;
1615 }
1616
1617 let f_statistic = if df_model > 0 && df_residuals > 0 {
1619 (ss_explained / F::from(df_model).expect("Failed to convert to float"))
1620 / (ss_residual / F::from(df_residuals).expect("Failed to convert to float"))
1621 } else {
1622 F::infinity()
1623 };
1624
1625 let f_p_value = f_test_p_value(f_statistic, df_model, df_residuals);
1628
1629 Ok(RegressionResults {
1631 coefficients: transformed_coefficients,
1632 std_errors,
1633 t_values,
1634 p_values,
1635 conf_intervals,
1636 r_squared,
1637 adj_r_squared,
1638 f_statistic,
1639 f_p_value,
1640 residual_std_error,
1641 df_residuals,
1642 residuals,
1643 fitted_values,
1644 inlier_mask: vec![true; n], })
1646}
1647
1648#[allow(dead_code)]
1650fn solve_group<F>(
1651 xtr: Array1<F>,
1652 xtx: Array2<F>,
1653 _alpha: F,
1654 tol: F,
1655 max_iter: usize,
1656) -> StatsResult<Array1<F>>
1657where
1658 F: Float
1659 + std::iter::Sum<F>
1660 + std::ops::Div<Output = F>
1661 + 'static
1662 + scirs2_core::numeric::NumAssign
1663 + scirs2_core::numeric::One
1664 + scirs2_core::ndarray::ScalarOperand
1665 + std::fmt::Display
1666 + Send
1667 + Sync,
1668{
1669 let p = xtr.len();
1670
1671 let mut beta = Array1::<F>::zeros(p);
1673
1674 match inv(&xtx.view(), None) {
1676 Ok(xtx_inv) => {
1677 beta = xtx_inv.dot(&xtr);
1678 return Ok(beta);
1679 }
1680 Err(_) => {
1681 }
1683 }
1684
1685 let mut _iter = 0;
1687 let mut converged = false;
1688
1689 let lr = F::from(0.01).expect("Failed to convert constant to float");
1691
1692 while !converged && _iter < max_iter {
1693 let old_beta = beta.clone();
1694
1695 let xtx_beta = xtx.dot(&beta);
1697 let grad = &xtx_beta - &xtr;
1698
1699 let lr_grad = grad.mapv(|g| g * lr);
1701 beta = &beta - &lr_grad;
1702
1703 let beta_diff = (&beta - &old_beta)
1705 .mapv(|x| scirs2_core::numeric::Float::abs(x))
1706 .sum();
1707 let beta_norm = old_beta
1708 .mapv(|x| scirs2_core::numeric::Float::abs(x))
1709 .sum()
1710 .max(F::epsilon());
1711
1712 if beta_diff / beta_norm < tol {
1713 converged = true;
1714 }
1715
1716 _iter += 1;
1717 }
1718
1719 Ok(beta)
1720}
1721
1722#[allow(dead_code)]
1724fn calculate_group_lasso_std_errors<F>(
1725 x: &ArrayView2<F>,
1726 residuals: &ArrayView1<F>,
1727 coefficients: &Array1<F>,
1728 groups: &[usize],
1729 fit_intercept: bool,
1730 df: usize,
1731) -> StatsResult<Array1<F>>
1732where
1733 F: Float
1734 + std::iter::Sum<F>
1735 + std::ops::Div<Output = F>
1736 + 'static
1737 + scirs2_core::numeric::NumAssign
1738 + scirs2_core::numeric::One
1739 + scirs2_core::ndarray::ScalarOperand
1740 + std::fmt::Display
1741 + Send
1742 + Sync,
1743{
1744 let mse = residuals
1746 .iter()
1747 .map(|&r| scirs2_core::numeric::Float::powi(r, 2))
1748 .sum::<F>()
1749 / F::from(df).expect("Failed to convert to float");
1750
1751 let p = coefficients.len();
1753 let mut active_groups = HashSet::new();
1754
1755 for (i, &g) in groups.iter().enumerate() {
1756 let idx = if fit_intercept { i + 1 } else { i };
1757 if crate::regression::utils::float_abs(coefficients[idx]) > F::epsilon() {
1758 active_groups.insert(g);
1759 }
1760 }
1761
1762 let mut active_set = Vec::new();
1764
1765 if fit_intercept && crate::regression::utils::float_abs(coefficients[0]) > F::epsilon() {
1766 active_set.push(0);
1767 }
1768
1769 for (i, &g) in groups.iter().enumerate() {
1770 if active_groups.contains(&g) {
1771 let idx = if fit_intercept { i + 1 } else { i };
1772 active_set.push(idx);
1773 }
1774 }
1775
1776 if active_set.is_empty() {
1778 return Ok(Array1::<F>::zeros(p));
1779 }
1780
1781 let n_active = active_set.len();
1783 let mut xtx_active = Array2::<F>::zeros((n_active, n_active));
1784
1785 for (i, &idx_i) in active_set.iter().enumerate() {
1786 for (j, &idx_j) in active_set.iter().enumerate() {
1787 let x_i = x.column(idx_i);
1788 let x_j = x.column(idx_j);
1789
1790 xtx_active[[i, j]] = x_i.iter().zip(x_j.iter()).map(|(&xi, &xj)| xi * xj).sum();
1791 }
1792 }
1793
1794 let xtx_active_inv = match inv(&xtx_active.view(), None) {
1796 Ok(inv_result) => inv_result,
1797 Err(_) => {
1798 return Ok(Array1::<F>::zeros(p));
1800 }
1801 };
1802
1803 let mut std_errors = Array1::<F>::zeros(p);
1805
1806 for (i, &idx) in active_set.iter().enumerate() {
1807 std_errors[idx] = scirs2_core::numeric::Float::sqrt(xtx_active_inv[[i, i]] * mse);
1808 }
1809
1810 Ok(std_errors)
1811}
1812
1813pub struct FittedRidgeRegression<F>
1819where
1820 F: Float + std::fmt::Debug + std::fmt::Display + 'static,
1821{
1822 inner: crate::regression::RegressionResults<F>,
1823}
1824
1825impl<F> FittedRidgeRegression<F>
1826where
1827 F: Float
1828 + std::iter::Sum<F>
1829 + std::ops::Div<Output = F>
1830 + std::fmt::Debug
1831 + std::fmt::Display
1832 + 'static
1833 + scirs2_core::numeric::NumAssign
1834 + scirs2_core::numeric::One
1835 + scirs2_core::ndarray::ScalarOperand
1836 + Send
1837 + Sync,
1838{
1839 pub fn predict(
1841 &self,
1842 x: &scirs2_core::ndarray::ArrayView2<F>,
1843 ) -> crate::error::StatsResult<scirs2_core::ndarray::Array1<F>> {
1844 if x.ncols() != self.inner.coefficients.len() {
1845 return Err(crate::error::StatsError::DimensionMismatch(format!(
1846 "predict: x has {} columns but model has {} coefficients",
1847 x.ncols(),
1848 self.inner.coefficients.len()
1849 )));
1850 }
1851 Ok(x.dot(&self.inner.coefficients))
1852 }
1853
1854 pub fn coefficients(&self) -> &scirs2_core::ndarray::Array1<F> {
1856 &self.inner.coefficients
1857 }
1858}
1859
1860#[derive(Debug, Clone)]
1881pub struct RidgeRegression {
1882 alpha: f64,
1883}
1884
1885impl RidgeRegression {
1886 pub fn new(alpha: f64) -> Self {
1892 Self { alpha }
1893 }
1894
1895 pub fn fit(
1897 &mut self,
1898 x: &scirs2_core::ndarray::ArrayView2<f64>,
1899 y: &scirs2_core::ndarray::ArrayView1<f64>,
1900 ) -> crate::error::StatsResult<FittedRidgeRegression<f64>> {
1901 let inner = ridge_regression(x, y, Some(self.alpha), Some(false), None, None, None, None)?;
1905 Ok(FittedRidgeRegression { inner })
1906 }
1907}
1908
1909#[cfg(test)]
1912#[path = "regularized_tests.rs"]
1913mod tests;