Skip to content

Scaling Methods

Residual scale estimation during robustness iterations.

Overview

When iterations > 0, LOWESS computes robustness weights by comparing each residual to the current residual scale estimate. The scaling_method parameter controls how that scale is measured.

The robustness weight for point \(i\) is:

\[w_i = B\!\left(\frac{|r_i|}{6 \cdot \hat{\sigma}}\right)\]

where \(B\) is the bisquare function and \(\hat{\sigma}\) is the scale estimate. A larger \(\hat{\sigma}\) makes the algorithm more tolerant of large residuals; a smaller one makes it more aggressive.

Method Formula Robustness Speed
"mad" Median of |residuals − median(residuals)| Very robust Moderate
"mar" Median of |residuals| Robust Fast
"mean" Mean of |residuals| Less robust Fastest

Scaling Methods Comparison


MAD — Median Absolute Deviation (Default)

\[\hat{\sigma} = \text{median}(|r_i - \text{median}(r_i)|)\]

First centers residuals at their median, then takes the median of the absolute deviations. Double use of the median makes it highly resistant to extreme outliers. This is the standard choice for robust regression.

Use when: Data may contain outliers (default for most applications).

library(rfastlowess)
set.seed(42)
x <- seq(0, 2 * pi, length.out = 100)
y <- sin(x) + rnorm(100, sd = 0.3)

model <- Lowess(iterations = 3, scaling_method = "mad")
result <- model$fit(x, y)
import fastlowess as fl
import numpy as np

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.Lowess(iterations=3, scaling_method="mad")
result = model.fit(x, y)
use fastLowess::prelude::*;
use std::f64::consts::TAU;

fn main() -> Result<(), LowessError> {
    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().map(|&xi| xi.sin() + 0.1).collect();

    let model = Lowess::new()
        .iterations(3)
        .scaling_method("mad")
        .build()?;
    let result = model.fit(&x, &y)?;

    Ok(())
}
using FastLOWESS
using Random, Statistics

rng = MersenneTwister(42)
x = collect(range(0, 2π, length=100))
y = sin.(x) .+ randn(rng, 100) .* 0.3

model = Lowess(; iterations=3, scaling_method="mad")
result = fit(model, x, y)
const { Lowess } = require('fastlowess');

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 Lowess({ iterations: 3, scaling_method: "mad" });
const result = model.fit(x, y);
import init, { Lowess } from 'fastlowess-wasm';
await init();

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 Lowess({ iterations: 3, scaling_method: "mad" });
const result = model.fit(x, y);
#include <fastlowess.hpp>
#include <cmath>
#include <iostream>
#include <vector>

int main() {
    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]) + 0.1;
    }

    fastlowess::Lowess model({ .iterations = 3, .scaling_method = "mad" });
    auto result = model.fit(x, y).value();

    return 0;
}

MAR — Median Absolute Residual

\[\hat{\sigma} = \text{median}(|r_i|)\]

Uses the uncentered median — unlike MAD it does not subtract the residual median first. Still robust (median-based) but slightly less resistant than MAD when residuals are systematically shifted. Faster than MAD in practice because it requires only one partial sort.

Use when: Speed matters and data have minimal systematic bias in residuals.

library(rfastlowess)
set.seed(42)
x <- seq(0, 2 * pi, length.out = 100)
y <- sin(x) + rnorm(100, sd = 0.3)

model <- Lowess(iterations = 3, scaling_method = "mar")
result <- model$fit(x, y)
import fastlowess as fl
import numpy as np

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.Lowess(iterations=3, scaling_method="mar")
result = model.fit(x, y)
use fastLowess::prelude::*;
use std::f64::consts::TAU;

fn main() -> Result<(), LowessError> {
    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().map(|&xi| xi.sin() + 0.1).collect();

    let model = Lowess::new()
        .iterations(3)
        .scaling_method("mar")
        .build()?;
    let result = model.fit(&x, &y)?;

    Ok(())
}
using FastLOWESS
using Random, Statistics

rng = MersenneTwister(42)
x = collect(range(0, 2π, length=100))
y = sin.(x) .+ randn(rng, 100) .* 0.3

model = Lowess(; iterations=3, scaling_method="mar")
result = fit(model, x, y)
const { Lowess } = require('fastlowess');

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 Lowess({ iterations: 3, scaling_method: "mar" });
const result = model.fit(x, y);
import init, { Lowess } from 'fastlowess-wasm';
await init();

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 Lowess({ iterations: 3, scaling_method: "mar" });
const result = model.fit(x, y);
#include <fastlowess.hpp>
#include <cmath>
#include <iostream>
#include <vector>

int main() {
    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]) + 0.1;
    }

    fastlowess::Lowess model({ .iterations = 3, .scaling_method = "mar" });
    auto result = model.fit(x, y).value();

    return 0;
}

Mean — Mean Absolute Residual

\[\hat{\sigma} = \frac{1}{n}\sum_i |r_i|\]

Arithmetic mean of absolute residuals. Non-robust: a single extreme outlier inflates \(\hat{\sigma}\), causing the algorithm to under-downweight it. Fastest to compute (no sort required). Useful when data are believed to be clean and speed is a priority.

Use when: Clean data with no outliers; maximum computation speed required.

library(rfastlowess)
set.seed(42)
x <- seq(0, 2 * pi, length.out = 100)
y <- sin(x) + rnorm(100, sd = 0.3)

model <- Lowess(iterations = 3, scaling_method = "mean")
result <- model$fit(x, y)
import fastlowess as fl
import numpy as np

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.Lowess(iterations=3, scaling_method="mean")
result = model.fit(x, y)
use fastLowess::prelude::*;
use std::f64::consts::TAU;

fn main() -> Result<(), LowessError> {
    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().map(|&xi| xi.sin() + 0.1).collect();

    let model = Lowess::new()
        .iterations(3)
        .scaling_method("mean")
        .build()?;
    let result = model.fit(&x, &y)?;

    Ok(())
}
using FastLOWESS
using Random, Statistics

rng = MersenneTwister(42)
x = collect(range(0, 2π, length=100))
y = sin.(x) .+ randn(rng, 100) .* 0.3

model = Lowess(; iterations=3, scaling_method="mean")
result = fit(model, x, y)
const { Lowess } = require('fastlowess');

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 Lowess({ iterations: 3, scaling_method: "mean" });
const result = model.fit(x, y);
import init, { Lowess } from 'fastlowess-wasm';
await init();

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 Lowess({ iterations: 3, scaling_method: "mean" });
const result = model.fit(x, y);
#include <fastlowess.hpp>
#include <cmath>
#include <iostream>
#include <vector>

int main() {
    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]) + 0.1;
    }

    fastlowess::Lowess model({ .iterations = 3, .scaling_method = "mean" });
    auto result = model.fit(x, y).value();

    return 0;
}

Choosing a Scaling Method

Situation Recommended Method
General purpose, possible outliers "mad" (default)
Speed matters; residuals have minimal systematic bias "mar"
Clean data, no outliers "mean"

See Robustness for a broader discussion of outlier handling.