Skip to content

Robustness

Outlier handling through iterative reweighting.

How Robustness Works

Standard LOWESS can be biased by outliers. Robustness iterations downweight points with large residuals:

  1. Fit initial LOWESS
  2. Compute residuals
  3. Assign robustness weights (large residuals → low weight)
  4. Refit using combined distance × robustness weights
  5. Repeat steps 2–4

Robust vs Standard


Robustness Methods

Bisquare (Default)

Smooth downweighting. Points transition gradually from full weight to zero.

\[w(u) = \begin{cases} (1 - u^2)^2 & |u| < 1 \\ 0 & |u| \geq 1 \end{cases}\]

Use when: General purpose, balanced approach.

result <- Lowess(iterations = 3, robustness_method = "bisquare")$fit(x, y)
result = fl.smooth(x, y, iterations=3, robustness_method="bisquare")
let model = Lowess::new()
    .iterations(3)
    .robustness_method(Bisquare)
    .adapter(Batch)
    .build()?;
result = smooth(x, y, iterations=3, robustness_method="bisquare")
const result = smooth(x, y, { iterations: 3, robustnessMethod: "bisquare" });
const result = smooth(x, y, { iterations: 3, robustnessMethod: "bisquare" });
auto result = fastlowess::smooth(x, y, {
    .iterations = 3,
    .robustness_method = "bisquare"
});

Huber

Linear penalty beyond threshold. Less aggressive than Bisquare.

\[w(u) = \begin{cases} 1 & |u| \leq k \\ k/|u| & |u| > k \end{cases}\]

Use when: Moderate outliers, want to retain some influence.

result <- Lowess(iterations = 3, robustness_method = "huber")$fit(x, y)
result = fl.smooth(x, y, iterations=3, robustness_method="huber")
let model = Lowess::new()
    .iterations(3)
    .robustness_method(Huber)
    .adapter(Batch)
    .build()?;
result = smooth(x, y, iterations=3, robustness_method="huber")
const result = smooth(x, y, { iterations: 3, robustnessMethod: "huber" });
const result = smooth(x, y, { iterations: 3, robustnessMethod: "huber" });
auto result = fastlowess::smooth(x, y, {
    .iterations = 3,
    .robustness_method = "huber"
});

Talwar

Hard threshold. Points are either fully weighted or completely excluded.

\[w(u) = \begin{cases} 1 & |u| \leq k \\ 0 & |u| > k \end{cases}\]

Use when: Extreme outliers, want binary exclusion.

result <- Lowess(iterations = 3, robustness_method = "talwar")$fit(x, y)
result = fl.smooth(x, y, iterations=3, robustness_method="talwar")
let model = Lowess::new()
    .iterations(3)
    .robustness_method(Talwar)
    .adapter(Batch)
    .build()?;
result = smooth(x, y, iterations=3, robustness_method="talwar")
const result = smooth(x, y, { iterations: 3, robustnessMethod: "talwar" });
const result = smooth(x, y, { iterations: 3, robustnessMethod: "talwar" });
auto result = fastlowess::smooth(x, y, {
    .iterations = 3,
    .robustness_method = "talwar"
});

Comparison

Method Transition Aggressiveness Use Case
Bisquare Smooth Moderate General purpose
Huber Gradual Mild Preserve influence
Talwar Hard Strong Extreme contamination

Detecting Outliers

Use robustness weights to identify potential outliers:

result <- Lowess(iterations = 5, return_robustness_weights = TRUE)$fit(x, y)

weights <- result$robustness_weights
outliers <- which(weights < 0.5)
cat("Potential outliers at indices:", outliers, "\n")
result = fl.smooth(x, y, iterations=5, return_robustness_weights=True)

for i, w in enumerate(result["robustness_weights"]):
    if w < 0.5:
        print(f"Potential outlier at index {i}: weight = {w:.3f}")
let model = Lowess::new()
    .iterations(5)
    .return_robustness_weights()
    .adapter(Batch)
    .build()?;

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

if let Some(weights) = &result.robustness_weights {
    for (i, &w) in weights.iter().enumerate() {
        if w < 0.5 {
            println!("Potential outlier at index {}: weight = {:.3}", i, w);
        }
    }
}
result = smooth(x, y, iterations=5, return_robustness_weights=true)

for (i, w) in enumerate(result.robustness_weights)
    if w < 0.5
        println("Potential outlier at index $i: weight = $w")
    end
end
const result = smooth(x, y, { iterations: 5, returnRobustnessWeights: true });

result.robustnessWeights.forEach((w, i) => {
    if (w < 0.5) {
        console.log(`Potential outlier at index ${i}: weight = ${w.toFixed(3)}`);
    }
});
const result = smooth(x, y, { iterations: 5, returnRobustnessWeights: true });

result.robustnessWeights.forEach((w, i) => {
    if (w < 0.5) {
        console.log(`Potential outlier at index ${i}: weight = ${w.toFixed(3)}`);
    }
});
auto result = fastlowess::smooth(x, y, {
    .iterations = 5,
    .return_robustness_weights = true
});

auto weights = result.robustnessWeights();
for (size_t i = 0; i < weights.size(); ++i) {
    if (weights[i] < 0.5) {
        std::cout << "Potential outlier at " << i << std::endl;
    }
}

Scale Estimation

Residuals are scaled before computing robustness weights. Two methods:

Method Description Robustness
MAD Median Absolute Deviation Very robust
MAR Mean Absolute Residual Less robust, faster
result <- Lowess(iterations = 3, scaling_method = "mad")$fit(x, y)
result = fl.smooth(x, y, iterations=3, scaling_method="mad")
let model = Lowess::new()
    .iterations(3)
    .scaling_method(MAD)  // Default
    .adapter(Batch)
    .build()?;
result = smooth(x, y, iterations=3, scaling_method="mad")
const result = smooth(x, y, { iterations: 3, scalingMethod: "mad" });
const result = smooth(x, y, { iterations: 3, scalingMethod: "mad" });
auto result = fastlowess::smooth(x, y, {
    .iterations = 3,
    .scaling_method = "mad"
});

Auto-Convergence

Auto-Convergence

Stop iterations early when weights stabilize:

Performance

Auto-convergence can significantly reduce computation when weights stabilize before reaching max iterations.

result <- Lowess(iterations = 10, auto_converge = 1e-6)$fit(x, y)
result = fl.smooth(x, y, iterations=10, auto_converge=1e-6)
let model = Lowess::new()
    .iterations(10)           // Maximum iterations
    .auto_converge(1e-6)      // Stop when change < 1e-6
    .adapter(Batch)
    .build()?;
result = smooth(x, y, iterations=10, auto_converge=1e-6)
const result = smooth(x, y, { iterations: 10, autoConverge: 1e-6 });
const result = smooth(x, y, { iterations: 10, autoConverge: 1e-6 });
auto result = fastlowess::smooth(x, y, {
    .iterations = 10,
    .auto_converge = 1e-6
});