linreg-core 0.6.0 → 0.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -7,6 +7,7 @@
7
7
  [![npm](https://img.shields.io/npm/v/linreg-core?color=red)](https://www.npmjs.com/package/linreg-core)
8
8
  [![PyPI](https://img.shields.io/pypi/v/linreg-core)](https://pypi.org/project/linreg-core/)
9
9
  [![docs.rs](https://img.shields.io/badge/docs.rs-linreg__core-green)](https://docs.rs/linreg-core)
10
+ [![Live Demo](https://img.shields.io/badge/demo-online-brightgreen)](https://jesse-anderson.net/linreg-core/)
10
11
 
11
12
 
12
13
  A lightweight, self-contained linear regression library written in Rust. Compiles to WebAssembly for browser use, Python bindings via PyO3, or runs as a native Rust crate.
@@ -37,6 +38,8 @@ A lightweight, self-contained linear regression library written in Rust. Compile
37
38
  - **Lasso Regression:** L1-regularized regression via coordinate descent with automatic variable selection, convergence tracking, model selection criteria
38
39
  - **Elastic Net:** Combined L1 + L2 regularization for variable selection with multicollinearity handling, active set convergence, model selection criteria
39
40
  - **LOESS:** Locally estimated scatterplot smoothing for non-parametric curve fitting with configurable span, polynomial degree, and robust fitting
41
+ - **WLS (Weighted Least Squares):** Regression with observation weights for heteroscedastic data, includes confidence intervals
42
+ - **K-Fold Cross Validation:** Model evaluation and hyperparameter tuning for all regression types (OLS, Ridge, Lasso, Elastic Net) with customizable folds, shuffling, and seeding
40
43
  - **Lambda Path Generation:** Create regularization paths for cross-validation
41
44
 
42
45
  ### Model Statistics
@@ -64,7 +67,7 @@ Add to your `Cargo.toml`:
64
67
 
65
68
  ```toml
66
69
  [dependencies]
67
- linreg-core = { version = "0.5", default-features = false }
70
+ linreg-core = { version = "0.6", default-features = false }
68
71
  ```
69
72
 
70
73
  ### OLS Regression (Rust)
@@ -198,35 +201,199 @@ fn main() -> Result<(), linreg_core::Error> {
198
201
  ```rust
199
202
  use linreg_core::diagnostics::{
200
203
  breusch_pagan_test, durbin_watson_test, jarque_bera_test,
201
- shapiro_wilk_test, RainbowMethod, rainbow_test
204
+ shapiro_wilk_test, rainbow_test, harvey_collier_test,
205
+ white_test, anderson_darling_test, breusch_godfrey_test,
206
+ cooks_distance_test, dfbetas_test, dffits_test, vif_test,
207
+ reset_test, BGTestType, RainbowMethod, ResetType, WhiteMethod
202
208
  };
203
209
 
204
210
  fn main() -> Result<(), linreg_core::Error> {
205
211
  let y = vec![/* your data */];
206
212
  let x = vec![vec![/* predictor 1 */], vec![/* predictor 2 */]];
207
213
 
208
- // Heteroscedasticity
214
+ // Heteroscedasticity tests
209
215
  let bp = breusch_pagan_test(&y, &x)?;
210
216
  println!("Breusch-Pagan: LM={:.4}, p={:.4}", bp.statistic, bp.p_value);
211
217
 
212
- // Autocorrelation
218
+ let white = white_test(&y, &x, WhiteMethod::R)?;
219
+ println!("White: statistic={:.4}, p={:.4}", white.statistic, white.p_value);
220
+
221
+ // Autocorrelation tests
213
222
  let dw = durbin_watson_test(&y, &x)?;
214
223
  println!("Durbin-Watson: {:.4}", dw.statistic);
215
224
 
216
- // Normality
225
+ let bg = breusch_godfrey_test(&y, &x, 2, BGTestType::Chisq)?;
226
+ println!("Breusch-Godfrey (order 2): statistic={:.4}, p={:.4}", bg.statistic, bg.p_value);
227
+
228
+ // Normality tests
217
229
  let jb = jarque_bera_test(&y, &x)?;
218
230
  println!("Jarque-Bera: JB={:.4}, p={:.4}", jb.statistic, jb.p_value);
219
231
 
220
- // Linearity
232
+ let sw = shapiro_wilk_test(&y, &x)?;
233
+ println!("Shapiro-Wilk: W={:.4}, p={:.4}", sw.statistic, sw.p_value);
234
+
235
+ let ad = anderson_darling_test(&y, &x)?;
236
+ println!("Anderson-Darling: A={:.4}, p={:.4}", ad.statistic, ad.p_value);
237
+
238
+ // Linearity tests
221
239
  let rainbow = rainbow_test(&y, &x, 0.5, RainbowMethod::R)?;
222
240
  println!("Rainbow: F={:.4}, p={:.4}",
223
241
  rainbow.r_result.as_ref().unwrap().statistic,
224
242
  rainbow.r_result.as_ref().unwrap().p_value);
225
243
 
244
+ let hc = harvey_collier_test(&y, &x)?;
245
+ println!("Harvey-Collier: t={:.4}, p={:.4}", hc.statistic, hc.p_value);
246
+
247
+ let reset = reset_test(&y, &x, &[2, 3], ResetType::Fitted)?;
248
+ println!("RESET: F={:.4}, p={:.4}", reset.f_statistic, reset.p_value);
249
+
250
+ // Influence diagnostics
251
+ let cd = cooks_distance_test(&y, &x)?;
252
+ println!("Cook's Distance: {} influential points", cd.influential_4_over_n.len());
253
+
254
+ let dfbetas = dfbetas_test(&y, &x)?;
255
+ println!("DFBETAS: {} influential observations", dfbetas.influential_observations.len());
256
+
257
+ let dffits = dffits_test(&y, &x)?;
258
+ println!("DFFITS: {} influential observations", dffits.influential_observations.len());
259
+
260
+ // Multicollinearity
261
+ let vif = vif_test(&y, &x)?;
262
+ println!("VIF: {:?}", vif.vif_values);
263
+
264
+ Ok(())
265
+ }
266
+ ```
267
+
268
+ ### WLS Regression (Rust)
269
+
270
+ ```rust,no_run
271
+ use linreg_core::weighted_regression::wls_regression;
272
+
273
+ fn main() -> Result<(), linreg_core::Error> {
274
+ let y = vec![2.0, 4.0, 6.0, 8.0, 10.0];
275
+ let x1 = vec![1.0, 2.0, 3.0, 4.0, 5.0];
276
+
277
+ // Equal weights = OLS
278
+ let weights = vec![1.0, 1.0, 1.0, 1.0, 1.0];
279
+
280
+ let fit = wls_regression(&y, &[x1], &weights)?;
281
+
282
+ println!("Intercept: {} (SE: {}, t: {}, p: {})",
283
+ fit.coefficients[0],
284
+ fit.standard_errors[0],
285
+ fit.t_statistics[0],
286
+ fit.p_values[0]
287
+ );
288
+ println!("F-statistic: {} (p: {})", fit.f_statistic, fit.f_p_value);
289
+ println!("R-squared: {:.4}", fit.r_squared);
290
+
291
+ // Access confidence intervals
292
+ for (i, (&coef, &lower, &upper)) in fit.coefficients.iter()
293
+ .zip(fit.conf_int_lower.iter())
294
+ .zip(fit.conf_int_upper.iter())
295
+ .enumerate()
296
+ {
297
+ println!("Coefficient {}: [{}, {}]", i, lower, upper);
298
+ }
299
+
300
+ Ok(())
301
+ }
302
+ ```
303
+
304
+ ### LOESS Regression (Rust)
305
+
306
+ ```rust,no_run
307
+ use linreg_core::loess::{loess_fit, LoessOptions};
308
+
309
+ fn main() -> Result<(), linreg_core::Error> {
310
+ // Single predictor only
311
+ let x = vec![0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0];
312
+ let y = vec![1.0, 3.5, 4.8, 6.2, 8.5, 11.0, 13.2, 14.8, 17.5, 19.0, 22.0];
313
+
314
+ // Default options: span=0.75, degree=2, robust iterations=0
315
+ let options = LoessOptions::default();
316
+
317
+ let result = loess_fit(&y, &[x], &options)?;
318
+
319
+ println!("Fitted values: {:?}", result.fitted_values);
320
+ println!("Residuals: {:?}", result.residuals);
321
+
226
322
  Ok(())
227
323
  }
228
324
  ```
229
325
 
326
+ **Custom LOESS options:**
327
+
328
+ ```rust,no_run
329
+ use linreg_core::loess::{loess_fit, LoessOptions, LoessSurface};
330
+
331
+ let options = LoessOptions {
332
+ span: 0.5, // Smoothing parameter (0-1, smaller = less smooth)
333
+ degree: 1, // Polynomial degree (0=constant, 1=linear, 2=quadratic)
334
+ surface: LoessSurface::Direct, // Note: only "direct" is currently supported; "interpolate" is planned
335
+ robust_iterations: 3, // Number of robust fitting iterations (0 = disabled)
336
+ };
337
+
338
+ let result = loess_fit(&y, &[x], &options)?;
339
+ ```
340
+
341
+ ### K-Fold Cross Validation (Rust)
342
+
343
+ Cross-validation is used for model evaluation and hyperparameter tuning. The library supports K-Fold CV for all regression types:
344
+
345
+ ```rust,no_run
346
+ use linreg_core::cross_validation::{kfold_cv_ols, kfold_cv_ridge, kfold_cv_lasso, kfold_cv_elastic_net, KFoldOptions};
347
+
348
+ fn main() -> Result<(), linreg_core::Error> {
349
+ let y = vec![2.5, 3.7, 4.2, 5.1, 6.3, 7.0, 7.5, 8.1];
350
+ let x1 = vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0];
351
+ let x2 = vec![2.0, 4.0, 5.0, 4.0, 3.0, 4.5, 5.5, 6.0];
352
+ let names = vec!["Intercept".to_string(), "X1".to_string(), "X2".to_string()];
353
+
354
+ // Configure CV options
355
+ let options = KFoldOptions {
356
+ n_folds: 5,
357
+ shuffle: true,
358
+ seed: Some(42), // For reproducibility
359
+ };
360
+
361
+ // OLS cross-validation
362
+ let ols_cv = kfold_cv_ols(&y, &[x1.clone(), x2.clone()], &names, &options)?;
363
+ println!("OLS CV RMSE: {:.4} (±{:.4})", ols_cv.mean_rmse, ols_cv.std_rmse);
364
+ println!("OLS CV R²: {:.4} (±{:.4})", ols_cv.mean_r_squared, ols_cv.std_r_squared);
365
+
366
+ // Ridge cross-validation (for lambda selection)
367
+ let lambda = 1.0;
368
+ let ridge_cv = kfold_cv_ridge(&[x1.clone(), x2.clone()], &y, lambda, true, &options)?;
369
+ println!("Ridge CV RMSE: {:.4}", ridge_cv.mean_rmse);
370
+
371
+ // Lasso cross-validation
372
+ let lasso_cv = kfold_cv_lasso(&[x1.clone(), x2.clone()], &y, 0.1, true, &options)?;
373
+ println!("Lasso CV RMSE: {:.4}", lasso_cv.mean_rmse);
374
+
375
+ // Elastic Net cross-validation
376
+ let enet_cv = kfold_cv_elastic_net(&[x1, x2], &y, 0.1, 0.5, true, &options)?;
377
+ println!("Elastic Net CV RMSE: {:.4}", enet_cv.mean_rmse);
378
+
379
+ // Access per-fold results
380
+ for fold in &ols_cv.fold_results {
381
+ println!("Fold {}: train={}, test={}, R²={:.4}",
382
+ fold.fold_index, fold.train_size, fold.test_size, fold.r_squared);
383
+ }
384
+
385
+ Ok(())
386
+ }
387
+ ```
388
+
389
+ **CV Result fields:**
390
+ - `mean_rmse`, `std_rmse` - Mean and std of RMSE across folds
391
+ - `mean_mae`, `std_mae` - Mean and std of MAE across folds
392
+ - `mean_r_squared`, `std_r_squared` - Mean and std of R² across folds
393
+ - `mean_train_r_squared` - Mean training R² (for overfitting detection)
394
+ - `fold_results` - Per-fold metrics (train/test sizes, MSE, RMSE, MAE, R²)
395
+ - `fold_coefficients` - Coefficients from each fold (for stability analysis)
396
+
230
397
  ### Lambda Path Generation (Rust)
231
398
 
232
399
  ```rust,no_run
@@ -250,10 +417,34 @@ for &lambda in lambdas.iter() {
250
417
  }
251
418
  ```
252
419
 
420
+ ### Model Save/Load (Rust)
421
+
422
+ All trained models can be saved to disk and loaded back later:
423
+
424
+ ```rust,no_run
425
+ use linreg_core::{ModelSave, ModelLoad};
426
+
427
+ // Train a model
428
+ let result = ols_regression(&y, &[x1], &names)?;
429
+
430
+ // Save to file
431
+ result.save("my_model.json")?;
432
+
433
+ // Or with a custom name
434
+ result.save_with_name("my_model.json", Some("My Housing Model".to_string()))?;
435
+
436
+ // Load back
437
+ let loaded = linreg_core::core::RegressionOutput::load("my_model.json")?;
438
+ ```
439
+
440
+ The same `save()` and `load()` methods work for all model types: `RegressionOutput`, `RidgeFit`, `LassoFit`, `ElasticNetFit`, `WlsFit`, and `LoessFit`.
441
+
253
442
  ---
254
443
 
255
444
  ## WebAssembly Usage
256
445
 
446
+ **[Live Demo →](https://jesse-anderson.net/linreg-core/)**
447
+
257
448
  Build with wasm-pack:
258
449
 
259
450
  ```bash
@@ -360,6 +551,24 @@ console.log("Lambda sequence:", path.lambda_path);
360
551
  console.log("Lambda max:", path.lambda_max);
361
552
  ```
362
553
 
554
+ ### WLS Regression (WASM)
555
+
556
+ ```javascript
557
+ const result = JSON.parse(wls_regression(
558
+ JSON.stringify([2, 4, 6, 8, 10]),
559
+ JSON.stringify([[1, 2, 3, 4, 5]]),
560
+ JSON.stringify([1, 1, 1, 1, 1]) // weights (equal weights = OLS)
561
+ ));
562
+
563
+ console.log("Coefficients:", result.coefficients);
564
+ console.log("Standard errors:", result.standard_errors);
565
+ console.log("P-values:", result.p_values);
566
+ console.log("R-squared:", result.r_squared);
567
+ console.log("F-statistic:", result.f_statistic);
568
+ console.log("Confidence intervals (lower):", result.conf_int_lower);
569
+ console.log("Confidence intervals (upper):", result.conf_int_upper);
570
+ ```
571
+
363
572
  ### LOESS Regression (WASM)
364
573
 
365
574
  ```javascript
@@ -368,7 +577,7 @@ const result = JSON.parse(loess_fit(
368
577
  JSON.stringify(x[0]), // Single predictor only (flattened array)
369
578
  0.5, // span (smoothing parameter: 0-1)
370
579
  1, // degree (0=constant, 1=linear, 2=quadratic)
371
- "direct", // surface method ("direct" or "interpolate")
580
+ "direct", // surface method ("direct" only; "interpolate" is planned)
372
581
  0 // robust iterations (0=disabled, >0=number of iterations)
373
582
  ));
374
583
 
@@ -376,6 +585,64 @@ console.log("Fitted values:", result.fitted_values);
376
585
  console.log("Residuals:", result.residuals);
377
586
  ```
378
587
 
588
+ ### K-Fold Cross Validation (WASM)
589
+
590
+ ```javascript
591
+ // OLS cross-validation
592
+ const ols_cv = JSON.parse(kfold_cv_ols(
593
+ JSON.stringify(y),
594
+ JSON.stringify(x),
595
+ JSON.stringify(["Intercept", "X1", "X2"]),
596
+ 5, // n_folds
597
+ "true", // shuffle (JSON boolean)
598
+ "42" // seed (JSON string number, or "null" for no seed)
599
+ ));
600
+
601
+ console.log("OLS CV RMSE:", ols_cv.mean_rmse, "±", ols_cv.std_rmse);
602
+ console.log("OLS CV R²:", ols_cv.mean_r_squared, "±", ols_cv.std_r_squared);
603
+
604
+ // Ridge cross-validation
605
+ const ridge_cv = JSON.parse(kfold_cv_ridge(
606
+ JSON.stringify(y),
607
+ JSON.stringify(x),
608
+ 1.0, // lambda
609
+ true, // standardize
610
+ 5, // n_folds
611
+ "true", // shuffle
612
+ "42" // seed
613
+ ));
614
+
615
+ // Lasso cross-validation
616
+ const lasso_cv = JSON.parse(kfold_cv_lasso(
617
+ JSON.stringify(y),
618
+ JSON.stringify(x),
619
+ 0.1, // lambda
620
+ true, // standardize
621
+ 5, // n_folds
622
+ "true", // shuffle
623
+ "42" // seed
624
+ ));
625
+
626
+ // Elastic Net cross-validation
627
+ const enet_cv = JSON.parse(kfold_cv_elastic_net(
628
+ JSON.stringify(y),
629
+ JSON.stringify(x),
630
+ 0.1, // lambda
631
+ 0.5, // alpha (0 = Ridge, 1 = Lasso)
632
+ true, // standardize
633
+ 5, // n_folds
634
+ "true", // shuffle
635
+ "42" // seed
636
+ ));
637
+
638
+ // Access per-fold results
639
+ ols_cv.fold_results.forEach(fold => {
640
+ console.log(`Fold ${fold.fold_index}: R²=${fold.r_squared.toFixed(4)}`);
641
+ });
642
+ ```
643
+
644
+ **Note:** In WASM, boolean and seed parameters are passed as JSON strings. Use `"true"`/`"false"` for shuffle and `"42"` or `"null"` for seed.
645
+
379
646
  ### Diagnostic Tests (WASM)
380
647
 
381
648
  ```javascript
@@ -524,6 +791,43 @@ const version = get_version(); // e.g., "0.5.0"
524
791
  const msg = test(); // "Rust WASM is working!"
525
792
  ```
526
793
 
794
+ ### Model Serialization (WASM)
795
+
796
+ ```javascript
797
+ // Train a model
798
+ const resultJson = ols_regression(
799
+ JSON.stringify(y),
800
+ JSON.stringify(x),
801
+ JSON.stringify(names)
802
+ );
803
+ const result = JSON.parse(resultJson);
804
+
805
+ // Serialize with metadata
806
+ const serialized = serialize_model(
807
+ resultJson, // model JSON
808
+ "OLS", // model type: "OLS", "Ridge", "Lasso", "ElasticNet", "WLS", "LOESS"
809
+ "My Model" // optional name (null to omit)
810
+ );
811
+
812
+ // Get metadata without loading full model
813
+ const metadataJson = get_model_metadata(serialized);
814
+ const metadata = JSON.parse(metadataJson);
815
+ console.log("Model type:", metadata.model_type);
816
+ console.log("Created:", metadata.created_at);
817
+
818
+ // Deserialize to get model data back
819
+ const modelJson = deserialize_model(serialized);
820
+ const model = JSON.parse(modelJson);
821
+
822
+ // Download in browser
823
+ const blob = new Blob([serialized], { type: 'application/json' });
824
+ const url = URL.createObjectURL(blob);
825
+ const a = document.createElement('a');
826
+ a.href = url;
827
+ a.download = 'model.json';
828
+ a.click();
829
+ ```
830
+
527
831
  ### Domain Security (WASM)
528
832
 
529
833
  Optional domain restriction via build-time environment variable:
@@ -654,6 +958,20 @@ print(f"AIC: {result.aic}")
654
958
  print(f"BIC: {result.bic}")
655
959
  ```
656
960
 
961
+ ### LOESS Regression (Python)
962
+
963
+ ```python
964
+ result = linreg_core.loess_fit(
965
+ y, # Single predictor only
966
+ [0.5], # span (smoothing parameter: 0-1)
967
+ 2, # degree (0=constant, 1=linear, 2=quadratic)
968
+ "direct", # surface ("direct" only; "interpolate" is planned)
969
+ 0 # robust iterations (0=disabled, >0=number of iterations)
970
+ )
971
+ print(f"Fitted values: {result.fitted_values}")
972
+ print(f"Residuals: {result.residuals}")
973
+ ```
974
+
657
975
  ### Lambda Path Generation (Python)
658
976
 
659
977
  ```python
@@ -767,6 +1085,23 @@ print(f"Numeric columns: {result.numeric_columns}")
767
1085
  print(f"Data rows: {result.n_rows}")
768
1086
  ```
769
1087
 
1088
+ ### Model Save/Load (Python)
1089
+
1090
+ ```python
1091
+ # Train a model
1092
+ result = linreg_core.ols_regression(y, x, names)
1093
+
1094
+ # Save to file
1095
+ linreg_core.save_model(result, "my_model.json", name="My Housing Model")
1096
+
1097
+ # Load back
1098
+ loaded = linreg_core.load_model("my_model.json")
1099
+ print(f"R²: {loaded.r_squared}")
1100
+ print(f"Coefficients: {loaded.coefficients}")
1101
+ ```
1102
+
1103
+ The `save_model()` and `load_model()` functions work with all result types: `OLSResult`, `RidgeResult`, `LassoResult`, `ElasticNetResult`, `LoessResult`, and `WlsResult`.
1104
+
770
1105
  ---
771
1106
 
772
1107
  ## Feature Flags
@@ -780,7 +1115,7 @@ print(f"Data rows: {result.n_rows}")
780
1115
  For native Rust without WASM overhead:
781
1116
 
782
1117
  ```toml
783
- linreg-core = { version = "0.5", default-features = false }
1118
+ linreg-core = { version = "0.6", default-features = false }
784
1119
  ```
785
1120
 
786
1121
  For Python bindings (built with maturin):
@@ -843,6 +1178,181 @@ This library is under active development and has not reached 1.0 stability. Whil
843
1178
 
844
1179
  ---
845
1180
 
1181
+ ## Benchmarks
1182
+
1183
+ <details>
1184
+ <summary><strong>Click to expand v0.6.0 benchmark results</strong></summary>
1185
+
1186
+ Benchmark results run on Windows with `cargo bench --no-default-features`. Times are median values.
1187
+
1188
+ ### Core Regression Benchmarks
1189
+
1190
+ | Benchmark | Size (n × p) | Time | Throughput |
1191
+ |-----------|--------------|------|------------|
1192
+ | OLS Regression | 10 × 2 | 12.46 µs | 802.71 Kelem/s |
1193
+ | OLS Regression | 50 × 3 | 53.72 µs | 930.69 Kelem/s |
1194
+ | OLS Regression | 100 × 5 | 211.09 µs | 473.73 Kelem/s |
1195
+ | OLS Regression | 500 × 10 | 7.46 ms | 67.04 Kelem/s |
1196
+ | OLS Regression | 1000 × 20 | 47.81 ms | 20.91 Kelem/s |
1197
+ | OLS Regression | 5000 × 50 | 2.86 s | 1.75 Kelem/s |
1198
+ | Ridge Regression | 50 × 3 | 9.61 µs | 5.20 Melem/s |
1199
+ | Ridge Regression | 100 × 5 | 70.41 µs | 1.42 Melem/s |
1200
+ | Ridge Regression | 500 × 10 | 842.37 µs | 593.56 Kelem/s |
1201
+ | Ridge Regression | 1000 × 20 | 1.38 ms | 724.71 Kelem/s |
1202
+ | Ridge Regression | 5000 × 50 | 10.25 ms | 487.78 Kelem/s |
1203
+ | Lasso Regression | 50 × 3 | 258.82 µs | 193.18 Kelem/s |
1204
+ | Lasso Regression | 100 × 5 | 247.89 µs | 403.41 Kelem/s |
1205
+ | Lasso Regression | 500 × 10 | 3.58 ms | 139.86 Kelem/s |
1206
+ | Lasso Regression | 1000 × 20 | 1.54 ms | 651.28 Kelem/s |
1207
+ | Lasso Regression | 5000 × 50 | 12.52 ms | 399.50 Kelem/s |
1208
+ | Elastic Net Regression | 50 × 3 | 46.15 µs | 1.08 Melem/s |
1209
+ | Elastic Net Regression | 100 × 5 | 358.07 µs | 279.27 Kelem/s |
1210
+ | Elastic Net Regression | 500 × 10 | 1.61 ms | 310.18 Kelem/s |
1211
+ | Elastic Net Regression | 1000 × 20 | 1.60 ms | 623.66 Kelem/s |
1212
+ | Elastic Net Regression | 5000 × 50 | 12.57 ms | 397.77 Kelem/s |
1213
+ | WLS Regression | 50 × 3 | 32.92 µs | 1.52 Melem/s |
1214
+ | WLS Regression | 100 × 5 | 155.30 µs | 643.93 Kelem/s |
1215
+ | WLS Regression | 500 × 10 | 6.63 ms | 75.37 Kelem/s |
1216
+ | WLS Regression | 1000 × 20 | 42.68 ms | 23.43 Kelem/s |
1217
+ | WLS Regression | 5000 × 50 | 2.64 s | 1.89 Kelem/s |
1218
+ | LOESS Fit | 50 × 1 | 132.83 µs | 376.42 Kelem/s |
1219
+ | LOESS Fit | 100 × 1 | 1.16 ms | 86.00 Kelem/s |
1220
+ | LOESS Fit | 500 × 1 | 28.42 ms | 17.59 Kelem/s |
1221
+ | LOESS Fit | 1000 × 1 | 113.00 ms | 8.85 Kelem/s |
1222
+ | LOESS Fit | 100 × 2 | 7.10 ms | 14.09 Kelem/s |
1223
+ | LOESS Fit | 500 × 2 | 1.05 s | 476.19 elem/s |
1224
+
1225
+ ### Lambda Path & Elastic Net Path Benchmarks
1226
+
1227
+ | Benchmark | Size (n × p) | Time | Throughput |
1228
+ |-----------|--------------|------|------------|
1229
+ | Elastic Net Path | 100 × 5 | 198.60 ms | 503.52 elem/s |
1230
+ | Elastic Net Path | 500 × 10 | 69.46 ms | 7.20 Kelem/s |
1231
+ | Elastic Net Path | 1000 × 20 | 39.08 ms | 25.59 Kelem/s |
1232
+ | Make Lambda Path | 100 × 5 | 1.09 µs | 91.58 Melem/s |
1233
+ | Make Lambda Path | 500 × 10 | 8.10 µs | 61.70 Melem/s |
1234
+ | Make Lambda Path | 1000 × 20 | 29.96 µs | 33.37 Melem/s |
1235
+ | Make Lambda Path | 5000 × 50 | 424.18 µs | 11.79 Melem/s |
1236
+
1237
+ ### Diagnostic Test Benchmarks
1238
+
1239
+ | Benchmark | Size (n × p) | Time |
1240
+ |-----------|--------------|------|
1241
+ | Rainbow Test | 50 × 3 | 40.34 µs |
1242
+ | Rainbow Test | 100 × 5 | 187.94 µs |
1243
+ | Rainbow Test | 500 × 10 | 8.63 ms |
1244
+ | Rainbow Test | 1000 × 20 | 60.09 ms |
1245
+ | Rainbow Test | 5000 × 50 | 3.45 s |
1246
+ | Harvey-Collier Test | 50 × 1 | 15.26 µs |
1247
+ | Harvey-Collier Test | 100 × 1 | 30.32 µs |
1248
+ | Harvey-Collier Test | 500 × 1 | 138.44 µs |
1249
+ | Harvey-Collier Test | 1000 × 1 | 298.33 µs |
1250
+ | Breusch-Pagan Test | 50 × 3 | 58.07 µs |
1251
+ | Breusch-Pagan Test | 100 × 5 | 296.74 µs |
1252
+ | Breusch-Pagan Test | 500 × 10 | 13.79 ms |
1253
+ | Breusch-Pagan Test | 1000 × 20 | 96.49 ms |
1254
+ | Breusch-Pagan Test | 5000 × 50 | 5.56 s |
1255
+ | White Test | 50 × 3 | 14.31 µs |
1256
+ | White Test | 100 × 5 | 44.25 µs |
1257
+ | White Test | 500 × 10 | 669.40 µs |
1258
+ | White Test | 1000 × 20 | 4.89 ms |
1259
+ | Jarque-Bera Test | 50 × 3 | 30.13 µs |
1260
+ | Jarque-Bera Test | 100 × 5 | 149.29 µs |
1261
+ | Jarque-Bera Test | 500 × 10 | 6.64 ms |
1262
+ | Jarque-Bera Test | 1000 × 20 | 47.89 ms |
1263
+ | Jarque-Bera Test | 5000 × 50 | 2.75 s |
1264
+ | Durbin-Watson Test | 50 × 3 | 31.80 µs |
1265
+ | Durbin-Watson Test | 100 × 5 | 152.56 µs |
1266
+ | Durbin-Watson Test | 500 × 10 | 6.87 ms |
1267
+ | Durbin-Watson Test | 1000 × 20 | 48.65 ms |
1268
+ | Durbin-Watson Test | 5000 × 50 | 2.76 s |
1269
+ | Breusch-Godfrey Test | 50 × 3 | 71.73 µs |
1270
+ | Breusch-Godfrey Test | 100 × 5 | 348.94 µs |
1271
+ | Breusch-Godfrey Test | 500 × 10 | 14.77 ms |
1272
+ | Breusch-Godfrey Test | 1000 × 20 | 100.08 ms |
1273
+ | Breusch-Godfrey Test | 5000 × 50 | 5.64 s |
1274
+ | Shapiro-Wilk Test | 10 × 2 | 2.04 µs |
1275
+ | Shapiro-Wilk Test | 50 × 3 | 4.87 µs |
1276
+ | Shapiro-Wilk Test | 100 × 5 | 10.67 µs |
1277
+ | Shapiro-Wilk Test | 500 × 10 | 110.02 µs |
1278
+ | Shapiro-Wilk Test | 1000 × 20 | 635.13 µs |
1279
+ | Shapiro-Wilk Test | 5000 × 50 | 17.53 ms |
1280
+ | Anderson-Darling Test | 50 × 3 | 34.02 µs |
1281
+ | Anderson-Darling Test | 100 × 5 | 162.28 µs |
1282
+ | Anderson-Darling Test | 500 × 10 | 6.95 ms |
1283
+ | Anderson-Darling Test | 1000 × 20 | 48.15 ms |
1284
+ | Anderson-Darling Test | 5000 × 50 | 2.78 s |
1285
+ | Cook's Distance Test | 50 × 3 | 64.52 µs |
1286
+ | Cook's Distance Test | 100 × 5 | 297.69 µs |
1287
+ | Cook's Distance Test | 500 × 10 | 12.73 ms |
1288
+ | Cook's Distance Test | 1000 × 20 | 94.02 ms |
1289
+ | Cook's Distance Test | 5000 × 50 | 5.31 s |
1290
+ | DFBETAS Test | 50 × 3 | 46.34 µs |
1291
+ | DFBETAS Test | 100 × 5 | 185.52 µs |
1292
+ | DFBETAS Test | 500 × 10 | 7.04 ms |
1293
+ | DFBETAS Test | 1000 × 20 | 49.68 ms |
1294
+ | DFFITS Test | 50 × 3 | 33.56 µs |
1295
+ | DFFITS Test | 100 × 5 | 157.62 µs |
1296
+ | DFFITS Test | 500 × 10 | 6.82 ms |
1297
+ | DFFITS Test | 1000 × 20 | 48.35 ms |
1298
+ | VIF Test | 50 × 3 | 5.36 µs |
1299
+ | VIF Test | 100 × 5 | 12.68 µs |
1300
+ | VIF Test | 500 × 10 | 128.04 µs |
1301
+ | VIF Test | 1000 × 20 | 807.30 µs |
1302
+ | VIF Test | 5000 × 50 | 26.33 ms |
1303
+ | RESET Test | 50 × 3 | 77.85 µs |
1304
+ | RESET Test | 100 × 5 | 359.12 µs |
1305
+ | RESET Test | 500 × 10 | 14.40 ms |
1306
+ | RESET Test | 1000 × 20 | 100.52 ms |
1307
+ | RESET Test | 5000 × 50 | 5.67 s |
1308
+ | Full Diagnostics | 100 × 5 | 2.75 ms |
1309
+ | Full Diagnostics | 500 × 10 | 104.01 ms |
1310
+ | Full Diagnostics | 1000 × 20 | 740.52 ms |
1311
+
1312
+ ### Linear Algebra Benchmarks
1313
+
1314
+ | Benchmark | Size | Time |
1315
+ |-----------|------|------|
1316
+ | Matrix Transpose | 10 × 10 | 209.50 ns |
1317
+ | Matrix Transpose | 50 × 50 | 3.67 µs |
1318
+ | Matrix Transpose | 100 × 100 | 14.92 µs |
1319
+ | Matrix Transpose | 500 × 500 | 924.23 µs |
1320
+ | Matrix Transpose | 1000 × 1000 | 5.56 ms |
1321
+ | Matrix Multiply (matmul) | 10 × 10 × 10 | 1.54 µs |
1322
+ | Matrix Multiply (matmul) | 50 × 50 × 50 | 144.15 µs |
1323
+ | Matrix Multiply (matmul) | 100 × 100 × 100 | 1.39 ms |
1324
+ | Matrix Multiply (matmul) | 200 × 200 × 200 | 11.90 ms |
1325
+ | Matrix Multiply (matmul) | 1000 × 100 × 100 | 13.94 ms |
1326
+ | QR Decomposition | 10 × 5 | 1.41 µs |
1327
+ | QR Decomposition | 50 × 10 | 14.81 µs |
1328
+ | QR Decomposition | 100 × 20 | 57.61 µs |
1329
+ | QR Decomposition | 500 × 50 | 2.19 ms |
1330
+ | QR Decomposition | 1000 × 100 | 19.20 ms |
1331
+ | QR Decomposition | 5000 × 100 | 1.48 s |
1332
+ | QR Decomposition | 10000 × 100 | 8.09 s |
1333
+ | QR Decomposition | 1000 × 500 | 84.48 ms |
1334
+ | SVD | 10 × 5 | 150.36 µs |
1335
+ | SVD | 50 × 10 | 505.41 µs |
1336
+ | SVD | 100 × 20 | 2.80 ms |
1337
+ | SVD | 500 × 50 | 60.00 ms |
1338
+ | SVD | 1000 × 100 | 513.35 ms |
1339
+ | Matrix Invert | 5 × 5 | 877.32 ns |
1340
+ | Matrix Invert | 10 × 10 | 2.48 µs |
1341
+ | Matrix Invert | 20 × 20 | 5.46 µs |
1342
+ | Matrix Invert | 50 × 50 | 31.94 µs |
1343
+ | Matrix Invert | 100 × 100 | 141.38 µs |
1344
+ | Matrix Invert | 200 × 200 | 647.03 µs |
1345
+
1346
+ ### Pressure Benchmarks (Large Datasets)
1347
+
1348
+ | Benchmark | Size (n) | Time |
1349
+ |-----------|----------|------|
1350
+ | Pressure (OLS + all diagnostics) | 10000 | 11.28 s |
1351
+
1352
+ </details>
1353
+
1354
+ ---
1355
+
846
1356
  ## License
847
1357
 
848
1358
  Dual-licensed under [MIT](LICENSE-MIT) or [Apache-2.0](LICENSE-APACHE).
package/linreg_core.d.ts CHANGED
@@ -95,6 +95,41 @@ export function breusch_pagan_test(y_json: string, x_vars_json: string): string;
95
95
  */
96
96
  export function cooks_distance_test(y_json: string, x_vars_json: string): string;
97
97
 
98
+ /**
99
+ * Deserialize a serialized model, extracting the inner model data.
100
+ *
101
+ * This function takes a serialized model JSON (as created by serialize_model),
102
+ * validates the format version, and returns the inner model data as JSON.
103
+ *
104
+ * # Arguments
105
+ *
106
+ * * `json_string` - JSON string of the serialized model (with metadata wrapper)
107
+ *
108
+ * # Returns
109
+ *
110
+ * JSON string of the inner model data (coefficients, statistics, etc.),
111
+ * or a JSON error object if the input is invalid, the format version is
112
+ * incompatible, or the domain check fails.
113
+ *
114
+ * # Example
115
+ *
116
+ * ```javascript
117
+ * import { deserialize_model } from './linreg_core.js';
118
+ *
119
+ * // Load from file (browser-side)
120
+ * const response = await fetch('my_model.json');
121
+ * const serialized = await response.text();
122
+ *
123
+ * // Deserialize to get the model data
124
+ * const modelJson = deserialize_model(serialized);
125
+ * const model = JSON.parse(modelJson);
126
+ *
127
+ * console.log(model.coefficients);
128
+ * console.log(model.r_squared);
129
+ * ```
130
+ */
131
+ export function deserialize_model(json_string: string): string;
132
+
98
133
  /**
99
134
  * Performs DFBETAS analysis via WASM.
100
135
  *
@@ -188,6 +223,46 @@ export function durbin_watson_test(y_json: string, x_vars_json: string): string;
188
223
  */
189
224
  export function elastic_net_regression(y_json: string, x_vars_json: string, _variable_names: string, lambda: number, alpha: number, standardize: boolean, max_iter: number, tol: number): string;
190
225
 
226
+ /**
227
+ * Extract metadata from a serialized model without deserializing the full model.
228
+ *
229
+ * This function returns only the metadata portion of a serialized model,
230
+ * which includes information like model type, library version, creation time,
231
+ * and optional model name.
232
+ *
233
+ * # Arguments
234
+ *
235
+ * * `json_string` - JSON string of the serialized model
236
+ *
237
+ * # Returns
238
+ *
239
+ * JSON string containing the metadata object with fields:
240
+ * - `format_version` - Format version (e.g., "1.0")
241
+ * - `library_version` - linreg-core version used to create the model
242
+ * - `model_type` - Type of model ("OLS", "Ridge", etc.)
243
+ * - `created_at` - ISO 8601 timestamp of creation
244
+ * - `name` - Optional custom model name
245
+ *
246
+ * Returns a JSON error object if the input is invalid or the domain check fails.
247
+ *
248
+ * # Example
249
+ *
250
+ * ```javascript
251
+ * import { get_model_metadata } from './linreg_core.js';
252
+ *
253
+ * const response = await fetch('my_model.json');
254
+ * const serialized = await response.text();
255
+ *
256
+ * const metadataJson = get_model_metadata(serialized);
257
+ * const metadata = JSON.parse(metadataJson);
258
+ *
259
+ * console.log('Model type:', metadata.model_type);
260
+ * console.log('Created:', metadata.created_at);
261
+ * console.log('Name:', metadata.name || '(unnamed)');
262
+ * ```
263
+ */
264
+ export function get_model_metadata(json_string: string): string;
265
+
191
266
  /**
192
267
  * Computes the inverse of the standard normal CDF (probit function).
193
268
  *
@@ -291,6 +366,110 @@ export function harvey_collier_test(y_json: string, x_vars_json: string): string
291
366
  */
292
367
  export function jarque_bera_test(y_json: string, x_vars_json: string): string;
293
368
 
369
+ /**
370
+ * Performs K-Fold Cross Validation for Elastic Net regression via WASM.
371
+ *
372
+ * # Arguments
373
+ *
374
+ * * `y_json` - JSON array of response variable values
375
+ * * `x_vars_json` - JSON array of predictor arrays
376
+ * * `lambda` - Regularization strength (>= 0)
377
+ * * `alpha` - Mixing parameter (0 = Ridge, 1 = Lasso)
378
+ * * `standardize` - Whether to standardize predictors
379
+ * * `n_folds` - Number of folds (must be >= 2)
380
+ * * `shuffle_json` - JSON boolean: whether to shuffle data before splitting
381
+ * * `seed_json` - JSON string with seed number or "null" for no seed
382
+ *
383
+ * # Returns
384
+ *
385
+ * JSON string containing CV results (same structure as OLS).
386
+ *
387
+ * # Errors
388
+ *
389
+ * Returns a JSON error object if parsing fails, parameters are invalid,
390
+ * or domain check fails.
391
+ */
392
+ export function kfold_cv_elastic_net(y_json: string, x_vars_json: string, lambda: number, alpha: number, standardize: boolean, n_folds: number, shuffle_json: string, seed_json: string): string;
393
+
394
+ /**
395
+ * Performs K-Fold Cross Validation for Lasso regression via WASM.
396
+ *
397
+ * # Arguments
398
+ *
399
+ * * `y_json` - JSON array of response variable values
400
+ * * `x_vars_json` - JSON array of predictor arrays
401
+ * * `lambda` - Regularization strength (>= 0)
402
+ * * `standardize` - Whether to standardize predictors
403
+ * * `n_folds` - Number of folds (must be >= 2)
404
+ * * `shuffle_json` - JSON boolean: whether to shuffle data before splitting
405
+ * * `seed_json` - JSON string with seed number or "null" for no seed
406
+ *
407
+ * # Returns
408
+ *
409
+ * JSON string containing CV results (same structure as OLS).
410
+ *
411
+ * # Errors
412
+ *
413
+ * Returns a JSON error object if parsing fails, parameters are invalid,
414
+ * or domain check fails.
415
+ */
416
+ export function kfold_cv_lasso(y_json: string, x_vars_json: string, lambda: number, standardize: boolean, n_folds: number, shuffle_json: string, seed_json: string): string;
417
+
418
+ /**
419
+ * Performs K-Fold Cross Validation for OLS regression via WASM.
420
+ *
421
+ * # Arguments
422
+ *
423
+ * * `y_json` - JSON array of response variable values
424
+ * * `x_vars_json` - JSON array of predictor arrays
425
+ * * `variable_names_json` - JSON array of variable names
426
+ * * `n_folds` - Number of folds (must be >= 2)
427
+ * * `shuffle_json` - JSON boolean: whether to shuffle data before splitting
428
+ * * `seed_json` - JSON string with seed number or "null" for no seed
429
+ *
430
+ * # Returns
431
+ *
432
+ * JSON string containing CV results:
433
+ * - `n_folds` - Number of folds used
434
+ * - `n_samples` - Total number of observations
435
+ * - `mean_mse`, `std_mse` - Mean and std of MSE across folds
436
+ * - `mean_rmse`, `std_rmse` - Mean and std of RMSE across folds
437
+ * - `mean_mae`, `std_mae` - Mean and std of MAE across folds
438
+ * - `mean_r_squared`, `std_r_squared` - Mean and std of R² across folds
439
+ * - `fold_results` - Array of individual fold results
440
+ * - `fold_coefficients` - Array of coefficient arrays from each fold
441
+ *
442
+ * # Errors
443
+ *
444
+ * Returns a JSON error object if parsing fails, parameters are invalid,
445
+ * or domain check fails.
446
+ */
447
+ export function kfold_cv_ols(y_json: string, x_vars_json: string, variable_names_json: string, n_folds: number, shuffle_json: string, seed_json: string): string;
448
+
449
+ /**
450
+ * Performs K-Fold Cross Validation for Ridge regression via WASM.
451
+ *
452
+ * # Arguments
453
+ *
454
+ * * `y_json` - JSON array of response variable values
455
+ * * `x_vars_json` - JSON array of predictor arrays
456
+ * * `lambda` - Regularization strength (>= 0)
457
+ * * `standardize` - Whether to standardize predictors
458
+ * * `n_folds` - Number of folds (must be >= 2)
459
+ * * `shuffle_json` - JSON boolean: whether to shuffle data before splitting
460
+ * * `seed_json` - JSON string with seed number or "null" for no seed
461
+ *
462
+ * # Returns
463
+ *
464
+ * JSON string containing CV results (same structure as OLS).
465
+ *
466
+ * # Errors
467
+ *
468
+ * Returns a JSON error object if parsing fails, parameters are invalid,
469
+ * or domain check fails.
470
+ */
471
+ export function kfold_cv_ridge(y_json: string, x_vars_json: string, lambda: number, standardize: boolean, n_folds: number, shuffle_json: string, seed_json: string): string;
472
+
294
473
  /**
295
474
  * Performs Lasso regression via WASM.
296
475
  *
@@ -592,6 +771,46 @@ export function reset_test(y_json: string, x_vars_json: string, powers_json: str
592
771
  */
593
772
  export function ridge_regression(y_json: string, x_vars_json: string, _variable_names: string, lambda: number, standardize: boolean): string;
594
773
 
774
+ /**
775
+ * Serialize a model by wrapping its JSON data with metadata.
776
+ *
777
+ * This function takes a model's JSON representation (as returned by regression
778
+ * functions), wraps it with version and type metadata, and returns a serialized
779
+ * JSON string suitable for storage or download.
780
+ *
781
+ * # Arguments
782
+ *
783
+ * * `model_json` - JSON string of the model result (e.g., from ols_regression)
784
+ * * `model_type` - Type of model: "OLS", "Ridge", "Lasso", "ElasticNet", "WLS", or "LOESS"
785
+ * * `name` - Optional custom name for the model
786
+ *
787
+ * # Returns
788
+ *
789
+ * JSON string containing the serialized model with metadata, or a JSON error object
790
+ * if the input is invalid or the domain check fails.
791
+ *
792
+ * # Example
793
+ *
794
+ * ```javascript
795
+ * import { serialize_model, ols_regression } from './linreg_core.js';
796
+ *
797
+ * // Train a model
798
+ * const resultJson = ols_regression(yJson, xJson, namesJson);
799
+ *
800
+ * // Serialize it
801
+ * const serialized = serialize_model(resultJson, "OLS", "My Housing Model");
802
+ *
803
+ * // Download (browser-side)
804
+ * const blob = new Blob([serialized], { type: 'application/json' });
805
+ * const url = URL.createObjectURL(blob);
806
+ * const a = document.createElement('a');
807
+ * a.href = url;
808
+ * a.download = 'my_model.json';
809
+ * a.click();
810
+ * ```
811
+ */
812
+ export function serialize_model(model_json: string, model_type: string, name?: string | null): string;
813
+
595
814
  /**
596
815
  * Performs the Shapiro-Wilk test for normality via WASM.
597
816
  *
@@ -857,13 +1076,19 @@ export interface InitOutput {
857
1076
  readonly breusch_godfrey_test: (a: number, b: number, c: number, d: number, e: number, f: number, g: number) => [number, number];
858
1077
  readonly breusch_pagan_test: (a: number, b: number, c: number, d: number) => [number, number];
859
1078
  readonly cooks_distance_test: (a: number, b: number, c: number, d: number) => [number, number];
1079
+ readonly deserialize_model: (a: number, b: number) => [number, number];
860
1080
  readonly dfbetas_test: (a: number, b: number, c: number, d: number) => [number, number];
861
1081
  readonly dffits_test: (a: number, b: number, c: number, d: number) => [number, number];
862
1082
  readonly durbin_watson_test: (a: number, b: number, c: number, d: number) => [number, number];
863
1083
  readonly elastic_net_regression: (a: number, b: number, c: number, d: number, e: number, f: number, g: number, h: number, i: number, j: number, k: number) => [number, number];
1084
+ readonly get_model_metadata: (a: number, b: number) => [number, number];
864
1085
  readonly get_version: () => [number, number];
865
1086
  readonly harvey_collier_test: (a: number, b: number, c: number, d: number) => [number, number];
866
1087
  readonly jarque_bera_test: (a: number, b: number, c: number, d: number) => [number, number];
1088
+ readonly kfold_cv_elastic_net: (a: number, b: number, c: number, d: number, e: number, f: number, g: number, h: number, i: number, j: number, k: number, l: number) => [number, number];
1089
+ readonly kfold_cv_lasso: (a: number, b: number, c: number, d: number, e: number, f: number, g: number, h: number, i: number, j: number, k: number) => [number, number];
1090
+ readonly kfold_cv_ols: (a: number, b: number, c: number, d: number, e: number, f: number, g: number, h: number, i: number, j: number, k: number) => [number, number];
1091
+ readonly kfold_cv_ridge: (a: number, b: number, c: number, d: number, e: number, f: number, g: number, h: number, i: number, j: number, k: number) => [number, number];
867
1092
  readonly lasso_regression: (a: number, b: number, c: number, d: number, e: number, f: number, g: number, h: number, i: number, j: number) => [number, number];
868
1093
  readonly loess_fit: (a: number, b: number, c: number, d: number, e: number, f: number, g: number, h: number, i: number) => [number, number];
869
1094
  readonly loess_predict: (a: number, b: number, c: number, d: number, e: number, f: number, g: number, h: number, i: number, j: number, k: number) => [number, number];
@@ -875,6 +1100,7 @@ export interface InitOutput {
875
1100
  readonly rainbow_test: (a: number, b: number, c: number, d: number, e: number, f: number, g: number) => [number, number];
876
1101
  readonly reset_test: (a: number, b: number, c: number, d: number, e: number, f: number, g: number, h: number) => [number, number];
877
1102
  readonly ridge_regression: (a: number, b: number, c: number, d: number, e: number, f: number, g: number, h: number) => [number, number];
1103
+ readonly serialize_model: (a: number, b: number, c: number, d: number, e: number, f: number) => [number, number];
878
1104
  readonly shapiro_wilk_test: (a: number, b: number, c: number, d: number) => [number, number];
879
1105
  readonly stats_correlation: (a: number, b: number, c: number, d: number) => [number, number];
880
1106
  readonly stats_mean: (a: number, b: number) => [number, number];
package/linreg_core.js CHANGED
@@ -170,6 +170,56 @@ export function cooks_distance_test(y_json, x_vars_json) {
170
170
  }
171
171
  }
172
172
 
173
+ /**
174
+ * Deserialize a serialized model, extracting the inner model data.
175
+ *
176
+ * This function takes a serialized model JSON (as created by serialize_model),
177
+ * validates the format version, and returns the inner model data as JSON.
178
+ *
179
+ * # Arguments
180
+ *
181
+ * * `json_string` - JSON string of the serialized model (with metadata wrapper)
182
+ *
183
+ * # Returns
184
+ *
185
+ * JSON string of the inner model data (coefficients, statistics, etc.),
186
+ * or a JSON error object if the input is invalid, the format version is
187
+ * incompatible, or the domain check fails.
188
+ *
189
+ * # Example
190
+ *
191
+ * ```javascript
192
+ * import { deserialize_model } from './linreg_core.js';
193
+ *
194
+ * // Load from file (browser-side)
195
+ * const response = await fetch('my_model.json');
196
+ * const serialized = await response.text();
197
+ *
198
+ * // Deserialize to get the model data
199
+ * const modelJson = deserialize_model(serialized);
200
+ * const model = JSON.parse(modelJson);
201
+ *
202
+ * console.log(model.coefficients);
203
+ * console.log(model.r_squared);
204
+ * ```
205
+ * @param {string} json_string
206
+ * @returns {string}
207
+ */
208
+ export function deserialize_model(json_string) {
209
+ let deferred2_0;
210
+ let deferred2_1;
211
+ try {
212
+ const ptr0 = passStringToWasm0(json_string, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
213
+ const len0 = WASM_VECTOR_LEN;
214
+ const ret = wasm.deserialize_model(ptr0, len0);
215
+ deferred2_0 = ret[0];
216
+ deferred2_1 = ret[1];
217
+ return getStringFromWasm0(ret[0], ret[1]);
218
+ } finally {
219
+ wasm.__wbindgen_free(deferred2_0, deferred2_1, 1);
220
+ }
221
+ }
222
+
173
223
  /**
174
224
  * Performs DFBETAS analysis via WASM.
175
225
  *
@@ -343,6 +393,61 @@ export function elastic_net_regression(y_json, x_vars_json, _variable_names, lam
343
393
  }
344
394
  }
345
395
 
396
+ /**
397
+ * Extract metadata from a serialized model without deserializing the full model.
398
+ *
399
+ * This function returns only the metadata portion of a serialized model,
400
+ * which includes information like model type, library version, creation time,
401
+ * and optional model name.
402
+ *
403
+ * # Arguments
404
+ *
405
+ * * `json_string` - JSON string of the serialized model
406
+ *
407
+ * # Returns
408
+ *
409
+ * JSON string containing the metadata object with fields:
410
+ * - `format_version` - Format version (e.g., "1.0")
411
+ * - `library_version` - linreg-core version used to create the model
412
+ * - `model_type` - Type of model ("OLS", "Ridge", etc.)
413
+ * - `created_at` - ISO 8601 timestamp of creation
414
+ * - `name` - Optional custom model name
415
+ *
416
+ * Returns a JSON error object if the input is invalid or the domain check fails.
417
+ *
418
+ * # Example
419
+ *
420
+ * ```javascript
421
+ * import { get_model_metadata } from './linreg_core.js';
422
+ *
423
+ * const response = await fetch('my_model.json');
424
+ * const serialized = await response.text();
425
+ *
426
+ * const metadataJson = get_model_metadata(serialized);
427
+ * const metadata = JSON.parse(metadataJson);
428
+ *
429
+ * console.log('Model type:', metadata.model_type);
430
+ * console.log('Created:', metadata.created_at);
431
+ * console.log('Name:', metadata.name || '(unnamed)');
432
+ * ```
433
+ * @param {string} json_string
434
+ * @returns {string}
435
+ */
436
+ export function get_model_metadata(json_string) {
437
+ let deferred2_0;
438
+ let deferred2_1;
439
+ try {
440
+ const ptr0 = passStringToWasm0(json_string, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
441
+ const len0 = WASM_VECTOR_LEN;
442
+ const ret = wasm.get_model_metadata(ptr0, len0);
443
+ deferred2_0 = ret[0];
444
+ deferred2_1 = ret[1];
445
+ return getStringFromWasm0(ret[0], ret[1]);
446
+ } finally {
447
+ wasm.__wbindgen_free(deferred2_0, deferred2_1, 1);
448
+ }
449
+ }
450
+
346
451
  /**
347
452
  * Computes the inverse of the standard normal CDF (probit function).
348
453
  *
@@ -511,6 +616,220 @@ export function jarque_bera_test(y_json, x_vars_json) {
511
616
  }
512
617
  }
513
618
 
619
+ /**
620
+ * Performs K-Fold Cross Validation for Elastic Net regression via WASM.
621
+ *
622
+ * # Arguments
623
+ *
624
+ * * `y_json` - JSON array of response variable values
625
+ * * `x_vars_json` - JSON array of predictor arrays
626
+ * * `lambda` - Regularization strength (>= 0)
627
+ * * `alpha` - Mixing parameter (0 = Ridge, 1 = Lasso)
628
+ * * `standardize` - Whether to standardize predictors
629
+ * * `n_folds` - Number of folds (must be >= 2)
630
+ * * `shuffle_json` - JSON boolean: whether to shuffle data before splitting
631
+ * * `seed_json` - JSON string with seed number or "null" for no seed
632
+ *
633
+ * # Returns
634
+ *
635
+ * JSON string containing CV results (same structure as OLS).
636
+ *
637
+ * # Errors
638
+ *
639
+ * Returns a JSON error object if parsing fails, parameters are invalid,
640
+ * or domain check fails.
641
+ * @param {string} y_json
642
+ * @param {string} x_vars_json
643
+ * @param {number} lambda
644
+ * @param {number} alpha
645
+ * @param {boolean} standardize
646
+ * @param {number} n_folds
647
+ * @param {string} shuffle_json
648
+ * @param {string} seed_json
649
+ * @returns {string}
650
+ */
651
+ export function kfold_cv_elastic_net(y_json, x_vars_json, lambda, alpha, standardize, n_folds, shuffle_json, seed_json) {
652
+ let deferred5_0;
653
+ let deferred5_1;
654
+ try {
655
+ const ptr0 = passStringToWasm0(y_json, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
656
+ const len0 = WASM_VECTOR_LEN;
657
+ const ptr1 = passStringToWasm0(x_vars_json, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
658
+ const len1 = WASM_VECTOR_LEN;
659
+ const ptr2 = passStringToWasm0(shuffle_json, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
660
+ const len2 = WASM_VECTOR_LEN;
661
+ const ptr3 = passStringToWasm0(seed_json, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
662
+ const len3 = WASM_VECTOR_LEN;
663
+ const ret = wasm.kfold_cv_elastic_net(ptr0, len0, ptr1, len1, lambda, alpha, standardize, n_folds, ptr2, len2, ptr3, len3);
664
+ deferred5_0 = ret[0];
665
+ deferred5_1 = ret[1];
666
+ return getStringFromWasm0(ret[0], ret[1]);
667
+ } finally {
668
+ wasm.__wbindgen_free(deferred5_0, deferred5_1, 1);
669
+ }
670
+ }
671
+
672
+ /**
673
+ * Performs K-Fold Cross Validation for Lasso regression via WASM.
674
+ *
675
+ * # Arguments
676
+ *
677
+ * * `y_json` - JSON array of response variable values
678
+ * * `x_vars_json` - JSON array of predictor arrays
679
+ * * `lambda` - Regularization strength (>= 0)
680
+ * * `standardize` - Whether to standardize predictors
681
+ * * `n_folds` - Number of folds (must be >= 2)
682
+ * * `shuffle_json` - JSON boolean: whether to shuffle data before splitting
683
+ * * `seed_json` - JSON string with seed number or "null" for no seed
684
+ *
685
+ * # Returns
686
+ *
687
+ * JSON string containing CV results (same structure as OLS).
688
+ *
689
+ * # Errors
690
+ *
691
+ * Returns a JSON error object if parsing fails, parameters are invalid,
692
+ * or domain check fails.
693
+ * @param {string} y_json
694
+ * @param {string} x_vars_json
695
+ * @param {number} lambda
696
+ * @param {boolean} standardize
697
+ * @param {number} n_folds
698
+ * @param {string} shuffle_json
699
+ * @param {string} seed_json
700
+ * @returns {string}
701
+ */
702
+ export function kfold_cv_lasso(y_json, x_vars_json, lambda, standardize, n_folds, shuffle_json, seed_json) {
703
+ let deferred5_0;
704
+ let deferred5_1;
705
+ try {
706
+ const ptr0 = passStringToWasm0(y_json, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
707
+ const len0 = WASM_VECTOR_LEN;
708
+ const ptr1 = passStringToWasm0(x_vars_json, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
709
+ const len1 = WASM_VECTOR_LEN;
710
+ const ptr2 = passStringToWasm0(shuffle_json, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
711
+ const len2 = WASM_VECTOR_LEN;
712
+ const ptr3 = passStringToWasm0(seed_json, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
713
+ const len3 = WASM_VECTOR_LEN;
714
+ const ret = wasm.kfold_cv_lasso(ptr0, len0, ptr1, len1, lambda, standardize, n_folds, ptr2, len2, ptr3, len3);
715
+ deferred5_0 = ret[0];
716
+ deferred5_1 = ret[1];
717
+ return getStringFromWasm0(ret[0], ret[1]);
718
+ } finally {
719
+ wasm.__wbindgen_free(deferred5_0, deferred5_1, 1);
720
+ }
721
+ }
722
+
723
+ /**
724
+ * Performs K-Fold Cross Validation for OLS regression via WASM.
725
+ *
726
+ * # Arguments
727
+ *
728
+ * * `y_json` - JSON array of response variable values
729
+ * * `x_vars_json` - JSON array of predictor arrays
730
+ * * `variable_names_json` - JSON array of variable names
731
+ * * `n_folds` - Number of folds (must be >= 2)
732
+ * * `shuffle_json` - JSON boolean: whether to shuffle data before splitting
733
+ * * `seed_json` - JSON string with seed number or "null" for no seed
734
+ *
735
+ * # Returns
736
+ *
737
+ * JSON string containing CV results:
738
+ * - `n_folds` - Number of folds used
739
+ * - `n_samples` - Total number of observations
740
+ * - `mean_mse`, `std_mse` - Mean and std of MSE across folds
741
+ * - `mean_rmse`, `std_rmse` - Mean and std of RMSE across folds
742
+ * - `mean_mae`, `std_mae` - Mean and std of MAE across folds
743
+ * - `mean_r_squared`, `std_r_squared` - Mean and std of R² across folds
744
+ * - `fold_results` - Array of individual fold results
745
+ * - `fold_coefficients` - Array of coefficient arrays from each fold
746
+ *
747
+ * # Errors
748
+ *
749
+ * Returns a JSON error object if parsing fails, parameters are invalid,
750
+ * or domain check fails.
751
+ * @param {string} y_json
752
+ * @param {string} x_vars_json
753
+ * @param {string} variable_names_json
754
+ * @param {number} n_folds
755
+ * @param {string} shuffle_json
756
+ * @param {string} seed_json
757
+ * @returns {string}
758
+ */
759
+ export function kfold_cv_ols(y_json, x_vars_json, variable_names_json, n_folds, shuffle_json, seed_json) {
760
+ let deferred6_0;
761
+ let deferred6_1;
762
+ try {
763
+ const ptr0 = passStringToWasm0(y_json, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
764
+ const len0 = WASM_VECTOR_LEN;
765
+ const ptr1 = passStringToWasm0(x_vars_json, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
766
+ const len1 = WASM_VECTOR_LEN;
767
+ const ptr2 = passStringToWasm0(variable_names_json, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
768
+ const len2 = WASM_VECTOR_LEN;
769
+ const ptr3 = passStringToWasm0(shuffle_json, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
770
+ const len3 = WASM_VECTOR_LEN;
771
+ const ptr4 = passStringToWasm0(seed_json, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
772
+ const len4 = WASM_VECTOR_LEN;
773
+ const ret = wasm.kfold_cv_ols(ptr0, len0, ptr1, len1, ptr2, len2, n_folds, ptr3, len3, ptr4, len4);
774
+ deferred6_0 = ret[0];
775
+ deferred6_1 = ret[1];
776
+ return getStringFromWasm0(ret[0], ret[1]);
777
+ } finally {
778
+ wasm.__wbindgen_free(deferred6_0, deferred6_1, 1);
779
+ }
780
+ }
781
+
782
+ /**
783
+ * Performs K-Fold Cross Validation for Ridge regression via WASM.
784
+ *
785
+ * # Arguments
786
+ *
787
+ * * `y_json` - JSON array of response variable values
788
+ * * `x_vars_json` - JSON array of predictor arrays
789
+ * * `lambda` - Regularization strength (>= 0)
790
+ * * `standardize` - Whether to standardize predictors
791
+ * * `n_folds` - Number of folds (must be >= 2)
792
+ * * `shuffle_json` - JSON boolean: whether to shuffle data before splitting
793
+ * * `seed_json` - JSON string with seed number or "null" for no seed
794
+ *
795
+ * # Returns
796
+ *
797
+ * JSON string containing CV results (same structure as OLS).
798
+ *
799
+ * # Errors
800
+ *
801
+ * Returns a JSON error object if parsing fails, parameters are invalid,
802
+ * or domain check fails.
803
+ * @param {string} y_json
804
+ * @param {string} x_vars_json
805
+ * @param {number} lambda
806
+ * @param {boolean} standardize
807
+ * @param {number} n_folds
808
+ * @param {string} shuffle_json
809
+ * @param {string} seed_json
810
+ * @returns {string}
811
+ */
812
+ export function kfold_cv_ridge(y_json, x_vars_json, lambda, standardize, n_folds, shuffle_json, seed_json) {
813
+ let deferred5_0;
814
+ let deferred5_1;
815
+ try {
816
+ const ptr0 = passStringToWasm0(y_json, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
817
+ const len0 = WASM_VECTOR_LEN;
818
+ const ptr1 = passStringToWasm0(x_vars_json, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
819
+ const len1 = WASM_VECTOR_LEN;
820
+ const ptr2 = passStringToWasm0(shuffle_json, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
821
+ const len2 = WASM_VECTOR_LEN;
822
+ const ptr3 = passStringToWasm0(seed_json, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
823
+ const len3 = WASM_VECTOR_LEN;
824
+ const ret = wasm.kfold_cv_ridge(ptr0, len0, ptr1, len1, lambda, standardize, n_folds, ptr2, len2, ptr3, len3);
825
+ deferred5_0 = ret[0];
826
+ deferred5_1 = ret[1];
827
+ return getStringFromWasm0(ret[0], ret[1]);
828
+ } finally {
829
+ wasm.__wbindgen_free(deferred5_0, deferred5_1, 1);
830
+ }
831
+ }
832
+
514
833
  /**
515
834
  * Performs Lasso regression via WASM.
516
835
  *
@@ -1049,6 +1368,67 @@ export function ridge_regression(y_json, x_vars_json, _variable_names, lambda, s
1049
1368
  }
1050
1369
  }
1051
1370
 
1371
+ /**
1372
+ * Serialize a model by wrapping its JSON data with metadata.
1373
+ *
1374
+ * This function takes a model's JSON representation (as returned by regression
1375
+ * functions), wraps it with version and type metadata, and returns a serialized
1376
+ * JSON string suitable for storage or download.
1377
+ *
1378
+ * # Arguments
1379
+ *
1380
+ * * `model_json` - JSON string of the model result (e.g., from ols_regression)
1381
+ * * `model_type` - Type of model: "OLS", "Ridge", "Lasso", "ElasticNet", "WLS", or "LOESS"
1382
+ * * `name` - Optional custom name for the model
1383
+ *
1384
+ * # Returns
1385
+ *
1386
+ * JSON string containing the serialized model with metadata, or a JSON error object
1387
+ * if the input is invalid or the domain check fails.
1388
+ *
1389
+ * # Example
1390
+ *
1391
+ * ```javascript
1392
+ * import { serialize_model, ols_regression } from './linreg_core.js';
1393
+ *
1394
+ * // Train a model
1395
+ * const resultJson = ols_regression(yJson, xJson, namesJson);
1396
+ *
1397
+ * // Serialize it
1398
+ * const serialized = serialize_model(resultJson, "OLS", "My Housing Model");
1399
+ *
1400
+ * // Download (browser-side)
1401
+ * const blob = new Blob([serialized], { type: 'application/json' });
1402
+ * const url = URL.createObjectURL(blob);
1403
+ * const a = document.createElement('a');
1404
+ * a.href = url;
1405
+ * a.download = 'my_model.json';
1406
+ * a.click();
1407
+ * ```
1408
+ * @param {string} model_json
1409
+ * @param {string} model_type
1410
+ * @param {string | null} [name]
1411
+ * @returns {string}
1412
+ */
1413
+ export function serialize_model(model_json, model_type, name) {
1414
+ let deferred4_0;
1415
+ let deferred4_1;
1416
+ try {
1417
+ const ptr0 = passStringToWasm0(model_json, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
1418
+ const len0 = WASM_VECTOR_LEN;
1419
+ const ptr1 = passStringToWasm0(model_type, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
1420
+ const len1 = WASM_VECTOR_LEN;
1421
+ var ptr2 = isLikeNone(name) ? 0 : passStringToWasm0(name, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
1422
+ var len2 = WASM_VECTOR_LEN;
1423
+ const ret = wasm.serialize_model(ptr0, len0, ptr1, len1, ptr2, len2);
1424
+ deferred4_0 = ret[0];
1425
+ deferred4_1 = ret[1];
1426
+ return getStringFromWasm0(ret[0], ret[1]);
1427
+ } finally {
1428
+ wasm.__wbindgen_free(deferred4_0, deferred4_1, 1);
1429
+ }
1430
+ }
1431
+
1052
1432
  /**
1053
1433
  * Performs the Shapiro-Wilk test for normality via WASM.
1054
1434
  *
@@ -1579,6 +1959,10 @@ function getUint8ArrayMemory0() {
1579
1959
  return cachedUint8ArrayMemory0;
1580
1960
  }
1581
1961
 
1962
+ function isLikeNone(x) {
1963
+ return x === undefined || x === null;
1964
+ }
1965
+
1582
1966
  function passStringToWasm0(arg, malloc, realloc) {
1583
1967
  if (realloc === undefined) {
1584
1968
  const buf = cachedTextEncoder.encode(arg);
Binary file
package/package.json CHANGED
@@ -5,12 +5,13 @@
5
5
  "Jesse Anderson"
6
6
  ],
7
7
  "description": "Lightweight linear regression (OLS, Ridge, Lasso, Elastic Net) with diagnostic tests. Pure Rust - no external math dependencies.",
8
- "version": "0.6.0",
8
+ "version": "0.6.1",
9
9
  "license": "MIT OR Apache-2.0",
10
10
  "repository": {
11
11
  "type": "git",
12
12
  "url": "https://github.com/jesse-anderson/linreg-core"
13
13
  },
14
+ "homepage": "https://jesse-anderson.net/linreg-core/",
14
15
  "files": [
15
16
  "linreg_core_bg.wasm",
16
17
  "linreg_core.js",