// Copyright 2016 The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. package fit import ( "math" "sort" ) // LOESS computes the locally-weighted least squares polynomial // regression to the data (xs[i], ys[i]). 0 < span <= 1 is the // smoothing parameter, where smaller values fit the data more // tightly. Degree is typically 2 and span is typically between 0.5 // and 0.75. // // The regression is "local" because the weights used for the // polynomial regression depend on the x at which the regression // function is evaluated. The weight of observation i is // W((x-xs[i])/d(x)) where d(x) is the distance from x to the // span*len(xs)'th closest point to x and W is the tricube weight // function W(u) = (1-|u|³)³ for |u| < 1, 0 otherwise. One consequence // of this is that only the span*len(xs) points closest to x affect // the regression at x, and that the effect of these points falls off // further from x. // // References // // Cleveland, William S., and Susan J. Devlin. "Locally weighted // regression: an approach to regression analysis by local fitting." // Journal of the American Statistical Association 83.403 (1988): // 596-610. // // http://www.itl.nist.gov/div898/handbook/pmd/section1/dep/dep144.htm func LOESS(xs, ys []float64, degree int, span float64) func(x float64) float64 { if degree < 0 { panic("degree must be non-negative") } if span <= 0 { panic("span must be positive") } // q is the window width in data points. q := int(math.Ceil(span * float64(len(xs)))) if q >= len(xs) { q = len(xs) } // Sort xs. if !sort.Float64sAreSorted(xs) { xs = append([]float64(nil), xs...) ys = append([]float64(nil), ys...) sort.Sort(&pairSlice{xs, ys}) } return func(x float64) float64 { // Find the q points closest to x. n := 0 if len(xs) > q { n = sort.Search(len(xs)-q, func(i int) bool { // The cut-off between xs[i:i+q] and // xs[i+1:i+1+q] is avg(xs[i], // xs[i+q]). return (xs[i] + xs[i+q]) >= x*2 }) } closest := xs[n : n+q] // Compute the distance to the q'th farthest point. // This will be either the first or last point in // closest. d := x - closest[0] if closest[q-1]-x > d { d = closest[q-1] - x } // Compute the weights. weights := make([]float64, q) for i, c := range closest { // u is the normalized distance from x to // closest[i]. u := math.Abs(x-c) / d // Compute the tricube weight (1-|u|³)³ for // |u| < 1. We know 0 <= u <= 1, so we can // simplify this a bit. tmp := 1 - u*u*u weights[i] = tmp * tmp * tmp } // Compute the polynomial regression at x. pr := PolynomialRegression(closest, ys[n:n+q], weights, degree) // Evaluate the polynomial at x. return pr.F(x) } } type pairSlice struct { xs, ys []float64 } func (s *pairSlice) Len() int { return len(s.xs) } func (s *pairSlice) Less(i, j int) bool { return s.xs[i] < s.xs[j] } func (s *pairSlice) Swap(i, j int) { s.xs[i], s.xs[j] = s.xs[j], s.xs[i] s.ys[i], s.ys[j] = s.ys[j], s.ys[i] }