1use super::helpers::*;
4use crate::matrix::FdMatrix;
5use crate::scalar_on_function::{sigmoid, FregreLmResult, FunctionalLogisticResult};
6
7pub struct FunctionalPdpResult {
9 pub grid_values: Vec<f64>,
11 pub pdp_curve: Vec<f64>,
13 pub ice_curves: FdMatrix,
15 pub component: usize,
17}
18
19pub fn functional_pdp(
33 fit: &FregreLmResult,
34 data: &FdMatrix,
35 _scalar_covariates: Option<&FdMatrix>,
36 component: usize,
37 n_grid: usize,
38) -> Option<FunctionalPdpResult> {
39 let (n, m) = data.shape();
40 if component >= fit.ncomp
41 || n_grid < 2
42 || n == 0
43 || m != fit.fpca.mean.len()
44 || n != fit.fitted_values.len()
45 {
46 return None;
47 }
48
49 let ncomp = fit.ncomp;
50 let scores = project_scores(data, &fit.fpca.mean, &fit.fpca.rotation, ncomp);
51 let grid_values = make_grid(&scores, component, n_grid);
52
53 let coef_c = fit.coefficients[1 + component];
54 let mut ice_curves = FdMatrix::zeros(n, n_grid);
55 for i in 0..n {
56 let base = fit.fitted_values[i] - coef_c * scores[(i, component)];
57 for g in 0..n_grid {
58 ice_curves[(i, g)] = base + coef_c * grid_values[g];
59 }
60 }
61
62 let pdp_curve = ice_to_pdp(&ice_curves, n, n_grid);
63
64 Some(FunctionalPdpResult {
65 grid_values,
66 pdp_curve,
67 ice_curves,
68 component,
69 })
70}
71
72pub fn functional_pdp_logistic(
83 fit: &FunctionalLogisticResult,
84 data: &FdMatrix,
85 scalar_covariates: Option<&FdMatrix>,
86 component: usize,
87 n_grid: usize,
88) -> Option<FunctionalPdpResult> {
89 let (n, m) = data.shape();
90 if component >= fit.ncomp || n_grid < 2 || n == 0 || m != fit.fpca.mean.len() {
91 return None;
92 }
93
94 let ncomp = fit.ncomp;
95 let p_scalar = fit.gamma.len();
96 if p_scalar > 0 && scalar_covariates.is_none() {
97 return None;
98 }
99
100 let scores = project_scores(data, &fit.fpca.mean, &fit.fpca.rotation, ncomp);
101 let grid_values = make_grid(&scores, component, n_grid);
102
103 let mut ice_curves = FdMatrix::zeros(n, n_grid);
104 let coef_c = fit.coefficients[1 + component];
105 for i in 0..n {
106 let eta_base = logistic_eta_base(
107 fit.intercept,
108 &fit.coefficients,
109 &fit.gamma,
110 &scores,
111 scalar_covariates,
112 i,
113 ncomp,
114 component,
115 );
116 for g in 0..n_grid {
117 ice_curves[(i, g)] = sigmoid(eta_base + coef_c * grid_values[g]);
118 }
119 }
120
121 let pdp_curve = ice_to_pdp(&ice_curves, n, n_grid);
122
123 Some(FunctionalPdpResult {
124 grid_values,
125 pdp_curve,
126 ice_curves,
127 component,
128 })
129}
130
131pub struct BetaDecomposition {
137 pub components: Vec<Vec<f64>>,
139 pub coefficients: Vec<f64>,
141 pub variance_proportion: Vec<f64>,
143}
144
145pub fn beta_decomposition(fit: &FregreLmResult) -> Option<BetaDecomposition> {
147 let ncomp = fit.ncomp;
148 let m = fit.fpca.mean.len();
149 if ncomp == 0 || m == 0 {
150 return None;
151 }
152 decompose_beta(&fit.coefficients, &fit.fpca.rotation, ncomp, m)
153}
154
155pub fn beta_decomposition_logistic(fit: &FunctionalLogisticResult) -> Option<BetaDecomposition> {
157 let ncomp = fit.ncomp;
158 let m = fit.fpca.mean.len();
159 if ncomp == 0 || m == 0 {
160 return None;
161 }
162 decompose_beta(&fit.coefficients, &fit.fpca.rotation, ncomp, m)
163}
164
165fn decompose_beta(
166 coefficients: &[f64],
167 rotation: &FdMatrix,
168 ncomp: usize,
169 m: usize,
170) -> Option<BetaDecomposition> {
171 let mut components = Vec::with_capacity(ncomp);
172 let mut coefs = Vec::with_capacity(ncomp);
173 let mut norms_sq = Vec::with_capacity(ncomp);
174
175 for k in 0..ncomp {
176 let ck = coefficients[1 + k];
177 coefs.push(ck);
178 let comp: Vec<f64> = (0..m).map(|j| ck * rotation[(j, k)]).collect();
179 let nsq: f64 = comp.iter().map(|v| v * v).sum();
180 norms_sq.push(nsq);
181 components.push(comp);
182 }
183
184 let total_sq: f64 = norms_sq.iter().sum();
185 let variance_proportion = if total_sq > 0.0 {
186 norms_sq.iter().map(|&s| s / total_sq).collect()
187 } else {
188 vec![0.0; ncomp]
189 };
190
191 Some(BetaDecomposition {
192 components,
193 coefficients: coefs,
194 variance_proportion,
195 })
196}
197
198#[derive(Debug, Clone, Copy, PartialEq, Eq)]
204pub enum SignificanceDirection {
205 Positive,
206 Negative,
207}
208
209#[derive(Debug, Clone)]
211pub struct SignificantRegion {
212 pub start_idx: usize,
214 pub end_idx: usize,
216 pub direction: SignificanceDirection,
218}
219
220pub fn significant_regions(lower: &[f64], upper: &[f64]) -> Option<Vec<SignificantRegion>> {
222 if lower.len() != upper.len() || lower.is_empty() {
223 return None;
224 }
225 let n = lower.len();
226 let mut regions = Vec::new();
227 let mut i = 0;
228 while i < n {
229 if let Some(d) = detect_direction(lower[i], upper[i]) {
230 let start = i;
231 i += 1;
232 while i < n && detect_direction(lower[i], upper[i]) == Some(d) {
233 i += 1;
234 }
235 regions.push(SignificantRegion {
236 start_idx: start,
237 end_idx: i - 1,
238 direction: d,
239 });
240 } else {
241 i += 1;
242 }
243 }
244 Some(regions)
245}
246
247pub fn significant_regions_from_se(
249 beta_t: &[f64],
250 beta_se: &[f64],
251 z_alpha: f64,
252) -> Option<Vec<SignificantRegion>> {
253 if beta_t.len() != beta_se.len() || beta_t.is_empty() {
254 return None;
255 }
256 let lower: Vec<f64> = beta_t
257 .iter()
258 .zip(beta_se)
259 .map(|(b, s)| b - z_alpha * s)
260 .collect();
261 let upper: Vec<f64> = beta_t
262 .iter()
263 .zip(beta_se)
264 .map(|(b, s)| b + z_alpha * s)
265 .collect();
266 significant_regions(&lower, &upper)
267}
268
269fn detect_direction(lower: f64, upper: f64) -> Option<SignificanceDirection> {
271 if lower > 0.0 {
272 Some(SignificanceDirection::Positive)
273 } else if upper < 0.0 {
274 Some(SignificanceDirection::Negative)
275 } else {
276 None
277 }
278}