Skip to content

Custom Weights

Per-observation weights that encode data quality directly into the LOWESS fit.

How Custom Weights Work

Standard LOWESS assigns equal prior trust to all observations. Custom weights let you override this assumption point by point — before any distance or robustness weighting is applied.

The effective weight of observation \(j\) in a local fit centred at \(x_i\) is:

\[w_{ij} = \text{custom\_weights}[j] \times K\!\left(\frac{d_{ij}}{h_i}\right) \times r_j\]

where \(K\) is the distance kernel, \(h_i\) is the local bandwidth, and \(r_j\) is the robustness weight from the current iteration.

Batch adapter only

custom_weights applies in Batch mode. It is silently ignored in Streaming and Online adapters.


When to Use Custom Weights

Situation Recommended weight
Point known to be erroneous 0.0 — fully excluded
Unreliable sensor / low precision 0.1 – 0.5
Standard observation 1.0 (default)
Carefully calibrated measurement > 1.0
Measurement uncertainty \(\sigma_i\) \(1 / \sigma_i^2\)

Custom Weights vs. Robustness Iterations

Both mechanisms handle unreliable data, but they serve different purposes:

Custom Weights Robustness Iterations
When known Before fitting Computed from residuals
Knowledge required Prior knowledge of quality None — data-driven
Effect Fixed throughout fit Adapts each iteration
Use case Known bad sensors, calibration Unknown outlier contamination

They compose: you can use both simultaneously. Custom weights suppress a priori bad points; robustness iterations then handle any residual outliers that remain.


Basic Usage

Suppress a Known Outlier

Set the weight to 0 at the bad point — it is excluded from every local fit that would otherwise include it.

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

x <- 1:10
y <- x * 2.0
y[6] <- 100.0              # spike at index 6

weights <- rep(1.0, 10)
weights[6] <- 0.0          # exclude the spike

model <- Lowess(fraction = 0.5, iterations = 0L)
result <- model$fit(x, y, custom_weights = weights)
import numpy as np
from fastlowess import Lowess

x = np.arange(10, dtype=float)
y = x * 2.0
y[5] = 100.0               # spike at index 5

weights = np.ones(10)
weights[5] = 0.0           # exclude the spike

model = Lowess(fraction=0.5, iterations=0)
result = model.fit(x, y, custom_weights=weights)
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 x: Vec<f64> = (0..10).map(|i| i as f64).collect();
    let mut y: Vec<f64> = x.iter().map(|v| v * 2.0).collect();
    y[5] = 100.0; // spike

    let mut weights = vec![1.0_f64; 10];
    weights[5] = 0.0; // exclude the spike

    let model = Lowess::new()
        .fraction(0.5)
        .iterations(0)
        .custom_weights(weights)
        .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

x = collect(1.0:10.0)
y = x .* 2.0
y[6] = 100.0               # spike at index 6 (1-indexed)

weights = ones(10)
weights[6] = 0.0           # exclude the spike

model = Lowess(fraction = 0.5, iterations = 0)
result = fit(model, x, y; custom_weights = weights)
const fastlowess = require('fastlowess');

const x = Float64Array.from({length: 10}, (_, i) => i);
const y = Float64Array.from(x, v => v * 2);
y[5] = 100.0; // spike

const weights = new Float64Array(10).fill(1.0);
weights[5] = 0.0; // exclude the spike

const model = new fastlowess.Lowess({fraction: 0.5, iterations: 0});
const result = model.fit(x, y, weights);
import init, { Lowess } from 'fastlowess-wasm';
await init();

const x = Float64Array.from({length: 10}, (_, i) => i);
const y = Float64Array.from(x, v => v * 2);
y[5] = 100.0; // spike

const weights = new Float64Array(10).fill(1.0);
weights[5] = 0.0; // exclude the spike

const model = new Lowess({fraction: 0.5, iterations: 0});
const result = model.fit(x, y, weights);
#include <fastlowess.hpp>
#include <cmath>
#include <iostream>
#include <vector>

int main() {
    std::vector<double> xw(10), yw(10);
    for (std::size_t i = 0; i < 10; ++i) {
        xw[i] = static_cast<double>(i);
        yw[i] = xw[i] * 2.0;
    }
    yw[5] = 100.0; // spike

    fastlowess::LowessOptions opts;
    opts.fraction = 0.5;
    opts.iterations = 0;

    std::vector<double> weights(10, 1.0);
    weights[5] = 0.0; // exclude the spike

    auto result = fastlowess::Lowess(opts).fit(xw, yw, weights).value();

    return 0;
}

Emphasize Important Points

Assign high weights to measurements you trust most — calibration standards, reference instruments, or low-noise observations.

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

weights <- rep(1.0, length(x))
weights[calibration_indices] <- 10.0   # trust calibration 10× more

model <- Lowess(fraction = 0.5)
result <- model$fit(x, y, custom_weights = weights)
import fastlowess as fl
from fastlowess import Lowess
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)
calibration_indices = [5, 20, 40, 60, 80]
sigma = rng.uniform(0.1, 0.5, 100)

weights = np.ones(len(x))
for i in calibration_indices:
    weights[i] = 10.0      # trust calibration 10× more

model = Lowess(fraction=0.5)
result = model.fit(x, y, custom_weights=weights)
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 calibration_indices = vec![5usize, 20, 40, 60, 80];
    let mut weights = vec![1.0_f64; x.len()];
    for &i in &calibration_indices {
        weights[i] = 10.0; // trust calibration 10× more
    }

    let model = Lowess::new()
        .fraction(0.5)
        .custom_weights(weights)
        .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
calibration_indices = [5, 20, 40, 60, 80]

weights = ones(length(x))
weights[calibration_indices] .= 10.0   # trust calibration 10× more

model = Lowess(fraction = 0.5)
result = fit(model, x, y; custom_weights = weights)
const fastlowess = require('fastlowess');

const n = 100;
const x = Float64Array.from({ length: n }, (_, i) => i * 2 * Math.PI / (n - 1));
const y = Float64Array.from(x, xi => Math.sin(xi) + (Math.random() - 0.5) * 0.6);
const calibrationIndices = [5, 20, 40, 60, 80];
const sigma = Float64Array.from({ length: n }, () => 0.1 + Math.random() * 0.4);

const weights = new Float64Array(x.length).fill(1.0);
for (const i of calibrationIndices) weights[i] = 10.0;

const model = new fastlowess.Lowess({fraction: 0.5});
const result = model.fit(x, y, weights);
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 calibrationIndices = [5, 20, 40, 60, 80];
const weights = new Float64Array(x.length).fill(1.0);
for (const i of calibrationIndices) weights[i] = 10.0;

const model = new Lowess({fraction: 0.5});
const result = model.fit(x, y, weights);
#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;
    }

    std::vector<std::size_t> calibration_indices = {5, 20, 40, 60, 80};
    std::vector<double> weights(x.size(), 1.0);
    for (std::size_t idx : calibration_indices) weights[idx] = 10.0;

    fastlowess::LowessOptions opts;
    opts.fraction = 0.5;
    auto result = fastlowess::Lowess(opts).fit(x, y, weights).value();

    return 0;
}

Propagate Measurement Uncertainty

If each observation has a known standard deviation \(\sigma_i\), set \(w_i = 1 / \sigma_i^2\) to give the fit information-theoretically optimal weighting.

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

# sigma is a vector of measurement uncertainties
weights <- 1.0 / sigma^2

model <- Lowess(fraction = 0.5)
result <- model$fit(x, y, custom_weights = weights)
import fastlowess as fl
from fastlowess import Lowess
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)
calibration_indices = [5, 20, 40, 60, 80]
sigma = rng.uniform(0.1, 0.5, 100)

weights = 1.0 / sigma**2
model = Lowess(fraction=0.5)
result = model.fit(x, y, custom_weights=weights)
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 sigma: Vec<f64> = (0..n).map(|i| 0.1 + (i % 4) as f64 * 0.1).collect();
    let weights: Vec<f64> = sigma.iter().map(|s| 1.0 / (s * s)).collect();

    let model = Lowess::new()
        .fraction(0.5)
        .custom_weights(weights)
        .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
sigma = rand(rng, 100) .* 0.4 .+ 0.1

weights = 1.0 ./ sigma .^ 2
model = Lowess(fraction = 0.5)
result = fit(model, x, y; custom_weights = weights)
const fastlowess = require('fastlowess');

const n = 100;
const x = Float64Array.from({ length: n }, (_, i) => i * 2 * Math.PI / (n - 1));
const y = Float64Array.from(x, xi => Math.sin(xi) + (Math.random() - 0.5) * 0.6);
const calibrationIndices = [5, 20, 40, 60, 80];
const sigma = Float64Array.from({ length: n }, () => 0.1 + Math.random() * 0.4);

const weights = Float64Array.from(sigma, s => 1.0 / (s * s));
const model = new fastlowess.Lowess({fraction: 0.5});
const result = model.fit(x, y, weights);
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 sigma = Float64Array.from({ length: n }, (_, i) => 0.1 + (i % 4) * 0.1);
const weights = Float64Array.from(sigma, s => 1.0 / (s * s));
const model = new Lowess({fraction: 0.5});
const result = model.fit(x, y, weights);
#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;
    }

    std::vector<double> sigma(n, 0.1);
    for (int i = 0; i < n; ++i) sigma[i] = 0.1 + (i % 4) * 0.1;
    std::vector<double> weights(sigma.size());
    for (std::size_t i = 0; i < sigma.size(); ++i) weights[i] = 1.0 / (sigma[i] * sigma[i]);

    fastlowess::LowessOptions opts;
    opts.fraction = 0.5;
    auto result = fastlowess::Lowess(opts).fit(x, y, weights).value();

    return 0;
}

Combined with Robustness Iterations

Custom weights and robustness iterations compose naturally: use custom weights for known bad points and robustness for unknown contamination.

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

x <- 0:19
y <- x * 1.5
y[4]  <- -50.0   # known bad — zero out
y[13] <- 80.0    # unknown outlier — let robustness handle it

weights <- rep(1.0, 20)
weights[4] <- 0.0

model <- Lowess(fraction = 0.4, iterations = 3L)
result <- model$fit(
    x, y, custom_weights = weights
)
import fastlowess as fl
from fastlowess import Lowess
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)
calibration_indices = [5, 20, 40, 60, 80]
sigma = rng.uniform(0.1, 0.5, 100)

x = np.arange(20, dtype=float)
y = x * 1.5
y[3]  = -50.0   # known bad
y[12] = 80.0    # unknown outlier

weights = np.ones(20)
weights[3] = 0.0

model = Lowess(fraction=0.4, iterations=3)
result = model.fit(x, y, custom_weights=weights)
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 x: Vec<f64> = (0..20).map(|i| i as f64).collect();
    let mut y: Vec<f64> = x.iter().map(|v| v * 1.5).collect();
    y[3]  = -50.0; // known bad
    y[12] = 80.0;  // unknown outlier

    let mut weights = vec![1.0_f64; 20];
    weights[3] = 0.0;

    let model = Lowess::new()
        .fraction(0.4)
        .iterations(3)
        .custom_weights(weights)
        .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

x = collect(0.0:19.0)
y = x .* 1.5
y[4]  = -50.0   # known bad (1-indexed)
y[13] = 80.0    # unknown outlier (1-indexed)

weights = ones(20)
weights[4] = 0.0

model = Lowess(fraction = 0.4, iterations = 3)
result = fit(model, x, y; custom_weights = weights)
const fastlowess = require('fastlowess');

const x = Float64Array.from({length: 20}, (_, i) => i);
const y = Float64Array.from({length: 20}, (_, i) => i * 1.5);
y[3]  = -50.0;  // known bad
y[12] = 80.0;   // unknown outlier

const weights = new Float64Array(20).fill(1.0);
weights[3] = 0.0;

const model = new fastlowess.Lowess({fraction: 0.4, iterations: 3});
const result = model.fit(x, y, weights);
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 weights = new Float64Array(x.length).fill(1.0);
weights[3] = 0.0;

const model = new Lowess({fraction: 0.4, iterations: 3});
const result = model.fit(x, y, weights);
#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;
    }

    std::vector<double> weights(n, 1.0);
    weights[3] = 0.0;

    fastlowess::LowessOptions opts;
    opts.fraction = 0.4;
    opts.iterations = 3;

    auto result = fastlowess::Lowess(opts).fit(x, y, weights).value();

    return 0;
}

Validation Rules

Rule Effect
Length must equal n Error at fit time if mismatched
All values must be ≥ 0 Negative weights are rejected
All-zero weight vector Error: no points remain for any local fit
Uniform weights (1.0 everywhere) Identical result to omitting weights

Zero-weight windows

If a local neighbourhood contains only zero-weight points, the fit at that centre point falls back to the behaviour specified by zero_weight_fallback (default: "use_local_mean").


See Also