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}