1use crate::ols::OlsResult;
2use crate::ols::OLS;
3use greeners_core::error::GreenersError;
4use greeners_core::{CovarianceType, DataFrame, Formula};
5use ndarray::{Array1, Array2};
6
7pub struct WLS;
12
13impl WLS {
14 pub fn from_formula(
16 formula: &Formula,
17 data: &DataFrame,
18 weights: &Array1<f64>,
19 cov_type: CovarianceType,
20 ) -> Result<OlsResult, GreenersError> {
21 let (y, x) = data.to_design_matrix(formula)?;
22 let var_names = data.formula_var_names(formula)?;
23 Self::fit_with_names(&y, &x, weights, cov_type, Some(var_names))
24 }
25
26 pub fn fit(
28 y: &Array1<f64>,
29 x: &Array2<f64>,
30 weights: &Array1<f64>,
31 cov_type: CovarianceType,
32 ) -> Result<OlsResult, GreenersError> {
33 Self::fit_with_names(y, x, weights, cov_type, None)
34 }
35
36 pub fn fit_with_names(
38 y: &Array1<f64>,
39 x: &Array2<f64>,
40 weights: &Array1<f64>,
41 cov_type: CovarianceType,
42 variable_names: Option<Vec<String>>,
43 ) -> Result<OlsResult, GreenersError> {
44 let n = y.len();
45 if weights.len() != n {
46 return Err(GreenersError::ShapeMismatch(format!(
47 "weights length ({}) must match observations ({})",
48 weights.len(),
49 n
50 )));
51 }
52 if x.nrows() != n {
53 return Err(GreenersError::ShapeMismatch(format!(
54 "X rows ({}) must match y length ({})",
55 x.nrows(),
56 n
57 )));
58 }
59
60 if weights.iter().any(|&w| w <= 0.0 || !w.is_finite()) {
62 return Err(GreenersError::InvalidOperation(
63 "Weights must be positive and finite".into(),
64 ));
65 }
66
67 let sqrt_w = weights.mapv(f64::sqrt);
69
70 let y_star = &sqrt_w * y;
71 let mut x_star = x.clone();
72 for i in 0..n {
73 x_star.row_mut(i).mapv_inplace(|val| val * sqrt_w[i]);
74 }
75
76 let has_intercept =
77 (0..x.ncols()).any(|j| x.column(j).iter().all(|&val| (val - 1.0).abs() < 1e-12));
78
79 OLS::fit_internal(
80 &y_star,
81 &x_star,
82 cov_type,
83 variable_names,
84 Some(has_intercept),
85 )
86 }
87}