loess_rs/
lib.rs

1//! # LOESS — Locally Estimated Scatterplot Smoothing for Rust
2//!
3//! A production-ready, high-performance LOESS implementation with comprehensive
4//! features for robust nonparametric regression and trend estimation.
5//!
6//! ## What is LOESS?
7//!
8//! LOESS (Locally Estimated Scatterplot Smoothing) is a nonparametric regression
9//! method that fits smooth curves through scatter plots. At each point, it fits
10//! a weighted polynomial (typically linear) using nearby data points, with weights
11//! decreasing smoothly with distance. This creates flexible, data-adaptive curves
12//! without assuming a global functional form.
13//!
14//! **Key advantages:**
15//! - No parametric assumptions about the underlying relationship
16//! - Automatic adaptation to local data structure
17//! - Robust to outliers (with robustness iterations enabled)
18//! - Provides uncertainty estimates via confidence/prediction intervals
19//! - Handles irregular sampling and missing regions gracefully
20//!
21//! **Common applications:**
22//! - Exploratory data analysis and visualization
23//! - Trend estimation in time series
24//! - Baseline correction in spectroscopy and signal processing
25//! - Quality control and process monitoring
26//! - Genomic and epigenomic data smoothing
27//! - Removing systematic effects in scientific measurements
28//!
29//! **How LOESS works:**
30//!
31//! <div align="center">
32//! <object data="../../../docs/loess_concept.svg" type="image/svg+xml" width="800" height="500">
33//! <img src="https://raw.githubusercontent.com/thisisamirv/loess-rs/main/docs/loess_concept.svg" alt="LOESS Concept" width="800"/>
34//! </object>
35//! </div>
36//!
37//! 1. **Select Neighborhood**: Identify the $k$ nearest neighbors for the target point based on the smoothing `fraction`.
38//! 2. **Assign Weights**: Apply a distance-based kernel function (e.g., tricube) to weight these neighbors, prioritizing closer points.
39//! 3. **Local Fit**: Fit a weighted polynomial (linear or quadratic) to the neighborhood using Weighted Least Squares (WLS).
40//! 4. **Predict**: Evaluate the polynomial at the target point to obtain the smoothed value.
41//!
42//! ## LOESS vs. LOWESS
43//!
44//! | Feature               | LOESS (This Crate)                | LOWESS                         |
45//! |-----------------------|-----------------------------------|--------------------------------|
46//! | **Polynomial Degree** | Linear, Quadratic, Cubic, Quartic | Linear (Degree 1)              |
47//! | **Dimensions**        | Multivariate (n-D support)        | Univariate (1-D only)          |
48//! | **Flexibility**       | High (Distance metrics)           | Standard                       |
49//! | **Complexity**        | Higher (Matrix inversion)         | Lower (Weighted average/slope) |
50//!
51//! LOESS can fit higher-degree polynomials for more complex data:
52//!
53//! <div align="left">
54//! <object data="../../../docs/degree_comparison.svg" type="image/svg+xml" width="800" height="450">
55//! <img src="https://raw.githubusercontent.com/thisisamirv/loess-rs/main/docs/degree_comparison.svg" alt="Degree Comparison" width="800"/>
56//! </object>
57//! </div>
58//!
59//! LOESS can also handle multivariate data (n-D), while LOWESS is limited to univariate data (1-D):
60//!
61//! <div align="left">
62//! <object data="../../../docs/multivariate_loess.svg" type="image/svg+xml" width="800" height="450">
63//! <img src="https://raw.githubusercontent.com/thisisamirv/loess-rs/main/docs/multivariate_loess.svg" alt="Multivariate LOESS" width="800"/>
64//! </object>
65//! </div>
66//!
67//! <div style="background-color: #4c4b4fff; border-left: 5px solid #ff1818ff; padding: 12px; margin: 15px 0;">
68//!   <strong>Note:</strong> For a simple, lightweight, and fast <strong>LOWESS</strong> implementation, use <a href="https://github.com/thisisamirv/lowess">lowess</a> crate.
69//! </div>
70//!
71//! ## Quick Start
72//!
73//! ### Typical Use
74//!
75//! ```rust
76//! use loess_rs::prelude::*;
77//!
78//! let x = vec![1.0, 2.0, 3.0, 4.0, 5.0];
79//! let y = vec![2.0, 4.1, 5.9, 8.2, 9.8];
80//!
81//! // Build the model
82//! let model = Loess::new()
83//!     .fraction(0.5)      // Use 50% of data for each local fit
84//!     .iterations(3)      // 3 robustness iterations
85//!     .adapter(Batch)
86//!     .build()?;
87//!
88//! // Fit the model to the data
89//! let result = model.fit(&x, &y)?;
90//!
91//! println!("{}", result);
92//! # Result::<(), LoessError>::Ok(())
93//! ```
94//!
95//! ```text
96//! Summary:
97//!   Data points: 5
98//!   Fraction: 0.5
99//!
100//! Smoothed Data:
101//!        X     Y_smooth
102//!   --------------------
103//!     1.00     2.00000
104//!     2.00     4.10000
105//!     3.00     5.90000
106//!     4.00     8.20000
107//!     5.00     9.80000
108//! ```
109//!
110//! ### Full Features
111//!
112//! ```rust
113//! use loess_rs::prelude::*;
114//!
115//! let x = vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0];
116//! let y = vec![2.1, 3.8, 6.2, 7.9, 10.3, 11.8, 14.1, 15.7];
117//!
118//! // Build model with all features enabled
119//! let model = Loess::new()
120//!     .fraction(0.5)                                   // Use 50% of data for each local fit
121//!     .iterations(3)                                   // 3 robustness iterations
122//!     .degree(Linear)                                  // Polynomial degree (Linear default)
123//!     .dimensions(1)                                   // Number of dimensions
124//!     .distance_metric(Euclidean)                      // Distance metric
125//!     .weight_function(Tricube)                        // Kernel function
126//!     .robustness_method(Bisquare)                     // Outlier handling
127//!     .surface_mode(Interpolation)                     // Surface evaluation mode
128//!     .boundary_policy(Extend)                         // Boundary handling
129//!     .boundary_degree_fallback(true)                  // Boundary degree fallback
130//!     .scaling_method(MAD)                             // Scaling method
131//!     .cell(0.2)                                       // Interpolation cell size
132//!     .interpolation_vertices(1000)                    // Maximum vertices for interpolation
133//!     .zero_weight_fallback(UseLocalMean)              // Fallback policy
134//!     .auto_converge(1e-6)                             // Auto-convergence threshold
135//!     .confidence_intervals(0.95)                      // 95% confidence intervals
136//!     .prediction_intervals(0.95)                      // 95% prediction intervals
137//!     .return_diagnostics()                            // Fit quality metrics
138//!     .return_residuals()                              // Include residuals
139//!     .return_robustness_weights()                     // Include robustness weights
140//!     .return_se()                                     // Enable standard error computation
141//!     .cross_validate(KFold(5, &[0.3, 0.7]).seed(123)) // K-fold CV with 5 folds and 2 fraction options
142//!     .adapter(Batch)                                  // Batch adapter
143//!     .build()?;
144//!
145//! let result = model.fit(&x, &y)?;
146//! println!("{}", result);
147//! # Result::<(), LoessError>::Ok(())
148//! ```
149//!
150//! ```text
151//! Summary:
152//!   Data points: 8
153//!   Fraction: 0.5
154//!   Robustness: Applied
155//!
156//! LOESS Diagnostics:
157//!   RMSE:         0.191925
158//!   MAE:          0.181676
159//!   R^2:           0.998205
160//!   Residual SD:  0.297750
161//!   Effective DF: 8.00
162//!   AIC:          -10.41
163//!   AICc:         inf
164//!
165//! Smoothed Data:
166//!        X     Y_smooth      Std_Err   Conf_Lower   Conf_Upper   Pred_Lower   Pred_Upper     Residual Rob_Weight
167//!   ----------------------------------------------------------------------------------------------------------------
168//!     1.00     2.01963     0.389365     1.256476     2.782788     1.058911     2.980353     0.080368     1.0000
169//!     2.00     4.00251     0.345447     3.325438     4.679589     3.108641     4.896386    -0.202513     1.0000
170//!     3.00     5.99959     0.423339     5.169846     6.829335     4.985168     7.014013     0.200410     1.0000
171//!     4.00     8.09859     0.489473     7.139224     9.057960     6.975666     9.221518    -0.198592     1.0000
172//!     5.00    10.03881     0.551687     8.957506    11.120118     8.810073    11.267551     0.261188     1.0000
173//!     6.00    12.02872     0.539259    10.971775    13.085672    10.821364    13.236083    -0.228723     1.0000
174//!     7.00    13.89828     0.371149    13.170829    14.625733    12.965670    14.830892     0.201719     1.0000
175//!     8.00    15.77990     0.408300    14.979631    16.580167    14.789441    16.770356    -0.079899     1.0000
176//! ```
177//!
178//! ### Result and Error Handling
179//!
180//! The `fit` method returns a `Result<LoessResult<T>, LoessError>`.
181//!
182//! - **`Ok(LoessResult<T>)`**: Contains the smoothed data and diagnostics.
183//! - **`Err(LoessError)`**: Indicates a failure (e.g., mismatched input lengths, insufficient data).
184//!
185//! The `?` operator is idiomatic:
186//!
187//! ```rust
188//! use loess_rs::prelude::*;
189//! # let x = vec![1.0, 2.0, 3.0, 4.0, 5.0];
190//! # let y = vec![2.0, 4.1, 5.9, 8.2, 9.8];
191//!
192//! let model = Loess::new().adapter(Batch).build()?;
193//!
194//! let result = model.fit(&x, &y)?;
195//! // or to be more explicit:
196//! // let result: LoessResult<f64> = model.fit(&x, &y)?;
197//! # Result::<(), LoessError>::Ok(())
198//! ```
199//!
200//! But you can also handle results explicitly:
201//!
202//! ```rust
203//! use loess_rs::prelude::*;
204//! # let x = vec![1.0, 2.0, 3.0, 4.0, 5.0];
205//! # let y = vec![2.0, 4.1, 5.9, 8.2, 9.8];
206//!
207//! let model = Loess::new().adapter(Batch).build()?;
208//!
209//! match model.fit(&x, &y) {
210//!     Ok(result) => {
211//!         // result is LoessResult<f64>
212//!         println!("Smoothed: {:?}", result.y);
213//!     }
214//!     Err(e) => {
215//!         // e is LoessError
216//!         eprintln!("Fitting failed: {}", e);
217//!     }
218//! }
219//! # Result::<(), LoessError>::Ok(())
220//! ```
221//!
222//! ## Minimal Usage (no_std / Embedded)
223//!
224//! The crate supports `no_std` environments for embedded devices and resource-constrained systems.
225//! Disable default features to remove the standard library dependency:
226//!
227//! ```toml
228//! [dependencies]
229//! loess_rs = { version = "0.1", default-features = false }
230//! ```
231//!
232//! **Minimal example for embedded systems:**
233//!
234//! ```rust
235//! # #[cfg(feature = "std")] {
236//! use loess_rs::prelude::*;
237//!
238//! // In an embedded context (e.g., sensor data processing)
239//! fn smooth_sensor_data() -> Result<(), LoessError> {
240//!     // Small dataset from sensor readings
241//!     let x = vec![1.0_f32, 2.0, 3.0, 4.0, 5.0];
242//!     let y = vec![2.1, 3.9, 6.2, 7.8, 10.1];
243//!
244//!     // Build minimal model (no intervals, no diagnostics)
245//!     let model = Loess::new()
246//!         .fraction(0.5)
247//!         .iterations(2)      // Fewer iterations for speed
248//!         .adapter(Batch)
249//!         .build()?;
250//!
251//!     // Fit the model
252//!     let result = model.fit(&x, &y)?;
253//!
254//!     // Use smoothed values (result.y)
255//!     // ...
256//!
257//!     Ok(())
258//! }
259//! # smooth_sensor_data().unwrap();
260//! # }
261//! ```
262//!
263//! **Tips for embedded/no_std usage:**
264//! - Use `f32` instead of `f64` to reduce memory footprint
265//! - Keep datasets small (< 1000 points)
266//! - Disable optional features (intervals, diagnostics) to reduce code size
267//! - Use fewer iterations (1-2) to reduce computation time
268//! - Allocate buffers statically when possible to avoid heap fragmentation
269//!
270//! ## Parameters
271//!
272//! All builder parameters have sensible defaults. You only need to specify what you want to change.
273//!
274//! | Parameter                     | Default                                       | Range/Options        | Description                                      | Adapter          |
275//! |-------------------------------|-----------------------------------------------|----------------------|--------------------------------------------------|------------------|
276//! | **fraction**                  | (varies by adapter)                           | (0, 1]               | Smoothing span (fraction of data used per fit)   | All              |
277//! | **iterations**                | (varies by adapter)                           | [0, 1000]            | Number of robustness iterations                  | All              |
278//! | **weight_function**           | `Tricube`                                     | 7 kernel options     | Distance weighting kernel                        | All              |
279//! | **robustness_method**         | `Bisquare`                                    | 3 methods            | Outlier downweighting method                     | All              |
280//! | **zero_weight_fallback**      | `UseLocalMean`                                | 3 fallback options   | Behavior when all weights are zero               | All              |
281//! | **return_residuals**          | false                                         | true/false           | Include residuals in output                      | All              |
282//! | **boundary_policy**           | `Extend`                                      | 4 policy options     | Edge handling strategy (reduces boundary bias)   | All              |
283//! | **boundary_degree_fallback**  | true                                          | true/false           | Use linear fit at boundaries                     | All              |
284//! | **auto_converge**             | None                                          | Tolerance value      | Early stopping for robustness                    | All              |
285//! | **return_robustness_weights** | false                                         | true/false           | Include final weights in output                  | All              |
286//! | **degree**                    | `Linear`                                      | 0, 1, 2, 3, 4        | Polynomial degree (constant to quartic)          | All              |
287//! | **dimensions**                | 1                                             | [1, ∞)               | Number of predictor dimensions                   | All              |
288//! | **distance_metric**           | `Euclidean`                                   | 2 metrics            | Distance metric for nD data                      | All              |
289//! | **surface_mode**              | `Interpolation`                               | 2 modes              | Surface evaluation mode (speed vs accuracy)      | All              |
290//! | **cell**                      | 0.2                                           | (0, 1]               | Interpolation cell size (smaller = higher res)   | All              |
291//! | **interpolation_vertices**    | None (no limit)                               | [1, ∞)               | Optional vertex limit for interpolation surface  | All              |
292//! | **scaling_method**            | `MAD`                                         | 2 methods            | Scale estimation method                          | All              |
293//! | **return_diagnostics**        | false                                         | true/false           | Include RMSE, MAE, R^2, etc. in output           | Batch, Streaming |
294//! | **return_se**                 | false                                         | true/false           | Enable standard error computation                | Batch            |
295//! | **confidence_intervals**      | None                                          | 0..1 (level)         | Uncertainty in mean curve                        | Batch            |
296//! | **prediction_intervals**      | None                                          | 0..1 (level)         | Uncertainty for new observations                 | Batch            |
297//! | **cross_validate**            | None                                          | Method (fractions)   | Automated bandwidth selection                    | Batch            |
298//! | **chunk_size**                | 5000                                          | [10, ∞)              | Points per chunk for streaming                   | Streaming        |
299//! | **overlap**                   | 500                                           | [0, chunk_size)      | Overlapping points between chunks                | Streaming        |
300//! | **merge_strategy**            | `Average`                                     | 4 strategies         | How to merge overlapping regions                 | Streaming        |
301//! | **update_mode**               | `Incremental`                                 | 2 modes              | Online update strategy (Incremental vs Full)     | Online           |
302//! | **window_capacity**           | 1000                                          | [3, ∞)               | Maximum points in sliding window                 | Online           |
303//! | **min_points**                | 3                                             | [2, window_capacity] | Minimum points before smoothing starts           | Online           |
304//!
305//! > **Note on Defaults**: Some parameters have different defaults depending on the adapter:
306//! > - **Batch**: `fraction = 0.67`, `iterations = 3`
307//! > - **Streaming**: `fraction = 0.1`, `iterations = 2`
308//! > - **Online**: `fraction = 0.2`, `iterations = 1`
309//!
310//! ### Parameter Options Reference
311//!
312//! For parameters with multiple options, here are the available choices:
313//!
314//! | Parameter                | Available Options                                                                  |
315//! |--------------------------|------------------------------------------------------------------------------------|
316//! | **weight_function**      | `Tricube`, `Epanechnikov`, `Gaussian`, `Biweight`, `Cosine`, `Triangle`, `Uniform` |
317//! | **robustness_method**    | `Bisquare`, `Huber`, `Talwar`                                                      |
318//! | **zero_weight_fallback** | `UseLocalMean`, `ReturnOriginal`, `ReturnNone`                                     |
319//! | **boundary_policy**      | `Extend`, `Reflect`, `Zero`, `NoBoundary`                                          |
320//! | **update_mode**          | `Incremental`, `Full`                                                              |
321//! | **degree**               | `Constant`, `Linear`, `Quadratic`, `Cubic`, `Quartic`                              |
322//! | **distance_metric**      | `Euclidean`, `Normalized`, `Chebyshev`, `Manhattan`, `Minkowski`, `Weighted`       |
323//! | **surface_mode**         | `Interpolation`, `Direct`                                                          |
324//! | **scaling_method**       | `MAR`, `MAD`                                                                       |
325//!
326//! See the detailed sections below for guidance on choosing between these options.
327//!
328//! ## Builder
329//!
330//! The crate uses a fluent builder pattern for configuration. All parameters have
331//! sensible defaults, so you only need to specify what you want to change.
332//!
333//! ### Basic Workflow
334//!
335//! 1. **Create builder**: `Loess::new()`
336//! 2. **Configure parameters**: Chain method calls (`.fraction()`, `.iterations()`, etc.)
337//! 3. **Select adapter**: Choose execution mode (`.adapter(Batch)`, `.adapter(Streaming)`, etc.)
338//! 4. **Build model**: Call `.build()` to create the configured model
339//! 5. **Fit data**: Call `.fit(&x, &y)` to perform smoothing
340//!
341//! ```rust
342//! use loess_rs::prelude::*;
343//! # let x = vec![1.0, 2.0, 3.0, 4.0, 5.0];
344//! # let y = vec![2.0, 4.1, 5.9, 8.2, 9.8];
345//!
346//! // Build the model with custom configuration
347//! let model = Loess::new()
348//!     .fraction(0.3)               // Smoothing span
349//!     .iterations(5)               // Robustness iterations
350//!     .weight_function(Tricube)    // Kernel function
351//!     .robustness_method(Bisquare) // Outlier handling
352//!     .adapter(Batch)
353//!     .build()?;
354//!
355//! // Fit the model to the data
356//! let result = model.fit(&x, &y)?;
357//! println!("{}", result);
358//! # Result::<(), LoessError>::Ok(())
359//! ```
360//!
361//! ```text
362//! Summary:
363//!   Data points: 5
364//!   Fraction: 0.3
365//!
366//! Smoothed Data:
367//!        X     Y_smooth
368//!   --------------------
369//!     1.00     2.00000
370//!     2.00     4.10000
371//!     3.00     5.90000
372//!     4.00     8.20000
373//!     5.00     9.80000
374//! ```
375//!
376//! ### Execution Mode (Adapter) Comparison
377//!
378//! Choose the right execution mode based on your use case:
379//!
380//! | Adapter     | Use Case                                                                    | Features                                                                                        | Limitations                                                               |
381//! |-------------|-----------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------|
382//! | `Batch`     | Complete datasets in memory<br>Standard analysis<br>Full diagnostics needed | All features supported                                                                          | Requires entire dataset in memory<br>Not suitable for very large datasets |
383//! | `Streaming` | Large datasets (>100K points)<br>Limited memory<br>Batch pipelines          | Chunked processing<br>Configurable overlap<br>Robustness iterations<br>Residuals<br>Diagnostics | No intervals<br>No cross-validation                                       |
384//! | `Online`    | Real-time data<br>Sensor streams<br>Embedded systems                        | Incremental updates<br>Sliding window<br>Memory-bounded<br>Residuals<br>Robustness              | No intervals<br>No cross-validation<br>Limited history                    |
385//!
386//! **Recommendation:**
387//! - **Start with Batch** for most use cases - it's the most feature-complete
388//! - **Use Streaming** when dataset size exceeds available memory
389//! - **Use Online** for real-time applications or when data arrives incrementally
390//!
391//! #### Batch Adapter
392//!
393//! Standard mode for complete datasets in memory. Supports all features.
394//!
395//! ```rust
396//! use loess_rs::prelude::*;
397//! # let x = vec![1.0, 2.0, 3.0, 4.0, 5.0];
398//! # let y = vec![2.0, 4.1, 5.9, 8.2, 9.8];
399//!
400//! // Build model with batch adapter
401//! let model = Loess::new()
402//!     .fraction(0.5)
403//!     .iterations(3)
404//!     .confidence_intervals(0.95)
405//!     .prediction_intervals(0.95)
406//!     .return_diagnostics()
407//!     .adapter(Batch)  // Full feature support
408//!     .build()?;
409//!
410//! let result = model.fit(&x, &y)?;
411//! println!("{}", result);
412//! # Result::<(), LoessError>::Ok(())
413//! ```
414//!
415//! ```text
416//! Summary:
417//!   Data points: 5
418//!   Fraction: 0.5
419//!
420//! Smoothed Data:
421//!        X     Y_smooth      Std_Err   Conf_Lower   Conf_Upper   Pred_Lower   Pred_Upper
422//!   ----------------------------------------------------------------------------------
423//!     1.00     2.00000     0.000000     2.000000     2.000000     2.000000     2.000000
424//!     2.00     4.10000     0.000000     4.100000     4.100000     4.100000     4.100000
425//!     3.00     5.90000     0.000000     5.900000     5.900000     5.900000     5.900000
426//!     4.00     8.20000     0.000000     8.200000     8.200000     8.200000     8.200000
427//!     5.00     9.80000     0.000000     9.800000     9.800000     9.800000     9.800000
428//!
429//! Diagnostics:
430//!   RMSE: 0.0000
431//!   MAE: 0.0000
432//!   R²: 1.0000
433//! ```
434//!
435//! **Use batch when:**
436//! - Dataset fits in memory
437//! - Need all features (intervals, CV, diagnostics)
438//! - Processing complete datasets
439//!
440//! #### Streaming Adapter
441//!
442//! Process large datasets in chunks with configurable overlap. Use `process_chunk()`
443//! to process each chunk and `finalize()` to get remaining buffered data.
444//!
445//! ```rust
446//! use loess_rs::prelude::*;
447//!
448//! // Simulate chunks of data (in practice, read from file/stream)
449//! let chunk1_x: Vec<f64> = (0..50).map(|i| i as f64).collect();
450//! let chunk1_y: Vec<f64> = chunk1_x.iter().map(|&xi| 2.0 * xi + 1.0).collect();
451//!
452//! let chunk2_x: Vec<f64> = (40..100).map(|i| i as f64).collect();
453//! let chunk2_y: Vec<f64> = chunk2_x.iter().map(|&xi| 2.0 * xi + 1.0).collect();
454//!
455//! // Build streaming processor with chunk configuration
456//! let mut processor = Loess::new()
457//!     .fraction(0.3)
458//!     .iterations(2)
459//!     .adapter(Streaming)
460//!     .chunk_size(50)   // Process 50 points at a time
461//!     .overlap(10)      // 10 points overlap between chunks
462//!     .build()?;
463//!
464//! // Process first chunk
465//! let result1 = processor.process_chunk(&chunk1_x, &chunk1_y)?;
466//! // result1.y contains smoothed values for the non-overlapping portion
467//!
468//! // Process second chunk (overlaps with end of first chunk)
469//! let result2 = processor.process_chunk(&chunk2_x, &chunk2_y)?;
470//! // result2.y contains smoothed values, with overlap merged from first chunk
471//!
472//! // IMPORTANT: Call finalize() to get remaining buffered overlap data
473//! let final_result = processor.finalize()?;
474//! // final_result.y contains the final overlap buffer
475//!
476//! // Total processed = all chunks + finalize
477//! let total = result1.y.len() + result2.y.len() + final_result.y.len();
478//! println!("Processed {} points total", total);
479//! # Result::<(), LoessError>::Ok(())
480//! ```
481//!
482//! **Use streaming when:**
483//! - Dataset is very large (>100,000 points)
484//! - Memory is limited
485//! - Processing data in chunks
486//!
487//! #### Online Adapter
488//!
489//! Incremental updates with a sliding window for real-time data.
490//!
491//! ```rust
492//! use loess_rs::prelude::*;
493//!
494//! // Build model with online adapter
495//! let model = Loess::new()
496//!     .fraction(0.2)
497//!     .iterations(1)
498//!     .adapter(Online)
499//!     .build()?;
500//!
501//! let mut online_model = model;
502//!
503//! // Add points incrementally
504//! for i in 1..=10 {
505//!     let x = i as f64;
506//!     let y = 2.0 * x + 1.0;
507//!     if let Some(result) = online_model.add_point(&[x], y)? {
508//!         println!("Latest smoothed value: {:.2}", result.smoothed);
509//!     }
510//! }
511//! # Result::<(), LoessError>::Ok(())
512//! ```
513//!
514//! **Use online when:**
515//! - Data arrives incrementally
516//! - Need real-time updates
517//! - Maintaining a sliding window
518//!
519//! ### Fraction (Smoothing Span)
520//!
521//! The `fraction` parameter controls the proportion of data used for each local fit.
522//! Larger fractions create smoother curves; smaller fractions preserve more detail.
523//!
524//! <div align="center">
525//! <object data="../../../docs/fraction_effect_comparison.svg" type="image/svg+xml" width="1200" height="450">
526//! <img src="https://raw.githubusercontent.com/thisisamirv/loess-rs/main/docs/fraction_effect_comparison.svg" alt="Fraction Effect" width="1200"/>
527//! </object>
528//!
529//! *Under-smoothing (fraction too small), optimal smoothing, and over-smoothing (fraction too large)*
530//! </div>
531//!
532//! - **Range**: (0, 1]
533//! - **Effect**: Larger = smoother, smaller = more detail
534//!
535//! ```rust
536//! use loess_rs::prelude::*;
537//! # let x = vec![1.0, 2.0, 3.0, 4.0, 5.0];
538//! # let y = vec![2.0, 4.1, 5.9, 8.2, 9.8];
539//!
540//! // Build model with small fraction (more detail)
541//! let model = Loess::new()
542//!     .fraction(0.2)  // Use 20% of data for each local fit
543//!     .adapter(Batch)
544//!     .build()?;
545//!
546//! let result = model.fit(&x, &y)?;
547//! # Result::<(), LoessError>::Ok(())
548//! ```
549//!
550//! **Choosing fraction:**
551//! - **0.1-0.3**: Fine detail, may be noisy
552//! - **0.3-0.5**: Moderate smoothing (good for most cases)
553//! - **0.5-0.7**: Heavy smoothing, emphasizes trends
554//! - **0.7-1.0**: Very smooth, may over-smooth
555//!
556//! ### Iterations (Robustness)
557//!
558//! The `iterations` parameter controls outlier resistance through iterative
559//! reweighting. More iterations provide stronger robustness but increase computation time.
560//!
561//! <div align="center">
562//! <object data="../../../docs/robust_vs_standard_loess.svg" type="image/svg+xml" width="900" height="500">
563//! <img src="https://raw.githubusercontent.com/thisisamirv/loess-rs/main/docs/robust_vs_standard_loess.svg" alt="Robustness Effect" width="900"/>
564//! </object>
565//!
566//! *Standard LOESS (left) vs Robust LOESS (right) - robustness iterations downweight outliers*
567//! </div>
568//!
569//! - **Range**: [0, 1000]
570//! - **Effect**: More iterations = stronger outlier downweighting
571//!
572//! ```rust
573//! use loess_rs::prelude::*;
574//! # let x = vec![1.0, 2.0, 3.0, 4.0, 5.0];
575//! # let y = vec![2.0, 4.1, 5.9, 8.2, 9.8];
576//!
577//! // Build model with strong outlier resistance
578//! let model = Loess::new()
579//!     .fraction(0.5)
580//!     .iterations(5)  // More iterations for stronger robustness
581//!     .adapter(Batch)
582//!     .build()?;
583//!
584//! let result = model.fit(&x, &y)?;
585//! # Result::<(), LoessError>::Ok(())
586//! ```
587//!
588//! **Choosing iterations:**
589//! - **0**: No robustness (fastest, sensitive to outliers)
590//! - **1-3**: Light to moderate robustness (recommended)
591//! - **4-6**: Strong robustness (for contaminated data)
592//! - **7+**: Very strong (may over-smooth)
593//!
594//! ### Weight Functions (Kernels)
595//!
596//! Control how neighboring points are weighted by distance.
597//!
598//! ```rust
599//! use loess_rs::prelude::*;
600//! # let x = vec![1.0, 2.0, 3.0, 4.0, 5.0];
601//! # let y = vec![2.0, 4.1, 5.9, 8.2, 9.8];
602//!
603//! // Build model with Epanechnikov kernel
604//! let model = Loess::new()
605//!     .fraction(0.5)
606//!     .weight_function(Epanechnikov)
607//!     .adapter(Batch)
608//!     .build()?;
609//!
610//! let result = model.fit(&x, &y)?;
611//! # Result::<(), LoessError>::Ok(())
612//! ```
613//!
614//! **Kernel selection guide:**
615//!
616//! | Kernel         | Efficiency | Smoothness        |
617//! |----------------|------------|-------------------|
618//! | `Tricube`      | 0.998      | Very smooth       |
619//! | `Epanechnikov` | 1.000      | Smooth            |
620//! | `Gaussian`     | 0.961      | Infinitely smooth |
621//! | `Biweight`     | 0.995      | Very smooth       |
622//! | `Cosine`       | 0.999      | Smooth            |
623//! | `Triangle`     | 0.989      | Moderate          |
624//! | `Uniform`      | 0.943      | None              |
625//!
626//! *Efficiency = AMISE relative to Epanechnikov (1.0 = optimal)*
627//!
628//! **Choosing a Kernel:**
629//!
630//! * **Tricube** (default): Best all-around choice
631//!   - High efficiency (0.9983)
632//!   - Smooth derivatives
633//!   - Compact support (computationally efficient)
634//!   - Cleveland's original choice
635//!
636//! * **Epanechnikov**: Theoretically optimal
637//!   - AMISE-optimal for kernel density estimation
638//!   - Less smooth than tricube
639//!   - Efficiency = 1.0 by definition
640//!
641//! * **Gaussian**: Maximum smoothness
642//!   - Infinitely smooth
643//!   - No boundary effects
644//!   - More expensive to compute
645//!   - Good for very smooth data
646//!
647//! * **Biweight**: Good balance
648//!   - High efficiency (0.9951)
649//!   - Smoother than Epanechnikov
650//!   - Compact support
651//!
652//! * **Cosine**: Smooth and compact
653//!   - Good for robust smoothing contexts
654//!   - High efficiency (0.9995)
655//!
656//! * **Triangle**: Simple and fast
657//!   - Linear taper
658//!   - Less smooth than other kernels
659//!   - Easy to understand
660//!
661//! * **Uniform**: Simplest
662//!   - Equal weights within window
663//!   - Fastest to compute
664//!   - Least smooth results
665//!
666//! ### Robustness Methods
667//!
668//! Different methods for downweighting outliers during iterative refinement.
669//!
670//! ```rust
671//! use loess_rs::prelude::*;
672//! # let x = vec![1.0, 2.0, 3.0, 4.0, 5.0];
673//! # let y = vec![2.0, 4.1, 5.9, 100.0, 9.8];  // Point 3 is an outlier
674//!
675//! // Build model with Talwar robustness (hard threshold)
676//! let model = Loess::new()
677//!     .fraction(0.5)
678//!     .iterations(3)
679//!     .robustness_method(Talwar)
680//!     .return_robustness_weights()  // Include weights in output
681//!     .adapter(Batch)
682//!     .build()?;
683//!
684//! // Fit the model to the data
685//! let result = model.fit(&x, &y)?;
686//!
687//! // Check which points were downweighted
688//! if let Some(weights) = &result.robustness_weights {
689//!     for (i, &w) in weights.iter().enumerate() {
690//!         if w < 0.5 {
691//!             println!("Point {} is likely an outlier (weight: {:.3})", i, w);
692//!         }
693//!     }
694//! }
695//! # Result::<(), LoessError>::Ok(())
696//! ```
697//!
698//! ```text
699//! Point 3 is likely an outlier (weight: 0.000)
700//! ```
701//!
702//! **Available methods:**
703//!
704//! | Method     | Behavior                | Use Case                  |
705//! |------------|-------------------------|---------------------------|
706//! | `Bisquare` | Smooth downweighting    | General-purpose, balanced |
707//! | `Huber`    | Linear beyond threshold | Moderate outliers         |
708//! | `Talwar`   | Hard threshold (0 or 1) | Extreme contamination     |
709//!
710//! ### Zero-Weight Fallback
711//!
712//! Control behavior when all neighborhood weights are zero.
713//!
714//! ```rust
715//! use loess_rs::prelude::*;
716//! # let x = vec![1.0, 2.0, 3.0, 4.0, 5.0];
717//! # let y = vec![2.0, 4.1, 5.9, 8.2, 9.8];
718//!
719//! // Build model with custom zero-weight fallback
720//! let model = Loess::new()
721//!     .fraction(0.5)
722//!     .zero_weight_fallback(UseLocalMean)
723//!     .adapter(Batch)
724//!     .build()?;
725//!
726//! let result = model.fit(&x, &y)?;
727//! # Result::<(), LoessError>::Ok(())
728//! ```
729//!
730//! **Fallback options:**
731//! - `UseLocalMean`: Use mean of neighborhood
732//! - `ReturnOriginal`: Return original y value
733//! - `ReturnNone`: Return NaN (for explicit handling)
734//!
735//! ### Return Residuals
736//!
737//! Include residuals (y - smoothed) in the output for all adapters.
738//!
739//! ```rust
740//! use loess_rs::prelude::*;
741//! # let x = vec![1.0, 2.0, 3.0, 4.0, 5.0];
742//! # let y = vec![2.0, 4.1, 5.9, 8.2, 9.8];
743//!
744//! let model = Loess::new()
745//!     .fraction(0.5)
746//!     .return_residuals()
747//!     .adapter(Batch)
748//!     .build()?;
749//!
750//! let result = model.fit(&x, &y)?;
751//! // Access residuals
752//! if let Some(residuals) = result.residuals {
753//!     println!("Residuals: {:?}", residuals);
754//! }
755//! # Result::<(), LoessError>::Ok(())
756//! ```
757//!
758//! ### Boundary Policy
759//!
760//! LOESS traditionally uses asymmetric windows at boundaries, which can introduce bias.
761//! The `boundary_policy` parameter pads the data before smoothing to enable centered windows:
762//!
763//! - **`Extend`** (default): Pad with constant values (first/last y-value)
764//! - **`Reflect`**: Mirror the data at boundaries
765//! - **`Zero`**: Pad with zeros
766//! - **`NoBoundary`**: Do not pad the data (original Cleveland behavior)
767//!
768//! ```rust
769//! use loess_rs::prelude::*;
770//! # let x = vec![1.0, 2.0, 3.0, 4.0, 5.0];
771//! # let y = vec![2.0, 4.1, 5.9, 8.2, 9.8];
772//!
773//! // Use reflective padding for better edge handling
774//! let model = Loess::new()
775//!     .fraction(0.5)
776//!     .boundary_policy(Reflect)
777//!     .adapter(Batch)
778//!     .build()?;
779//!
780//! let result = model.fit(&x, &y)?;
781//! # Result::<(), LoessError>::Ok(())
782//! ```
783//!
784//! **Choosing a policy:**
785//! - Use `Extend` for most cases (default)
786//! - Use `Reflect` for periodic or symmetric data
787//! - Use `Zero` when data naturally approaches zero at boundaries
788//! - Use `NoBoundary` to disable padding
789//!
790//! > **Note:** For nD (multivariate) data, `Extend` currently defaults to `NoBoundary` behavior
791//! > to preserve regression accuracy, as constant extension can distort local gradients.
792//! > `Reflect` and `Zero` are fully supported in nD.
793//!
794//! ### Boundary Degree Fallback
795//!
796//! Controls whether polynomial degree is reduced at boundary vertices during interpolation.
797//!
798//! When using `Interpolation` surface mode with higher polynomial degrees (Quadratic, Cubic, etc.),
799//! vertices outside the "tight data bounds" can produce unstable extrapolation. This option
800//! controls whether to fall back to Linear fits at those boundary vertices:
801//!
802//! - **`true`** (default): Reduce to Linear at boundary vertices (more stable)
803//! - **`false`**: Use full requested degree everywhere (matches R's `loess` exactly)
804//!
805//! ```rust
806//! use loess_rs::prelude::*;
807//! # let x = vec![1.0, 2.0, 3.0, 4.0, 5.0];
808//! # let y = vec![2.0, 4.1, 5.9, 8.2, 9.8];
809//!
810//! // Default (stable boundary handling)
811//! let stable_model = Loess::<f64>::new()
812//!     .degree(Quadratic)
813//!     .adapter(Batch)
814//!     .build()?;
815//!
816//! // Match R's loess behavior exactly
817//! let r_compatible = Loess::<f64>::new()
818//!     .degree(Quadratic)
819//!     .boundary_degree_fallback(false)
820//!     .adapter(Batch)
821//!     .build()?;
822//! # Result::<(), LoessError>::Ok(())
823//! ```
824//!
825//! > **Note:** This setting only affects `Interpolation` mode. In `Direct` mode, the full
826//! > polynomial degree is always used at every point.
827//!
828//! ### Auto-Convergence
829//!
830//! Automatically stop iterations when the smoothed values converge.
831//!
832//! ```rust
833//! use loess_rs::prelude::*;
834//! # let x = vec![1.0, 2.0, 3.0, 4.0, 5.0];
835//! # let y = vec![2.0, 4.1, 5.9, 8.2, 9.8];
836//!
837//! // Build model with auto-convergence
838//! let model = Loess::new()
839//!     .fraction(0.5)
840//!     .auto_converge(1e-6)      // Stop when change < 1e-6
841//!     .iterations(20)           // Maximum iterations
842//!     .adapter(Batch)
843//!     .build()?;
844//!
845//! // Fit the model to the data
846//! let result = model.fit(&x, &y)?;
847//!
848//! println!("Converged after {} iterations", result.iterations_used.unwrap());
849//! # Result::<(), LoessError>::Ok(())
850//! ```
851//!
852//! ```text
853//! Converged after 1 iterations
854//! ```
855//!
856//! ### Return Robustness Weights
857//!
858//! Include final robustness weights in the output.
859//!
860//! ```rust
861//! use loess_rs::prelude::*;
862//! # let x = vec![1.0, 2.0, 3.0, 4.0, 5.0];
863//! # let y = vec![2.0, 4.1, 5.9, 8.2, 9.8];
864//!
865//! let model = Loess::new()
866//!     .fraction(0.5)
867//!     .iterations(3)
868//!     .return_robustness_weights()
869//!     .adapter(Batch)
870//!     .build()?;
871//!
872//! let result = model.fit(&x, &y)?;
873//! // Access robustness weights
874//! if let Some(weights) = result.robustness_weights {
875//!     println!("Robustness weights: {:?}", weights);
876//! }
877//! # Result::<(), LoessError>::Ok(())
878//! ```
879//!
880//! ### Polynomial Degree
881//!
882//! Set the degree of the local polynomial fit (default: Linear).
883//!
884//! - **`Constant`** (0): Local weighted mean. Fastest, stable, but high bias.
885//! - **`Linear`** (1): Local linear regression. Standard choice, good bias-variance balance.
886//! - **`Quadratic`** (2): Local quadratic regression. Better for peaks/valleys, but higher variance.
887//! - **`Cubic`** (3): Local cubic regression. Better for peaks/valleys, but higher variance.
888//! - **`Quartic`** (4): Local quartic regression. Better for peaks/valleys, but higher variance.
889//!
890//! ```rust
891//! use loess_rs::prelude::*;
892//! # let x = vec![1.0, 2.0, 3.0, 4.0, 5.0];
893//! # let y = vec![2.0, 4.1, 5.9, 8.2, 9.8];
894//!
895//! let model = Loess::new()
896//!     .degree(Quadratic)  // Fit local parabolas
897//!     .fraction(0.5)
898//!     .adapter(Batch)
899//!     .build()?;
900//! # Result::<(), LoessError>::Ok(())
901//! ```
902//!
903//! ### Dimensions
904//!
905//! Specify the number of predictor dimensions for multivariate smoothing (default: 1).
906//!
907//! ```rust
908//! use loess_rs::prelude::*;
909//!
910//! // 2D input data (flattened: [x1_0, x2_0, x1_1, x2_1, ...])
911//! let x_2d = vec![1.0, 1.0, 2.0, 1.0, 1.0, 2.0, 2.0, 2.0];
912//! let y = vec![2.0, 3.0, 4.0, 5.0];
913//!
914//! let model = Loess::new()
915//!     .dimensions(2)  // 2 predictor variables
916//!     .adapter(Batch)
917//!     .build()?;
918//!
919//! let result = model.fit(&x_2d, &y)?;
920//! # Result::<(), LoessError>::Ok(())
921//! ```
922//!
923//! ### Distance Metric
924//!
925//! Choose the distance metric for nD neighborhood computation.
926//!
927//! - **`Euclidean`**:
928//!    - Standard Euclidean distance.
929//!    - When predictors are on comparable scales.
930//! - **`Normalized`**:
931//!    - Standardizes variables (divides by MAD/range).
932//!    - When predictors have different ranges (recommended default).
933//! - **`Manhattan`**:
934//!    - L1 norm (sum of absolute differences).
935//!    - Robust to outliers.
936//! - **`Chebyshev`**:
937//!    - L∞ norm (max absolute difference).
938//!    - Useful for finding the "farthest" point.
939//! - **`Minkowski(p)`**:
940//!    - Lp norm.
941//!    - Generalized p-norm (p >= 1).
942//! - **`Weighted(w)`**:
943//!    - Weighted Euclidean distance.
944//!    - Useful when features have different importance.
945//!
946//! ```rust
947//! use loess_rs::prelude::*;
948//! # let x_2d = vec![1.0, 1.0, 2.0, 1.0, 1.0, 2.0, 2.0, 2.0];
949//! # let y = vec![2.0, 3.0, 4.0, 5.0];
950//!
951//! let model = Loess::new()
952//!     .dimensions(2)
953//!     .distance_metric(Manhattan)
954//!     .adapter(Batch)
955//!     .build()?;
956//!
957//! let result = model.fit(&x_2d, &y)?;
958//! # Result::<(), LoessError>::Ok(())
959//! ```
960//!
961//! ### Surface Mode
962//!
963//! Choose the surface evaluation mode for streaming data.
964//!
965//! - **`Interpolation`**:
966//!    - Fastest, but may introduce bias.
967//!    - Suitable for most cases.
968//!    - The default mode in R's and Python's loess implementations.
969//! - **`Direct`**:
970//!    - Slower, but more accurate.
971//!    - Recommended for critical applications.
972//!
973//! ```rust
974//! use loess_rs::prelude::*;
975//! # let x = vec![1.0, 2.0, 3.0, 4.0, 5.0];
976//! # let y = vec![2.0, 4.1, 5.9, 8.2, 9.8];
977//!
978//! let model = Loess::new()
979//!     .fraction(0.5)
980//!     .surface_mode(Direct)
981//!     .adapter(Batch)
982//!     .build()?;
983//!
984//! let result = model.fit(&x, &y)?;
985//! # Result::<(), LoessError>::Ok(())
986//! ```
987//!
988//! ### Cell Size
989//!
990//! Set the cell size for interpolation subdivision (default: 0.2, range: (0, 1]).
991//!
992//! This is a "Resolution First" approach: grid resolution is controlled by
993//! `cell`, where `effective_cell = fraction * cell`.
994//!
995//! | Cell Size | Evaluation Speed | Accuracy | Memory |
996//! |-----------|------------------|----------|--------|
997//! | Higher    | Faster           | Lower    | Less   |
998//! | Lower     | Slower           | Higher   | More   |
999//!
1000//! ```rust
1001//! use loess_rs::prelude::*;
1002//! # let x = vec![1.0, 2.0, 3.0, 4.0, 5.0];
1003//! # let y = vec![2.0, 4.1, 5.9, 8.2, 9.8];
1004//!
1005//! let model = Loess::new()
1006//!     .fraction(0.5)
1007//!     .cell(0.1)   // Finer grid, higher accuracy
1008//!     .adapter(Batch)
1009//!     .build()?;
1010//!
1011//! let result = model.fit(&x, &y)?;
1012//! # Result::<(), LoessError>::Ok(())
1013//! ```
1014//!
1015//! ### Interpolation Vertices
1016//!
1017//! Optional limit on the number of vertices for the interpolation surface.
1018//!
1019//! **Resolution First behavior:** By default, no limit is enforced—grid size is
1020//! purely determined by `cell`. A consistency check only occurs when **both**
1021//! `cell` and `interpolation_vertices` are explicitly provided by the user.
1022//!
1023//! ```rust
1024//! use loess_rs::prelude::*;
1025//! # let x = vec![1.0, 2.0, 3.0, 4.0, 5.0];
1026//! # let y = vec![2.0, 4.1, 5.9, 8.2, 9.8];
1027//!
1028//! let model = Loess::new()
1029//!     .fraction(0.5)
1030//!     .cell(0.1)
1031//!     .interpolation_vertices(1000)  // Explicit limit: consistency check applies
1032//!     .adapter(Batch)
1033//!     .build()?;
1034//!
1035//! let result = model.fit(&x, &y)?;
1036//! # Result::<(), LoessError>::Ok(())
1037//! ```
1038//!
1039//! ### Scaling Method
1040//!
1041//! The scaling method controls how the residuals are scaled.
1042//!
1043//! - **`MAR`**:
1044//!   - Median Absolute Residual: `median(|r|)`
1045//!   - Default Cleveland implementation
1046//! - **`MAD`** (default):
1047//!   - Median Absolute Deviation: `median(|r - median(r)|)`
1048//!   - More robust to outliers
1049//!
1050//! ```rust
1051//! use loess_rs::prelude::*;
1052//! # let x = vec![1.0, 2.0, 3.0, 4.0, 5.0];
1053//! # let y = vec![2.0, 4.1, 5.9, 8.2, 9.8];
1054//!
1055//! let model = Loess::new()
1056//!     .fraction(0.5)
1057//!     .scaling_method(MAD)
1058//!     .adapter(Batch)
1059//!     .build()?;
1060//!
1061//! let result = model.fit(&x, &y)?;
1062//! # Result::<(), LoessError>::Ok(())
1063//! ```
1064//!
1065//! ### Diagnostics (Batch and Streaming)
1066//!
1067//! Compute diagnostic statistics to assess fit quality.
1068//!
1069//! ```rust
1070//! use loess_rs::prelude::*;
1071//! # let x = vec![1.0, 2.0, 3.0, 4.0, 5.0];
1072//! # let y = vec![2.0, 4.1, 5.9, 8.2, 9.8];
1073//!
1074//! // Build model with diagnostics
1075//! let model = Loess::new()
1076//!     .fraction(0.5)
1077//!     .return_diagnostics()
1078//!     .return_residuals()
1079//!     .adapter(Batch)
1080//!     .build()?;
1081//!
1082//! // Fit the model to the data
1083//! let result = model.fit(&x, &y)?;
1084//!
1085//! if let Some(diag) = &result.diagnostics {
1086//!     println!("RMSE: {:.4}", diag.rmse);
1087//!     println!("MAE: {:.4}", diag.mae);
1088//!     println!("R²: {:.4}", diag.r_squared);
1089//! }
1090//! # Result::<(), LoessError>::Ok(())
1091//! ```
1092//!
1093//! ```text
1094//! RMSE: 0.1234
1095//! MAE: 0.0987
1096//! R^2: 0.9876
1097//! ```
1098//!
1099//! **Available diagnostics:**
1100//! - **RMSE**: Root mean squared error
1101//! - **MAE**: Mean absolute error
1102//! - **R^2**: Coefficient of determination
1103//! - **Residual SD**: Standard deviation of residuals
1104//! - **AIC/AICc**: Information criteria (when applicable)
1105//!
1106//! ### Confidence Intervals (Batch only)
1107//!
1108//! Confidence intervals quantify uncertainty in the smoothed mean function.
1109//!
1110//! ```rust
1111//! use loess_rs::prelude::*;
1112//! # let x = vec![1.0, 2.0, 3.0, 4.0, 5.0];
1113//! # let y = vec![2.0, 4.1, 5.9, 8.2, 9.8];
1114//!
1115//! // Build model with confidence intervals
1116//! let model = Loess::new()
1117//!     .fraction(0.5)
1118//!     .confidence_intervals(0.95)  // 95% confidence intervals
1119//!     .adapter(Batch)
1120//!     .build()?;
1121//!
1122//! // Fit the model to the data
1123//! let result = model.fit(&x, &y)?;
1124//!
1125//! // Access confidence intervals
1126//! for i in 0..x.len() {
1127//!     println!(
1128//!         "x={:.1}: y={:.2} [{:.2}, {:.2}]",
1129//!         x[i],
1130//!         result.y[i],
1131//!         result.confidence_lower.as_ref().unwrap()[i],
1132//!         result.confidence_upper.as_ref().unwrap()[i]
1133//!     );
1134//! }
1135//! # Result::<(), LoessError>::Ok(())
1136//! ```
1137//!
1138//! ```text
1139//! x=1.0: y=2.00 [1.85, 2.15]
1140//! x=2.0: y=4.10 [3.92, 4.28]
1141//! x=3.0: y=5.90 [5.71, 6.09]
1142//! x=4.0: y=8.20 [8.01, 8.39]
1143//! x=5.0: y=9.80 [9.65, 9.95]
1144//! ```
1145//!
1146//! ### Prediction Intervals (Batch only)
1147//!
1148//! Prediction intervals quantify where new individual observations will likely fall.
1149//!
1150//! ```rust
1151//! use loess_rs::prelude::*;
1152//! # let x = vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0];
1153//! # let y = vec![2.1, 3.8, 6.2, 7.9, 10.3, 11.8, 14.1, 15.7];
1154//!
1155//! // Build model with both interval types
1156//! let model = Loess::new()
1157//!     .fraction(0.5)
1158//!     .confidence_intervals(0.95)
1159//!     .prediction_intervals(0.95)  // Both can be enabled
1160//!     .adapter(Batch)
1161//!     .build()?;
1162//!
1163//! // Fit the model to the data
1164//! let result = model.fit(&x, &y)?;
1165//! println!("{}", result);
1166//! # Result::<(), LoessError>::Ok(())
1167//! ```
1168//!
1169//! ```text
1170//! Summary:
1171//!   Data points: 8
1172//!   Fraction: 0.5
1173//!
1174//! Smoothed Data:
1175//!        X     Y_smooth      Std_Err   Conf_Lower   Conf_Upper   Pred_Lower   Pred_Upper
1176//!   ----------------------------------------------------------------------------------
1177//!     1.00     2.01963     0.389365     1.256476     2.782788     1.058911     2.980353
1178//!     2.00     4.00251     0.345447     3.325438     4.679589     3.108641     4.896386
1179//!     3.00     5.99959     0.423339     5.169846     6.829335     4.985168     7.014013
1180//!     4.00     8.09859     0.489473     7.139224     9.057960     6.975666     9.221518
1181//!     5.00    10.03881     0.551687     8.957506    11.120118     8.810073    11.267551
1182//!     6.00    12.02872     0.539259    10.971775    13.085672    10.821364    13.236083
1183//!     7.00    13.89828     0.371149    13.170829    14.625733    12.965670    14.830892
1184//!     8.00    15.77990     0.408300    14.979631    16.580167    14.789441    16.770356
1185//! ```
1186//!
1187//! **Interval types:**
1188//! - **Confidence intervals**: Uncertainty in the smoothed mean
1189//!   - Narrower intervals
1190//!   - Use for: Understanding precision of the trend estimate
1191//! - **Prediction intervals**: Uncertainty for new observations
1192//!   - Wider intervals (includes data scatter + estimation uncertainty)
1193//!   - Use for: Forecasting where new data points will fall
1194//!
1195//! ### Cross-Validation (Batch only)
1196//!
1197//! Automatically select the optimal smoothing fraction using cross-validation.
1198//!
1199//! ```rust
1200//! use loess_rs::prelude::*;
1201//! # let x: Vec<f64> = (1..=20).map(|i| i as f64).collect();
1202//! # let y: Vec<f64> = x.iter().map(|&xi| 2.0 * xi + 1.0).collect();
1203//!
1204//! // Build model with K-fold cross-validation
1205//! let model = Loess::new()
1206//!     .cross_validate(KFold(5, &[0.2, 0.3, 0.5, 0.7]).seed(42)) // K-fold CV with 5 folds and 4 fraction options
1207//!     .adapter(Batch)
1208//!     .build()?;
1209//!
1210//! // Fit the model to the data
1211//! let result = model.fit(&x, &y)?;
1212//!
1213//! println!("Selected fraction: {}", result.fraction_used);
1214//! println!("CV scores: {:?}", result.cv_scores);
1215//! # Result::<(), LoessError>::Ok(())
1216//! ```
1217//!
1218//! ```text
1219//! Selected fraction: 0.5
1220//! CV scores: Some([0.123, 0.098, 0.145, 0.187])
1221//! ```
1222//!
1223//! ```rust
1224//! use loess_rs::prelude::*;
1225//! # let x: Vec<f64> = (1..=20).map(|i| i as f64).collect();
1226//! # let y: Vec<f64> = x.iter().map(|&xi| 2.0 * xi + 1.0).collect();
1227//!
1228//! // Build model with leave-one-out cross-validation
1229//! let model = Loess::new()
1230//!     .cross_validate(LOOCV(&[0.2, 0.3, 0.5, 0.7])) // Leave-one-out CV with 4 fraction options
1231//!     .adapter(Batch)
1232//!     .build()?;
1233//!
1234//! let result = model.fit(&x, &y)?;
1235//! println!("{}", result);
1236//! # Result::<(), LoessError>::Ok(())
1237//! ```
1238//!
1239//! ```text
1240//! Summary:
1241//!   Data points: 20
1242//!   Fraction: 0.5 (selected via LOOCV)
1243//!
1244//! Smoothed Data:
1245//!        X     Y_smooth
1246//!   --------------------
1247//!     1.00     3.00000
1248//!     2.00     5.00000
1249//!     3.00     7.00000
1250//!   ... (17 more rows)
1251//! ```
1252//!
1253//! **Choosing a Method:**
1254//!
1255//! * **K-Fold**: Good balance between accuracy and speed. Common choices:
1256//!   - k=5: Fast, reasonable accuracy
1257//!   - k=10: Standard choice, good accuracy
1258//!   - k=20: Higher accuracy, slower
1259//!
1260//! * **LOOCV**: Maximum accuracy but computationally expensive (O(n^2) evaluations).
1261//!   Best for small datasets (n < 100) where accuracy is critical.
1262//!
1263//! ### Chunk Size (Streaming Adapter)
1264//!
1265//! Set the number of points to process in each chunk for the Streaming adapter.
1266//!
1267//! ```rust
1268//! use loess_rs::prelude::*;
1269//!
1270//! let mut processor = Loess::new()
1271//!     .fraction(0.3)
1272//!     .adapter(Streaming)
1273//!     .chunk_size(10000)  // Process 10K points at a time
1274//!     .overlap(1000)      // 1K point overlap
1275//!     .build()?;
1276//! # Result::<(), LoessError>::Ok(())
1277//! ```
1278//!
1279//! **Typical values:**
1280//! - Small chunks: 1,000-5,000 (low memory, more overhead)
1281//! - Medium chunks: 5,000-20,000 (balanced, recommended)
1282//! - Large chunks: 20,000-100,000 (high memory, less overhead)
1283//!
1284//! ### Overlap (Streaming Adapter)
1285//!
1286//! Set the number of overlapping points between chunks for the Streaming adapter.
1287//!
1288//! **Rule of thumb:** `overlap = 2 × window_size`, where `window_size = fraction × chunk_size`
1289//!
1290//! Larger overlap provides better boundary handling but increases computation.
1291//! Must be less than `chunk_size`.
1292//!
1293//! ### Merge Strategy (Streaming Adapter)
1294//!
1295//! Control how overlapping values are merged between chunks in the Streaming adapter.
1296//!
1297//! - **`WeightedAverage`** (default): Distance-weighted average
1298//! - **`Average`**: Simple average
1299//! - **`TakeFirst`**: Use value from first chunk
1300//! - **`TakeLast`**: Use value from last chunk
1301//!
1302//! ```rust
1303//! use loess_rs::prelude::*;
1304//!
1305//! let mut processor = Loess::new()
1306//!     .fraction(0.3)
1307//!     .merge_strategy(WeightedAverage)
1308//!     .adapter(Streaming)
1309//!     .build()?;
1310//! # Result::<(), LoessError>::Ok(())
1311//! ```
1312//!
1313//! ### Window Capacity (Online Adapter)
1314//!
1315//! Set the maximum number of points to retain in the sliding window for the Online adapter.
1316//!
1317//! ```rust
1318//! use loess_rs::prelude::*;
1319//!
1320//! let mut processor = Loess::new()
1321//!     .fraction(0.3)
1322//!     .adapter(Online)
1323//!     .window_capacity(500)  // Keep last 500 points
1324//!     .build()?;
1325//! # Result::<(), LoessError>::Ok(())
1326//! ```
1327//!
1328//! **Typical values:**
1329//! - Small windows: 100-500 (fast, less smooth)
1330//! - Medium windows: 500-2000 (balanced)
1331//! - Large windows: 2000-10000 (slow, very smooth)
1332//!
1333//! ### Min Points (Online Adapter)
1334//!
1335//! Set the minimum number of points required before smoothing starts in the Online adapter.
1336//!
1337//! Must be at least 2 (required for linear regression) and at most `window_capacity`.
1338//!
1339//! ```rust
1340//! use loess_rs::prelude::*;
1341//!
1342//! let mut processor = Loess::new()
1343//!     .fraction(0.3)
1344//!     .adapter(Online)
1345//!     .window_capacity(100)
1346//!     .min_points(10)  // Wait for 10 points before smoothing
1347//!     .build()?;
1348//! # Result::<(), LoessError>::Ok(())
1349//! ```
1350//!
1351//! ### Update Mode (Online Adapter)
1352//!
1353//! Choose between incremental and full window updates for the Online adapter.
1354//!
1355//! - **`Incremental`** (default): Fit only the latest point - O(q) per point
1356//! - **`Full`**: Re-smooth entire window - O(q^2) per point
1357//!
1358//! ```rust
1359//! use loess_rs::prelude::*;
1360//!
1361//! // High-performance incremental updates
1362//! let mut processor = Loess::new()
1363//!     .fraction(0.3)
1364//!     .adapter(Online)
1365//!     .window_capacity(100)
1366//!     .update_mode(Incremental)
1367//!     .build()?;
1368//!
1369//! for i in 0..1000 {
1370//!     let x = i as f64;
1371//!     let y = 2.0 * x + 1.0;
1372//!     if let Some(output) = processor.add_point(&[x], y)? {
1373//!         println!("Smoothed: {}", output.smoothed);
1374//!     }
1375//! }
1376//! # Result::<(), LoessError>::Ok(())
1377//! ```
1378//!
1379//! ## A comprehensive example showing multiple features:
1380//!
1381//! ```rust
1382//! use loess_rs::prelude::*;
1383//!
1384//! // Generate sample data with outliers
1385//! let x: Vec<f64> = (1..=50).map(|i| i as f64).collect();
1386//! let mut y: Vec<f64> = x.iter().map(|&xi| 2.0 * xi + 1.0 + (xi * 0.5).sin() * 5.0).collect();
1387//! y[10] = 100.0;  // Add an outlier
1388//! y[25] = -50.0;  // Add another outlier
1389//!
1390//! // Build the model with comprehensive configuration
1391//! let model = Loess::new()
1392//!     .fraction(0.3)                                  // Moderate smoothing
1393//!     .iterations(5)                                  // Strong outlier resistance
1394//!     .weight_function(Tricube)                       // Default kernel
1395//!     .robustness_method(Bisquare)                    // Bisquare robustness
1396//!     .confidence_intervals(0.95)                     // 95% confidence intervals
1397//!     .prediction_intervals(0.95)                     // 95% prediction intervals
1398//!     .return_diagnostics()                           // Include diagnostics
1399//!     .return_residuals()                             // Include residuals
1400//!     .return_robustness_weights()                    // Include robustness weights
1401//!     .zero_weight_fallback(UseLocalMean)             // Fallback policy
1402//!     .adapter(Batch)
1403//!     .build()?;
1404//!
1405//! // Fit the model to the data
1406//! let result = model.fit(&x, &y)?;
1407//!
1408//! // Examine results
1409//! println!("Smoothed {} points", result.y.len());
1410//!
1411//! // Check diagnostics
1412//! if let Some(diag) = &result.diagnostics {
1413//!     println!("Fit quality:");
1414//!     println!("  RMSE: {:.4}", diag.rmse);
1415//!     println!("  R²: {:.4}", diag.r_squared);
1416//! }
1417//!
1418//! // Identify outliers
1419//! if let Some(weights) = &result.robustness_weights {
1420//!     println!("\nOutliers detected:");
1421//!     for (i, &w) in weights.iter().enumerate() {
1422//!         if w < 0.1 {
1423//!             println!("  Point {}: y={:.1}, weight={:.3}", i, y[i], w);
1424//!         }
1425//!     }
1426//! }
1427//!
1428//! // Show confidence intervals for first few points
1429//! println!("\nFirst 5 points with intervals:");
1430//! for i in 0..5 {
1431//!     println!(
1432//!         "  x={:.0}: {:.2} [{:.2}, {:.2}] | [{:.2}, {:.2}]",
1433//!         x[i],
1434//!         result.y[i],
1435//!         result.confidence_lower.as_ref().unwrap()[i],
1436//!         result.confidence_upper.as_ref().unwrap()[i],
1437//!         result.prediction_lower.as_ref().unwrap()[i],
1438//!         result.prediction_upper.as_ref().unwrap()[i]
1439//!     );
1440//! }
1441//! # Result::<(), LoessError>::Ok(())
1442//! ```
1443//!
1444//! ```text
1445//! Smoothed 50 points
1446//! Fit quality:
1447//!   RMSE: 0.5234
1448//!   R^2: 0.9987
1449//!
1450//! Outliers detected:
1451//!   Point 10: y=100.0, weight=0.000
1452//!   Point 25: y=-50.0, weight=0.000
1453//!
1454//! First 5 points with intervals:
1455//!   x=1: 3.12 [2.98, 3.26] | [2.45, 3.79]
1456//!   x=2: 5.24 [5.10, 5.38] | [4.57, 5.91]
1457//!   x=3: 7.36 [7.22, 7.50] | [6.69, 8.03]
1458//!   x=4: 9.48 [9.34, 9.62] | [8.81, 10.15]
1459//!   x=5: 11.60 [11.46, 11.74] | [10.93, 12.27]
1460//! ```
1461//!
1462//! ## References
1463//!
1464//! - Cleveland, W. S. (1979). "Robust Locally Weighted Regression and Smoothing Scatterplots"
1465//! - Cleveland, W. S. & Devlin, S. J. (1988). "Locally Weighted Regression: An Approach to Regression Analysis by Local Fitting"
1466//!
1467//! ## License
1468//!
1469//! See the repository for license information and contribution guidelines.
1470
1471#![cfg_attr(not(feature = "std"), no_std)]
1472#![deny(missing_docs)]
1473
1474#[cfg(not(feature = "std"))]
1475#[macro_use]
1476extern crate alloc;
1477
1478// ============================================================================
1479// Internal Modules
1480// ============================================================================
1481
1482// Layer 1: Primitives - data structures and basic utilities.
1483//
1484// Contains fundamental data structures (`Window`, `errors`), and
1485// low-level utilities for sorting and windowing.
1486mod primitives;
1487
1488// Layer 2: Math - pure mathematical functions.
1489//
1490// Contains kernel functions for distance-based weighting,
1491// distance metrics, boundary handling, and robust statistics (MAD).
1492mod math;
1493
1494// Layer 3: Algorithms - core LOESS algorithms.
1495//
1496// Contains the implementations of local regression (via `RegressionContext`),
1497// robustness weighting (`Bisquare`, `Huber`, `Talwar`), and
1498// interpolation.
1499mod algorithms;
1500
1501// Layer 4: Evaluation - post-processing and diagnostics.
1502//
1503// Contains cross-validation for parameter selection, diagnostic metrics
1504// (RMSE, R^2, AIC), and confidence/prediction interval computation.
1505mod evaluation;
1506
1507// Layer 5: Engine - orchestration and execution control.
1508//
1509// Contains the core smoothing iteration logic, automatic convergence
1510// detection, and result assembly.
1511mod engine;
1512
1513// Layer 6: Adapters - execution mode adapters.
1514//
1515// Contains execution adapters for different use cases:
1516// batch (standard), streaming (large datasets), online (incremental).
1517mod adapters;
1518
1519// High-level fluent API for LOESS smoothing.
1520//
1521// Provides the `Loess` builder for configuring and running LOESS smoothing.
1522mod api;
1523
1524// ============================================================================
1525// Prelude
1526// ============================================================================
1527
1528/// Standard LOESS prelude.
1529pub mod prelude {
1530    pub use crate::api::{
1531        Adapter::{Batch, Online, Streaming},
1532        BoundaryPolicy::{Extend, NoBoundary, Reflect, Zero},
1533        DistanceMetric::{Chebyshev, Euclidean, Manhattan, Minkowski, Normalized, Weighted},
1534        KFold, LOOCV, LoessBuilder as Loess, LoessError, LoessResult,
1535        MergeStrategy::{Average, TakeFirst, WeightedAverage},
1536        PolynomialDegree::{Constant, Cubic, Linear, Quadratic, Quartic},
1537        RobustnessMethod::{Bisquare, Huber, Talwar},
1538        ScalingMethod::{MAD, MAR},
1539        SurfaceMode::{Direct, Interpolation},
1540        UpdateMode::{Full, Incremental},
1541        WeightFunction::{Biweight, Cosine, Epanechnikov, Gaussian, Triangle, Tricube, Uniform},
1542        ZeroWeightFallback::{ReturnNone, ReturnOriginal, UseLocalMean},
1543    };
1544}
1545
1546// ============================================================================
1547// Testing re-exports
1548// ============================================================================
1549
1550/// Internal modules for development and testing.
1551///
1552/// This module re-exports internal modules for development and testing purposes.
1553/// It is only available with the `dev` feature enabled.
1554///
1555/// **Warning**: These are internal implementation details and may change without notice.
1556/// Do not use in production code.
1557#[cfg(feature = "dev")]
1558pub mod internals {
1559    /// Internal primitive types and utilities.
1560    pub mod primitives {
1561        pub use crate::primitives::*;
1562    }
1563    /// Internal math functions.
1564    pub mod math {
1565        pub use crate::math::*;
1566    }
1567    /// Internal core algorithms.
1568    pub mod algorithms {
1569        pub use crate::algorithms::*;
1570    }
1571    /// Internal execution engine.
1572    pub mod engine {
1573        pub use crate::engine::*;
1574    }
1575    /// Internal evaluation and diagnostics.
1576    pub mod evaluation {
1577        pub use crate::evaluation::*;
1578    }
1579    /// Internal adapters.
1580    pub mod adapters {
1581        pub use crate::adapters::*;
1582    }
1583    /// Internal API.
1584    pub mod api {
1585        pub use crate::api::*;
1586    }
1587}