Crate lowess

Crate lowess 

Source
Expand description

§LOWESS — Locally Weighted Scatterplot Smoothing for Rust

A production-ready, high-performance LOWESS implementation with comprehensive features for robust nonparametric regression and trend estimation.

§What is LOWESS?

LOWESS (Locally Weighted Scatterplot Smoothing) is a nonparametric regression method that fits smooth curves through scatter plots. At each point, it fits a weighted polynomial (typically linear) using nearby data points, with weights decreasing smoothly with distance. This creates flexible, data-adaptive curves without assuming a global functional form.

Key advantages:

  • No parametric assumptions about the underlying relationship
  • Automatic adaptation to local data structure
  • Robust to outliers (with robustness iterations enabled)
  • Provides uncertainty estimates via confidence/prediction intervals
  • Handles irregular sampling and missing regions gracefully

Common applications:

  • Exploratory data analysis and visualization
  • Trend estimation in time series
  • Baseline correction in spectroscopy and signal processing
  • Quality control and process monitoring
  • Genomic and epigenomic data smoothing
  • Removing systematic effects in scientific measurements

How LOWESS works:

LOWESS Smoothing Concept

LOWESS creates smooth curves through scattered data using local weighted neighborhoods

  1. For each point, select nearby neighbors (controlled by fraction)
  2. Fit a weighted polynomial (closer points get higher weight)
  3. Use the fitted value as the smoothed estimate
  4. Optionally iterate to downweight outliers (robustness)

§Quick Start

§Typical Use

use lowess::prelude::*;

let x = vec![1.0, 2.0, 3.0, 4.0, 5.0];
let y = vec![2.0, 4.1, 5.9, 8.2, 9.8];

// Build the model
let model = Lowess::new()
    .fraction(0.5)      // Use 50% of data for each local fit
    .iterations(3)      // 3 robustness iterations
    .adapter(Batch)
    .build()?;

// Fit the model to the data
let result = model.fit(&x, &y)?;

println!("{}", result);
Summary:
  Data points: 5
  Fraction: 0.5

Smoothed Data:
       X     Y_smooth
  --------------------
    1.00     2.00000
    2.00     4.10000
    3.00     5.90000
    4.00     8.20000
    5.00     9.80000

§Full Features

use lowess::prelude::*;

let x = vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0];
let y = vec![2.1, 3.8, 6.2, 7.9, 10.3, 11.8, 14.1, 15.7];

// Build model with all features enabled
let model = Lowess::new()
    .fraction(0.5)                                  // Use 50% of data for each local fit
    .iterations(3)                                  // 3 robustness iterations
    .weight_function(WeightFunction::Tricube)       // Kernel function
    .robustness_method(RobustnessMethod::Bisquare)  // Outlier handling
    .delta(0.01)                                    // Interpolation optimization
    .zero_weight_fallback(ZeroWeightFallback::UseLocalMean)  // Fallback policy
    .auto_converge(1e-6)                            // Auto-convergence threshold
    .confidence_intervals(0.95)                     // 95% confidence intervals
    .prediction_intervals(0.95)                     // 95% prediction intervals
    .return_diagnostics()                           // Fit quality metrics
    .return_residuals()                             // Include residuals
    .return_robustness_weights()                    // Include robustness weights
    .adapter(Batch)                                 // Batch adapter
    .build()?;

let result = model.fit(&x, &y)?;
println!("{}", result);
Summary:
  Data points: 8
  Fraction: 0.5
  Robustness: Applied

LOWESS Diagnostics:
  RMSE:         0.191925
  MAE:          0.181676
  R^2:           0.998205
  Residual SD:  0.297750
  Effective DF: 8.00
  AIC:          -10.41
  AICc:         inf

Smoothed Data:
       X     Y_smooth      Std_Err   Conf_Lower   Conf_Upper   Pred_Lower   Pred_Upper     Residual Rob_Weight
  ----------------------------------------------------------------------------------------------------------------
    1.00     2.01963     0.389365     1.256476     2.782788     1.058911     2.980353     0.080368     1.0000
    2.00     4.00251     0.345447     3.325438     4.679589     3.108641     4.896386    -0.202513     1.0000
    3.00     5.99959     0.423339     5.169846     6.829335     4.985168     7.014013     0.200410     1.0000
    4.00     8.09859     0.489473     7.139224     9.057960     6.975666     9.221518    -0.198592     1.0000
    5.00    10.03881     0.551687     8.957506    11.120118     8.810073    11.267551     0.261188     1.0000
    6.00    12.02872     0.539259    10.971775    13.085672    10.821364    13.236083    -0.228723     1.0000
    7.00    13.89828     0.371149    13.170829    14.625733    12.965670    14.830892     0.201719     1.0000
    8.00    15.77990     0.408300    14.979631    16.580167    14.789441    16.770356    -0.079899     1.0000

§Minimal Usage (no_std / Embedded)

The crate supports no_std environments for embedded devices and resource-constrained systems. Disable default features to remove the standard library dependency:

[dependencies]
lowess = { version = "0.5", default-features = false }

Minimal example for embedded systems:

use lowess::prelude::*;

// In an embedded context (e.g., sensor data processing)
fn smooth_sensor_data() -> Result<()> {
    // Small dataset from sensor readings
    let x = vec![1.0_f32, 2.0, 3.0, 4.0, 5.0];
    let y = vec![2.1, 3.9, 6.2, 7.8, 10.1];

    // Build minimal model (no intervals, no diagnostics)
    let model = Lowess::new()
        .fraction(0.5)
        .iterations(2)      // Fewer iterations for speed
        .adapter(Batch)
        .build()?;

    // Fit the model
    let result = model.fit(&x, &y)?;

    // Use smoothed values (result.y)
    // ...

    Ok(())
}

Tips for embedded/no_std usage:

  • Use f32 instead of f64 to reduce memory footprint
  • Keep datasets small (< 1000 points)
  • Disable optional features (intervals, diagnostics) to reduce code size
  • Use fewer iterations (1-2) to reduce computation time
  • Allocate buffers statically when possible to avoid heap fragmentation

§Parameters

All builder parameters have sensible defaults. You only need to specify what you want to change.

ParameterDefaultRange/OptionsDescriptionAdapter
fraction0.67 (or CV-selected)(0, 1]Smoothing span (fraction of data used per fit)All
iterations3[0, 1000]Number of robustness iterationsAll
delta1% of x-range (Batch), 0.0 (Streaming/Online)[0, ∞)Interpolation optimization thresholdAll
weight_functionTricube7 kernel optionsDistance weighting kernelAll
robustness_methodBisquare3 methodsOutlier downweighting methodAll
zero_weight_fallbackUseLocalMean3 fallback optionsBehavior when all weights are zeroAll
return_residualsfalsetrue/falseInclude residuals in outputAll
boundary_policyExtend3 policy optionsEdge handling strategy (reduces boundary bias)All
auto_convergenceNoneTolerance valueEarly stopping for robustnessAll
return_robustness_weightsfalsetrue/falseInclude final weights in outputAll
return_diagnosticsfalsetrue/falseInclude RMSE, MAE, R^2, etc. in outputBatch, Streaming
confidence_intervalsNone0..1 (level)Uncertainty in mean curveBatch
prediction_intervalsNone0..1 (level)Uncertainty for new observationsBatch
cross_validateNoneFractions + methodAutomated bandwidth selectionBatch
chunk_size5000[10, ∞)Points per chunk for streamingStreaming
overlap500[0, chunk_size)Overlapping points between chunksStreaming
merge_strategyAverage4 strategiesHow to merge overlapping regionsStreaming
update_modeIncremental2 modesOnline update strategy (Incremental vs Full)Online
window_capacity1000[3, ∞)Maximum points in sliding windowOnline
min_points3[2, window_capacity]Minimum points before smoothing startsOnline

§Parameter Options Reference

For parameters with multiple options, here are the available choices:

ParameterAvailable Options
weight_functionTricube, Epanechnikov, Gaussian, Biweight, Cosine, Triangle, Uniform
robustness_methodBisquare, Huber, Talwar
zero_weight_fallbackUseLocalMean, ReturnOriginal, ReturnNone
boundary_policyExtend, Reflect, Zero
update_modeIncremental, Full

See the detailed sections below for guidance on choosing between these options.

§Builder

The crate uses a fluent builder pattern for configuration. All parameters have sensible defaults, so you only need to specify what you want to change.

§Basic Workflow

  1. Create builder: Lowess::new()
  2. Configure parameters: Chain method calls (.fraction(), .iterations(), etc.)
  3. Select adapter: Choose execution mode (.adapter(Batch), .adapter(Streaming), etc.)
  4. Build model: Call .build() to create the configured model
  5. Fit data: Call .fit(&x, &y) to perform smoothing
use lowess::prelude::*;

// Build the model with custom configuration
let model = Lowess::new()
    .fraction(0.3)                                  // Smoothing span
    .iterations(5)                                  // Robustness iterations
    .weight_function(WeightFunction::Tricube)       // Kernel function
    .robustness_method(RobustnessMethod::Bisquare)  // Outlier handling
    .adapter(Batch)
    .build()?;

// Fit the model to the data
let result = model.fit(&x, &y)?;
println!("{}", result);
Summary:
  Data points: 5
  Fraction: 0.3

Smoothed Data:
       X     Y_smooth
  --------------------
    1.00     2.00000
    2.00     4.10000
    3.00     5.90000
    4.00     8.20000
    5.00     9.80000

§Execution Mode (Adapter) Comparison

Choose the right execution mode based on your use case:

AdapterUse CaseFeaturesLimitations
BatchComplete datasets in memory
Standard analysis
Full diagnostics needed
All features supportedRequires entire dataset in memory
Not suitable for very large datasets
StreamingLarge datasets (>100K points)
Limited memory
Batch pipelines
Chunked processing
Configurable overlap
Robustness iterations
Residuals
No intervals
No cross-validation
No diagnostics
OnlineReal-time data
Sensor streams
Embedded systems
Incremental updates
Sliding window
Memory-bounded
No intervals
No cross-validation
Limited history

Recommendation:

  • Start with Batch for most use cases - it’s the most feature-complete
  • Use Streaming when dataset size exceeds available memory
  • Use Online for real-time applications or when data arrives incrementally
§Batch Adapter

Standard mode for complete datasets in memory. Supports all features.

use lowess::prelude::*;

// Build model with batch adapter
let model = Lowess::new()
    .fraction(0.5)
    .iterations(3)
    .confidence_intervals(0.95)
    .prediction_intervals(0.95)
    .return_diagnostics()
    .adapter(Batch)  // Full feature support
    .build()?;

let result = model.fit(&x, &y)?;
println!("{}", result);
Summary:
  Data points: 5
  Fraction: 0.5

Smoothed Data:
       X     Y_smooth      Std_Err   Conf_Lower   Conf_Upper   Pred_Lower   Pred_Upper
  ----------------------------------------------------------------------------------
    1.00     2.00000     0.000000     2.000000     2.000000     2.000000     2.000000
    2.00     4.10000     0.000000     4.100000     4.100000     4.100000     4.100000
    3.00     5.90000     0.000000     5.900000     5.900000     5.900000     5.900000
    4.00     8.20000     0.000000     8.200000     8.200000     8.200000     8.200000
    5.00     9.80000     0.000000     9.800000     9.800000     9.800000     9.800000

Diagnostics:
  RMSE: 0.0000
  MAE: 0.0000
  R²: 1.0000

Use batch when:

  • Dataset fits in memory
  • Need all features (intervals, CV, diagnostics)
  • Processing complete datasets
§Streaming Adapter

Process large datasets in chunks with configurable overlap. Use process_chunk() to process each chunk and finalize() to get remaining buffered data.

use lowess::prelude::*;

// Simulate chunks of data (in practice, read from file/stream)
let chunk1_x: Vec<f64> = (0..50).map(|i| i as f64).collect();
let chunk1_y: Vec<f64> = chunk1_x.iter().map(|&xi| 2.0 * xi + 1.0).collect();

let chunk2_x: Vec<f64> = (40..100).map(|i| i as f64).collect();
let chunk2_y: Vec<f64> = chunk2_x.iter().map(|&xi| 2.0 * xi + 1.0).collect();

// Build streaming processor with chunk configuration
let mut processor = Lowess::new()
    .fraction(0.3)
    .iterations(2)
    .adapter(Streaming)
    .chunk_size(50)   // Process 50 points at a time
    .overlap(10)      // 10 points overlap between chunks
    .build()?;

// Process first chunk
let result1 = processor.process_chunk(&chunk1_x, &chunk1_y)?;
// result1.y contains smoothed values for the non-overlapping portion

// Process second chunk (overlaps with end of first chunk)
let result2 = processor.process_chunk(&chunk2_x, &chunk2_y)?;
// result2.y contains smoothed values, with overlap merged from first chunk

// IMPORTANT: Call finalize() to get remaining buffered overlap data
let final_result = processor.finalize()?;
// final_result.y contains the final overlap buffer

// Total processed = all chunks + finalize
let total = result1.y.len() + result2.y.len() + final_result.y.len();
println!("Processed {} points total", total);

Use streaming when:

  • Dataset is very large (>100,000 points)
  • Memory is limited
  • Processing data in chunks
§Online Adapter

Incremental updates with a sliding window for real-time data.

use lowess::prelude::*;

// Build model with online adapter
let model = Lowess::new()
    .fraction(0.2)
    .iterations(1)
    .adapter(Online)
    .build()?;

let mut online_model = model;

// Add points incrementally
for i in 1..=10 {
    let x = i as f64;
    let y = 2.0 * x + 1.0;
    if let Some(result) = online_model.add_point(x, y)? {
        println!("Latest smoothed value: {:.2}", result.smoothed);
    }
}

Use online when:

  • Data arrives incrementally
  • Need real-time updates
  • Maintaining a sliding window

§Fraction (Smoothing Span)

The fraction parameter controls the proportion of data used for each local fit. Larger fractions create smoother curves; smaller fractions preserve more detail.

Fraction Effect

Under-smoothing (fraction too small), optimal smoothing, and over-smoothing (fraction too large)

  • Range: (0, 1]
  • Effect: Larger = smoother, smaller = more detail
use lowess::prelude::*;

// Build model with small fraction (more detail)
let model = Lowess::new()
    .fraction(0.2)  // Use 20% of data for each local fit
    .adapter(Batch)
    .build()?;

let result = model.fit(&x, &y)?;

Choosing fraction:

  • 0.1-0.3: Fine detail, may be noisy
  • 0.3-0.5: Moderate smoothing (good for most cases)
  • 0.5-0.7: Heavy smoothing, emphasizes trends
  • 0.7-1.0: Very smooth, may over-smooth

§Iterations (Robustness)

The iterations parameter controls outlier resistance through iterative reweighting. More iterations provide stronger robustness but increase computation time.

Robustness Effect

Standard LOWESS (left) vs Robust LOWESS (right) - robustness iterations downweight outliers

- **Range**: [0, 1000] - **Effect**: More iterations = stronger outlier downweighting
use lowess::prelude::*;

// Build model with strong outlier resistance
let model = Lowess::new()
    .fraction(0.5)
    .iterations(5)  // More iterations for stronger robustness
    .adapter(Batch)
    .build()?;

let result = model.fit(&x, &y)?;

Choosing iterations:

  • 0: No robustness (fastest, sensitive to outliers)
  • 1-3: Light to moderate robustness (recommended)
  • 4-6: Strong robustness (for contaminated data)
  • 7+: Very strong (may over-smooth)

§Delta (Optimization)

The delta parameter enables interpolation optimization for large datasets. Points within delta distance reuse the previous fit.

  • Range: [0, ∞)
  • Effect: Larger = faster but less accurate
use lowess::prelude::*;

// Build model with custom delta
let model = Lowess::new()
    .fraction(0.5)
    .delta(0.05)  // Custom delta value
    .adapter(Batch)
    .build()?;

let result = model.fit(&x, &y)?;

When to use delta:

  • Large datasets (>10,000 points): Set to ~1% of range
  • Uniformly spaced data: Can use larger values
  • Irregular spacing: Use smaller values or 0

§Weight Functions (Kernels)

Control how neighboring points are weighted by distance.

use lowess::prelude::*;

// Build model with Epanechnikov kernel
let model = Lowess::new()
    .fraction(0.5)
    .weight_function(WeightFunction::Epanechnikov)
    .adapter(Batch)
    .build()?;

let result = model.fit(&x, &y)?;

Kernel selection guide:

KernelEfficiencySmoothness
Tricube0.998Very smooth
Epanechnikov1.000Smooth
Gaussian0.961Infinitely smooth
Biweight0.995Very smooth
Cosine0.999Smooth
Triangle0.989Moderate
Uniform0.943None

Efficiency = AMISE relative to Epanechnikov (1.0 = optimal)

Choosing a Kernel:

  • Tricube (default): Best all-around choice

    • High efficiency (0.9983)
    • Smooth derivatives
    • Compact support (computationally efficient)
    • Cleveland’s original choice
  • Epanechnikov: Theoretically optimal

    • AMISE-optimal for kernel density estimation
    • Less smooth than tricube
    • Efficiency = 1.0 by definition
  • Gaussian: Maximum smoothness

    • Infinitely smooth
    • No boundary effects
    • More expensive to compute
    • Good for very smooth data
  • Biweight: Good balance

    • High efficiency (0.9951)
    • Smoother than Epanechnikov
    • Compact support
  • Cosine: Smooth and compact

    • Good for robust smoothing contexts
    • High efficiency (0.9995)
  • Triangle: Simple and fast

    • Linear taper
    • Less smooth than other kernels
    • Easy to understand
  • Uniform: Simplest

    • Equal weights within window
    • Fastest to compute
    • Least smooth results

§Robustness Methods

Different methods for downweighting outliers during iterative refinement.

use lowess::prelude::*;

// Build model with Talwar robustness (hard threshold)
let model = Lowess::new()
    .fraction(0.5)
    .iterations(3)
    .robustness_method(RobustnessMethod::Talwar)
    .return_robustness_weights()  // Include weights in output
    .adapter(Batch)
    .build()?;

// Fit the model to the data
let result = model.fit(&x, &y)?;

// Check which points were downweighted
if let Some(weights) = &result.robustness_weights {
    for (i, &w) in weights.iter().enumerate() {
        if w < 0.5 {
            println!("Point {} is likely an outlier (weight: {:.3})", i, w);
        }
    }
}
Point 3 is likely an outlier (weight: 0.000)

Available methods:

MethodBehaviorUse Case
BisquareSmooth downweightingGeneral-purpose, balanced
HuberLinear beyond thresholdModerate outliers
TalwarHard threshold (0 or 1)Extreme contamination

§Zero-Weight Fallback

Control behavior when all neighborhood weights are zero.

use lowess::prelude::*;

// Build model with custom zero-weight fallback
let model = Lowess::new()
    .fraction(0.5)
    .zero_weight_fallback(ZeroWeightFallback::UseLocalMean)
    .adapter(Batch)
    .build()?;

let result = model.fit(&x, &y)?;

Fallback options:

  • UseLocalMean: Use mean of neighborhood
  • ReturnOriginal: Return original y value
  • ReturnNone: Return NaN (for explicit handling)

§Return Residuals

Include residuals (y - smoothed) in the output for all adapters.

use lowess::prelude::*;

let model = Lowess::new()
    .fraction(0.5)
    .return_residuals()
    .adapter(Batch)
    .build()?;

let result = model.fit(&x, &y)?;
// Access residuals
if let Some(residuals) = result.residuals {
    println!("Residuals: {:?}", residuals);
}

§Boundary Policy

LOWESS traditionally uses asymmetric windows at boundaries, which can introduce bias. The boundary_policy parameter pads the data before smoothing to enable centered windows:

  • Extend (default): Pad with constant values (first/last y-value)
  • Reflect: Mirror the data at boundaries
  • Zero: Pad with zeros
use lowess::prelude::*;

// Use reflective padding for better edge handling
let model = Lowess::new()
    .fraction(0.5)
    .boundary_policy(BoundaryPolicy::Reflect)
    .adapter(Batch)
    .build()?;

let result = model.fit(&x, &y)?;

Choosing a policy:

  • Use Extend for most cases (default)
  • Use Reflect for periodic or symmetric data
  • Use Zero when data naturally approaches zero at boundaries

§Auto-Convergence

Automatically stop iterations when the smoothed values converge.

use lowess::prelude::*;

// Build model with auto-convergence
let model = Lowess::new()
    .fraction(0.5)
    .auto_converge(1e-6)      // Stop when change < 1e-6
    .iterations(20)           // Maximum iterations
    .adapter(Batch)
    .build()?;

// Fit the model to the data
let result = model.fit(&x, &y)?;

println!("Converged after {} iterations", result.iterations_used.unwrap());
Converged after 1 iterations

§Return Robustness Weights

Include final robustness weights in the output.

use lowess::prelude::*;

let model = Lowess::new()
    .fraction(0.5)
    .iterations(3)
    .return_robustness_weights()
    .adapter(Batch)
    .build()?;

let result = model.fit(&x, &y)?;
// Access robustness weights
if let Some(weights) = result.robustness_weights {
    println!("Robustness weights: {:?}", weights);
}

§Diagnostics (Batch and Streaming)

Compute diagnostic statistics to assess fit quality.

use lowess::prelude::*;

// Build model with diagnostics
let model = Lowess::new()
    .fraction(0.5)
    .return_diagnostics()
    .return_residuals()
    .adapter(Batch)
    .build()?;

// Fit the model to the data
let result = model.fit(&x, &y)?;

if let Some(diag) = &result.diagnostics {
    println!("RMSE: {:.4}", diag.rmse);
    println!("MAE: {:.4}", diag.mae);
    println!("R²: {:.4}", diag.r_squared);
}
RMSE: 0.1234
MAE: 0.0987
R^2: 0.9876

Available diagnostics:

  • RMSE: Root mean squared error
  • MAE: Mean absolute error
  • R^2: Coefficient of determination
  • Residual SD: Standard deviation of residuals
  • AIC/AICc: Information criteria (when applicable)

§Confidence Intervals (Batch only)

Confidence intervals quantify uncertainty in the smoothed mean function.

use lowess::prelude::*;

// Build model with confidence intervals
let model = Lowess::new()
    .fraction(0.5)
    .confidence_intervals(0.95)  // 95% confidence intervals
    .adapter(Batch)
    .build()?;

// Fit the model to the data
let result = model.fit(&x, &y)?;

// Access confidence intervals
for i in 0..x.len() {
    println!(
        "x={:.1}: y={:.2} [{:.2}, {:.2}]",
        x[i],
        result.y[i],
        result.confidence_lower.as_ref().unwrap()[i],
        result.confidence_upper.as_ref().unwrap()[i]
    );
}
x=1.0: y=2.00 [1.85, 2.15]
x=2.0: y=4.10 [3.92, 4.28]
x=3.0: y=5.90 [5.71, 6.09]
x=4.0: y=8.20 [8.01, 8.39]
x=5.0: y=9.80 [9.65, 9.95]

§Prediction Intervals (Batch only)

Prediction intervals quantify where new individual observations will likely fall.

use lowess::prelude::*;

// Build model with both interval types
let model = Lowess::new()
    .fraction(0.5)
    .confidence_intervals(0.95)
    .prediction_intervals(0.95)  // Both can be enabled
    .adapter(Batch)
    .build()?;

// Fit the model to the data
let result = model.fit(&x, &y)?;
println!("{}", result);
Summary:
  Data points: 8
  Fraction: 0.5

Smoothed Data:
       X     Y_smooth      Std_Err   Conf_Lower   Conf_Upper   Pred_Lower   Pred_Upper
  ----------------------------------------------------------------------------------
    1.00     2.01963     0.389365     1.256476     2.782788     1.058911     2.980353
    2.00     4.00251     0.345447     3.325438     4.679589     3.108641     4.896386
    3.00     5.99959     0.423339     5.169846     6.829335     4.985168     7.014013
    4.00     8.09859     0.489473     7.139224     9.057960     6.975666     9.221518
    5.00    10.03881     0.551687     8.957506    11.120118     8.810073    11.267551
    6.00    12.02872     0.539259    10.971775    13.085672    10.821364    13.236083
    7.00    13.89828     0.371149    13.170829    14.625733    12.965670    14.830892
    8.00    15.77990     0.408300    14.979631    16.580167    14.789441    16.770356

Interval types:

  • Confidence intervals: Uncertainty in the smoothed mean
    • Narrower intervals
    • Use for: Understanding precision of the trend estimate
  • Prediction intervals: Uncertainty for new observations
    • Wider intervals (includes data scatter + estimation uncertainty)
    • Use for: Forecasting where new data points will fall

§Cross-Validation (Batch only)

Automatically select the optimal smoothing fraction using cross-validation.

use lowess::prelude::*;

// Build model with K-fold cross-validation
let model = Lowess::new()
    .cross_validate(
        &[0.2, 0.3, 0.5, 0.7],           // Candidate fractions to test
        CrossValidationStrategy::KFold,   // K-fold CV
        Some(5)                           // 5 folds
    )
    .adapter(Batch)
    .build()?;

// Fit the model to the data
let result = model.fit(&x, &y)?;

println!("Selected fraction: {}", result.fraction_used);
println!("CV scores: {:?}", result.cv_scores);
Selected fraction: 0.5
CV scores: Some([0.123, 0.098, 0.145, 0.187])
use lowess::prelude::*;

// Build model with leave-one-out cross-validation
let model = Lowess::new()
    .cross_validate(
        &[0.2, 0.3, 0.5, 0.7],
        CrossValidationStrategy::LOOCV,  // Leave-one-out
        None                             // k parameter not used for LOOCV
    )
    .adapter(Batch)
    .build()?;

let result = model.fit(&x, &y)?;
println!("{}", result);
Summary:
  Data points: 20
  Fraction: 0.5 (selected via LOOCV)

Smoothed Data:
       X     Y_smooth
  --------------------
    1.00     3.00000
    2.00     5.00000
    3.00     7.00000
  ... (17 more rows)

Choosing a Method:

  • K-Fold: Good balance between accuracy and speed. Common choices:

    • k=5: Fast, reasonable accuracy
    • k=10: Standard choice, good accuracy
    • k=20: Higher accuracy, slower
  • LOOCV: Maximum accuracy but computationally expensive (O(n^2) evaluations). Best for small datasets (n < 100) where accuracy is critical.

§Chunk Size (Streaming Adapter)

Set the number of points to process in each chunk for the Streaming adapter.

use lowess::prelude::*;

let mut processor = Lowess::new()
    .fraction(0.3)
    .adapter(Streaming)
    .chunk_size(10000)  // Process 10K points at a time
    .overlap(1000)      // 1K point overlap
    .build()?;

Typical values:

  • Small chunks: 1,000-5,000 (low memory, more overhead)
  • Medium chunks: 5,000-20,000 (balanced, recommended)
  • Large chunks: 20,000-100,000 (high memory, less overhead)

§Overlap (Streaming Adapter)

Set the number of overlapping points between chunks for the Streaming adapter.

Rule of thumb: overlap = 2 × window_size, where window_size = fraction × chunk_size

Larger overlap provides better boundary handling but increases computation. Must be less than chunk_size.

§Merge Strategy (Streaming Adapter)

Control how overlapping values are merged between chunks in the Streaming adapter.

  • WeightedAverage (default): Distance-weighted average
  • Average: Simple average
  • TakeFirst: Use value from first chunk
  • TakeLast: Use value from last chunk
use lowess::prelude::*;

let mut processor = Lowess::new()
    .fraction(0.3)
    .merge_strategy(MergeStrategy::WeightedAverage)
    .adapter(Streaming)
    .build()?;

§Window Capacity (Online Adapter)

Set the maximum number of points to retain in the sliding window for the Online adapter.

use lowess::prelude::*;

let mut processor = Lowess::new()
    .fraction(0.3)
    .adapter(Online)
    .window_capacity(500)  // Keep last 500 points
    .build()?;

Typical values:

  • Small windows: 100-500 (fast, less smooth)
  • Medium windows: 500-2000 (balanced)
  • Large windows: 2000-10000 (slow, very smooth)

§Min Points (Online Adapter)

Set the minimum number of points required before smoothing starts in the Online adapter.

Must be at least 2 (required for linear regression) and at most window_capacity.

use lowess::prelude::*;

let mut processor = Lowess::new()
    .fraction(0.3)
    .adapter(Online)
    .window_capacity(100)
    .min_points(10)  // Wait for 10 points before smoothing
    .build()?;

§Update Mode (Online Adapter)

Choose between incremental and full window updates for the Online adapter.

  • Incremental (default): Fit only the latest point - O(q) per point
  • Full: Re-smooth entire window - O(q^2) per point
use lowess::prelude::*;

// High-performance incremental updates
let mut processor = Lowess::new()
    .fraction(0.3)
    .adapter(Online)
    .window_capacity(100)
    .update_mode(UpdateMode::Incremental)
    .build()?;

for i in 0..1000 {
    let x = i as f64;
    let y = 2.0 * x + 1.0;
    if let Some(output) = processor.add_point(x, y)? {
        println!("Smoothed: {}", output.smoothed);
    }
}

§Custom Smooth Pass Function

Advanced users can provide custom smoothing functions via custom_smooth_pass.

This allows replacing the default smoothing algorithm with custom implementations for:

  • Parallel execution strategies
  • Custom regression models
  • Specialized optimizations

See the SmoothPassFn type documentation for the function signature and requirements. This is an advanced feature mainly for library developers.

§A comprehensive example showing multiple features:

use lowess::prelude::*;

// Generate sample data with outliers
let x: Vec<f64> = (1..=50).map(|i| i as f64).collect();
let mut y: Vec<f64> = x.iter().map(|&xi| 2.0 * xi + 1.0 + (xi * 0.5).sin() * 5.0).collect();
y[10] = 100.0;  // Add an outlier
y[25] = -50.0;  // Add another outlier

// Build the model with comprehensive configuration
let model = Lowess::new()
    .fraction(0.3)                                  // Moderate smoothing
    .iterations(5)                                  // Strong outlier resistance
    .weight_function(WeightFunction::Tricube)       // Default kernel
    .robustness_method(RobustnessMethod::Bisquare)  // Bisquare robustness
    .confidence_intervals(0.95)                     // 95% confidence intervals
    .prediction_intervals(0.95)                     // 95% prediction intervals
    .return_diagnostics()                           // Include diagnostics
    .return_residuals()                             // Include residuals
    .return_robustness_weights()                    // Include robustness weights
    .zero_weight_fallback(ZeroWeightFallback::UseLocalMean)
    .adapter(Batch)
    .build()?;

// Fit the model to the data
let result = model.fit(&x, &y)?;

// Examine results
println!("Smoothed {} points", result.y.len());

// Check diagnostics
if let Some(diag) = &result.diagnostics {
    println!("Fit quality:");
    println!("  RMSE: {:.4}", diag.rmse);
    println!("  R²: {:.4}", diag.r_squared);
}

// Identify outliers
if let Some(weights) = &result.robustness_weights {
    println!("\nOutliers detected:");
    for (i, &w) in weights.iter().enumerate() {
        if w < 0.1 {
            println!("  Point {}: y={:.1}, weight={:.3}", i, y[i], w);
        }
    }
}

// Show confidence intervals for first few points
println!("\nFirst 5 points with intervals:");
for i in 0..5 {
    println!(
        "  x={:.0}: {:.2} [{:.2}, {:.2}] | [{:.2}, {:.2}]",
        x[i],
        result.y[i],
        result.confidence_lower.as_ref().unwrap()[i],
        result.confidence_upper.as_ref().unwrap()[i],
        result.prediction_lower.as_ref().unwrap()[i],
        result.prediction_upper.as_ref().unwrap()[i]
    );
}
Smoothed 50 points
Fit quality:
  RMSE: 0.5234
  R^2: 0.9987

Outliers detected:
  Point 10: y=100.0, weight=0.000
  Point 25: y=-50.0, weight=0.000

First 5 points with intervals:
  x=1: 3.12 [2.98, 3.26] | [2.45, 3.79]
  x=2: 5.24 [5.10, 5.38] | [4.57, 5.91]
  x=3: 7.36 [7.22, 7.50] | [6.69, 8.03]
  x=4: 9.48 [9.34, 9.62] | [8.81, 10.15]
  x=5: 11.60 [11.46, 11.74] | [10.93, 12.27]

§References

  • Cleveland, W. S. (1979). “Robust Locally Weighted Regression and Smoothing Scatterplots”
  • Cleveland, W. S. (1981). “LOWESS: A Program for Smoothing Scatterplots by Robust Locally Weighted Regression”

§License

See the repository for license information and contribution guidelines.

Modules§

Adapter
Marker types for selecting execution adapters.
internals
Internal modules for development and testing.
prelude
Standard LOWESS prelude.

Structs§

Lowess
Fluent builder for configuring LOWESS parameters and execution modes.
LowessResult
Comprehensive LOWESS result containing smoothed values and diagnostics.

Enums§

BoundaryPolicy
Policy for handling boundaries at the start and end of a data stream.
CrossValidationStrategy
Strategy for dataset splitting during cross-validation.
LowessError
Error type for LOWESS operations.
MergeStrategy
Strategy for merging overlapping regions between streaming chunks.
RobustnessMethod
Robustness weighting method for outlier downweighting.
UpdateMode
Update mode for online LOWESS processing.
WeightFunction
Weight function (kernel) for LOWESS smoothing.
ZeroWeightFallback
Policy for handling cases where all weights are zero.

Type Aliases§

Result
Result type alias for LOWESS operations.