355 lines
10 KiB
Python
355 lines
10 KiB
Python
|
"""
|
||
|
Univariate lowess function, like in R.
|
||
|
|
||
|
References
|
||
|
----------
|
||
|
Hastie, Tibshirani, Friedman. (2009) The Elements of Statistical Learning: Data Mining, Inference, and Prediction, Second Edition: Chapter 6.
|
||
|
|
||
|
Cleveland, W.S. (1979) "Robust Locally Weighted Regression and Smoothing Scatterplots". Journal of the American Statistical Association 74 (368): 829-836.
|
||
|
"""
|
||
|
import numpy as np
|
||
|
from numpy.linalg import lstsq
|
||
|
|
||
|
|
||
|
def lowess(endog, exog, frac=2./3, it=3):
|
||
|
"""
|
||
|
LOWESS (Locally Weighted Scatterplot Smoothing)
|
||
|
|
||
|
A lowess function that outs smoothed estimates of endog
|
||
|
at the given exog values from points (exog, endog)
|
||
|
|
||
|
Parameters
|
||
|
----------
|
||
|
endog : 1-D numpy array
|
||
|
The y-values of the observed points
|
||
|
exog : 1-D numpy array
|
||
|
The x-values of the observed points
|
||
|
frac : float
|
||
|
Between 0 and 1. The fraction of the data used
|
||
|
when estimating each y-value.
|
||
|
it : int
|
||
|
The number of residual-based reweightings
|
||
|
to perform.
|
||
|
|
||
|
Returns
|
||
|
-------
|
||
|
out: numpy array
|
||
|
A numpy array with two columns. The first column
|
||
|
is the sorted x values and the second column the
|
||
|
associated estimated y-values.
|
||
|
|
||
|
Notes
|
||
|
-----
|
||
|
This lowess function implements the algorithm given in the
|
||
|
reference below using local linear estimates.
|
||
|
|
||
|
Suppose the input data has N points. The algorithm works by
|
||
|
estimating the true ``y_i`` by taking the frac*N closest points
|
||
|
to ``(x_i,y_i)`` based on their x values and estimating ``y_i``
|
||
|
using a weighted linear regression. The weight for ``(x_j,y_j)``
|
||
|
is `_lowess_tricube` function applied to ``|x_i-x_j|``.
|
||
|
|
||
|
If ``iter > 0``, then further weighted local linear regressions
|
||
|
are performed, where the weights are the same as above
|
||
|
times the `_lowess_bisquare` function of the residuals. Each iteration
|
||
|
takes approximately the same amount of time as the original fit,
|
||
|
so these iterations are expensive. They are most useful when
|
||
|
the noise has extremely heavy tails, such as Cauchy noise.
|
||
|
Noise with less heavy-tails, such as t-distributions with ``df > 2``,
|
||
|
are less problematic. The weights downgrade the influence of
|
||
|
points with large residuals. In the extreme case, points whose
|
||
|
residuals are larger than 6 times the median absolute residual
|
||
|
are given weight 0.
|
||
|
|
||
|
Some experimentation is likely required to find a good
|
||
|
choice of frac and iter for a particular dataset.
|
||
|
|
||
|
References
|
||
|
----------
|
||
|
Cleveland, W.S. (1979) "Robust Locally Weighted Regression
|
||
|
and Smoothing Scatterplots". Journal of the American Statistical
|
||
|
Association 74 (368): 829-836.
|
||
|
|
||
|
Examples
|
||
|
--------
|
||
|
The below allows a comparison between how different the fits from
|
||
|
`lowess` for different values of frac can be.
|
||
|
|
||
|
>>> import numpy as np
|
||
|
>>> import statsmodels.api as sm
|
||
|
>>> lowess = sm.nonparametric.lowess
|
||
|
>>> x = np.random.uniform(low=-2*np.pi, high=2*np.pi, size=500)
|
||
|
>>> y = np.sin(x) + np.random.normal(size=len(x))
|
||
|
>>> z = lowess(y, x)
|
||
|
>>> w = lowess(y, x, frac=1./3)
|
||
|
|
||
|
This gives a similar comparison for when it is 0 vs not.
|
||
|
|
||
|
>>> import scipy.stats as stats
|
||
|
>>> x = np.random.uniform(low=-2*np.pi, high=2*np.pi, size=500)
|
||
|
>>> y = np.sin(x) + stats.cauchy.rvs(size=len(x))
|
||
|
>>> z = lowess(y, x, frac= 1./3, it=0)
|
||
|
>>> w = lowess(y, x, frac=1./3)
|
||
|
"""
|
||
|
x = exog
|
||
|
|
||
|
if exog.ndim != 1:
|
||
|
raise ValueError('exog must be a vector')
|
||
|
if endog.ndim != 1:
|
||
|
raise ValueError('endog must be a vector')
|
||
|
if endog.shape[0] != x.shape[0] :
|
||
|
raise ValueError('exog and endog must have same length')
|
||
|
|
||
|
n = exog.shape[0]
|
||
|
fitted = np.zeros(n)
|
||
|
|
||
|
k = int(frac * n)
|
||
|
|
||
|
index_array = np.argsort(exog)
|
||
|
x_copy = np.array(exog[index_array]) #, dtype ='float32')
|
||
|
y_copy = endog[index_array]
|
||
|
|
||
|
fitted, weights = _lowess_initial_fit(x_copy, y_copy, k, n)
|
||
|
|
||
|
for i in range(it):
|
||
|
_lowess_robustify_fit(x_copy, y_copy, fitted,
|
||
|
weights, k, n)
|
||
|
|
||
|
out = np.array([x_copy, fitted]).T
|
||
|
out.shape = (n,2)
|
||
|
|
||
|
return out
|
||
|
|
||
|
|
||
|
def _lowess_initial_fit(x_copy, y_copy, k, n):
|
||
|
"""
|
||
|
The initial weighted local linear regression for lowess.
|
||
|
|
||
|
Parameters
|
||
|
----------
|
||
|
x_copy : 1-d ndarray
|
||
|
The x-values/exogenous part of the data being smoothed
|
||
|
y_copy : 1-d ndarray
|
||
|
The y-values/ endogenous part of the data being smoothed
|
||
|
k : int
|
||
|
The number of data points which affect the linear fit for
|
||
|
each estimated point
|
||
|
n : int
|
||
|
The total number of points
|
||
|
|
||
|
Returns
|
||
|
-------
|
||
|
fitted : 1-d ndarray
|
||
|
The fitted y-values
|
||
|
weights : 2-d ndarray
|
||
|
An n by k array. The contribution to the weights in the
|
||
|
local linear fit coming from the distances between the
|
||
|
x-values
|
||
|
|
||
|
"""
|
||
|
weights = np.zeros((n,k), dtype = x_copy.dtype)
|
||
|
nn_indices = [0,k]
|
||
|
|
||
|
X = np.ones((k,2))
|
||
|
fitted = np.zeros(n)
|
||
|
|
||
|
for i in range(n):
|
||
|
#note: all _lowess functions are inplace, no return
|
||
|
left_width = x_copy[i] - x_copy[nn_indices[0]]
|
||
|
right_width = x_copy[nn_indices[1]-1] - x_copy[i]
|
||
|
width = max(left_width, right_width)
|
||
|
_lowess_wt_standardize(weights[i,:],
|
||
|
x_copy[nn_indices[0]:nn_indices[1]],
|
||
|
x_copy[i], width)
|
||
|
_lowess_tricube(weights[i,:])
|
||
|
weights[i,:] = np.sqrt(weights[i,:])
|
||
|
|
||
|
X[:,1] = x_copy[nn_indices[0]:nn_indices[1]]
|
||
|
y_i = weights[i,:] * y_copy[nn_indices[0]:nn_indices[1]]
|
||
|
|
||
|
beta = lstsq(weights[i,:].reshape(k,1) * X, y_i, rcond=-1)[0]
|
||
|
fitted[i] = beta[0] + beta[1]*x_copy[i]
|
||
|
|
||
|
_lowess_update_nn(x_copy, nn_indices, i+1)
|
||
|
|
||
|
|
||
|
return fitted, weights
|
||
|
|
||
|
|
||
|
def _lowess_wt_standardize(weights, new_entries, x_copy_i, width):
|
||
|
"""
|
||
|
The initial phase of creating the weights.
|
||
|
Subtract the current x_i and divide by the width.
|
||
|
|
||
|
Parameters
|
||
|
----------
|
||
|
weights : ndarray
|
||
|
The memory where (new_entries - x_copy_i)/width will be placed
|
||
|
new_entries : ndarray
|
||
|
The x-values of the k closest points to x[i]
|
||
|
x_copy_i : float
|
||
|
x[i], the i'th point in the (sorted) x values
|
||
|
width : float
|
||
|
The maximum distance between x[i] and any point in new_entries
|
||
|
|
||
|
Returns
|
||
|
-------
|
||
|
Nothing. The modifications are made to weight in place.
|
||
|
"""
|
||
|
weights[:] = new_entries
|
||
|
weights -= x_copy_i
|
||
|
weights /= width
|
||
|
|
||
|
|
||
|
def _lowess_robustify_fit(x_copy, y_copy, fitted, weights, k, n):
|
||
|
"""
|
||
|
Additional weighted local linear regressions, performed if
|
||
|
iter>0. They take into account the sizes of the residuals,
|
||
|
to eliminate the effect of extreme outliers.
|
||
|
|
||
|
Parameters
|
||
|
----------
|
||
|
x_copy : 1-d ndarray
|
||
|
The x-values/exogenous part of the data being smoothed
|
||
|
y_copy : 1-d ndarray
|
||
|
The y-values/ endogenous part of the data being smoothed
|
||
|
fitted : 1-d ndarray
|
||
|
The fitted y-values from the previous iteration
|
||
|
weights : 2-d ndarray
|
||
|
An n by k array. The contribution to the weights in the
|
||
|
local linear fit coming from the distances between the
|
||
|
x-values
|
||
|
k : int
|
||
|
The number of data points which affect the linear fit for
|
||
|
each estimated point
|
||
|
n : int
|
||
|
The total number of points
|
||
|
|
||
|
Returns
|
||
|
-------
|
||
|
Nothing. The fitted values are modified in place.
|
||
|
"""
|
||
|
nn_indices = [0,k]
|
||
|
X = np.ones((k,2))
|
||
|
|
||
|
residual_weights = np.copy(y_copy)
|
||
|
residual_weights.shape = (n,)
|
||
|
residual_weights -= fitted
|
||
|
residual_weights = np.absolute(residual_weights)#, out=residual_weights)
|
||
|
s = np.median(residual_weights)
|
||
|
residual_weights /= (6*s)
|
||
|
too_big = residual_weights>=1
|
||
|
_lowess_bisquare(residual_weights)
|
||
|
residual_weights[too_big] = 0
|
||
|
|
||
|
|
||
|
for i in range(n):
|
||
|
total_weights = weights[i,:] * np.sqrt(residual_weights[nn_indices[0]:
|
||
|
nn_indices[1]])
|
||
|
|
||
|
X[:,1] = x_copy[nn_indices[0]:nn_indices[1]]
|
||
|
y_i = total_weights * y_copy[nn_indices[0]:nn_indices[1]]
|
||
|
total_weights.shape = (k,1)
|
||
|
|
||
|
beta = lstsq(total_weights * X, y_i, rcond=-1)[0]
|
||
|
|
||
|
fitted[i] = beta[0] + beta[1] * x_copy[i]
|
||
|
|
||
|
_lowess_update_nn(x_copy, nn_indices, i+1)
|
||
|
|
||
|
|
||
|
def _lowess_update_nn(x, cur_nn,i):
|
||
|
"""
|
||
|
Update the endpoints of the nearest neighbors to
|
||
|
the ith point.
|
||
|
|
||
|
Parameters
|
||
|
----------
|
||
|
x : iterable
|
||
|
The sorted points of x-values
|
||
|
cur_nn : list of length 2
|
||
|
The two current indices between which are the
|
||
|
k closest points to x[i]. (The actual value of
|
||
|
k is irrelevant for the algorithm.
|
||
|
i : int
|
||
|
The index of the current value in x for which
|
||
|
the k closest points are desired.
|
||
|
|
||
|
Returns
|
||
|
-------
|
||
|
Nothing. It modifies cur_nn in place.
|
||
|
"""
|
||
|
while True:
|
||
|
if cur_nn[1]<x.size:
|
||
|
left_dist = x[i] - x[cur_nn[0]]
|
||
|
new_right_dist = x[cur_nn[1]] - x[i]
|
||
|
if new_right_dist < left_dist:
|
||
|
cur_nn[0] = cur_nn[0] + 1
|
||
|
cur_nn[1] = cur_nn[1] + 1
|
||
|
else:
|
||
|
break
|
||
|
else:
|
||
|
break
|
||
|
|
||
|
|
||
|
def _lowess_tricube(t):
|
||
|
"""
|
||
|
The _tricube function applied to a numpy array.
|
||
|
The tricube function is (1-abs(t)**3)**3.
|
||
|
|
||
|
Parameters
|
||
|
----------
|
||
|
t : ndarray
|
||
|
Array the tricube function is applied to elementwise and
|
||
|
in-place.
|
||
|
|
||
|
Returns
|
||
|
-------
|
||
|
Nothing
|
||
|
"""
|
||
|
#t = (1-np.abs(t)**3)**3
|
||
|
t[:] = np.absolute(t) #, out=t) #numpy version?
|
||
|
_lowess_mycube(t)
|
||
|
t[:] = np.negative(t) #, out = t)
|
||
|
t += 1
|
||
|
_lowess_mycube(t)
|
||
|
|
||
|
|
||
|
def _lowess_mycube(t):
|
||
|
"""
|
||
|
Fast matrix cube
|
||
|
|
||
|
Parameters
|
||
|
----------
|
||
|
t : ndarray
|
||
|
Array that is cubed, elementwise and in-place
|
||
|
|
||
|
Returns
|
||
|
-------
|
||
|
Nothing
|
||
|
"""
|
||
|
#t **= 3
|
||
|
t2 = t*t
|
||
|
t *= t2
|
||
|
|
||
|
|
||
|
def _lowess_bisquare(t):
|
||
|
"""
|
||
|
The bisquare function applied to a numpy array.
|
||
|
The bisquare function is (1-t**2)**2.
|
||
|
|
||
|
Parameters
|
||
|
----------
|
||
|
t : ndarray
|
||
|
array bisquare function is applied to, element-wise and in-place.
|
||
|
|
||
|
Returns
|
||
|
-------
|
||
|
Nothing
|
||
|
"""
|
||
|
#t = (1-t**2)**2
|
||
|
t *= t
|
||
|
t[:] = np.negative(t) #, out=t)
|
||
|
t += 1
|
||
|
t *= t
|