linreg_core/lib.rs
1//! # linreg-core
2//!
3//! A lightweight, self-contained linear regression library in pure Rust.
4//!
5//! **No external math dependencies.** All linear algebra (matrices, QR decomposition)
6//! and statistical functions (distributions, hypothesis tests) are implemented from
7//! scratch. Compiles to WebAssembly for browser use or runs as a native Rust crate.
8//!
9//! ## What This Does
10//!
11//! - **OLS Regression** — Ordinary Least Squares with numerically stable QR decomposition
12//! - **Regularized Regression** — Ridge, Lasso, and Elastic Net via coordinate descent
13//! - **Diagnostic Tests** — 8+ statistical tests for validating regression assumptions
14//! - **WASM Support** — Same API works in browsers via WebAssembly
15//!
16//! ## Quick Start
17//!
18//! ### Native Rust
19//!
20//! Add to `Cargo.toml` (no WASM overhead):
21//!
22//! ```toml
23//! [dependencies]
24//! linreg-core = { version = "0.2", default-features = false }
25//! ```
26//!
27//! ```rust
28//! use linreg_core::core::ols_regression;
29//!
30//! let y = vec![2.5, 3.7, 4.2, 5.1, 6.3];
31//! let x1 = vec![1.0, 2.0, 3.0, 4.0, 5.0];
32//! let x2 = vec![2.0, 4.0, 5.0, 4.0, 3.0];
33//! let names = vec!["Intercept".into(), "Temp".into(), "Pressure".into()];
34//!
35//! let result = ols_regression(&y, &[x1, x2], &names)?;
36//! println!("R²: {}", result.r_squared);
37//! println!("F-statistic: {}", result.f_statistic);
38//! # Ok::<(), linreg_core::Error>(())
39//! ```
40//!
41//! ### WebAssembly (JavaScript)
42//!
43//! ```toml
44//! [dependencies]
45//! linreg-core = "0.2"
46//! ```
47//!
48//! Build with `wasm-pack build --target web`, then use in JavaScript:
49//!
50//! ```text
51//! import init, { ols_regression } from './linreg_core.js';
52//! await init();
53//!
54//! const result = JSON.parse(ols_regression(
55//! JSON.stringify([2.5, 3.7, 4.2, 5.1, 6.3]),
56//! JSON.stringify([[1,2,3,4,5], [2,4,5,4,3]]),
57//! JSON.stringify(["Intercept", "X1", "X2"])
58//! ));
59//! console.log("R²:", result.r_squared);
60//! ```
61//!
62//! ## Regularized Regression
63//!
64//! ```no_run
65//! use linreg_core::regularized::{ridge, lasso};
66//! use linreg_core::linalg::Matrix;
67//!
68//! let x = Matrix::new(100, 3, vec![0.0; 300]);
69//! let y = vec![0.0; 100];
70//!
71//! // Ridge regression (L2 penalty - shrinks coefficients, handles multicollinearity)
72//! let ridge_result = ridge::ridge_fit(&x, &y, &ridge::RidgeFitOptions {
73//! lambda: 1.0,
74//! intercept: true,
75//! standardize: true,
76//! })?;
77//!
78//! // Lasso regression (L1 penalty - does variable selection by zeroing coefficients)
79//! let lasso_result = lasso::lasso_fit(&x, &y, &lasso::LassoFitOptions {
80//! lambda: 0.1,
81//! intercept: true,
82//! standardize: true,
83//! ..Default::default()
84//! })?;
85//! # Ok::<(), linreg_core::Error>(())
86//! ```
87//!
88//! ## Diagnostic Tests
89//!
90//! After fitting a model, validate its assumptions:
91//!
92//! | Test | Tests For | Use When |
93//! |------|-----------|----------|
94//! | [`rainbow_test`] | Linearity | Checking if relationships are linear |
95//! | [`harvey_collier_test`] | Functional form | Suspecting model misspecification |
96//! | [`breusch_pagan_test`] | Heteroscedasticity | Variance changes with predictors |
97//! | [`white_test`] | Heteroscedasticity | More general than Breusch-Pagan |
98//! | [`shapiro_wilk_test`] | Normality | Small to moderate samples (n ≤ 5000) |
99//! | [`jarque_bera_test`] | Normality | Large samples, skewness/kurtosis |
100//! | [`anderson_darling_test`] | Normality | Tail-sensitive, any sample size |
101//! | [`durbin_watson_test`] | Autocorrelation | Time series or ordered data |
102//! | [`cooks_distance_test`] | Influential points | Identifying high-impact observations |
103//!
104//! ```rust
105//! use linreg_core::diagnostics::{rainbow_test, breusch_pagan_test, RainbowMethod};
106//!
107//! # let y = vec![2.5, 3.7, 4.2, 5.1, 6.3];
108//! # let x1 = vec![1.0, 2.0, 3.0, 4.0, 5.0];
109//! # let x2 = vec![2.0, 4.0, 5.0, 4.0, 3.0];
110//! // Rainbow test for linearity
111//! let rainbow = rainbow_test(&y, &[x1.clone(), x2.clone()], 0.5, RainbowMethod::R)?;
112//! if rainbow.r_result.as_ref().map_or(false, |r| r.p_value < 0.05) {
113//! println!("Warning: relationship may be non-linear");
114//! }
115//!
116//! // Breusch-Pagan test for heteroscedasticity
117//! let bp = breusch_pagan_test(&y, &[x1, x2])?;
118//! if bp.p_value < 0.05 {
119//! println!("Warning: residuals have non-constant variance");
120//! }
121//! # Ok::<(), linreg_core::Error>(())
122//! ```
123//!
124//! ## Feature Flags
125//!
126//! | Flag | Default | Description |
127//! |------|---------|-------------|
128//! | `wasm` | Yes | Enables WASM bindings and browser support |
129//! | `validation` | No | Includes test data for validation tests |
130//!
131//! For native-only builds (smaller binary, no WASM deps):
132//!
133//! ```toml
134//! linreg-core = { version = "0.2", default-features = false }
135//! ```
136//!
137//! ## Why This Library?
138//!
139//! - **Zero dependencies** — No `nalgebra`, no `statrs`, no `ndarray`. Pure Rust.
140//! - **Validated** — Outputs match R's `lm()` and Python's `statsmodels`
141//! - **WASM-ready** — Same code runs natively and in browsers
142//! - **Small** — Core is ~2000 lines, compiles quickly
143//! - **Permissive license** — MIT OR Apache-2.0
144//!
145//! ## Module Structure
146//!
147//! - [`core`] — OLS regression, coefficients, residuals, VIF
148//! - [`regularized`] — Ridge, Lasso, Elastic Net, regularization paths
149//! - [`diagnostics`] — All diagnostic tests (linearity, heteroscedasticity, normality, autocorrelation)
150//! - [`distributions`] — Statistical distributions (t, F, χ², normal, beta, gamma)
151//! - [`linalg`] — Matrix operations, QR decomposition, linear system solver
152//! - [`error`] — Error types and Result alias
153//!
154//! ## Links
155//!
156//! - [Repository](https://github.com/jesse-anderson/linreg-core)
157//! - [Documentation](https://docs.rs/linreg-core)
158//! - [Examples](https://github.com/jesse-anderson/linreg-core/tree/main/examples)
159//!
160//! ## Disclaimer
161//!
162//! This library is under active development and has not reached 1.0 stability.
163//! While outputs are validated against R and Python implementations, **do not
164//! use this library for critical applications** (medical, financial, safety-critical
165//! systems) without independent verification. See the LICENSE for full terms.
166//! The software is provided "as is" without warranty of any kind.
167
168// Import core modules (always available)
169pub mod core;
170pub mod diagnostics;
171pub mod distributions;
172pub mod linalg;
173pub mod regularized;
174pub mod error;
175
176// Unit tests are now in tests/unit/ directory
177// - error_tests.rs -> tests/unit/error_tests.rs
178// - core_tests.rs -> tests/unit/core_tests.rs
179// - linalg_tests.rs -> tests/unit/linalg_tests.rs
180// - validation_tests.rs -> tests/validation/main.rs
181// - diagnostics_tests.rs: disabled (references unimplemented functions)
182
183// Re-export public API (always available)
184pub use core::{RegressionOutput, VifResult};
185pub use diagnostics::{
186 DiagnosticTestResult,
187 RainbowTestOutput, RainbowSingleResult, RainbowMethod,
188 WhiteTestOutput, WhiteSingleResult, WhiteMethod,
189 CooksDistanceResult,
190};
191
192// Re-export core test functions with different names to avoid WASM conflicts
193pub use diagnostics::rainbow_test as rainbow_test_core;
194pub use diagnostics::white_test as white_test_core;
195
196pub use error::{Error, Result, error_json, error_to_json};
197
198// ============================================================================
199// WASM-specific code (only compiled when "wasm" feature is enabled)
200// ============================================================================
201
202#[cfg(feature = "wasm")]
203use wasm_bindgen::prelude::*;
204
205#[cfg(feature = "wasm")]
206use std::collections::HashSet;
207
208#[cfg(feature = "wasm")]
209use serde::Serialize;
210
211#[cfg(feature = "wasm")]
212use crate::distributions::{student_t_cdf, normal_inverse_cdf};
213
214// ============================================================================
215// CSV Parsing (WASM-only)
216// ============================================================================
217
218#[cfg(feature = "wasm")]
219#[derive(Serialize)]
220struct ParsedCsv {
221 headers: Vec<String>,
222 data: Vec<serde_json::Map<String, serde_json::Value>>,
223 numeric_columns: Vec<String>,
224}
225
226#[cfg(feature = "wasm")]
227#[wasm_bindgen]
228/// Parses CSV data and returns it as a JSON string.
229///
230/// Parses the CSV content and identifies numeric columns. Returns a JSON object
231/// with headers, data rows, and a list of numeric column names.
232///
233/// # Arguments
234///
235/// * `content` - CSV content as a string
236///
237/// # Returns
238///
239/// JSON string with structure:
240/// ```json
241/// {
242/// "headers": ["col1", "col2", ...],
243/// "data": [{"col1": 1.0, "col2": "text"}, ...],
244/// "numeric_columns": ["col1", ...]
245/// }
246/// ```
247///
248/// # Errors
249///
250/// Returns a JSON error object if parsing fails or domain check fails.
251pub fn parse_csv(content: &str) -> String {
252 if let Err(e) = check_domain() {
253 return error_to_json(&e);
254 }
255
256 let mut reader = csv::ReaderBuilder::new()
257 .has_headers(true)
258 .flexible(true)
259 .from_reader(content.as_bytes());
260
261 // Get headers
262 let headers: Vec<String> = match reader.headers() {
263 Ok(h) => h.iter().map(|s| s.to_string()).collect(),
264 Err(e) => return error_json(&format!("Failed to read headers: {}", e)),
265 };
266
267 let mut data = Vec::new();
268 let mut numeric_col_set = HashSet::new();
269
270 for result in reader.records() {
271 let record = match result {
272 Ok(r) => r,
273 Err(e) => return error_json(&format!("Failed to parse CSV record: {}", e)),
274 };
275
276 if record.len() != headers.len() {
277 continue;
278 }
279
280 let mut row_map = serde_json::Map::new();
281
282 for (i, field) in record.iter().enumerate() {
283 if i >= headers.len() {
284 continue;
285 }
286
287 let header = &headers[i];
288 let val_trimmed = field.trim();
289
290 // Try to parse as f64
291 if let Ok(num) = val_trimmed.parse::<f64>() {
292 if num.is_finite() {
293 row_map.insert(
294 header.clone(),
295 serde_json::Value::Number(serde_json::Number::from_f64(num).unwrap()),
296 );
297 numeric_col_set.insert(header.clone());
298 continue;
299 }
300 }
301
302 // Fallback to string
303 row_map.insert(header.clone(), serde_json::Value::String(val_trimmed.to_string()));
304 }
305 data.push(row_map);
306 }
307
308 let mut numeric_columns: Vec<String> = numeric_col_set.into_iter().collect();
309 numeric_columns.sort();
310
311 let output = ParsedCsv {
312 headers,
313 data,
314 numeric_columns,
315 };
316
317 serde_json::to_string(&output).unwrap_or_else(|_| error_json("Failed to serialize CSV output"))
318}
319
320// ============================================================================
321// OLS Regression WASM Wrapper
322// ============================================================================
323
324#[cfg(feature = "wasm")]
325#[wasm_bindgen]
326/// Performs OLS regression via WASM.
327///
328/// All parameters and return values are JSON-encoded strings for JavaScript
329/// interoperability. Returns regression output including coefficients,
330/// standard errors, diagnostic statistics, and VIF analysis.
331///
332/// # Arguments
333///
334/// * `y_json` - JSON array of response variable values: `[1.0, 2.0, 3.0]`
335/// * `x_vars_json` - JSON array of predictor arrays: `[[1.0, 2.0], [0.5, 1.0]]`
336/// * `variable_names` - JSON array of variable names: `["Intercept", "X1", "X2"]`
337///
338/// # Returns
339///
340/// JSON string containing the complete regression output with coefficients,
341/// standard errors, t-statistics, p-values, R², F-statistic, residuals, leverage, VIF, etc.
342///
343/// # Errors
344///
345/// Returns a JSON error object if:
346/// - JSON parsing fails
347/// - Insufficient data (n ≤ k + 1)
348/// - Matrix is singular
349/// - Domain check fails
350pub fn ols_regression(
351 y_json: &str,
352 x_vars_json: &str,
353 variable_names: &str,
354) -> String {
355 if let Err(e) = check_domain() {
356 return error_to_json(&e);
357 }
358
359 // Parse JSON input
360 let y: Vec<f64> = match serde_json::from_str(y_json) {
361 Ok(v) => v,
362 Err(e) => return error_json(&format!("Failed to parse y: {}", e)),
363 };
364
365 let x_vars: Vec<Vec<f64>> = match serde_json::from_str(x_vars_json) {
366 Ok(v) => v,
367 Err(e) => return error_json(&format!("Failed to parse x_vars: {}", e)),
368 };
369
370 let names: Vec<String> = match serde_json::from_str(variable_names) {
371 Ok(v) => v,
372 Err(_) => vec!["Intercept".to_string()],
373 };
374
375 // Call core function
376 match core::ols_regression(&y, &x_vars, &names) {
377 Ok(output) => serde_json::to_string(&output)
378 .unwrap_or_else(|_| error_json("Failed to serialize output")),
379 Err(e) => error_json(&e.to_string()),
380 }
381}
382
383// ============================================================================
384// Diagnostic Tests WASM Wrappers
385// ============================================================================
386
387/// Performs the Rainbow test for linearity via WASM.
388///
389/// The Rainbow test checks whether the relationship between predictors and response
390/// is linear. A significant p-value suggests non-linearity.
391///
392/// # Arguments
393///
394/// * `y_json` - JSON array of response variable values
395/// * `x_vars_json` - JSON array of predictor arrays
396/// * `fraction` - Fraction of data to use in the central subset (0.0 to 1.0, typically 0.5)
397/// * `method` - Method to use: "r", "python", or "both" (case-insensitive, defaults to "r")
398///
399/// # Returns
400///
401/// JSON string containing test statistic, p-value, and interpretation.
402///
403/// # Errors
404///
405/// Returns a JSON error object if parsing fails or domain check fails.
406#[cfg(feature = "wasm")]
407#[wasm_bindgen]
408pub fn rainbow_test(
409 y_json: &str,
410 x_vars_json: &str,
411 fraction: f64,
412 method: &str,
413) -> String {
414 if let Err(e) = check_domain() {
415 return error_to_json(&e);
416 }
417
418 let y: Vec<f64> = match serde_json::from_str(y_json) {
419 Ok(v) => v,
420 Err(e) => return error_json(&format!("Failed to parse y: {}", e)),
421 };
422
423 let x_vars: Vec<Vec<f64>> = match serde_json::from_str(x_vars_json) {
424 Ok(v) => v,
425 Err(e) => return error_json(&format!("Failed to parse x_vars: {}", e)),
426 };
427
428 // Parse method parameter (default to "r" for R)
429 let method = match method.to_lowercase().as_str() {
430 "python" => diagnostics::RainbowMethod::Python,
431 "both" => diagnostics::RainbowMethod::Both,
432 _ => diagnostics::RainbowMethod::R, // Default to R
433 };
434
435 match diagnostics::rainbow_test(&y, &x_vars, fraction, method) {
436 Ok(output) => serde_json::to_string(&output)
437 .unwrap_or_else(|_| error_json("Failed to serialize Rainbow test result")),
438 Err(e) => error_json(&e.to_string()),
439 }
440}
441
442/// Performs the Harvey-Collier test for linearity via WASM.
443///
444/// The Harvey-Collier test checks whether the residuals exhibit a linear trend,
445/// which would indicate that the model's functional form is misspecified.
446/// A significant p-value suggests non-linearity.
447///
448/// # Arguments
449///
450/// * `y_json` - JSON array of response variable values
451/// * `x_vars_json` - JSON array of predictor arrays
452///
453/// # Returns
454///
455/// JSON string containing test statistic, p-value, and interpretation.
456///
457/// # Errors
458///
459/// Returns a JSON error object if parsing fails or domain check fails.
460#[cfg(feature = "wasm")]
461#[wasm_bindgen]
462pub fn harvey_collier_test(
463 y_json: &str,
464 x_vars_json: &str,
465) -> String {
466 if let Err(e) = check_domain() {
467 return error_to_json(&e);
468 }
469
470 let y: Vec<f64> = match serde_json::from_str(y_json) {
471 Ok(v) => v,
472 Err(e) => return error_json(&format!("Failed to parse y: {}", e)),
473 };
474
475 let x_vars: Vec<Vec<f64>> = match serde_json::from_str(x_vars_json) {
476 Ok(v) => v,
477 Err(e) => return error_json(&format!("Failed to parse x_vars: {}", e)),
478 };
479
480 match diagnostics::harvey_collier_test(&y, &x_vars) {
481 Ok(output) => serde_json::to_string(&output)
482 .unwrap_or_else(|_| error_json("Failed to serialize Harvey-Collier test result")),
483 Err(e) => error_json(&e.to_string()),
484 }
485}
486
487/// Performs the Breusch-Pagan test for heteroscedasticity via WASM.
488///
489/// The Breusch-Pagan test checks whether the variance of residuals is constant
490/// across the range of predicted values (homoscedasticity assumption).
491/// A significant p-value suggests heteroscedasticity.
492///
493/// # Arguments
494///
495/// * `y_json` - JSON array of response variable values
496/// * `x_vars_json` - JSON array of predictor arrays
497///
498/// # Returns
499///
500/// JSON string containing test statistic, p-value, and interpretation.
501///
502/// # Errors
503///
504/// Returns a JSON error object if parsing fails or domain check fails.
505#[cfg(feature = "wasm")]
506#[wasm_bindgen]
507pub fn breusch_pagan_test(
508 y_json: &str,
509 x_vars_json: &str,
510) -> String {
511 if let Err(e) = check_domain() {
512 return error_to_json(&e);
513 }
514
515 let y: Vec<f64> = match serde_json::from_str(y_json) {
516 Ok(v) => v,
517 Err(e) => return error_json(&format!("Failed to parse y: {}", e)),
518 };
519
520 let x_vars: Vec<Vec<f64>> = match serde_json::from_str(x_vars_json) {
521 Ok(v) => v,
522 Err(e) => return error_json(&format!("Failed to parse x_vars: {}", e)),
523 };
524
525 match diagnostics::breusch_pagan_test(&y, &x_vars) {
526 Ok(output) => serde_json::to_string(&output)
527 .unwrap_or_else(|_| error_json("Failed to serialize Breusch-Pagan test result")),
528 Err(e) => error_json(&e.to_string()),
529 }
530}
531
532/// Performs the White test for heteroscedasticity via WASM.
533///
534/// The White test is a more general test for heteroscedasticity that does not
535/// assume a specific form of heteroscedasticity. A significant p-value suggests
536/// that the error variance is not constant.
537///
538/// # Arguments
539///
540/// * `y_json` - JSON array of response variable values
541/// * `x_vars_json` - JSON array of predictor arrays
542/// * `method` - Method to use: "r", "python", or "both" (case-insensitive, defaults to "r")
543///
544/// # Returns
545///
546/// JSON string containing test statistic, p-value, and interpretation.
547///
548/// # Errors
549///
550/// Returns a JSON error object if parsing fails or domain check fails.
551#[cfg(feature = "wasm")]
552#[wasm_bindgen]
553pub fn white_test(
554 y_json: &str,
555 x_vars_json: &str,
556 method: &str,
557) -> String {
558 if let Err(e) = check_domain() {
559 return error_to_json(&e);
560 }
561
562 let y: Vec<f64> = match serde_json::from_str(y_json) {
563 Ok(v) => v,
564 Err(e) => return error_json(&format!("Failed to parse y: {}", e)),
565 };
566
567 let x_vars: Vec<Vec<f64>> = match serde_json::from_str(x_vars_json) {
568 Ok(v) => v,
569 Err(e) => return error_json(&format!("Failed to parse x_vars: {}", e)),
570 };
571
572 // Parse method parameter (default to "r" for R)
573 let method = match method.to_lowercase().as_str() {
574 "python" => diagnostics::WhiteMethod::Python,
575 "both" => diagnostics::WhiteMethod::Both,
576 _ => diagnostics::WhiteMethod::R, // Default to R
577 };
578
579 match diagnostics::white_test(&y, &x_vars, method) {
580 Ok(output) => serde_json::to_string(&output)
581 .unwrap_or_else(|_| error_json("Failed to serialize White test result")),
582 Err(e) => error_json(&e.to_string()),
583 }
584}
585
586/// Performs the R method White test for heteroscedasticity via WASM.
587///
588/// This implementation matches R's `skedastic::white()` function behavior.
589/// Uses the standard QR decomposition and the R-specific auxiliary matrix
590/// structure (intercept, X, X² only - no cross-products).
591///
592/// # Arguments
593///
594/// * `y_json` - JSON array of response variable values
595/// * `x_vars_json` - JSON array of predictor arrays (each array is a column)
596///
597/// # Returns
598///
599/// JSON string containing test statistic, p-value, and interpretation.
600///
601/// # Errors
602///
603/// Returns a JSON error object if parsing fails or domain check fails.
604#[cfg(feature = "wasm")]
605#[wasm_bindgen]
606pub fn r_white_test(
607 y_json: &str,
608 x_vars_json: &str,
609) -> String {
610 if let Err(e) = check_domain() {
611 return error_to_json(&e);
612 }
613
614 let y: Vec<f64> = match serde_json::from_str(y_json) {
615 Ok(v) => v,
616 Err(e) => return error_json(&format!("Failed to parse y: {}", e)),
617 };
618
619 let x_vars: Vec<Vec<f64>> = match serde_json::from_str(x_vars_json) {
620 Ok(v) => v,
621 Err(e) => return error_json(&format!("Failed to parse x_vars: {}", e)),
622 };
623
624 match diagnostics::r_white_method(&y, &x_vars) {
625 Ok(output) => serde_json::to_string(&output)
626 .unwrap_or_else(|_| error_json("Failed to serialize R White test result")),
627 Err(e) => error_json(&e.to_string()),
628 }
629}
630
631/// Performs the Python method White test for heteroscedasticity via WASM.
632///
633/// This implementation matches Python's `statsmodels.stats.diagnostic.het_white()` function.
634/// Uses the LINPACK QR decomposition with column pivoting and the Python-specific
635/// auxiliary matrix structure (intercept, X, X², and cross-products).
636///
637/// # Arguments
638///
639/// * `y_json` - JSON array of response variable values
640/// * `x_vars_json` - JSON array of predictor arrays (each array is a column)
641///
642/// # Returns
643///
644/// JSON string containing test statistic, p-value, and interpretation.
645///
646/// # Errors
647///
648/// Returns a JSON error object if parsing fails or domain check fails.
649#[cfg(feature = "wasm")]
650#[wasm_bindgen]
651pub fn python_white_test(
652 y_json: &str,
653 x_vars_json: &str,
654) -> String {
655 if let Err(e) = check_domain() {
656 return error_to_json(&e);
657 }
658
659 let y: Vec<f64> = match serde_json::from_str(y_json) {
660 Ok(v) => v,
661 Err(e) => return error_json(&format!("Failed to parse y: {}", e)),
662 };
663
664 let x_vars: Vec<Vec<f64>> = match serde_json::from_str(x_vars_json) {
665 Ok(v) => v,
666 Err(e) => return error_json(&format!("Failed to parse x_vars: {}", e)),
667 };
668
669 match diagnostics::python_white_method(&y, &x_vars) {
670 Ok(output) => serde_json::to_string(&output)
671 .unwrap_or_else(|_| error_json("Failed to serialize Python White test result")),
672 Err(e) => error_json(&e.to_string()),
673 }
674}
675
676/// Performs the Jarque-Bera test for normality via WASM.
677///
678/// The Jarque-Bera test checks whether the residuals are normally distributed
679/// by examining skewness and kurtosis. A significant p-value suggests that
680/// the residuals deviate from normality.
681///
682/// # Arguments
683///
684/// * `y_json` - JSON array of response variable values
685/// * `x_vars_json` - JSON array of predictor arrays
686///
687/// # Returns
688///
689/// JSON string containing test statistic, p-value, and interpretation.
690///
691/// # Errors
692///
693/// Returns a JSON error object if parsing fails or domain check fails.
694#[cfg(feature = "wasm")]
695#[wasm_bindgen]
696pub fn jarque_bera_test(
697 y_json: &str,
698 x_vars_json: &str,
699) -> String {
700 if let Err(e) = check_domain() {
701 return error_to_json(&e);
702 }
703
704 let y: Vec<f64> = match serde_json::from_str(y_json) {
705 Ok(v) => v,
706 Err(e) => return error_json(&format!("Failed to parse y: {}", e)),
707 };
708
709 let x_vars: Vec<Vec<f64>> = match serde_json::from_str(x_vars_json) {
710 Ok(v) => v,
711 Err(e) => return error_json(&format!("Failed to parse x_vars: {}", e)),
712 };
713
714 match diagnostics::jarque_bera_test(&y, &x_vars) {
715 Ok(output) => serde_json::to_string(&output)
716 .unwrap_or_else(|_| error_json("Failed to serialize Jarque-Bera test result")),
717 Err(e) => error_json(&e.to_string()),
718 }
719}
720
721// ============================================================================
722// Durbin-Watson Test (WASM wrapper)
723// ============================================================================
724
725/// Performs the Durbin-Watson test for autocorrelation via WASM.
726///
727/// The Durbin-Watson test checks for autocorrelation in the residuals.
728/// Values near 2 indicate no autocorrelation, values near 0 suggest positive
729/// autocorrelation, and values near 4 suggest negative autocorrelation.
730///
731/// # Arguments
732///
733/// * `y_json` - JSON array of response variable values
734/// * `x_vars_json` - JSON array of predictor arrays
735///
736/// # Returns
737///
738/// JSON string containing the DW statistic and interpretation.
739///
740/// # Errors
741///
742/// Returns a JSON error object if parsing fails or domain check fails.
743#[cfg(feature = "wasm")]
744#[wasm_bindgen]
745pub fn durbin_watson_test(
746 y_json: &str,
747 x_vars_json: &str,
748) -> String {
749 if let Err(e) = check_domain() {
750 return error_to_json(&e);
751 }
752
753 let y: Vec<f64> = match serde_json::from_str(y_json) {
754 Ok(v) => v,
755 Err(e) => return error_json(&format!("Failed to parse y: {}", e)),
756 };
757
758 let x_vars: Vec<Vec<f64>> = match serde_json::from_str(x_vars_json) {
759 Ok(v) => v,
760 Err(e) => return error_json(&format!("Failed to parse x_vars: {}", e)),
761 };
762
763 match diagnostics::durbin_watson_test(&y, &x_vars) {
764 Ok(output) => serde_json::to_string(&output)
765 .unwrap_or_else(|_| error_json("Failed to serialize Durbin-Watson test result")),
766 Err(e) => error_json(&e.to_string()),
767 }
768}
769
770// ============================================================================
771// Shapiro-Wilk Test (WASM wrapper)
772// ============================================================================
773
774/// Performs the Shapiro-Wilk test for normality via WASM.
775///
776/// The Shapiro-Wilk test is a powerful test for normality,
777/// especially for small to moderate sample sizes (3 ≤ n ≤ 5000). It tests
778/// the null hypothesis that the residuals are normally distributed.
779///
780/// # Arguments
781///
782/// * `y_json` - JSON array of response variable values
783/// * `x_vars_json` - JSON array of predictor arrays
784///
785/// # Returns
786///
787/// JSON string containing the W statistic (ranges from 0 to 1), p-value,
788/// and interpretation.
789///
790/// # Errors
791///
792/// Returns a JSON error object if parsing fails or domain check fails.
793#[cfg(feature = "wasm")]
794#[wasm_bindgen]
795pub fn shapiro_wilk_test(
796 y_json: &str,
797 x_vars_json: &str,
798) -> String {
799 if let Err(e) = check_domain() {
800 return error_to_json(&e);
801 }
802
803 let y: Vec<f64> = match serde_json::from_str(y_json) {
804 Ok(v) => v,
805 Err(e) => return error_json(&format!("Failed to parse y: {}", e)),
806 };
807
808 let x_vars: Vec<Vec<f64>> = match serde_json::from_str(x_vars_json) {
809 Ok(v) => v,
810 Err(e) => return error_json(&format!("Failed to parse x_vars: {}", e)),
811 };
812
813 match diagnostics::shapiro_wilk_test(&y, &x_vars) {
814 Ok(output) => serde_json::to_string(&output)
815 .unwrap_or_else(|_| error_json("Failed to serialize Shapiro-Wilk test result")),
816 Err(e) => error_json(&e.to_string()),
817 }
818}
819
820/// Performs the Anderson-Darling test for normality via WASM.
821///
822/// The Anderson-Darling test checks whether the residuals are normally distributed
823/// by comparing the empirical distribution to the expected normal distribution.
824/// This test is particularly sensitive to deviations in the tails of the distribution.
825/// A significant p-value suggests that the residuals deviate from normality.
826///
827/// # Arguments
828///
829/// * `y_json` - JSON array of response variable values
830/// * `x_vars_json` - JSON array of predictor arrays
831///
832/// # Returns
833///
834/// JSON string containing the A² statistic, p-value, and interpretation.
835///
836/// # Errors
837///
838/// Returns a JSON error object if parsing fails or domain check fails.
839#[cfg(feature = "wasm")]
840#[wasm_bindgen]
841pub fn anderson_darling_test(
842 y_json: &str,
843 x_vars_json: &str,
844) -> String {
845 if let Err(e) = check_domain() {
846 return error_to_json(&e);
847 }
848
849 let y: Vec<f64> = match serde_json::from_str(y_json) {
850 Ok(v) => v,
851 Err(e) => return error_json(&format!("Failed to parse y: {}", e)),
852 };
853
854 let x_vars: Vec<Vec<f64>> = match serde_json::from_str(x_vars_json) {
855 Ok(v) => v,
856 Err(e) => return error_json(&format!("Failed to parse x_vars: {}", e)),
857 };
858
859 match diagnostics::anderson_darling_test(&y, &x_vars) {
860 Ok(output) => serde_json::to_string(&output)
861 .unwrap_or_else(|_| error_json("Failed to serialize Anderson-Darling test result")),
862 Err(e) => error_json(&e.to_string()),
863 }
864}
865
866// ============================================================================
867// Cook's Distance (WASM wrapper)
868// ============================================================================
869
870/// Computes Cook's distance for identifying influential observations via WASM.
871///
872/// Cook's distance measures how much each observation influences the regression
873/// model by comparing coefficient estimates with and without that observation.
874/// Unlike hypothesis tests, this is an influence measure - not a test with p-values.
875///
876/// # Arguments
877///
878/// * `y_json` - JSON array of response variable values
879/// * `x_vars_json` - JSON array of predictor arrays
880///
881/// # Returns
882///
883/// JSON string containing:
884/// - Vector of Cook's distances (one per observation)
885/// - Thresholds for identifying influential observations
886/// - Indices of potentially influential observations
887/// - Interpretation and guidance
888///
889/// # Errors
890///
891/// Returns a JSON error object if parsing fails or domain check fails.
892#[cfg(feature = "wasm")]
893#[wasm_bindgen]
894pub fn cooks_distance_test(
895 y_json: &str,
896 x_vars_json: &str,
897) -> String {
898 if let Err(e) = check_domain() {
899 return error_to_json(&e);
900 }
901
902 let y: Vec<f64> = match serde_json::from_str(y_json) {
903 Ok(v) => v,
904 Err(e) => return error_json(&format!("Failed to parse y: {}", e)),
905 };
906
907 let x_vars: Vec<Vec<f64>> = match serde_json::from_str(x_vars_json) {
908 Ok(v) => v,
909 Err(e) => return error_json(&format!("Failed to parse x_vars: {}", e)),
910 };
911
912 match diagnostics::cooks_distance_test(&y, &x_vars) {
913 Ok(output) => serde_json::to_string(&output)
914 .unwrap_or_else(|_| error_json("Failed to serialize Cook's distance result")),
915 Err(e) => error_json(&e.to_string()),
916 }
917}
918
919// ============================================================================
920// Regularized Regression WASM Wrappers
921// ============================================================================
922
923#[cfg(feature = "wasm")]
924#[wasm_bindgen]
925/// Performs Ridge regression via WASM.
926///
927/// Ridge regression adds an L2 penalty to the coefficients, which helps with
928/// multicollinearity and overfitting. The intercept is never penalized.
929///
930/// # Arguments
931///
932/// * `y_json` - JSON array of response variable values
933/// * `x_vars_json` - JSON array of predictor arrays
934/// * `variable_names` - JSON array of variable names
935/// * `lambda` - Regularization strength (>= 0, typical range 0.01 to 100)
936/// * `standardize` - Whether to standardize predictors (recommended: true)
937///
938/// # Returns
939///
940/// JSON string containing:
941/// - `lambda` - Lambda value used
942/// - `intercept` - Intercept coefficient
943/// - `coefficients` - Slope coefficients
944/// - `fitted_values` - Predictions on training data
945/// - `residuals` - Residuals (y - fitted_values)
946/// - `df` - Effective degrees of freedom
947///
948/// # Errors
949///
950/// Returns a JSON error object if parsing fails, lambda is negative,
951/// or domain check fails.
952pub fn ridge_regression(
953 y_json: &str,
954 x_vars_json: &str,
955 _variable_names: &str,
956 lambda: f64,
957 standardize: bool,
958) -> String {
959 if let Err(e) = check_domain() {
960 return error_to_json(&e);
961 }
962
963 // Parse JSON input
964 let y: Vec<f64> = match serde_json::from_str(y_json) {
965 Ok(v) => v,
966 Err(e) => return error_json(&format!("Failed to parse y: {}", e)),
967 };
968
969 let x_vars: Vec<Vec<f64>> = match serde_json::from_str(x_vars_json) {
970 Ok(v) => v,
971 Err(e) => return error_json(&format!("Failed to parse x_vars: {}", e)),
972 };
973
974 // Build design matrix with intercept column
975 let n = y.len();
976 let p = x_vars.len();
977
978 if n <= p + 1 {
979 return error_json(&format!(
980 "Insufficient data: need at least {} observations for {} predictors",
981 p + 2,
982 p
983 ));
984 }
985
986 let mut x_data = vec![1.0; n * (p + 1)]; // Intercept column
987 for (j, x_var) in x_vars.iter().enumerate() {
988 if x_var.len() != n {
989 return error_json(&format!(
990 "x_vars[{}] has {} elements, expected {}",
991 j,
992 x_var.len(),
993 n
994 ));
995 }
996 for (i, &val) in x_var.iter().enumerate() {
997 x_data[i * (p + 1) + j + 1] = val;
998 }
999 }
1000
1001 let x = linalg::Matrix::new(n, p + 1, x_data);
1002
1003 // Configure ridge options
1004 let options = regularized::ridge::RidgeFitOptions {
1005 lambda,
1006 intercept: true,
1007 standardize,
1008 };
1009
1010 match regularized::ridge::ridge_fit(&x, &y, &options) {
1011 Ok(output) => serde_json::to_string(&output)
1012 .unwrap_or_else(|_| error_json("Failed to serialize ridge regression result")),
1013 Err(e) => error_json(&e.to_string()),
1014 }
1015}
1016
1017#[cfg(feature = "wasm")]
1018#[wasm_bindgen]
1019/// Performs Lasso regression via WASM.
1020///
1021/// Lasso regression adds an L1 penalty to the coefficients, which performs
1022/// automatic variable selection by shrinking some coefficients to exactly zero.
1023/// The intercept is never penalized.
1024///
1025/// # Arguments
1026///
1027/// * `y_json` - JSON array of response variable values
1028/// * `x_vars_json` - JSON array of predictor arrays
1029/// * `variable_names` - JSON array of variable names
1030/// * `lambda` - Regularization strength (>= 0, typical range 0.01 to 10)
1031/// * `standardize` - Whether to standardize predictors (recommended: true)
1032///
1033/// # Returns
1034///
1035/// JSON string containing:
1036/// - `lambda` - Lambda value used
1037/// - `intercept` - Intercept coefficient
1038/// - `coefficients` - Slope coefficients (some may be exactly zero)
1039/// - `fitted_values` - Predictions on training data
1040/// - `residuals` - Residuals (y - fitted_values)
1041/// - `n_nonzero` - Number of non-zero coefficients (excluding intercept)
1042/// - `iterations` - Number of coordinate descent iterations
1043/// - `converged` - Whether the algorithm converged
1044///
1045/// # Errors
1046///
1047/// Returns a JSON error object if parsing fails, lambda is negative,
1048/// or domain check fails.
1049pub fn lasso_regression(
1050 y_json: &str,
1051 x_vars_json: &str,
1052 _variable_names: &str,
1053 lambda: f64,
1054 standardize: bool,
1055) -> String {
1056 if let Err(e) = check_domain() {
1057 return error_to_json(&e);
1058 }
1059
1060 // Parse JSON input
1061 let y: Vec<f64> = match serde_json::from_str(y_json) {
1062 Ok(v) => v,
1063 Err(e) => return error_json(&format!("Failed to parse y: {}", e)),
1064 };
1065
1066 let x_vars: Vec<Vec<f64>> = match serde_json::from_str(x_vars_json) {
1067 Ok(v) => v,
1068 Err(e) => return error_json(&format!("Failed to parse x_vars: {}", e)),
1069 };
1070
1071 // Build design matrix with intercept column
1072 let n = y.len();
1073 let p = x_vars.len();
1074
1075 if n <= p + 1 {
1076 return error_json(&format!(
1077 "Insufficient data: need at least {} observations for {} predictors",
1078 p + 2,
1079 p
1080 ));
1081 }
1082
1083 let mut x_data = vec![1.0; n * (p + 1)]; // Intercept column
1084 for (j, x_var) in x_vars.iter().enumerate() {
1085 if x_var.len() != n {
1086 return error_json(&format!(
1087 "x_vars[{}] has {} elements, expected {}",
1088 j,
1089 x_var.len(),
1090 n
1091 ));
1092 }
1093 for (i, &val) in x_var.iter().enumerate() {
1094 x_data[i * (p + 1) + j + 1] = val;
1095 }
1096 }
1097
1098 let x = linalg::Matrix::new(n, p + 1, x_data);
1099
1100 // Configure lasso options
1101 let options = regularized::lasso::LassoFitOptions {
1102 lambda,
1103 intercept: true,
1104 standardize,
1105 ..Default::default()
1106 };
1107
1108 match regularized::lasso::lasso_fit(&x, &y, &options) {
1109 Ok(output) => serde_json::to_string(&output)
1110 .unwrap_or_else(|_| error_json("Failed to serialize lasso regression result")),
1111 Err(e) => error_json(&e.to_string()),
1112 }
1113}
1114
1115#[cfg(feature = "wasm")]
1116#[wasm_bindgen]
1117/// Generates a lambda path for regularized regression via WASM.
1118///
1119/// Creates a logarithmically-spaced sequence of lambda values from lambda_max
1120/// (where all penalized coefficients are zero) down to lambda_min. This is
1121/// useful for fitting regularization paths and selecting optimal lambda via
1122/// cross-validation.
1123///
1124/// # Arguments
1125///
1126/// * `y_json` - JSON array of response variable values
1127/// * `x_vars_json` - JSON array of predictor arrays
1128/// * `n_lambda` - Number of lambda values to generate (default: 100)
1129/// * `lambda_min_ratio` - Ratio for smallest lambda (default: 0.0001 if n >= p, else 0.01)
1130///
1131/// # Returns
1132///
1133/// JSON string containing:
1134/// - `lambda_path` - Array of lambda values in decreasing order
1135/// - `lambda_max` - Maximum lambda value
1136/// - `lambda_min` - Minimum lambda value
1137/// - `n_lambda` - Number of lambda values
1138///
1139/// # Errors
1140///
1141/// Returns a JSON error object if parsing fails or domain check fails.
1142pub fn make_lambda_path(
1143 y_json: &str,
1144 x_vars_json: &str,
1145 n_lambda: usize,
1146 lambda_min_ratio: f64,
1147) -> String {
1148 if let Err(e) = check_domain() {
1149 return error_to_json(&e);
1150 }
1151
1152 // Parse JSON input
1153 let y: Vec<f64> = match serde_json::from_str(y_json) {
1154 Ok(v) => v,
1155 Err(e) => return error_json(&format!("Failed to parse y: {}", e)),
1156 };
1157
1158 let x_vars: Vec<Vec<f64>> = match serde_json::from_str(x_vars_json) {
1159 Ok(v) => v,
1160 Err(e) => return error_json(&format!("Failed to parse x_vars: {}", e)),
1161 };
1162
1163 // Build design matrix with intercept column
1164 let n = y.len();
1165 let p = x_vars.len();
1166
1167 let mut x_data = vec![1.0; n * (p + 1)]; // Intercept column
1168 for (j, x_var) in x_vars.iter().enumerate() {
1169 if x_var.len() != n {
1170 return error_json(&format!(
1171 "x_vars[{}] has {} elements, expected {}",
1172 j,
1173 x_var.len(),
1174 n
1175 ));
1176 }
1177 for (i, &val) in x_var.iter().enumerate() {
1178 x_data[i * (p + 1) + j + 1] = val;
1179 }
1180 }
1181
1182 let x = linalg::Matrix::new(n, p + 1, x_data);
1183
1184 // Standardize X for lambda path computation
1185 let x_mean: Vec<f64> = (0..x.cols)
1186 .map(|j| {
1187 if j == 0 {
1188 1.0 // Intercept column
1189 } else {
1190 (0..n).map(|i| x.get(i, j)).sum::<f64>() / n as f64
1191 }
1192 })
1193 .collect();
1194
1195 let x_std: Vec<f64> = (0..x.cols)
1196 .map(|j| {
1197 if j == 0 {
1198 0.0 // Intercept column - no centering
1199 } else {
1200 let mean = x_mean[j];
1201 let variance = (0..n).map(|i| (x.get(i, j) - mean).powi(2)).sum::<f64>() / (n - 1) as f64;
1202 variance.sqrt()
1203 }
1204 })
1205 .collect();
1206
1207 // Build standardized X matrix
1208 let mut x_std_data = vec![1.0; n * (p + 1)];
1209 for j in 0..x.cols {
1210 for i in 0..n {
1211 if j == 0 {
1212 x_std_data[i * (p + 1)] = 1.0; // Intercept
1213 } else {
1214 let std = x_std[j];
1215 if std > 1e-10 {
1216 x_std_data[i * (p + 1) + j] = (x.get(i, j) - x_mean[j]) / std;
1217 } else {
1218 x_std_data[i * (p + 1) + j] = 0.0;
1219 }
1220 }
1221 }
1222 }
1223 let x_std = linalg::Matrix::new(n, p + 1, x_std_data);
1224
1225 // Center y
1226 let y_mean: f64 = y.iter().sum::<f64>() / n as f64;
1227 let y_centered: Vec<f64> = y.iter().map(|&yi| yi - y_mean).collect();
1228
1229 // Configure lambda path options
1230 let options = regularized::path::LambdaPathOptions {
1231 nlambda: n_lambda.max(1),
1232 lambda_min_ratio: if lambda_min_ratio > 0.0 { Some(lambda_min_ratio) } else { None },
1233 alpha: 1.0, // Lasso
1234 ..Default::default()
1235 };
1236
1237 let lambda_path = regularized::path::make_lambda_path(&x_std, &y_centered, &options, None, Some(0));
1238
1239 let lambda_max = lambda_path.first().copied().unwrap_or(0.0);
1240 let lambda_min = lambda_path.last().copied().unwrap_or(0.0);
1241
1242 // Return as JSON
1243 let result = serde_json::json!({
1244 "lambda_path": lambda_path,
1245 "lambda_max": lambda_max,
1246 "lambda_min": lambda_min,
1247 "n_lambda": lambda_path.len()
1248 });
1249
1250 result.to_string()
1251}
1252
1253// ============================================================================
1254// Statistical Utility Functions (WASM wrappers)
1255// ============================================================================
1256
1257#[cfg(feature = "wasm")]
1258#[wasm_bindgen]
1259/// Computes the Student's t-distribution cumulative distribution function.
1260///
1261/// Returns P(T ≤ t) for a t-distribution with the given degrees of freedom.
1262///
1263/// # Arguments
1264///
1265/// * `t` - t-statistic value
1266/// * `df` - Degrees of freedom
1267///
1268/// # Returns
1269///
1270/// The CDF value, or `NaN` if domain check fails.
1271pub fn get_t_cdf(t: f64, df: f64) -> f64 {
1272 if check_domain().is_err() {
1273 return f64::NAN;
1274 }
1275
1276 student_t_cdf(t, df)
1277}
1278
1279#[cfg(feature = "wasm")]
1280#[wasm_bindgen]
1281/// Computes the critical t-value for a given significance level.
1282///
1283/// Returns the t-value such that the area under the t-distribution curve
1284/// to the right equals alpha/2 (two-tailed test).
1285///
1286/// # Arguments
1287///
1288/// * `alpha` - Significance level (typically 0.05 for 95% confidence)
1289/// * `df` - Degrees of freedom
1290///
1291/// # Returns
1292///
1293/// The critical t-value, or `NaN` if domain check fails.
1294pub fn get_t_critical(alpha: f64, df: f64) -> f64 {
1295 if check_domain().is_err() {
1296 return f64::NAN;
1297 }
1298
1299 core::t_critical_quantile(df, alpha)
1300}
1301
1302#[cfg(feature = "wasm")]
1303#[wasm_bindgen]
1304/// Computes the inverse of the standard normal CDF (probit function).
1305///
1306/// Returns the z-score such that P(Z ≤ z) = p for a standard normal distribution.
1307///
1308/// # Arguments
1309///
1310/// * `p` - Probability (0 < p < 1)
1311///
1312/// # Returns
1313///
1314/// The z-score, or `NaN` if domain check fails.
1315pub fn get_normal_inverse(p: f64) -> f64 {
1316 if check_domain().is_err() {
1317 return f64::NAN;
1318 }
1319
1320 normal_inverse_cdf(p)
1321}
1322
1323// ============================================================================
1324// Domain Check (WASM-only)
1325// ============================================================================
1326//
1327// By default, all domains are allowed. To enable domain restriction, set the
1328// LINREG_DOMAIN_RESTRICT environment variable at build time:
1329//
1330// LINREG_DOMAIN_RESTRICT=example.com,yoursite.com wasm-pack build
1331//
1332// Example for jesse-anderson.net:
1333// LINREG_DOMAIN_RESTRICT=jesse-anderson.net,tools.jesse-anderson.net,localhost,127.0.0.1 wasm-pack build
1334//
1335// This allows downstream users to use the library without modification while
1336// still providing domain restriction as an opt-in security feature.
1337
1338#[cfg(feature = "wasm")]
1339fn check_domain() -> Result<()> {
1340 // Read allowed domains from build-time environment variable
1341 let allowed_domains = option_env!("LINREG_DOMAIN_RESTRICT");
1342
1343 match allowed_domains {
1344 Some(domains) if !domains.is_empty() => {
1345 // Domain restriction is enabled
1346 let window = web_sys::window().ok_or(Error::DomainCheck("No window found".to_string()))?;
1347 let location = window.location();
1348 let hostname = location.hostname().map_err(|_| Error::DomainCheck("No hostname found".to_string()))?;
1349
1350 let domain_list: Vec<&str> = domains.split(',').map(|s| s.trim()).collect();
1351
1352 if domain_list.contains(&hostname.as_str()) {
1353 Ok(())
1354 } else {
1355 Err(Error::DomainCheck(format!(
1356 "Unauthorized domain: {}. Allowed: {}",
1357 hostname, domains
1358 )))
1359 }
1360 }
1361 _ => {
1362 // No restriction - allow all domains
1363 Ok(())
1364 }
1365 }
1366}
1367
1368// ============================================================================
1369// Test Functions (WASM-only)
1370// ============================================================================
1371
1372#[cfg(test)]
1373mod tests {
1374 use super::*;
1375
1376 #[test]
1377 fn verify_housing_regression_integrity() {
1378 let result = test_housing_regression_native();
1379 if let Err(e) = result {
1380 panic!("Regression test failed: {}", e);
1381 }
1382 }
1383}
1384
1385#[cfg(feature = "wasm")]
1386#[wasm_bindgen]
1387/// Simple test function to verify WASM is working.
1388///
1389/// Returns a success message confirming the WASM module loaded correctly.
1390///
1391/// # Errors
1392///
1393/// Returns a JSON error object if domain check fails.
1394pub fn test() -> String {
1395 if let Err(e) = check_domain() {
1396 return error_to_json(&e);
1397 }
1398 "Rust WASM is working!".to_string()
1399}
1400
1401#[cfg(feature = "wasm")]
1402#[wasm_bindgen]
1403/// Returns the current version of the library.
1404///
1405/// Returns the Cargo package version as a string (e.g., "0.1.0").
1406///
1407/// # Errors
1408///
1409/// Returns a JSON error object if domain check fails.
1410pub fn get_version() -> String {
1411 if let Err(e) = check_domain() {
1412 return error_to_json(&e);
1413 }
1414 env!("CARGO_PKG_VERSION").to_string()
1415}
1416
1417#[cfg(feature = "wasm")]
1418#[wasm_bindgen]
1419/// Test function for t-critical value computation.
1420///
1421/// Returns JSON with the computed t-critical value for the given parameters.
1422///
1423/// # Errors
1424///
1425/// Returns a JSON error object if domain check fails.
1426pub fn test_t_critical(df: f64, alpha: f64) -> String {
1427 if let Err(e) = check_domain() {
1428 return error_to_json(&e);
1429 }
1430 let t_crit = core::t_critical_quantile(df, alpha);
1431 format!(r#"{{"df": {}, "alpha": {}, "t_critical": {}}}"#, df, alpha, t_crit)
1432}
1433
1434#[cfg(feature = "wasm")]
1435#[wasm_bindgen]
1436/// Test function for confidence interval computation.
1437///
1438/// Returns JSON with the computed confidence interval for a coefficient.
1439///
1440/// # Errors
1441///
1442/// Returns a JSON error object if domain check fails.
1443pub fn test_ci(coef: f64, se: f64, df: f64, alpha: f64) -> String {
1444 if let Err(e) = check_domain() {
1445 return error_to_json(&e);
1446 }
1447 let t_crit = core::t_critical_quantile(df, alpha);
1448 format!(r#"{{"lower": {}, "upper": {}}}"#, coef - t_crit * se, coef + t_crit * se)
1449}
1450
1451#[cfg(feature = "wasm")]
1452#[wasm_bindgen]
1453/// Test function for R accuracy validation.
1454///
1455/// Returns JSON comparing our statistical functions against R reference values.
1456///
1457/// # Errors
1458///
1459/// Returns a JSON error object if domain check fails.
1460pub fn test_r_accuracy() -> String {
1461 if let Err(e) = check_domain() {
1462 return error_to_json(&e);
1463 }
1464 format!(
1465 r#"{{"two_tail_p": {}, "qt_975": {}}}"#,
1466 core::two_tailed_p_value(1.6717, 21.0),
1467 core::t_critical_quantile(21.0, 0.05)
1468 )
1469}
1470
1471#[cfg(feature = "wasm")]
1472#[wasm_bindgen]
1473/// Test function for regression validation against R reference values.
1474///
1475/// Runs a regression on a housing dataset and compares results against R's lm() output.
1476/// Returns JSON with status "PASS" or "FAIL" with details.
1477///
1478/// # Errors
1479///
1480/// Returns a JSON error object if domain check fails.
1481pub fn test_housing_regression() -> String {
1482 if let Err(e) = check_domain() {
1483 return error_to_json(&e);
1484 }
1485
1486 match test_housing_regression_native() {
1487 Ok(result) => result,
1488 Err(e) => serde_json::json!({ "status": "ERROR", "error": e.to_string() }).to_string()
1489 }
1490}
1491
1492// Native Rust test function (works without WASM feature)
1493fn test_housing_regression_native() -> Result<String> {
1494 let y = vec![
1495 245.5, 312.8, 198.4, 425.6, 278.9, 356.2, 189.5, 512.3, 234.7, 298.1,
1496 445.8, 167.9, 367.4, 289.6, 198.2, 478.5, 256.3, 334.7, 178.5, 398.9,
1497 223.4, 312.5, 156.8, 423.7, 267.9
1498 ];
1499
1500 let square_feet = vec![
1501 1200.0, 1800.0, 950.0, 2400.0, 1450.0, 2000.0, 1100.0, 2800.0, 1350.0, 1650.0,
1502 2200.0, 900.0, 1950.0, 1500.0, 1050.0, 2600.0, 1300.0, 1850.0, 1000.0, 2100.0,
1503 1250.0, 1700.0, 850.0, 2350.0, 1400.0
1504 ];
1505 let bedrooms = vec![
1506 3.0, 4.0, 2.0, 4.0, 3.0, 4.0, 2.0, 5.0, 3.0, 3.0,
1507 4.0, 2.0, 4.0, 3.0, 2.0, 5.0, 3.0, 4.0, 2.0, 4.0,
1508 3.0, 3.0, 2.0, 4.0, 3.0
1509 ];
1510 let age = vec![
1511 15.0, 10.0, 25.0, 5.0, 8.0, 12.0, 20.0, 2.0, 18.0, 7.0,
1512 3.0, 30.0, 6.0, 14.0, 22.0, 1.0, 16.0, 9.0, 28.0, 4.0,
1513 19.0, 11.0, 35.0, 3.0, 13.0
1514 ];
1515
1516 let x_vars = vec![square_feet, bedrooms, age];
1517 let names = vec!["Intercept".to_string(), "Square_Feet".to_string(), "Bedrooms".to_string(), "Age".to_string()];
1518
1519 let result = core::ols_regression(&y, &x_vars, &names)?;
1520
1521 // Check against R results
1522 let expected_coeffs = [52.1271333, 0.1613877, 0.9545492, -1.1811815];
1523 let expected_std_errs = [31.18201809, 0.01875072, 10.44400198, 0.73219949];
1524
1525 let tolerance = 1e-4;
1526 let mut mismatches = vec![];
1527
1528 for i in 0..4 {
1529 if (result.coefficients[i] - expected_coeffs[i]).abs() > tolerance {
1530 mismatches.push(format!("coeff[{}] differs: got {}, expected {}", i, result.coefficients[i], expected_coeffs[i]));
1531 }
1532 if (result.std_errors[i] - expected_std_errs[i]).abs() > tolerance {
1533 mismatches.push(format!("std_err[{}] differs: got {}, expected {}", i, result.std_errors[i], expected_std_errs[i]));
1534 }
1535 }
1536
1537 if mismatches.is_empty() {
1538 Ok(serde_json::json!({ "status": "PASS" }).to_string())
1539 } else {
1540 Ok(serde_json::json!({ "status": "FAIL", "mismatches": mismatches }).to_string())
1541 }
1542}