Smoothers
The greybox.smoothers module provides two non-parametric smoothers
that reproduce R’s stats::lowess() and stats::supsmu() to
machine precision. Each wraps a native pybind11 extension and returns
a dictionary with sorted abscissa and smoothed ordinate.
LOWESS
- greybox.smoothers.lowess(x, y=None, f=0.6666666666666666, iter=3, delta=None)[source]
LOWESS smoother (Locally Weighted Scatterplot Smoothing).
Performs locally weighted polynomial regression using Cleveland’s LOWESS algorithm with a tricube weight function and iterative reweighting for robustness to outliers. The implementation matches R’s
stats::lowess()to machine precision.- Parameters:
x (array_like) – The abscissa values. May also be a 2-D array with two columns, in which case the first column is used as
xand the second asy.y (array_like, optional) – The ordinate values. Required unless
xalready contains both columns.f (float, default=2/3) – Smoother span – the fraction of points in the local neighbourhood used for each fit. Larger values produce smoother curves.
iter (int, default=3) – Number of robustifying iterations. Each iteration downweights observations with large residuals from the previous fit.
delta (float, optional) – Distance threshold for interpolation. Within
deltaof an evaluated point, the fit is linearly interpolated rather than recomputed. IfNone, defaults to0.01 * (max(x) - min(x)).
- Returns:
Dictionary with two keys:
"x":numpy.ndarrayof sorted abscissa values."y":numpy.ndarrayof smoothed ordinate values, aligned with"x".
- Return type:
dict
Notes
The smoother fits a weighted linear regression at each point using a tricube weight function
\[w(u) = (1 - |u|^3)^3\]applied to neighbours within the local span. Robustness iterations downweight outliers using a bisquare weight on the residuals.
References
Cleveland, W. S. (1979). “Robust Locally Weighted Regression and Smoothing Scatterplots”. Journal of the American Statistical Association, 74(368), 829-836. DOI: https://doi.org/10.1080/01621459.1979.10481038
Examples
Smooth a noisy sine wave:
>>> import numpy as np >>> from greybox import lowess >>> rng = np.random.default_rng(0) >>> x = np.linspace(0, 6, 60) >>> y = np.sin(x) + rng.normal(0, 0.2, 60) >>> out = lowess(x, y, f=0.4) >>> out["y"].shape (60,)
Pass both columns as a 2-D array:
>>> xy = np.column_stack([x, y]) >>> out = lowess(xy)
Example:
import numpy as np
from greybox import lowess
rng = np.random.default_rng(0)
x = np.linspace(0, 6, 80)
y = np.sin(x) + rng.normal(0, 0.2, 80)
smoothed = lowess(x, y, f=0.4)
# smoothed["x"] are the sorted x values; smoothed["y"] is the smoothed curve.
Reference
Cleveland, W. S. (1979). “Robust Locally Weighted Regression and Smoothing Scatterplots”. Journal of the American Statistical Association, 74(368), 829-836. DOI: 10.1080/01621459.1979.10481038.
SuperSmoother (supsmu)
- greybox.smoothers.supsmu(x, y, wt=None, span='cv', periodic=False, bass=0.0)[source]
Friedman’s variable-span super-smoother (SuperSmoother).
Smooths a scatter-plot using Friedman’s super-smoother algorithm. The smoother evaluates three running linear smoothers (“tweeters”) with spans
0.05,0.2, and0.5of the sample size, then chooses the best span at each abscissa value via cross-validated residuals. The implementation is a direct port of R’sstats::supsmu()(FORTRANsupsmufromppr.f) and matches R to machine precision.- Parameters:
x (array_like) – The abscissa values. Will be sorted internally if not already ascending.
y (array_like) – The ordinate values, same length as
x.wt (array_like, optional) – Per-observation weights. Default is uniform.
span (float or {"cv"}, default="cv") – Smoother span. Pass a float in
(0, 1]for a fixed span;"cv"(or0) selects the span automatically via leave-one-out cross-validation at each point.periodic (bool, default=False) – If
True, treatxas a periodic variable in[0, 1].bass (float, default=0.0) – Bass-tone control in
[0, 10]. Larger values shift the cross-validated span towards the smoother end of the range (more smoothing in noisy regions). Values outside the range disable the adjustment.
- Returns:
Dictionary with two keys:
"x":numpy.ndarrayof sorted abscissa values."y":numpy.ndarrayof smoothed ordinate values, aligned with"x".
- Return type:
dict
Notes
For small samples (
n < 40) or data with substantial serial correlation between observations close inx, a prespecified fixed span (span=0.2tospan=0.4) is recommended over cross-validated selection.The cross-validation step uses leave-one-out residuals from each of the three tweeters, smooths them with the medium-span smoother, then picks the smallest residual at each abscissa. A final smooth with the smallest span produces the output.
References
Friedman, J. H. (1984). “A Variable Span Smoother”. Technical Report 5 (SLAC-PUB-3477; STAN-LCS-005), Laboratory for Computational Statistics, Department of Statistics, Stanford University. https://www.osti.gov/biblio/1447470
Examples
Smooth count-data noise with the default cross-validated span:
>>> import numpy as np >>> from greybox import supsmu >>> rng = np.random.default_rng(1) >>> x = np.arange(100, dtype=float) >>> y = 0.05 * x + rng.normal(0, 1, 100) >>> out = supsmu(x, y) >>> out["y"].shape (100,)
Force a fixed span of 0.3:
>>> out_fixed = supsmu(x, y, span=0.3)
Example:
import numpy as np
from greybox import supsmu
rng = np.random.default_rng(1)
x = np.arange(100, dtype=float)
y = 0.05 * x + rng.normal(0, 1.0, 100)
# Default: cross-validated span selection
cv = supsmu(x, y)
# Fixed span (0 < span <= 1)
fixed = supsmu(x, y, span=0.3)
# Bass-tone control increases smoothness in noisy regions
smoother = supsmu(x, y, bass=5.0)
Reference
Friedman, J. H. (1984). “A Variable Span Smoother”. Technical Report 5 (SLAC-PUB-3477; STAN-LCS-005), Laboratory for Computational Statistics, Department of Statistics, Stanford University. OSTI 1447470.
Plotting smoother output
Both lowess() and supsmu() return plain dictionaries, mirroring
R’s list(x, y). They can be overlaid on a scatter plot with
matplotlib using the standard pattern:
import matplotlib.pyplot as plt
import numpy as np
from greybox import lowess, supsmu
rng = np.random.default_rng(0)
x = np.linspace(0, 6, 80)
y = np.sin(x) + rng.normal(0, 0.2, 80)
plt.scatter(x, y, s=10, alpha=0.5, label="data")
lo = lowess(x, y, f=0.4)
sm = supsmu(x, y)
plt.plot(lo["x"], lo["y"], color="red", label="LOWESS")
plt.plot(sm["x"], sm["y"], color="blue", label="SuperSmoother")
plt.legend()
plt.show()
The same shape works as an overlay on an existing Axes:
ax.plot(out["x"], out["y"]).