Quick Start¶
Get up and running with LOESS in minutes.
Basic Smoothing¶
Smooth a noisy sine wave — the kind of signal where LOESS shines. Each example recovers the underlying trend from 100 points of Gaussian noise.
import fastloess as fl
import numpy as np
# 100-point noisy sine wave
rng = np.random.default_rng(42)
x = np.linspace(0, 2 * np.pi, 100)
y = np.sin(x) + rng.normal(0, 0.3, 100)
model = fl.Loess(fraction=0.3, iterations=3)
result = model.fit(x, y)
print(f"First smoothed value: {result.y[0]:.4f} (true: {np.sin(x[0]):.4f})")
use fastLoess::prelude::*;
use std::f64::consts::TAU;
fn main() -> Result<(), LoessError> {
// 100-point noisy sine wave (deterministic)
let n = 100usize;
let x: Vec<f64> = (0..n).map(|i| i as f64 * TAU / (n - 1) as f64).collect();
let y: Vec<f64> = x.iter().enumerate()
.map(|(i, &xi)| xi.sin() + ((i * 7 + 3) as f64 % 1.7 - 0.85) * 0.3)
.collect();
let model = Loess::new()
.fraction(0.3)
.iterations(3)
.build()?;
let result = model.fit(&x, &y)?;
println!("First smoothed: {:.4} (true: {:.4})", result.y[0], x[0].sin());
Ok(())
}
const { Loess } = require('fastloess');
// 100-point noisy sine wave
const n = 100;
const x = Float64Array.from({ length: n }, (_, i) => i * 2 * Math.PI / (n - 1));
const y = Float64Array.from(x, (xi, i) => Math.sin(xi) + (((i * 7 + 3) % 17) / 17 - 0.5) * 0.6);
const model = new Loess({ fraction: 0.3, iterations: 3 });
const result = model.fit(x, y);
console.log(`First smoothed: ${result.y[0].toFixed(4)} (true: ${Math.sin(x[0]).toFixed(4)})`);
import { Loess } from 'fastloess-wasm';
const n = 100;
const x = Float64Array.from({ length: n }, (_, i) => i * 2 * Math.PI / (n - 1));
const y = Float64Array.from(x, (xi, i) => Math.sin(xi) + (((i * 7 + 3) % 17) / 17 - 0.5) * 0.6);
const model = new Loess({ fraction: 0.3, iterations: 3 });
const result = model.fit(x, y);
console.log(`First smoothed: ${result.y[0].toFixed(4)}`);
#include <fastloess.hpp>
#include <cmath>
#include <iostream>
#include <vector>
int main() {
// 100-point noisy sine wave (deterministic)
const int n = 100;
std::vector<double> x(n), y(n);
for (int i = 0; i < n; ++i) {
x[i] = i * 2 * M_PI / (n - 1);
y[i] = std::sin(x[i]) + ((i * 7 + 3) % 17 / 17.0 - 0.5) * 0.6;
}
fastloess::Loess model({ .fraction = 0.3, .iterations = 3 });
auto result = model.fit(x, y).value();
std::cout << "First smoothed: " << result.y_vector()[0]
<< " (true: " << std::sin(x[0]) << ")\n";
return 0;
}
With Confidence Intervals¶
model = fl.Loess(
fraction=0.5,
iterations=3,
confidence_intervals=0.95,
prediction_intervals=0.95,
return_diagnostics=True
)
result = model.fit(x, y)
print("Smoothed:", result.y)
print("CI Lower:", result.confidence_lower)
print("CI Upper:", result.confidence_upper)
print("R²:", result.diagnostics.r_squared)
use fastLoess::prelude::*;
let model = Loess::new()
.fraction(0.5)
.iterations(3)
.confidence_intervals(0.95) // 95% CI
.prediction_intervals(0.95) // 95% PI
.return_diagnostics()
.build()?;
let result = model.fit(&x, &y)?;
// Access intervals
if let Some(ci_lower) = &result.confidence_lower {
println!("CI Lower: {:?}", ci_lower);
}
model = Loess(;
fraction=0.5,
iterations=3,
confidence_intervals=0.95,
prediction_intervals=0.95,
return_diagnostics=true
)
result = fit(model, x, y)
println("Smoothed: ", result.y)
println("CI Lower: ", result.confidence_lower)
println("CI Upper: ", result.confidence_upper)
println("R²: ", result.diagnostics.r_squared)
const { Loess } = require('fastloess');
const model = new Loess({
fraction: 0.5,
iterations: 3,
confidence_intervals: 0.95,
prediction_intervals: 0.95,
return_diagnostics: true
});
const result = model.fit(x, y);
console.log("Smoothed:", result.y);
console.log("CI Lower:", result.confidence_lower);
console.log("CI Upper:", result.confidence_upper);
console.log("R²:", result.diagnostics.r_squared);
import { Loess } from 'fastloess-wasm';
// Sample data
const x = new Float64Array([1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0]);
const y = new Float64Array([2.1, 3.8, 6.2, 7.9, 10.3, 11.8, 14.1, 15.7]);
// Smooth the data
const model = new Loess({ fraction: 0.5, iterations: 3 });
const result = model.fit(x, y);
console.log("Smoothed values:", result.y);
fastloess::LoessOptions options;
options.fraction = 0.5;
options.iterations = 3;
options.confidence_intervals = 0.95;
options.prediction_intervals = 0.95;
options.return_diagnostics = true;
fastloess::Loess model(options);
auto result = model.fit(x, y).value();
// Access standard C++ vectors
auto lower = result.confidence_lower();
auto upper = result.confidence_upper();
double r2 = result.diagnostics().r_squared();
Handling Outliers¶
LOESS can robustly handle outliers through iterative reweighting:
x_out <- seq(1, 6)
y_with_outlier <- c(2, 4, 6, 50, 10, 12)
model <- Loess(
fraction = 0.5,
iterations = 5,
robustness_method = "bisquare",
return_robustness_weights = TRUE
)
result <- model$fit(x_out, y_with_outlier)
# Check downweighted points
weights <- result$robustness_weights
for (i in seq_along(weights)) {
if (weights[i] < 0.5) {
cat(sprintf("Point %d is likely an outlier (weight: %.3f)\n", i, weights[i]))
}
}
x_out = np.linspace(1, 6, 6)
y_with_outlier = np.array([2.0, 4.0, 6.0, 50.0, 10.0, 12.0])
model = fl.Loess(
fraction=0.5,
iterations=5,
robustness_method="bisquare",
return_robustness_weights=True
)
result = model.fit(x_out, y_with_outlier)
# Check which points were downweighted
for i, w in enumerate(result.robustness_weights):
if w < 0.5:
print(f"Point {i} is likely an outlier (weight: {w:.3f})")
// Data with an outlier at position 3
let x = vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0];
let y_with_outlier = vec![2.0, 4.0, 6.0, 50.0, 10.0, 12.0]; // 50.0 is outlier
let model = Loess::new()
.fraction(0.5)
.iterations(5) // More iterations for outliers
.robustness_method("bisquare") // Default, smooth downweighting
.return_robustness_weights() // See which points were downweighted
.build()?;
let result = model.fit(&x, &y_with_outlier)?;
// Outliers will have low robustness weights
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);
}
}
}
x = [1.0, 2.0, 3.0, 4.0, 5.0, 6.0]
y_with_outlier = [2.0, 4.0, 6.0, 50.0, 10.0, 12.0]
model = Loess(;
fraction=0.5,
iterations=5,
robustness_method="bisquare",
return_robustness_weights=true
)
result = fit(model, x, y_with_outlier)
# Check which points were downweighted
for (i, w) in enumerate(result.robustness_weights)
if w < 0.5
println("Point $i is likely an outlier (weight: $(round(w, digits=3)))")
end
end
const fl = require('fastloess');
const yWithOutlier = new Float64Array([2.0, 4.0, 6.0, 50.0, 10.0, 12.0]);
const model = new fl.Loess({
fraction: 0.5,
iterations: 5,
robustness_method: "bisquare",
return_robustness_weights: true
});
const result = model.fit(x, yWithOutlier);
// Outliers will have low robustness weights
result.robustness_weights.forEach((w, i) => {
if (w < 0.5) {
console.log(`Point ${i} is likely an outlier (weight: ${w.toFixed(3)})`);
}
});
import { Loess } from 'fastloess-wasm';
// Data with an outlier at position 3
const yWithOutlier = new Float64Array([2.0, 4.0, 6.0, 50.0, 10.0, 12.0]);
const model = new Loess({
fraction: 0.5,
iterations: 5,
robustness_method: "bisquare",
return_robustness_weights: true
});
const result = model.fit(x, yWithOutlier);
// Outliers will have low robustness weights
result.robustness_weights.forEach((w, i) => {
if (w < 0.5) {
console.log(`Point ${i} is likely an outlier (weight: ${w.toFixed(3)})`);
}
});
// Data with an outlier at index 3
std::vector<double> x_out = {1.0, 2.0, 3.0, 4.0, 5.0, 6.0};
std::vector<double> y_outlier = {2.0, 4.0, 6.0, 50.0, 10.0, 12.0};
fastloess::LoessOptions options;
options.fraction = 0.5;
options.iterations = 5;
options.robustness_method = "bisquare";
options.return_robustness_weights = true;
fastloess::Loess model(options);
auto result = model.fit(x_out, y_outlier).value();
// Check weights
auto weights = result.robustness_weights();
for (size_t i = 0; i < weights.size(); ++i) {
if (weights[i] < 0.5) {
std::cout << "Point " << i << " is outlier (weight: " << weights[i] << ")\n";
}
}
Streaming Mode¶
For datasets too large to fit in memory, stream them in fixed-size chunks with overlap.
library(rfastloess)
set.seed(42)
x <- seq(0, 10 * pi, length.out = 5000)
y <- sin(x / pi) * exp(-x / 30) + rnorm(5000, sd = 0.15)
# Process in 1000-point chunks with 100-point overlap
model <- StreamingLoess(
fraction = 0.2,
chunk_size = 1000L,
overlap = 100L,
merge_strategy = "weighted_average"
)
chunk_size <- 1000L
for (start in seq(1, 4001, by = chunk_size)) {
end <- min(start + chunk_size - 1L, length(x))
model$process_chunk(x[start:end], y[start:end])
}
result <- model$finalize()
cat(sprintf("Smoothed %d points across %d chunks\n",
length(result$y), ceiling(5000 / chunk_size)))
import fastloess as fl
import numpy as np
rng = np.random.default_rng(42)
x = np.linspace(0, 10 * np.pi, 5000)
y = np.sin(x / np.pi) * np.exp(-x / 30) + rng.normal(0, 0.15, 5000)
model = fl.StreamingLoess(
fraction=0.2,
chunk_size=1000,
overlap=100,
merge_strategy="weighted_average",
)
chunk_size = 1000
for start in range(0, 4001, chunk_size):
end = min(start + chunk_size, len(x))
model.process_chunk(x[start:end], y[start:end])
result = model.finalize()
print(f"Smoothed {len(result.y)} points in streaming mode")
use fastLoess::prelude::*;
use std::f64::consts::PI;
fn main() -> Result<(), LoessError> {
let n = 5_000usize;
let x: Vec<f64> = (0..n).map(|i| i as f64 * 10.0 * PI / (n - 1) as f64).collect();
let y: Vec<f64> = x.iter().enumerate()
.map(|(i, &xi)| (xi / PI).sin() * (-xi / 30.0).exp()
+ ((i * 7 + 3) as f64 % 1.7 - 0.85) * 0.15)
.collect();
let mut model = StreamingLoess::new()
.fraction(0.2)
.chunk_size(1000)
.overlap(100)
.build()?;
for chunk in x.chunks(1000).zip(y.chunks(1000)) {
model.process_chunk(chunk.0, chunk.1)?;
}
let result = model.finalize()?;
println!("Smoothed {} points", result.y.len());
Ok(())
}
using FastLOESS, Random
x = collect(range(0, 10π, length=5000))
rng = MersenneTwister(42)
y = @. sin(x / π) * exp(-x / 30) + randn(rng) * 0.15
model = StreamingLoess(; fraction=0.2, chunk_size=1000, overlap=100,
merge_strategy="weighted_average")
chunk_size = 1000
for start in 1:chunk_size:4001
stop = min(start + chunk_size - 1, length(x))
process_chunk(model, x[start:stop], y[start:stop])
end
result = finalize(model)
println("Smoothed $(length(result.y)) points in streaming mode")
const { StreamingLoess } = require('fastloess');
const n = 5000;
const x = Float64Array.from({ length: n }, (_, i) => i * 10 * Math.PI / (n - 1));
const y = Float64Array.from(x, (xi, i) =>
Math.sin(xi / Math.PI) * Math.exp(-xi / 30) +
(((i * 7 + 3) % 17) / 17 - 0.5) * 0.3
);
const model = new StreamingLoess(
{ fraction: 0.2 },
{ chunk_size: 1000, overlap: 100, merge_strategy: 'weighted_average' }
);
const chunk_size = 1000;
for (let start = 0; start <= 4000; start += chunk_size) {
const end = Math.min(start + chunk_size, n);
model.processChunk(x.slice(start, end), y.slice(start, end));
}
const result = model.finalize();
console.log(`Smoothed ${result.y.length} points in streaming mode`);
import { StreamingLoess } from 'fastloess-wasm';
const n = 5000;
const x = Float64Array.from({ length: n }, (_, i) => i * 10 * Math.PI / (n - 1));
const y = Float64Array.from(x, (xi, i) =>
Math.sin(xi / Math.PI) * Math.exp(-xi / 30) +
(((i * 7 + 3) % 17) / 17 - 0.5) * 0.3
);
const model = new StreamingLoess(
{ fraction: 0.2 },
{ chunk_size: 1000, overlap: 100, merge_strategy: 'weighted_average' }
);
const chunk_size = 1000;
for (let start = 0; start <= 4000; start += chunk_size) {
const end = Math.min(start + chunk_size, n);
model.processChunk(x.slice(start, end), y.slice(start, end));
}
const result = model.finalize();
console.log(`Smoothed ${result.y.length} points`);
#include <fastloess.hpp>
#include <cmath>
#include <iostream>
#include <vector>
int main() {
const int n = 5000;
std::vector<double> x(n), y(n);
for (int i = 0; i < n; ++i) {
x[i] = i * 10 * M_PI / (n - 1);
y[i] = std::sin(x[i] / M_PI) * std::exp(-x[i] / 30.0)
+ ((i * 7 + 3) % 17 / 17.0 - 0.5) * 0.3;
}
fastloess::StreamingOptions opts;
opts.fraction = 0.2;
opts.chunk_size = 1000;
opts.overlap = 100;
fastloess::StreamingLoess model(opts);
for (int start = 0; start <= 4000; start += 1000) {
int end = std::min(start + 1000, n);
model.process_chunk(
std::vector<double>(x.begin() + start, x.begin() + end),
std::vector<double>(y.begin() + start, y.begin() + end)
);
}
auto result = model.finalize().value();
std::cout << "Smoothed " << result.y_vector().size() << " points\n";
return 0;
}
Next Steps¶
| Topic | Link |
|---|---|
| How LOESS works | Concepts |
| All parameters explained | Parameters |
| Batch vs Streaming vs Online | Execution Modes |
| Polynomial degree choices | Degree |
| Multivariate smoothing | Dimensions |
| Edge handling | Boundary |
| Outlier handling in depth | Robustness |
| Full API per language | API Reference |