Skip to main content

linreg_core/
wasm.rs

1//! WASM-specific bindings for linreg-core
2//!
3//! This module contains all WebAssembly bindings using wasm-bindgen.
4//! These functions are only compiled when the "wasm" feature is enabled.
5//!
6//! All WASM functions accept and return JSON strings for JavaScript interoperability.
7
8#![cfg(feature = "wasm")]
9
10use wasm_bindgen::prelude::*;
11use std::collections::HashSet;
12use serde::Serialize;
13
14// Re-export from parent modules
15use crate::core;
16use crate::diagnostics;
17use crate::distributions::{normal_inverse_cdf, student_t_cdf};
18use crate::error::{error_json, error_to_json, Error, Result};
19use crate::linalg;
20use crate::loess;
21use crate::regularized;
22use crate::stats;
23
24// ============================================================================
25// CSV Parsing
26// ============================================================================
27
28#[derive(Serialize)]
29struct ParsedCsv {
30    headers: Vec<String>,
31    data: Vec<serde_json::Map<String, serde_json::Value>>,
32    numeric_columns: Vec<String>,
33}
34
35#[wasm_bindgen]
36/// Parses CSV data and returns it as a JSON string.
37///
38/// Parses the CSV content and identifies numeric columns. Returns a JSON object
39/// with headers, data rows, and a list of numeric column names.
40///
41/// # Arguments
42///
43/// * `content` - CSV content as a string
44///
45/// # Returns
46///
47/// JSON string with structure:
48/// ```json
49/// {
50///   "headers": ["col1", "col2", ...],
51///   "data": [{"col1": 1.0, "col2": "text"}, ...],
52///   "numeric_columns": ["col1", ...]
53/// }
54/// ```
55///
56/// # Errors
57///
58/// Returns a JSON error object if parsing fails or domain check fails.
59pub fn parse_csv(content: &str) -> String {
60    if let Err(e) = check_domain() {
61        return error_to_json(&e);
62    }
63
64    let mut reader = csv::ReaderBuilder::new()
65        .has_headers(true)
66        .flexible(true)
67        .from_reader(content.as_bytes());
68
69    // Get headers
70    let headers: Vec<String> = match reader.headers() {
71        Ok(h) => h.iter().map(|s| s.to_string()).collect(),
72        Err(e) => return error_json(&format!("Failed to read headers: {}", e)),
73    };
74
75    let mut data = Vec::new();
76    let mut numeric_col_set = HashSet::new();
77
78    for result in reader.records() {
79        let record = match result {
80            Ok(r) => r,
81            Err(e) => return error_json(&format!("Failed to parse CSV record: {}", e)),
82        };
83
84        if record.len() != headers.len() {
85            continue;
86        }
87
88        let mut row_map = serde_json::Map::new();
89
90        for (i, field) in record.iter().enumerate() {
91            if i >= headers.len() {
92                continue;
93            }
94
95            let header = &headers[i];
96            let val_trimmed = field.trim();
97
98            // Try to parse as f64
99            if let Ok(num) = val_trimmed.parse::<f64>() {
100                if num.is_finite() {
101                    row_map.insert(
102                        header.clone(),
103                        serde_json::Value::Number(serde_json::Number::from_f64(num).unwrap()),
104                    );
105                    numeric_col_set.insert(header.clone());
106                    continue;
107                }
108            }
109
110            // Fallback to string
111            row_map.insert(
112                header.clone(),
113                serde_json::Value::String(val_trimmed.to_string()),
114            );
115        }
116        data.push(row_map);
117    }
118
119    let mut numeric_columns: Vec<String> = numeric_col_set.into_iter().collect();
120    numeric_columns.sort();
121
122    let output = ParsedCsv {
123        headers,
124        data,
125        numeric_columns,
126    };
127
128    serde_json::to_string(&output).unwrap_or_else(|_| error_json("Failed to serialize CSV output"))
129}
130
131// ============================================================================
132// OLS Regression WASM Wrapper
133// ============================================================================
134
135#[wasm_bindgen]
136/// Performs OLS regression via WASM.
137///
138/// All parameters and return values are JSON-encoded strings for JavaScript
139/// interoperability. Returns regression output including coefficients,
140/// standard errors, diagnostic statistics, and VIF analysis.
141///
142/// # Arguments
143///
144/// * `y_json` - JSON array of response variable values: `[1.0, 2.0, 3.0]`
145/// * `x_vars_json` - JSON array of predictor arrays: `[[1.0, 2.0], [0.5, 1.0]]`
146/// * `variable_names` - JSON array of variable names: `["Intercept", "X1", "X2"]`
147///
148/// # Returns
149///
150/// JSON string containing the complete regression output with coefficients,
151/// standard errors, t-statistics, p-values, R², F-statistic, residuals, leverage, VIF, etc.
152///
153/// # Errors
154///
155/// Returns a JSON error object if:
156/// - JSON parsing fails
157/// - Insufficient data (n ≤ k + 1)
158/// - Matrix is singular
159/// - Domain check fails
160pub fn ols_regression(y_json: &str, x_vars_json: &str, variable_names: &str) -> String {
161    if let Err(e) = check_domain() {
162        return error_to_json(&e);
163    }
164
165    // Parse JSON input
166    let y: Vec<f64> = match serde_json::from_str(y_json) {
167        Ok(v) => v,
168        Err(e) => return error_json(&format!("Failed to parse y: {}", e)),
169    };
170
171    let x_vars: Vec<Vec<f64>> = match serde_json::from_str(x_vars_json) {
172        Ok(v) => v,
173        Err(e) => return error_json(&format!("Failed to parse x_vars: {}", e)),
174    };
175
176    let names: Vec<String> = match serde_json::from_str(variable_names) {
177        Ok(v) => v,
178        Err(_) => vec!["Intercept".to_string()],
179    };
180
181    // Call core function
182    match core::ols_regression(&y, &x_vars, &names) {
183        Ok(output) => serde_json::to_string(&output)
184            .unwrap_or_else(|_| error_json("Failed to serialize output")),
185        Err(e) => error_json(&e.to_string()),
186    }
187}
188
189// ============================================================================
190// Diagnostic Tests WASM Wrappers
191// ============================================================================
192
193/// Performs the Rainbow test for linearity via WASM.
194///
195/// The Rainbow test checks whether the relationship between predictors and response
196/// is linear. A significant p-value suggests non-linearity.
197///
198/// # Arguments
199///
200/// * `y_json` - JSON array of response variable values
201/// * `x_vars_json` - JSON array of predictor arrays
202/// * `fraction` - Fraction of data to use in the central subset (0.0 to 1.0, typically 0.5)
203/// * `method` - Method to use: "r", "python", or "both" (case-insensitive, defaults to "r")
204///
205/// # Returns
206///
207/// JSON string containing test statistic, p-value, and interpretation.
208///
209/// # Errors
210///
211/// Returns a JSON error object if parsing fails or domain check fails.
212#[wasm_bindgen]
213pub fn rainbow_test(y_json: &str, x_vars_json: &str, fraction: f64, method: &str) -> String {
214    if let Err(e) = check_domain() {
215        return error_to_json(&e);
216    }
217
218    let y: Vec<f64> = match serde_json::from_str(y_json) {
219        Ok(v) => v,
220        Err(e) => return error_json(&format!("Failed to parse y: {}", e)),
221    };
222
223    let x_vars: Vec<Vec<f64>> = match serde_json::from_str(x_vars_json) {
224        Ok(v) => v,
225        Err(e) => return error_json(&format!("Failed to parse x_vars: {}", e)),
226    };
227
228    // Parse method parameter (default to "r" for R)
229    let method = match method.to_lowercase().as_str() {
230        "python" => diagnostics::RainbowMethod::Python,
231        "both" => diagnostics::RainbowMethod::Both,
232        _ => diagnostics::RainbowMethod::R, // Default to R
233    };
234
235    match diagnostics::rainbow_test(&y, &x_vars, fraction, method) {
236        Ok(output) => serde_json::to_string(&output)
237            .unwrap_or_else(|_| error_json("Failed to serialize Rainbow test result")),
238        Err(e) => error_json(&e.to_string()),
239    }
240}
241
242/// Performs the Harvey-Collier test for linearity via WASM.
243///
244/// The Harvey-Collier test checks whether the residuals exhibit a linear trend,
245/// which would indicate that the model's functional form is misspecified.
246/// A significant p-value suggests non-linearity.
247///
248/// # Arguments
249///
250/// * `y_json` - JSON array of response variable values
251/// * `x_vars_json` - JSON array of predictor arrays
252///
253/// # Returns
254///
255/// JSON string containing test statistic, p-value, and interpretation.
256///
257/// # Errors
258///
259/// Returns a JSON error object if parsing fails or domain check fails.
260#[wasm_bindgen]
261pub fn harvey_collier_test(y_json: &str, x_vars_json: &str) -> String {
262    if let Err(e) = check_domain() {
263        return error_to_json(&e);
264    }
265
266    let y: Vec<f64> = match serde_json::from_str(y_json) {
267        Ok(v) => v,
268        Err(e) => return error_json(&format!("Failed to parse y: {}", e)),
269    };
270
271    let x_vars: Vec<Vec<f64>> = match serde_json::from_str(x_vars_json) {
272        Ok(v) => v,
273        Err(e) => return error_json(&format!("Failed to parse x_vars: {}", e)),
274    };
275
276    match diagnostics::harvey_collier_test(&y, &x_vars, diagnostics::HarveyCollierMethod::R) {
277        Ok(output) => serde_json::to_string(&output)
278            .unwrap_or_else(|_| error_json("Failed to serialize Harvey-Collier test result")),
279        Err(e) => error_json(&e.to_string()),
280    }
281}
282
283/// Performs the Breusch-Pagan test for heteroscedasticity via WASM.
284///
285/// The Breusch-Pagan test checks whether the variance of residuals is constant
286/// across the range of predicted values (homoscedasticity assumption).
287/// A significant p-value suggests heteroscedasticity.
288///
289/// # Arguments
290///
291/// * `y_json` - JSON array of response variable values
292/// * `x_vars_json` - JSON array of predictor arrays
293///
294/// # Returns
295///
296/// JSON string containing test statistic, p-value, and interpretation.
297///
298/// # Errors
299///
300/// Returns a JSON error object if parsing fails or domain check fails.
301#[wasm_bindgen]
302pub fn breusch_pagan_test(y_json: &str, x_vars_json: &str) -> String {
303    if let Err(e) = check_domain() {
304        return error_to_json(&e);
305    }
306
307    let y: Vec<f64> = match serde_json::from_str(y_json) {
308        Ok(v) => v,
309        Err(e) => return error_json(&format!("Failed to parse y: {}", e)),
310    };
311
312    let x_vars: Vec<Vec<f64>> = match serde_json::from_str(x_vars_json) {
313        Ok(v) => v,
314        Err(e) => return error_json(&format!("Failed to parse x_vars: {}", e)),
315    };
316
317    match diagnostics::breusch_pagan_test(&y, &x_vars) {
318        Ok(output) => serde_json::to_string(&output)
319            .unwrap_or_else(|_| error_json("Failed to serialize Breusch-Pagan test result")),
320        Err(e) => error_json(&e.to_string()),
321    }
322}
323
324/// Performs the White test for heteroscedasticity via WASM.
325///
326/// The White test is a more general test for heteroscedasticity that does not
327/// assume a specific form of heteroscedasticity. A significant p-value suggests
328/// that the error variance is not constant.
329///
330/// # Arguments
331///
332/// * `y_json` - JSON array of response variable values
333/// * `x_vars_json` - JSON array of predictor arrays
334/// * `method` - Method to use: "r", "python", or "both" (case-insensitive, defaults to "r")
335///
336/// # Returns
337///
338/// JSON string containing test statistic, p-value, and interpretation.
339///
340/// # Errors
341///
342/// Returns a JSON error object if parsing fails or domain check fails.
343#[wasm_bindgen]
344pub fn white_test(y_json: &str, x_vars_json: &str, method: &str) -> String {
345    if let Err(e) = check_domain() {
346        return error_to_json(&e);
347    }
348
349    let y: Vec<f64> = match serde_json::from_str(y_json) {
350        Ok(v) => v,
351        Err(e) => return error_json(&format!("Failed to parse y: {}", e)),
352    };
353
354    let x_vars: Vec<Vec<f64>> = match serde_json::from_str(x_vars_json) {
355        Ok(v) => v,
356        Err(e) => return error_json(&format!("Failed to parse x_vars: {}", e)),
357    };
358
359    // Parse method parameter (default to "r" for R)
360    let method = match method.to_lowercase().as_str() {
361        "python" => diagnostics::WhiteMethod::Python,
362        "both" => diagnostics::WhiteMethod::Both,
363        _ => diagnostics::WhiteMethod::R, // Default to R
364    };
365
366    match diagnostics::white_test(&y, &x_vars, method) {
367        Ok(output) => serde_json::to_string(&output)
368            .unwrap_or_else(|_| error_json("Failed to serialize White test result")),
369        Err(e) => error_json(&e.to_string()),
370    }
371}
372
373/// Performs the R method White test for heteroscedasticity via WASM.
374///
375/// This implementation matches R's `skedastic::white()` function behavior.
376/// Uses the standard QR decomposition and the R-specific auxiliary matrix
377/// structure (intercept, X, X² only - no cross-products).
378///
379/// # Arguments
380///
381/// * `y_json` - JSON array of response variable values
382/// * `x_vars_json` - JSON array of predictor arrays (each array is a column)
383///
384/// # Returns
385///
386/// JSON string containing test statistic, p-value, and interpretation.
387///
388/// # Errors
389///
390/// Returns a JSON error object if parsing fails or domain check fails.
391#[wasm_bindgen]
392pub fn r_white_test(y_json: &str, x_vars_json: &str) -> String {
393    if let Err(e) = check_domain() {
394        return error_to_json(&e);
395    }
396
397    let y: Vec<f64> = match serde_json::from_str(y_json) {
398        Ok(v) => v,
399        Err(e) => return error_json(&format!("Failed to parse y: {}", e)),
400    };
401
402    let x_vars: Vec<Vec<f64>> = match serde_json::from_str(x_vars_json) {
403        Ok(v) => v,
404        Err(e) => return error_json(&format!("Failed to parse x_vars: {}", e)),
405    };
406
407    match diagnostics::r_white_method(&y, &x_vars) {
408        Ok(output) => serde_json::to_string(&output)
409            .unwrap_or_else(|_| error_json("Failed to serialize R White test result")),
410        Err(e) => error_json(&e.to_string()),
411    }
412}
413
414/// Performs the Python method White test for heteroscedasticity via WASM.
415///
416/// This implementation matches Python's `statsmodels.stats.diagnostic.het_white()` function.
417/// Uses the LINPACK QR decomposition with column pivoting and the Python-specific
418/// auxiliary matrix structure (intercept, X, X², and cross-products).
419///
420/// # Arguments
421///
422/// * `y_json` - JSON array of response variable values
423/// * `x_vars_json` - JSON array of predictor arrays (each array is a column)
424///
425/// # Returns
426///
427/// JSON string containing test statistic, p-value, and interpretation.
428///
429/// # Errors
430///
431/// Returns a JSON error object if parsing fails or domain check fails.
432#[wasm_bindgen]
433pub fn python_white_test(y_json: &str, x_vars_json: &str) -> String {
434    if let Err(e) = check_domain() {
435        return error_to_json(&e);
436    }
437
438    let y: Vec<f64> = match serde_json::from_str(y_json) {
439        Ok(v) => v,
440        Err(e) => return error_json(&format!("Failed to parse y: {}", e)),
441    };
442
443    let x_vars: Vec<Vec<f64>> = match serde_json::from_str(x_vars_json) {
444        Ok(v) => v,
445        Err(e) => return error_json(&format!("Failed to parse x_vars: {}", e)),
446    };
447
448    match diagnostics::python_white_method(&y, &x_vars) {
449        Ok(output) => serde_json::to_string(&output)
450            .unwrap_or_else(|_| error_json("Failed to serialize Python White test result")),
451        Err(e) => error_json(&e.to_string()),
452    }
453}
454
455/// Performs the Jarque-Bera test for normality via WASM.
456///
457/// The Jarque-Bera test checks whether the residuals are normally distributed
458/// by examining skewness and kurtosis. A significant p-value suggests that
459/// the residuals deviate from normality.
460///
461/// # Arguments
462///
463/// * `y_json` - JSON array of response variable values
464/// * `x_vars_json` - JSON array of predictor arrays
465///
466/// # Returns
467///
468/// JSON string containing test statistic, p-value, and interpretation.
469///
470/// # Errors
471///
472/// Returns a JSON error object if parsing fails or domain check fails.
473#[wasm_bindgen]
474pub fn jarque_bera_test(y_json: &str, x_vars_json: &str) -> String {
475    if let Err(e) = check_domain() {
476        return error_to_json(&e);
477    }
478
479    let y: Vec<f64> = match serde_json::from_str(y_json) {
480        Ok(v) => v,
481        Err(e) => return error_json(&format!("Failed to parse y: {}", e)),
482    };
483
484    let x_vars: Vec<Vec<f64>> = match serde_json::from_str(x_vars_json) {
485        Ok(v) => v,
486        Err(e) => return error_json(&format!("Failed to parse x_vars: {}", e)),
487    };
488
489    match diagnostics::jarque_bera_test(&y, &x_vars) {
490        Ok(output) => serde_json::to_string(&output)
491            .unwrap_or_else(|_| error_json("Failed to serialize Jarque-Bera test result")),
492        Err(e) => error_json(&e.to_string()),
493    }
494}
495
496// ============================================================================
497// Durbin-Watson Test (WASM wrapper)
498// ============================================================================
499
500/// Performs the Durbin-Watson test for autocorrelation via WASM.
501///
502/// The Durbin-Watson test checks for autocorrelation in the residuals.
503/// Values near 2 indicate no autocorrelation, values near 0 suggest positive
504/// autocorrelation, and values near 4 suggest negative autocorrelation.
505///
506/// # Arguments
507///
508/// * `y_json` - JSON array of response variable values
509/// * `x_vars_json` - JSON array of predictor arrays
510///
511/// # Returns
512///
513/// JSON string containing the DW statistic and interpretation.
514///
515/// # Errors
516///
517/// Returns a JSON error object if parsing fails or domain check fails.
518#[wasm_bindgen]
519pub fn durbin_watson_test(y_json: &str, x_vars_json: &str) -> String {
520    if let Err(e) = check_domain() {
521        return error_to_json(&e);
522    }
523
524    let y: Vec<f64> = match serde_json::from_str(y_json) {
525        Ok(v) => v,
526        Err(e) => return error_json(&format!("Failed to parse y: {}", e)),
527    };
528
529    let x_vars: Vec<Vec<f64>> = match serde_json::from_str(x_vars_json) {
530        Ok(v) => v,
531        Err(e) => return error_json(&format!("Failed to parse x_vars: {}", e)),
532    };
533
534    match diagnostics::durbin_watson_test(&y, &x_vars) {
535        Ok(output) => serde_json::to_string(&output)
536            .unwrap_or_else(|_| error_json("Failed to serialize Durbin-Watson test result")),
537        Err(e) => error_json(&e.to_string()),
538    }
539}
540
541// ============================================================================
542// Shapiro-Wilk Test (WASM wrapper)
543// ============================================================================
544
545/// Performs the Shapiro-Wilk test for normality via WASM.
546///
547/// The Shapiro-Wilk test is a powerful test for normality,
548/// especially for small to moderate sample sizes (3 ≤ n ≤ 5000). It tests
549/// the null hypothesis that the residuals are normally distributed.
550///
551/// # Arguments
552///
553/// * `y_json` - JSON array of response variable values
554/// * `x_vars_json` - JSON array of predictor arrays
555///
556/// # Returns
557///
558/// JSON string containing the W statistic (ranges from 0 to 1), p-value,
559/// and interpretation.
560///
561/// # Errors
562///
563/// Returns a JSON error object if parsing fails or domain check fails.
564#[wasm_bindgen]
565pub fn shapiro_wilk_test(y_json: &str, x_vars_json: &str) -> String {
566    if let Err(e) = check_domain() {
567        return error_to_json(&e);
568    }
569
570    let y: Vec<f64> = match serde_json::from_str(y_json) {
571        Ok(v) => v,
572        Err(e) => return error_json(&format!("Failed to parse y: {}", e)),
573    };
574
575    let x_vars: Vec<Vec<f64>> = match serde_json::from_str(x_vars_json) {
576        Ok(v) => v,
577        Err(e) => return error_json(&format!("Failed to parse x_vars: {}", e)),
578    };
579
580    match diagnostics::shapiro_wilk_test(&y, &x_vars) {
581        Ok(output) => serde_json::to_string(&output)
582            .unwrap_or_else(|_| error_json("Failed to serialize Shapiro-Wilk test result")),
583        Err(e) => error_json(&e.to_string()),
584    }
585}
586
587/// Performs the Anderson-Darling test for normality via WASM.
588///
589/// The Anderson-Darling test checks whether the residuals are normally distributed
590/// by comparing the empirical distribution to the expected normal distribution.
591/// This test is particularly sensitive to deviations in the tails of the distribution.
592/// A significant p-value suggests that the residuals deviate from normality.
593///
594/// # Arguments
595///
596/// * `y_json` - JSON array of response variable values
597/// * `x_vars_json` - JSON array of predictor arrays
598///
599/// # Returns
600///
601/// JSON string containing the A² statistic, p-value, and interpretation.
602///
603/// # Errors
604///
605/// Returns a JSON error object if parsing fails or domain check fails.
606#[wasm_bindgen]
607pub fn anderson_darling_test(y_json: &str, x_vars_json: &str) -> String {
608    if let Err(e) = check_domain() {
609        return error_to_json(&e);
610    }
611
612    let y: Vec<f64> = match serde_json::from_str(y_json) {
613        Ok(v) => v,
614        Err(e) => return error_json(&format!("Failed to parse y: {}", e)),
615    };
616
617    let x_vars: Vec<Vec<f64>> = match serde_json::from_str(x_vars_json) {
618        Ok(v) => v,
619        Err(e) => return error_json(&format!("Failed to parse x_vars: {}", e)),
620    };
621
622    match diagnostics::anderson_darling_test(&y, &x_vars) {
623        Ok(output) => serde_json::to_string(&output)
624            .unwrap_or_else(|_| error_json("Failed to serialize Anderson-Darling test result")),
625        Err(e) => error_json(&e.to_string()),
626    }
627}
628
629// ============================================================================
630// Cook's Distance (WASM wrapper)
631// ============================================================================
632
633/// Computes Cook's distance for identifying influential observations via WASM.
634///
635/// Cook's distance measures how much each observation influences the regression
636/// model by comparing coefficient estimates with and without that observation.
637/// Unlike hypothesis tests, this is an influence measure - not a test with p-values.
638///
639/// # Arguments
640///
641/// * `y_json` - JSON array of response variable values
642/// * `x_vars_json` - JSON array of predictor arrays
643///
644/// # Returns
645///
646/// JSON string containing:
647/// - Vector of Cook's distances (one per observation)
648/// - Thresholds for identifying influential observations
649/// - Indices of potentially influential observations
650/// - Interpretation and guidance
651///
652/// # Errors
653///
654/// Returns a JSON error object if parsing fails or domain check fails.
655#[wasm_bindgen]
656pub fn cooks_distance_test(y_json: &str, x_vars_json: &str) -> String {
657    if let Err(e) = check_domain() {
658        return error_to_json(&e);
659    }
660
661    let y: Vec<f64> = match serde_json::from_str(y_json) {
662        Ok(v) => v,
663        Err(e) => return error_json(&format!("Failed to parse y: {}", e)),
664    };
665
666    let x_vars: Vec<Vec<f64>> = match serde_json::from_str(x_vars_json) {
667        Ok(v) => v,
668        Err(e) => return error_json(&format!("Failed to parse x_vars: {}", e)),
669    };
670
671    match diagnostics::cooks_distance_test(&y, &x_vars) {
672        Ok(output) => serde_json::to_string(&output)
673            .unwrap_or_else(|_| error_json("Failed to serialize Cook's distance result")),
674        Err(e) => error_json(&e.to_string()),
675    }
676}
677
678/// Performs DFBETAS analysis via WASM.
679///
680/// DFBETAS measures the influence of each observation on each regression coefficient.
681/// For each observation and each coefficient, it computes the standardized change
682/// in the coefficient when that observation is omitted.
683///
684/// # Arguments
685///
686/// * `y_json` - JSON array of response variable values
687/// * `x_vars_json` - JSON array of predictor arrays
688///
689/// # Returns
690///
691/// JSON string containing the DFBETAS matrix, threshold, and influential observations.
692///
693/// # Errors
694///
695/// Returns a JSON error object if parsing fails or domain check fails.
696#[wasm_bindgen]
697pub fn dfbetas_test(y_json: &str, x_vars_json: &str) -> String {
698    if let Err(e) = check_domain() {
699        return error_to_json(&e);
700    }
701
702    let y: Vec<f64> = match serde_json::from_str(y_json) {
703        Ok(v) => v,
704        Err(e) => return error_json(&format!("Failed to parse y: {}", e)),
705    };
706
707    let x_vars: Vec<Vec<f64>> = match serde_json::from_str(x_vars_json) {
708        Ok(v) => v,
709        Err(e) => return error_json(&format!("Failed to parse x_vars: {}", e)),
710    };
711
712    match diagnostics::dfbetas_test(&y, &x_vars) {
713        Ok(output) => serde_json::to_string(&output)
714            .unwrap_or_else(|_| error_json("Failed to serialize DFBETAS result")),
715        Err(e) => error_json(&e.to_string()),
716    }
717}
718
719/// Performs DFFITS analysis via WASM.
720///
721/// DFFITS measures the influence of each observation on its own fitted value.
722/// It is the standardized change in the fitted value when that observation
723/// is omitted from the model.
724///
725/// # Arguments
726///
727/// * `y_json` - JSON array of response variable values
728/// * `x_vars_json` - JSON array of predictor arrays
729///
730/// # Returns
731///
732/// JSON string containing the DFFITS vector, threshold, and influential observations.
733///
734/// # Errors
735///
736/// Returns a JSON error object if parsing fails or domain check fails.
737#[wasm_bindgen]
738pub fn dffits_test(y_json: &str, x_vars_json: &str) -> String {
739    if let Err(e) = check_domain() {
740        return error_to_json(&e);
741    }
742
743    let y: Vec<f64> = match serde_json::from_str(y_json) {
744        Ok(v) => v,
745        Err(e) => return error_json(&format!("Failed to parse y: {}", e)),
746    };
747
748    let x_vars: Vec<Vec<f64>> = match serde_json::from_str(x_vars_json) {
749        Ok(v) => v,
750        Err(e) => return error_json(&format!("Failed to parse x_vars: {}", e)),
751    };
752
753    match diagnostics::dffits_test(&y, &x_vars) {
754        Ok(output) => serde_json::to_string(&output)
755            .unwrap_or_else(|_| error_json("Failed to serialize DFFITS result")),
756        Err(e) => error_json(&e.to_string()),
757    }
758}
759
760/// Performs Variance Inflation Factor (VIF) analysis via WASM.
761///
762/// VIF measures how much the variance of regression coefficients is inflated
763/// due to multicollinearity among predictor variables. High VIF values indicate
764/// that a predictor is highly correlated with other predictors.
765///
766/// # Arguments
767///
768/// * `y_json` - JSON array of response variable values
769/// * `x_vars_json` - JSON array of predictor arrays
770///
771/// # Returns
772///
773/// JSON string containing the maximum VIF, detailed VIF results for each predictor,
774/// interpretation, and guidance.
775///
776/// # Interpretation
777///
778/// - VIF = 1: No correlation with other predictors
779/// - VIF > 5: Moderate multicollinearity (concerning)
780/// - VIF > 10: High multicollinearity (severe)
781///
782/// # Errors
783///
784/// Returns a JSON error object if parsing fails or domain check fails.
785#[wasm_bindgen]
786pub fn vif_test(y_json: &str, x_vars_json: &str) -> String {
787    if let Err(e) = check_domain() {
788        return error_to_json(&e);
789    }
790
791    let y: Vec<f64> = match serde_json::from_str(y_json) {
792        Ok(v) => v,
793        Err(e) => return error_json(&format!("Failed to parse y: {}", e)),
794    };
795
796    let x_vars: Vec<Vec<f64>> = match serde_json::from_str(x_vars_json) {
797        Ok(v) => v,
798        Err(e) => return error_json(&format!("Failed to parse x_vars: {}", e)),
799    };
800
801    match diagnostics::vif_test(&y, &x_vars) {
802        Ok(output) => serde_json::to_string(&output)
803            .unwrap_or_else(|_| error_json("Failed to serialize VIF result")),
804        Err(e) => error_json(&e.to_string()),
805    }
806}
807
808/// Performs the RESET test for model specification error via WASM.
809///
810/// The RESET (Regression Specification Error Test) test checks whether the model
811/// is correctly specified by testing if additional terms (powers of fitted values,
812/// regressors, or first principal component) significantly improve the model fit.
813///
814/// # Arguments
815///
816/// * `y_json` - JSON array of response variable values
817/// * `x_vars_json` - JSON array of predictor arrays
818/// * `powers_json` - JSON array of powers to use (e.g., [2, 3] for ŷ², ŷ³)
819/// * `type_` - Type of terms to add: "fitted", "regressor", or "princomp"
820///
821/// # Returns
822///
823/// JSON string containing the F-statistic, p-value, and interpretation.
824///
825/// # Errors
826///
827/// Returns a JSON error object if parsing fails or domain check fails.
828#[wasm_bindgen]
829pub fn reset_test(y_json: &str, x_vars_json: &str, powers_json: &str, type_: &str) -> String {
830    if let Err(e) = check_domain() {
831        return error_to_json(&e);
832    }
833
834    let y: Vec<f64> = match serde_json::from_str(y_json) {
835        Ok(v) => v,
836        Err(e) => return error_json(&format!("Failed to parse y: {}", e)),
837    };
838
839    let x_vars: Vec<Vec<f64>> = match serde_json::from_str(x_vars_json) {
840        Ok(v) => v,
841        Err(e) => return error_json(&format!("Failed to parse x_vars: {}", e)),
842    };
843
844    let powers: Vec<usize> = match serde_json::from_str(powers_json) {
845        Ok(v) => v,
846        Err(e) => return error_json(&format!("Failed to parse powers: {}", e)),
847    };
848
849    // Parse reset type (default to "fitted")
850    let reset_type = match type_.to_lowercase().as_str() {
851        "regressor" => diagnostics::ResetType::Regressor,
852        "princomp" => diagnostics::ResetType::PrincipalComponent,
853        _ => diagnostics::ResetType::Fitted,
854    };
855
856    match diagnostics::reset_test(&y, &x_vars, &powers, reset_type) {
857        Ok(output) => serde_json::to_string(&output)
858            .unwrap_or_else(|_| error_json("Failed to serialize RESET test result")),
859        Err(e) => error_json(&e.to_string()),
860    }
861}
862
863/// Performs the Breusch-Godfrey test for higher-order serial correlation via WASM.
864///
865/// Unlike the Durbin-Watson test which only detects first-order autocorrelation,
866/// the Breusch-Godfrey test can detect serial correlation at any lag order.
867///
868/// # Arguments
869///
870/// * `y_json` - JSON array of response variable values
871/// * `x_vars_json` - JSON array of predictor arrays
872/// * `order` - Maximum order of serial correlation to test (default: 1)
873/// * `test_type` - Type of test statistic: "chisq" or "f" (default: "chisq")
874///
875/// # Returns
876///
877/// JSON string containing test statistic, p-value, degrees of freedom, and interpretation.
878///
879/// # Errors
880///
881/// Returns a JSON error object if parsing fails or domain check fails.
882#[wasm_bindgen]
883pub fn breusch_godfrey_test(y_json: &str, x_vars_json: &str, order: usize, test_type: &str) -> String {
884    if let Err(e) = check_domain() {
885        return error_to_json(&e);
886    }
887
888    let y: Vec<f64> = match serde_json::from_str(y_json) {
889        Ok(v) => v,
890        Err(e) => return error_json(&format!("Failed to parse y: {}", e)),
891    };
892
893    let x_vars: Vec<Vec<f64>> = match serde_json::from_str(x_vars_json) {
894        Ok(v) => v,
895        Err(e) => return error_json(&format!("Failed to parse x_vars: {}", e)),
896    };
897
898    // Parse test type (default to "chisq")
899    let bg_test_type = match test_type.to_lowercase().as_str() {
900        "f" => diagnostics::BGTestType::F,
901        _ => diagnostics::BGTestType::Chisq,
902    };
903
904    match diagnostics::breusch_godfrey_test(&y, &x_vars, order, bg_test_type) {
905        Ok(output) => serde_json::to_string(&output)
906            .unwrap_or_else(|_| error_json("Failed to serialize Breusch-Godfrey test result")),
907        Err(e) => error_json(&e.to_string()),
908    }
909}
910
911// ============================================================================
912// LOESS (Locally Estimated Scatterplot Smoothing) WASM Wrappers
913// ============================================================================
914
915/// Performs LOESS regression via WASM.
916///
917/// LOESS (Locally Estimated Scatterplot Smoothing) is a non-parametric
918/// regression method that fits multiple regressions in local subsets
919/// of data to create a smooth curve through the data points.
920///
921/// # Arguments
922///
923/// * `y_json` - JSON array of response variable values
924/// * `x_vars_json` - JSON array of predictor arrays
925/// * `span` - Fraction of data used in each local fit (0.0 to 1.0)
926/// * `degree` - Degree of local polynomial: 0 (constant), 1 (linear), or 2 (quadratic)
927/// * `robust_iterations` - Number of robustness iterations (0 for non-robust fit)
928/// * `surface` - Surface computation method: "direct" or "interpolate"
929///
930/// # Returns
931///
932/// JSON string containing:
933/// - `fitted` - Fitted values at each observation point
934/// - `span` - Span parameter used
935/// - `degree` - Degree of polynomial used
936/// - `robust_iterations` - Number of robustness iterations performed
937/// - `surface` - Surface computation method used
938///
939/// # Errors
940///
941/// Returns a JSON error object if parsing fails or domain check fails.
942#[wasm_bindgen]
943pub fn loess_fit(
944    y_json: &str,
945    x_vars_json: &str,
946    span: f64,
947    degree: usize,
948    robust_iterations: usize,
949    surface: &str,
950) -> String {
951    if let Err(e) = check_domain() {
952        return error_to_json(&e);
953    }
954
955    let y: Vec<f64> = match serde_json::from_str(y_json) {
956        Ok(v) => v,
957        Err(e) => return error_json(&format!("Failed to parse y: {}", e)),
958    };
959
960    let x_vars: Vec<Vec<f64>> = match serde_json::from_str(x_vars_json) {
961        Ok(v) => v,
962        Err(e) => return error_json(&format!("Failed to parse x_vars: {}", e)),
963    };
964
965    let n_predictors = x_vars.len();
966
967    // Parse surface parameter (default to "direct")
968    let surface = match surface.to_lowercase().as_str() {
969        "interpolate" => loess::LoessSurface::Interpolate,
970        _ => loess::LoessSurface::Direct,
971    };
972
973    let options = loess::LoessOptions {
974        span,
975        degree,
976        robust_iterations,
977        n_predictors,
978        surface,
979    };
980
981    match loess::loess_fit(&y, &x_vars, &options) {
982        Ok(output) => serde_json::to_string(&output)
983            .unwrap_or_else(|_| error_json("Failed to serialize LOESS result")),
984        Err(e) => error_json(&e.to_string()),
985    }
986}
987
988/// Performs LOESS prediction at new query points via WASM.
989///
990/// Predicts LOESS fitted values at arbitrary new points by redoing the
991/// local fitting at each query point using the original training data.
992///
993/// # Arguments
994///
995/// * `new_x_json` - JSON array of new predictor values (p vectors, each of length m)
996/// * `original_x_json` - JSON array of original training predictors
997/// * `original_y_json` - JSON array of original training response values
998/// * `span` - Span parameter (must match the original fit)
999/// * `degree` - Degree of polynomial (must match the original fit)
1000/// * `robust_iterations` - Robustness iterations (must match the original fit)
1001/// * `surface` - Surface computation method: "direct" or "interpolate"
1002///
1003/// # Returns
1004///
1005/// JSON string containing:
1006/// - `predictions` - Vector of predicted values at query points
1007///
1008/// # Errors
1009///
1010/// Returns a JSON error object if parsing fails, parameters don't match
1011/// the original fit, or domain check fails.
1012#[wasm_bindgen]
1013pub fn loess_predict(
1014    new_x_json: &str,
1015    original_x_json: &str,
1016    original_y_json: &str,
1017    span: f64,
1018    degree: usize,
1019    robust_iterations: usize,
1020    surface: &str,
1021) -> String {
1022    if let Err(e) = check_domain() {
1023        return error_to_json(&e);
1024    }
1025
1026    let new_x: Vec<Vec<f64>> = match serde_json::from_str(new_x_json) {
1027        Ok(v) => v,
1028        Err(e) => return error_json(&format!("Failed to parse new_x: {}", e)),
1029    };
1030
1031    let original_x: Vec<Vec<f64>> = match serde_json::from_str(original_x_json) {
1032        Ok(v) => v,
1033        Err(e) => return error_json(&format!("Failed to parse original_x: {}", e)),
1034    };
1035
1036    let original_y: Vec<f64> = match serde_json::from_str(original_y_json) {
1037        Ok(v) => v,
1038        Err(e) => return error_json(&format!("Failed to parse original_y: {}", e)),
1039    };
1040
1041    let n_predictors = original_x.len();
1042
1043    // Create a LoessFit with the same parameters (for validation)
1044    // Parse surface parameter (default to "direct")
1045    let surface = match surface.to_lowercase().as_str() {
1046        "interpolate" => loess::LoessSurface::Interpolate,
1047        _ => loess::LoessSurface::Direct,
1048    };
1049
1050    let fit = loess::LoessFit {
1051        fitted: vec![0.0; original_y.len()], // Dummy fitted values
1052        predictions: None,
1053        span,
1054        degree,
1055        robust_iterations,
1056        surface,
1057    };
1058
1059    let options = loess::LoessOptions {
1060        span,
1061        degree,
1062        robust_iterations,
1063        n_predictors,
1064        surface,
1065    };
1066
1067    match fit.predict(&new_x, &original_x, &original_y, &options) {
1068        Ok(predictions) => {
1069            let result = serde_json::json!({
1070                "predictions": predictions
1071            });
1072            result.to_string()
1073        },
1074        Err(e) => error_json(&e.to_string()),
1075    }
1076}
1077
1078// ============================================================================
1079// Regularized Regression WASM Wrappers
1080// ============================================================================
1081
1082#[wasm_bindgen]
1083/// Performs Ridge regression via WASM.
1084///
1085/// Ridge regression adds an L2 penalty to the coefficients, which helps with
1086/// multicollinearity and overfitting. The intercept is never penalized.
1087///
1088/// # Arguments
1089///
1090/// * `y_json` - JSON array of response variable values
1091/// * `x_vars_json` - JSON array of predictor arrays
1092/// * `variable_names` - JSON array of variable names
1093/// * `lambda` - Regularization strength (>= 0, typical range 0.01 to 100)
1094/// * `standardize` - Whether to standardize predictors (recommended: true)
1095///
1096/// # Returns
1097///
1098/// JSON string containing:
1099/// - `lambda` - Lambda value used
1100/// - `intercept` - Intercept coefficient
1101/// - `coefficients` - Slope coefficients
1102/// - `fitted_values` - Predictions on training data
1103/// - `residuals` - Residuals (y - fitted_values)
1104/// - `df` - Effective degrees of freedom
1105///
1106/// # Errors
1107///
1108/// Returns a JSON error object if parsing fails, lambda is negative,
1109/// or domain check fails.
1110pub fn ridge_regression(
1111    y_json: &str,
1112    x_vars_json: &str,
1113    _variable_names: &str,
1114    lambda: f64,
1115    standardize: bool,
1116) -> String {
1117    if let Err(e) = check_domain() {
1118        return error_to_json(&e);
1119    }
1120
1121    // Parse JSON input
1122    let y: Vec<f64> = match serde_json::from_str(y_json) {
1123        Ok(v) => v,
1124        Err(e) => return error_json(&format!("Failed to parse y: {}", e)),
1125    };
1126
1127    let x_vars: Vec<Vec<f64>> = match serde_json::from_str(x_vars_json) {
1128        Ok(v) => v,
1129        Err(e) => return error_json(&format!("Failed to parse x_vars: {}", e)),
1130    };
1131
1132    // Build design matrix with intercept column
1133    let n = y.len();
1134    let p = x_vars.len();
1135
1136    if n <= p + 1 {
1137        return error_json(&format!(
1138            "Insufficient data: need at least {} observations for {} predictors",
1139            p + 2,
1140            p
1141        ));
1142    }
1143
1144    let mut x_data = vec![1.0; n * (p + 1)]; // Intercept column
1145    for (j, x_var) in x_vars.iter().enumerate() {
1146        if x_var.len() != n {
1147            return error_json(&format!(
1148                "x_vars[{}] has {} elements, expected {}",
1149                j,
1150                x_var.len(),
1151                n
1152            ));
1153        }
1154        for (i, &val) in x_var.iter().enumerate() {
1155            x_data[i * (p + 1) + j + 1] = val;
1156        }
1157    }
1158
1159    let x = linalg::Matrix::new(n, p + 1, x_data);
1160
1161    // Configure ridge options
1162    let options = regularized::ridge::RidgeFitOptions {
1163        lambda,
1164        intercept: true,
1165        standardize,
1166        max_iter: 100000,
1167        tol: 1e-7,
1168        warm_start: None,
1169        weights: None,
1170    };
1171
1172    match regularized::ridge::ridge_fit(&x, &y, &options) {
1173        Ok(output) => serde_json::to_string(&output)
1174            .unwrap_or_else(|_| error_json("Failed to serialize ridge regression result")),
1175        Err(e) => error_json(&e.to_string()),
1176    }
1177}
1178
1179#[wasm_bindgen]
1180/// Performs Lasso regression via WASM.
1181///
1182/// Lasso regression adds an L1 penalty to the coefficients, which performs
1183/// automatic variable selection by shrinking some coefficients to exactly zero.
1184/// The intercept is never penalized.
1185///
1186/// # Arguments
1187///
1188/// * `y_json` - JSON array of response variable values
1189/// * `x_vars_json` - JSON array of predictor arrays
1190/// * `variable_names` - JSON array of variable names
1191/// * `lambda` - Regularization strength (>= 0, typical range 0.01 to 10)
1192/// * `standardize` - Whether to standardize predictors (recommended: true)
1193/// * `max_iter` - Maximum coordinate descent iterations (default: 100000)
1194/// * `tol` - Convergence tolerance (default: 1e-7)
1195///
1196/// # Returns
1197///
1198/// JSON string containing:
1199/// - `lambda` - Lambda value used
1200/// - `intercept` - Intercept coefficient
1201/// - `coefficients` - Slope coefficients (some may be exactly zero)
1202/// - `fitted_values` - Predictions on training data
1203/// - `residuals` - Residuals (y - fitted_values)
1204/// - `n_nonzero` - Number of non-zero coefficients (excluding intercept)
1205/// - `iterations` - Number of coordinate descent iterations
1206/// - `converged` - Whether the algorithm converged
1207///
1208/// # Errors
1209///
1210/// Returns a JSON error object if parsing fails, lambda is negative,
1211/// or domain check fails.
1212pub fn lasso_regression(
1213    y_json: &str,
1214    x_vars_json: &str,
1215    _variable_names: &str,
1216    lambda: f64,
1217    standardize: bool,
1218    max_iter: usize,
1219    tol: f64,
1220) -> String {
1221    if let Err(e) = check_domain() {
1222        return error_to_json(&e);
1223    }
1224
1225    // Parse JSON input
1226    let y: Vec<f64> = match serde_json::from_str(y_json) {
1227        Ok(v) => v,
1228        Err(e) => return error_json(&format!("Failed to parse y: {}", e)),
1229    };
1230
1231    let x_vars: Vec<Vec<f64>> = match serde_json::from_str(x_vars_json) {
1232        Ok(v) => v,
1233        Err(e) => return error_json(&format!("Failed to parse x_vars: {}", e)),
1234    };
1235
1236    // Build design matrix with intercept column
1237    let n = y.len();
1238    let p = x_vars.len();
1239
1240    if n <= p + 1 {
1241        return error_json(&format!(
1242            "Insufficient data: need at least {} observations for {} predictors",
1243            p + 2,
1244            p
1245        ));
1246    }
1247
1248    let mut x_data = vec![1.0; n * (p + 1)]; // Intercept column
1249    for (j, x_var) in x_vars.iter().enumerate() {
1250        if x_var.len() != n {
1251            return error_json(&format!(
1252                "x_vars[{}] has {} elements, expected {}",
1253                j,
1254                x_var.len(),
1255                n
1256            ));
1257        }
1258        for (i, &val) in x_var.iter().enumerate() {
1259            x_data[i * (p + 1) + j + 1] = val;
1260        }
1261    }
1262
1263    let x = linalg::Matrix::new(n, p + 1, x_data);
1264
1265    // Configure lasso options
1266    let options = regularized::lasso::LassoFitOptions {
1267        lambda,
1268        intercept: true,
1269        standardize,
1270        max_iter,
1271        tol,
1272        ..Default::default()
1273    };
1274
1275    match regularized::lasso::lasso_fit(&x, &y, &options) {
1276        Ok(output) => serde_json::to_string(&output)
1277            .unwrap_or_else(|_| error_json("Failed to serialize lasso regression result")),
1278        Err(e) => error_json(&e.to_string()),
1279    }
1280}
1281
1282#[wasm_bindgen]
1283#[allow(clippy::too_many_arguments)]
1284/// Performs Elastic Net regression via WASM.
1285///
1286/// Elastic Net combines L1 (Lasso) and L2 (Ridge) penalties.
1287///
1288/// # Arguments
1289///
1290/// * `y_json` - JSON array of response variable values
1291/// * `x_vars_json` - JSON array of predictor arrays
1292/// * `variable_names` - JSON array of variable names
1293/// * `lambda` - Regularization strength (>= 0)
1294/// * `alpha` - Elastic net mixing parameter (0 = Ridge, 1 = Lasso)
1295/// * `standardize` - Whether to standardize predictors (recommended: true)
1296/// * `max_iter` - Maximum coordinate descent iterations
1297/// * `tol` - Convergence tolerance
1298///
1299/// # Returns
1300///
1301/// JSON string containing regression results (same fields as Lasso).
1302///
1303/// # Errors
1304///
1305/// Returns a JSON error object if parsing fails, parameters are invalid,
1306/// or domain check fails.
1307pub fn elastic_net_regression(
1308    y_json: &str,
1309    x_vars_json: &str,
1310    _variable_names: &str,
1311    lambda: f64,
1312    alpha: f64,
1313    standardize: bool,
1314    max_iter: usize,
1315    tol: f64,
1316) -> String {
1317    if let Err(e) = check_domain() {
1318        return error_to_json(&e);
1319    }
1320
1321    // Parse JSON input
1322    let y: Vec<f64> = match serde_json::from_str(y_json) {
1323        Ok(v) => v,
1324        Err(e) => return error_json(&format!("Failed to parse y: {}", e)),
1325    };
1326
1327    let x_vars: Vec<Vec<f64>> = match serde_json::from_str(x_vars_json) {
1328        Ok(v) => v,
1329        Err(e) => return error_json(&format!("Failed to parse x_vars: {}", e)),
1330    };
1331
1332    // Build design matrix with intercept column
1333    let n = y.len();
1334    let p = x_vars.len();
1335
1336    if n <= p + 1 {
1337        return error_json(&format!(
1338            "Insufficient data: need at least {} observations for {} predictors",
1339            p + 2,
1340            p
1341        ));
1342    }
1343
1344    let mut x_data = vec![1.0; n * (p + 1)]; // Intercept column
1345    for (j, x_var) in x_vars.iter().enumerate() {
1346        if x_var.len() != n {
1347            return error_json(&format!(
1348                "x_vars[{}] has {} elements, expected {}",
1349                j,
1350                x_var.len(),
1351                n
1352            ));
1353        }
1354        for (i, &val) in x_var.iter().enumerate() {
1355            x_data[i * (p + 1) + j + 1] = val;
1356        }
1357    }
1358
1359    let x = linalg::Matrix::new(n, p + 1, x_data);
1360
1361    // Configure elastic net options
1362    let options = regularized::elastic_net::ElasticNetOptions {
1363        lambda,
1364        alpha,
1365        intercept: true,
1366        standardize,
1367        max_iter,
1368        tol,
1369        ..Default::default()
1370    };
1371
1372    match regularized::elastic_net::elastic_net_fit(&x, &y, &options) {
1373        Ok(output) => serde_json::to_string(&output)
1374            .unwrap_or_else(|_| error_json("Failed to serialize elastic net regression result")),
1375        Err(e) => error_json(&e.to_string()),
1376    }
1377}
1378
1379#[wasm_bindgen]
1380/// Generates a lambda path for regularized regression via WASM.
1381///
1382/// Creates a logarithmically-spaced sequence of lambda values from lambda_max
1383/// (where all penalized coefficients are zero) down to lambda_min. This is
1384/// useful for fitting regularization paths and selecting optimal lambda via
1385/// cross-validation.
1386///
1387/// # Arguments
1388///
1389/// * `y_json` - JSON array of response variable values
1390/// * `x_vars_json` - JSON array of predictor arrays
1391/// * `n_lambda` - Number of lambda values to generate (default: 100)
1392/// * `lambda_min_ratio` - Ratio for smallest lambda (default: 0.0001 if n >= p, else 0.01)
1393///
1394/// # Returns
1395///
1396/// JSON string containing:
1397/// - `lambda_path` - Array of lambda values in decreasing order
1398/// - `lambda_max` - Maximum lambda value
1399/// - `lambda_min` - Minimum lambda value
1400/// - `n_lambda` - Number of lambda values
1401///
1402/// # Errors
1403///
1404/// Returns a JSON error object if parsing fails or domain check fails.
1405pub fn make_lambda_path(
1406    y_json: &str,
1407    x_vars_json: &str,
1408    n_lambda: usize,
1409    lambda_min_ratio: f64,
1410) -> String {
1411    if let Err(e) = check_domain() {
1412        return error_to_json(&e);
1413    }
1414
1415    // Parse JSON input
1416    let y: Vec<f64> = match serde_json::from_str(y_json) {
1417        Ok(v) => v,
1418        Err(e) => return error_json(&format!("Failed to parse y: {}", e)),
1419    };
1420
1421    let x_vars: Vec<Vec<f64>> = match serde_json::from_str(x_vars_json) {
1422        Ok(v) => v,
1423        Err(e) => return error_json(&format!("Failed to parse x_vars: {}", e)),
1424    };
1425
1426    // Build design matrix with intercept column
1427    let n = y.len();
1428    let p = x_vars.len();
1429
1430    let mut x_data = vec![1.0; n * (p + 1)]; // Intercept column
1431    for (j, x_var) in x_vars.iter().enumerate() {
1432        if x_var.len() != n {
1433            return error_json(&format!(
1434                "x_vars[{}] has {} elements, expected {}",
1435                j,
1436                x_var.len(),
1437                n
1438            ));
1439        }
1440        for (i, &val) in x_var.iter().enumerate() {
1441            x_data[i * (p + 1) + j + 1] = val;
1442        }
1443    }
1444
1445    let x = linalg::Matrix::new(n, p + 1, x_data);
1446
1447    // Standardize X for lambda path computation
1448    let x_mean: Vec<f64> = (0..x.cols)
1449        .map(|j| {
1450            if j == 0 {
1451                1.0 // Intercept column
1452            } else {
1453                (0..n).map(|i| x.get(i, j)).sum::<f64>() / n as f64
1454            }
1455        })
1456        .collect();
1457
1458    let x_standardized: Vec<f64> = (0..x.cols)
1459        .map(|j| {
1460            if j == 0 {
1461                0.0 // Intercept column - no centering
1462            } else {
1463                let mean = x_mean[j];
1464                let variance =
1465                    (0..n).map(|i| (x.get(i, j) - mean).powi(2)).sum::<f64>() / (n - 1) as f64;
1466                variance.sqrt()
1467            }
1468        })
1469        .collect();
1470
1471    // Build standardized X matrix
1472    let mut x_standardized_data = vec![1.0; n * (p + 1)];
1473    for j in 0..x.cols {
1474        for i in 0..n {
1475            if j == 0 {
1476                x_standardized_data[i * (p + 1)] = 1.0; // Intercept
1477            } else {
1478                let std = x_standardized[j];
1479                if std > 1e-10 {
1480                    x_standardized_data[i * (p + 1) + j] = (x.get(i, j) - x_mean[j]) / std;
1481                } else {
1482                    x_standardized_data[i * (p + 1) + j] = 0.0;
1483                }
1484            }
1485        }
1486    }
1487    let x_standardized = linalg::Matrix::new(n, p + 1, x_standardized_data);
1488
1489    // Center y
1490    let y_mean: f64 = y.iter().sum::<f64>() / n as f64;
1491    let y_centered: Vec<f64> = y.iter().map(|&yi| yi - y_mean).collect();
1492
1493    // Configure lambda path options
1494    let options = regularized::path::LambdaPathOptions {
1495        nlambda: n_lambda.max(1),
1496        lambda_min_ratio: if lambda_min_ratio > 0.0 {
1497            Some(lambda_min_ratio)
1498        } else {
1499            None
1500        },
1501        alpha: 1.0, // Lasso
1502        ..Default::default()
1503    };
1504
1505    let lambda_path =
1506        regularized::path::make_lambda_path(&x_standardized, &y_centered, &options, None, Some(0));
1507
1508    let lambda_max = lambda_path.first().copied().unwrap_or(0.0);
1509    let lambda_min = lambda_path.last().copied().unwrap_or(0.0);
1510
1511    // Return as JSON (note: infinity serializes as null in JSON, handled in JS)
1512    let result = serde_json::json!({
1513        "lambda_path": lambda_path,
1514        "lambda_max": lambda_max,
1515        "lambda_min": lambda_min,
1516        "n_lambda": lambda_path.len()
1517    });
1518
1519    result.to_string()
1520}
1521
1522// ============================================================================
1523// Statistical Utility Functions (WASM wrappers)
1524// ============================================================================
1525
1526#[wasm_bindgen]
1527/// Computes the Student's t-distribution cumulative distribution function.
1528///
1529/// Returns P(T ≤ t) for a t-distribution with the given degrees of freedom.
1530///
1531/// # Arguments
1532///
1533/// * `t` - t-statistic value
1534/// * `df` - Degrees of freedom
1535///
1536/// # Returns
1537///
1538/// The CDF value, or `NaN` if domain check fails.
1539pub fn get_t_cdf(t: f64, df: f64) -> f64 {
1540    if check_domain().is_err() {
1541        return f64::NAN;
1542    }
1543
1544    student_t_cdf(t, df)
1545}
1546
1547#[wasm_bindgen]
1548/// Computes the critical t-value for a given significance level.
1549///
1550/// Returns the t-value such that the area under the t-distribution curve
1551/// to the right equals alpha/2 (two-tailed test).
1552///
1553/// # Arguments
1554///
1555/// * `alpha` - Significance level (typically 0.05 for 95% confidence)
1556/// * `df` - Degrees of freedom
1557///
1558/// # Returns
1559///
1560/// The critical t-value, or `NaN` if domain check fails.
1561pub fn get_t_critical(alpha: f64, df: f64) -> f64 {
1562    if check_domain().is_err() {
1563        return f64::NAN;
1564    }
1565
1566    core::t_critical_quantile(df, alpha)
1567}
1568
1569#[wasm_bindgen]
1570/// Computes the inverse of the standard normal CDF (probit function).
1571///
1572/// Returns the z-score such that P(Z ≤ z) = p for a standard normal distribution.
1573///
1574/// # Arguments
1575///
1576/// * `p` - Probability (0 < p < 1)
1577///
1578/// # Returns
1579///
1580/// The z-score, or `NaN` if domain check fails.
1581pub fn get_normal_inverse(p: f64) -> f64 {
1582    if check_domain().is_err() {
1583        return f64::NAN;
1584    }
1585
1586    normal_inverse_cdf(p)
1587}
1588
1589// ============================================================================
1590// Statistical Utilities (WASM-only)
1591// ============================================================================
1592
1593#[wasm_bindgen]
1594/// Computes the arithmetic mean of a JSON array of f64 values.
1595///
1596/// # Arguments
1597///
1598/// * `data_json` - JSON string representing an array of f64 values
1599///
1600/// # Returns
1601///
1602/// JSON string with the mean, or "null" if input is invalid/empty
1603pub fn stats_mean(data_json: String) -> String {
1604    if check_domain().is_err() {
1605        return "null".to_string();
1606    }
1607
1608    let data: Vec<f64> = match serde_json::from_str(&data_json) {
1609        Ok(d) => d,
1610        Err(_) => return "null".to_string(),
1611    };
1612
1613    serde_json::to_string(&stats::mean(&data)).unwrap_or("null".to_string())
1614}
1615
1616#[wasm_bindgen]
1617/// Computes the sample standard deviation of a JSON array of f64 values.
1618///
1619/// Uses the (n-1) denominator for unbiased estimation.
1620///
1621/// # Arguments
1622///
1623/// * `data_json` - JSON string representing an array of f64 values
1624///
1625/// # Returns
1626///
1627/// JSON string with the standard deviation, or "null" if input is invalid
1628pub fn stats_stddev(data_json: String) -> String {
1629    if check_domain().is_err() {
1630        return "null".to_string();
1631    }
1632
1633    let data: Vec<f64> = match serde_json::from_str(&data_json) {
1634        Ok(d) => d,
1635        Err(_) => return "null".to_string(),
1636    };
1637
1638    serde_json::to_string(&stats::stddev(&data)).unwrap_or("null".to_string())
1639}
1640
1641#[wasm_bindgen]
1642/// Computes the sample variance of a JSON array of f64 values.
1643///
1644/// Uses the (n-1) denominator for unbiased estimation.
1645///
1646/// # Arguments
1647///
1648/// * `data_json` - JSON string representing an array of f64 values
1649///
1650/// # Returns
1651///
1652/// JSON string with the variance, or "null" if input is invalid
1653pub fn stats_variance(data_json: String) -> String {
1654    if check_domain().is_err() {
1655        return "null".to_string();
1656    }
1657
1658    let data: Vec<f64> = match serde_json::from_str(&data_json) {
1659        Ok(d) => d,
1660        Err(_) => return "null".to_string(),
1661    };
1662
1663    serde_json::to_string(&stats::variance(&data)).unwrap_or("null".to_string())
1664}
1665
1666#[wasm_bindgen]
1667/// Computes the median of a JSON array of f64 values.
1668///
1669/// # Arguments
1670///
1671/// * `data_json` - JSON string representing an array of f64 values
1672///
1673/// # Returns
1674///
1675/// JSON string with the median, or "null" if input is invalid/empty
1676pub fn stats_median(data_json: String) -> String {
1677    if check_domain().is_err() {
1678        return "null".to_string();
1679    }
1680
1681    let data: Vec<f64> = match serde_json::from_str(&data_json) {
1682        Ok(d) => d,
1683        Err(_) => return "null".to_string(),
1684    };
1685
1686    serde_json::to_string(&stats::median(&data)).unwrap_or("null".to_string())
1687}
1688
1689#[wasm_bindgen]
1690/// Computes a quantile of a JSON array of f64 values.
1691///
1692/// # Arguments
1693///
1694/// * `data_json` - JSON string representing an array of f64 values
1695/// * `q` - Quantile to calculate (0.0 to 1.0)
1696///
1697/// # Returns
1698///
1699/// JSON string with the quantile value, or "null" if input is invalid
1700pub fn stats_quantile(data_json: String, q: f64) -> String {
1701    if check_domain().is_err() {
1702        return "null".to_string();
1703    }
1704
1705    let data: Vec<f64> = match serde_json::from_str(&data_json) {
1706        Ok(d) => d,
1707        Err(_) => return "null".to_string(),
1708    };
1709
1710    serde_json::to_string(&stats::quantile(&data, q)).unwrap_or("null".to_string())
1711}
1712
1713#[wasm_bindgen]
1714/// Computes the correlation coefficient between two JSON arrays of f64 values.
1715///
1716/// # Arguments
1717///
1718/// * `x_json` - JSON string representing the first array of f64 values
1719/// * `y_json` - JSON string representing the second array of f64 values
1720///
1721/// # Returns
1722///
1723/// JSON string with the correlation coefficient, or "null" if input is invalid
1724pub fn stats_correlation(x_json: String, y_json: String) -> String {
1725    if check_domain().is_err() {
1726        return "null".to_string();
1727    }
1728
1729    let x: Vec<f64> = match serde_json::from_str(&x_json) {
1730        Ok(d) => d,
1731        Err(_) => return "null".to_string(),
1732    };
1733
1734    let y: Vec<f64> = match serde_json::from_str(&y_json) {
1735        Ok(d) => d,
1736        Err(_) => return "null".to_string(),
1737    };
1738
1739    serde_json::to_string(&stats::correlation(&x, &y)).unwrap_or("null".to_string())
1740}
1741
1742// ============================================================================
1743// Domain Check (WASM-only)
1744// ============================================================================
1745//
1746// By default, all domains are allowed. To enable domain restriction, set the
1747// LINREG_DOMAIN_RESTRICT environment variable at build time:
1748//
1749//   LINREG_DOMAIN_RESTRICT=example.com,yoursite.com wasm-pack build
1750//
1751// Example for jesse-anderson.net:
1752//   LINREG_DOMAIN_RESTRICT=jesse-anderson.net,tools.jesse-anderson.net,localhost,127.0.0.1 wasm-pack build
1753//
1754// This allows downstream users to use the library without modification while
1755// still providing domain restriction as an opt-in security feature.
1756
1757fn check_domain() -> Result<()> {
1758    // Read allowed domains from build-time environment variable
1759    let allowed_domains = option_env!("LINREG_DOMAIN_RESTRICT");
1760
1761    match allowed_domains {
1762        Some(domains) if !domains.is_empty() => {
1763            // Domain restriction is enabled
1764            let window =
1765                web_sys::window().ok_or(Error::DomainCheck("No window found".to_string()))?;
1766            let location = window.location();
1767            let hostname = location
1768                .hostname()
1769                .map_err(|_| Error::DomainCheck("No hostname found".to_string()))?;
1770
1771            let domain_list: Vec<&str> = domains.split(',').map(|s| s.trim()).collect();
1772
1773            if domain_list.contains(&hostname.as_str()) {
1774                Ok(())
1775            } else {
1776                Err(Error::DomainCheck(format!(
1777                    "Unauthorized domain: {}. Allowed: {}",
1778                    hostname, domains
1779                )))
1780            }
1781        },
1782        _ => {
1783            // No restriction - allow all domains
1784            Ok(())
1785        },
1786    }
1787}
1788
1789// ============================================================================
1790// Test Functions (WASM-only)
1791// ============================================================================
1792
1793#[wasm_bindgen]
1794/// Simple test function to verify WASM is working.
1795///
1796/// Returns a success message confirming the WASM module loaded correctly.
1797///
1798/// # Errors
1799///
1800/// Returns a JSON error object if domain check fails.
1801pub fn test() -> String {
1802    if let Err(e) = check_domain() {
1803        return error_to_json(&e);
1804    }
1805    "Rust WASM is working!".to_string()
1806}
1807
1808#[wasm_bindgen]
1809/// Returns the current version of the library.
1810///
1811/// Returns the Cargo package version as a string (e.g., "0.1.0").
1812///
1813/// # Errors
1814///
1815/// Returns a JSON error object if domain check fails.
1816pub fn get_version() -> String {
1817    if let Err(e) = check_domain() {
1818        return error_to_json(&e);
1819    }
1820    env!("CARGO_PKG_VERSION").to_string()
1821}
1822
1823#[wasm_bindgen]
1824/// Test function for t-critical value computation.
1825///
1826/// Returns JSON with the computed t-critical value for the given parameters.
1827///
1828/// # Errors
1829///
1830/// Returns a JSON error object if domain check fails.
1831pub fn test_t_critical(df: f64, alpha: f64) -> String {
1832    if let Err(e) = check_domain() {
1833        return error_to_json(&e);
1834    }
1835    let t_crit = core::t_critical_quantile(df, alpha);
1836    format!(
1837        r#"{{"df": {}, "alpha": {}, "t_critical": {}}}"#,
1838        df, alpha, t_crit
1839    )
1840}
1841
1842#[wasm_bindgen]
1843/// Test function for confidence interval computation.
1844///
1845/// Returns JSON with the computed confidence interval for a coefficient.
1846///
1847/// # Errors
1848///
1849/// Returns a JSON error object if domain check fails.
1850pub fn test_ci(coef: f64, se: f64, df: f64, alpha: f64) -> String {
1851    if let Err(e) = check_domain() {
1852        return error_to_json(&e);
1853    }
1854    let t_crit = core::t_critical_quantile(df, alpha);
1855    format!(
1856        r#"{{"lower": {}, "upper": {}}}"#,
1857        coef - t_crit * se,
1858        coef + t_crit * se
1859    )
1860}
1861
1862#[wasm_bindgen]
1863/// Test function for R accuracy validation.
1864///
1865/// Returns JSON comparing our statistical functions against R reference values.
1866///
1867/// # Errors
1868///
1869/// Returns a JSON error object if domain check fails.
1870pub fn test_r_accuracy() -> String {
1871    if let Err(e) = check_domain() {
1872        return error_to_json(&e);
1873    }
1874    format!(
1875        r#"{{"two_tail_p": {}, "qt_975": {}}}"#,
1876        core::two_tailed_p_value(1.6717, 21.0),
1877        core::t_critical_quantile(21.0, 0.05)
1878    )
1879}
1880
1881#[wasm_bindgen]
1882/// Test function for regression validation against R reference values.
1883///
1884/// Runs a regression on a housing dataset and compares results against R's lm() output.
1885/// Returns JSON with status "PASS" or "FAIL" with details.
1886///
1887/// # Errors
1888///
1889/// Returns a JSON error object if domain check fails.
1890pub fn test_housing_regression() -> String {
1891    if let Err(e) = check_domain() {
1892        return error_to_json(&e);
1893    }
1894
1895    match test_housing_regression_native() {
1896        Ok(result) => result,
1897        Err(e) => serde_json::json!({ "status": "ERROR", "error": e.to_string() }).to_string(),
1898    }
1899}
1900
1901// Native Rust test function (works without WASM feature)
1902#[cfg(any(test, feature = "wasm"))]
1903fn test_housing_regression_native() -> Result<String> {
1904    let y = vec![
1905        245.5, 312.8, 198.4, 425.6, 278.9, 356.2, 189.5, 512.3, 234.7, 298.1, 445.8, 167.9, 367.4,
1906        289.6, 198.2, 478.5, 256.3, 334.7, 178.5, 398.9, 223.4, 312.5, 156.8, 423.7, 267.9,
1907    ];
1908
1909    let square_feet = vec![
1910        1200.0, 1800.0, 950.0, 2400.0, 1450.0, 2000.0, 1100.0, 2800.0, 1350.0, 1650.0, 2200.0,
1911        900.0, 1950.0, 1500.0, 1050.0, 2600.0, 1300.0, 1850.0, 1000.0, 2100.0, 1250.0, 1700.0,
1912        850.0, 2350.0, 1400.0,
1913    ];
1914    let bedrooms = vec![
1915        3.0, 4.0, 2.0, 4.0, 3.0, 4.0, 2.0, 5.0, 3.0, 3.0, 4.0, 2.0, 4.0, 3.0, 2.0, 5.0, 3.0, 4.0,
1916        2.0, 4.0, 3.0, 3.0, 2.0, 4.0, 3.0,
1917    ];
1918    let age = vec![
1919        15.0, 10.0, 25.0, 5.0, 8.0, 12.0, 20.0, 2.0, 18.0, 7.0, 3.0, 30.0, 6.0, 14.0, 22.0, 1.0,
1920        16.0, 9.0, 28.0, 4.0, 19.0, 11.0, 35.0, 3.0, 13.0,
1921    ];
1922
1923    let x_vars = vec![square_feet, bedrooms, age];
1924    let names = vec![
1925        "Intercept".to_string(),
1926        "Square_Feet".to_string(),
1927        "Bedrooms".to_string(),
1928        "Age".to_string(),
1929    ];
1930
1931    let result = core::ols_regression(&y, &x_vars, &names)?;
1932
1933    // Check against R results
1934    let expected_coeffs = [52.1271333, 0.1613877, 0.9545492, -1.1811815];
1935    let expected_std_errs = [31.18201809, 0.01875072, 10.44400198, 0.73219949];
1936
1937    let tolerance = 1e-4;
1938    let mut mismatches = vec![];
1939
1940    for i in 0..4 {
1941        if (result.coefficients[i] - expected_coeffs[i]).abs() > tolerance {
1942            mismatches.push(format!(
1943                "coeff[{}] differs: got {}, expected {}",
1944                i, result.coefficients[i], expected_coeffs[i]
1945            ));
1946        }
1947        if (result.std_errors[i] - expected_std_errs[i]).abs() > tolerance {
1948            mismatches.push(format!(
1949                "std_err[{}] differs: got {}, expected {}",
1950                i, result.std_errors[i], expected_std_errs[i]
1951            ));
1952        }
1953    }
1954
1955    if mismatches.is_empty() {
1956        Ok(serde_json::json!({ "status": "PASS" }).to_string())
1957    } else {
1958        Ok(serde_json::json!({ "status": "FAIL", "mismatches": mismatches }).to_string())
1959    }
1960}
1961
1962// ============================================================================
1963// Unit Tests
1964// ============================================================================
1965
1966#[cfg(test)]
1967mod tests {
1968    use super::*;
1969
1970    #[test]
1971    fn verify_housing_regression_integrity() {
1972        let result = test_housing_regression_native();
1973        if let Err(e) = result {
1974            panic!("Regression test failed: {}", e);
1975        }
1976    }
1977
1978    /// Test that test_housing_regression_native produces valid JSON
1979    #[test]
1980    fn test_housing_regression_json_output() {
1981        let result = test_housing_regression_native().unwrap();
1982        // Should be valid JSON
1983        let parsed: serde_json::Value = serde_json::from_str(&result).unwrap();
1984        // Should have status field
1985        assert!(parsed.get("status").is_some());
1986        // Status should be PASS (we control the test data)
1987        assert_eq!(parsed["status"], "PASS");
1988    }
1989
1990    /// Test housing regression with actual R reference values
1991    #[test]
1992    fn test_housing_regression_coefficients() {
1993        let y = vec![
1994            245.5, 312.8, 198.4, 425.6, 278.9, 356.2, 189.5, 512.3, 234.7, 298.1, 445.8, 167.9,
1995            367.4, 289.6, 198.2, 478.5, 256.3, 334.7, 178.5, 398.9, 223.4, 312.5, 156.8, 423.7,
1996            267.9,
1997        ];
1998
1999        let square_feet = vec![
2000            1200.0, 1800.0, 950.0, 2400.0, 1450.0, 2000.0, 1100.0, 2800.0, 1350.0, 1650.0,
2001            2200.0, 900.0, 1950.0, 1500.0, 1050.0, 2600.0, 1300.0, 1850.0, 1000.0, 2100.0,
2002            1250.0, 1700.0, 850.0, 2350.0, 1400.0,
2003        ];
2004        let bedrooms = vec![
2005            3.0, 4.0, 2.0, 4.0, 3.0, 4.0, 2.0, 5.0, 3.0, 3.0, 4.0, 2.0, 4.0, 3.0, 2.0, 5.0,
2006            3.0, 4.0, 2.0, 4.0, 3.0, 3.0, 2.0, 4.0, 3.0,
2007        ];
2008        let age = vec![
2009            15.0, 10.0, 25.0, 5.0, 8.0, 12.0, 20.0, 2.0, 18.0, 7.0, 3.0, 30.0, 6.0, 14.0,
2010            22.0, 1.0, 16.0, 9.0, 28.0, 4.0, 19.0, 11.0, 35.0, 3.0, 13.0,
2011        ];
2012
2013        let x_vars = vec![square_feet, bedrooms, age];
2014        let names = vec![
2015            "Intercept".to_string(),
2016            "Square_Feet".to_string(),
2017            "Bedrooms".to_string(),
2018            "Age".to_string(),
2019        ];
2020
2021        let result = core::ols_regression(&y, &x_vars, &names).unwrap();
2022
2023        // Check against R results
2024        let expected_coeffs = [52.1271333, 0.1613877, 0.9545492, -1.1811815];
2025        let expected_std_errs = [31.18201809, 0.01875072, 10.44400198, 0.73219949];
2026
2027        let tolerance = 1e-4;
2028        for i in 0..4 {
2029            assert!(
2030                (result.coefficients[i] - expected_coeffs[i]).abs() < tolerance,
2031                "coeff[{}] differs: got {}, expected {}",
2032                i,
2033                result.coefficients[i],
2034                expected_coeffs[i]
2035            );
2036            assert!(
2037                (result.std_errors[i] - expected_std_errs[i]).abs() < tolerance,
2038                "std_err[{}] differs: got {}, expected {}",
2039                i,
2040                result.std_errors[i],
2041                expected_std_errs[i]
2042            );
2043        }
2044    }
2045
2046    /// Test R-squared calculation in housing regression
2047    #[test]
2048    fn test_housing_regression_r_squared() {
2049        let result = test_housing_regression_native().unwrap();
2050        let parsed: serde_json::Value = serde_json::from_str(&result).unwrap();
2051
2052        // If status is PASS, R² should be reasonable (between 0 and 1)
2053        assert_eq!(parsed["status"], "PASS");
2054    }
2055
2056    /// Test that housing regression handles all expected output fields
2057    #[test]
2058    fn test_housing_regression_comprehensive() {
2059        let y = vec![
2060            245.5, 312.8, 198.4, 425.6, 278.9, 356.2, 189.5, 512.3, 234.7, 298.1,
2061        ];
2062        let x1 = vec![1200.0, 1800.0, 950.0, 2400.0, 1450.0, 2000.0, 1100.0, 2800.0, 1350.0, 1650.0];
2063        let x2 = vec![3.0, 4.0, 2.0, 4.0, 3.0, 4.0, 2.0, 5.0, 3.0, 3.0];
2064
2065        let result = core::ols_regression(&y, &[x1, x2], &["Intercept".into(), "X1".into(), "X2".into()])
2066            .unwrap();
2067
2068        // Verify expected output fields exist
2069        assert!(!result.coefficients.is_empty());
2070        assert!(!result.std_errors.is_empty());
2071        assert!(!result.t_stats.is_empty());
2072        assert!(!result.p_values.is_empty());
2073        assert!(result.r_squared >= 0.0 && result.r_squared <= 1.0);
2074        assert!(result.residuals.len() == y.len());
2075    }
2076
2077    /// Test error handling when insufficient data is provided
2078    #[test]
2079    fn test_housing_regression_insufficient_data() {
2080        let y = vec![245.5, 312.8]; // Only 2 observations
2081        let x1 = vec![1200.0, 1800.0];
2082        let x2 = vec![3.0, 4.0];
2083
2084        let result = core::ols_regression(&y, &[x1, x2], &["Intercept".into(), "X1".into(), "X2".into()]);
2085        assert!(result.is_err());
2086    }
2087
2088    /// Test housing regression precision with tolerance check
2089    #[test]
2090    fn test_housing_regression_tolerance_check() {
2091        let y = vec![
2092            245.5, 312.8, 198.4, 425.6, 278.9, 356.2, 189.5, 512.3, 234.7, 298.1, 445.8, 167.9,
2093            367.4, 289.6, 198.2, 478.5, 256.3, 334.7, 178.5, 398.9, 223.4, 312.5, 156.8, 423.7,
2094            267.9,
2095        ];
2096
2097        let square_feet = vec![
2098            1200.0, 1800.0, 950.0, 2400.0, 1450.0, 2000.0, 1100.0, 2800.0, 1350.0, 1650.0,
2099            2200.0, 900.0, 1950.0, 1500.0, 1050.0, 2600.0, 1300.0, 1850.0, 1000.0, 2100.0,
2100            1250.0, 1700.0, 850.0, 2350.0, 1400.0,
2101        ];
2102        let bedrooms = vec![
2103            3.0, 4.0, 2.0, 4.0, 3.0, 4.0, 2.0, 5.0, 3.0, 3.0, 4.0, 2.0, 4.0, 3.0, 2.0, 5.0,
2104            3.0, 4.0, 2.0, 4.0, 3.0, 3.0, 2.0, 4.0, 3.0,
2105        ];
2106        let age = vec![
2107            15.0, 10.0, 25.0, 5.0, 8.0, 12.0, 20.0, 2.0, 18.0, 7.0, 3.0, 30.0, 6.0, 14.0,
2108            22.0, 1.0, 16.0, 9.0, 28.0, 4.0, 19.0, 11.0, 35.0, 3.0, 13.0,
2109        ];
2110
2111        let x_vars = vec![square_feet, bedrooms, age];
2112        let names = vec![
2113            "Intercept".to_string(),
2114            "Square_Feet".to_string(),
2115            "Bedrooms".to_string(),
2116            "Age".to_string(),
2117        ];
2118
2119        let result = core::ols_regression(&y, &x_vars, &names).unwrap();
2120
2121        // Verify all coefficient values are finite
2122        for coef in &result.coefficients {
2123            assert!(coef.is_finite(), "Coefficient should be finite");
2124        }
2125        // Verify all standard errors are positive and finite
2126        for se in &result.std_errors {
2127            assert!(se.is_finite(), "Standard error should be finite");
2128            if *se <= 0.0 {
2129                panic!("Standard error should be positive, got {}", se);
2130            }
2131        }
2132    }
2133}