├── README.md ├── asap.go ├── autocorrelation.go └── metrics.go /README.md: -------------------------------------------------------------------------------- 1 | go-asap: ASAP smoothing 2 | 3 | https://github.com/stanford-futuredata/ASAP 4 | 5 | ## Usage 6 | 7 | ```go 8 | package main 9 | 10 | import "github.com/errx/go-asap" 11 | 12 | func main() { 13 | data := []float64{12751, 8767, 7005, 5257, 4189, 3236, 2817, 2527, 2406, 1961} 14 | smoothed := asap.Smooth(data, 3) 15 | } 16 | ``` 17 | -------------------------------------------------------------------------------- /asap.go: -------------------------------------------------------------------------------- 1 | package asap 2 | 3 | import ( 4 | "math" 5 | ) 6 | 7 | // Smooth performs ASAP smoothing 8 | func Smooth(in []float64, resolution int) []float64 { 9 | step := len(in) / resolution 10 | data := sma(in, step, step) 11 | peaks, correlations, maxAcf := acf(data, int(float64(len(data)+5)/10)) 12 | m := newMetrics(data) 13 | origKurt := m.kurtosis 14 | minObj := m.roughness 15 | windowSize := 1 16 | lb := 1 17 | leastFeasible := -1 18 | 19 | tail := len(data) / 10 20 | 21 | for i := len(peaks) - 1; i >= 0; i-- { 22 | w := peaks[i] 23 | if w < lb || w == 1 { 24 | break 25 | } else if math.Sqrt(1-correlations[w])*float64(windowSize) > math.Sqrt(1-correlations[windowSize])*float64(w) { 26 | continue 27 | } 28 | 29 | smoothed := sma(data, w, 1) 30 | m2 := newMetrics(smoothed) 31 | if m2.kurtosis >= origKurt { 32 | if m2.roughness < minObj { 33 | minObj = m2.roughness 34 | windowSize = w 35 | } 36 | t := int(float64(w) * math.Sqrt((maxAcf-1)/(correlations[w]-1))) 37 | if t > lb { 38 | lb = t 39 | } 40 | if leastFeasible < 0 { 41 | leastFeasible = i 42 | } 43 | } 44 | } 45 | if leastFeasible > 0 { 46 | if leastFeasible < len(peaks)-3 { 47 | tail = peaks[leastFeasible+1] 48 | } 49 | if peaks[leastFeasible+1] > lb { 50 | lb = peaks[leastFeasible+1] 51 | } 52 | } 53 | 54 | windowSize = binarySearch(lb, tail, data, minObj, origKurt, windowSize) 55 | return sma(data, windowSize, 1) 56 | } 57 | 58 | func binarySearch(head, tail int, in []float64, minObj, origKurt float64, windowSize int) int { 59 | for head <= tail { 60 | w := (head + tail + 1) / 2 61 | smoothed := sma(in, w, 1) 62 | m := newMetrics(smoothed) 63 | if m.kurtosis >= origKurt { 64 | if m.roughness < minObj { 65 | windowSize = w 66 | minObj = m.roughness 67 | } 68 | head = w + 1 69 | } else { 70 | tail = w - 1 71 | } 72 | 73 | } 74 | return windowSize 75 | } 76 | 77 | // simple moving average 78 | func sma(in []float64, rng, slide int) []float64 { 79 | var ( 80 | s float64 81 | c float64 82 | windowStart int 83 | oldStart int 84 | ret []float64 85 | ) 86 | last := len(in) - 1 87 | for i, v := range in { 88 | if i-windowStart >= rng || i == last { 89 | if i == last || c == 0 { 90 | s += v 91 | c += 1 92 | } 93 | ret = append(ret, s/c) 94 | oldStart = windowStart 95 | for windowStart < len(in) && windowStart-oldStart < slide { 96 | s -= in[windowStart] 97 | c -= 1 98 | windowStart += 1 99 | } 100 | } 101 | s += v 102 | c += 1 103 | } 104 | return ret 105 | } 106 | -------------------------------------------------------------------------------- /autocorrelation.go: -------------------------------------------------------------------------------- 1 | package asap 2 | 3 | import ( 4 | "math" 5 | "github.com/mjibson/go-dsp/fft" 6 | ) 7 | 8 | const acfCorrThreshold = 0.2 9 | 10 | func acf(in []float64, maxLag int) ([]int, []float64, float64) { 11 | pow := float64(int(math.Log2(float64(len(in))) + 1)) 12 | l := int(math.Pow(2.0, pow)) 13 | 14 | fftv := make([]float64, len(in)+(l-len(in))) 15 | copy(fftv, in) 16 | if len(fftv) != l { 17 | panic("fftv wtf") 18 | } 19 | f_f := fft.FFTReal(fftv) 20 | s_f := make([]float64, len(fftv)) 21 | for i, x := range f_f { 22 | s_f[i] = real(x)*real(x) + imag(x)*imag(x) 23 | } 24 | r_t := fft.IFFTReal(s_f) 25 | 26 | correlations := make([]float64, maxLag) 27 | r_t0 := real(r_t[0]) 28 | for i := range correlations { 29 | if i < 1 { 30 | continue 31 | } 32 | correlations[i] = real(r_t[i]) / r_t0 33 | } 34 | 35 | var peaks []int 36 | var maxAcf float64 37 | if len(correlations) < 2 { 38 | return peaks, correlations, maxAcf 39 | } 40 | 41 | positive := correlations[1] > correlations[0] 42 | max := 1 43 | for i := range correlations { 44 | if i < 2 { 45 | continue 46 | } 47 | if !positive && correlations[i] > correlations[i-1] { 48 | max = i 49 | positive = !positive 50 | } else if positive && correlations[i] > correlations[max] { 51 | max = i 52 | } else if positive && correlations[i] < correlations[i-1] { 53 | if max > 1 && correlations[max] > acfCorrThreshold { 54 | peaks = append(peaks, max) 55 | } 56 | if correlations[max] > maxAcf { 57 | maxAcf = correlations[max] 58 | } 59 | positive = !positive 60 | } 61 | } 62 | return peaks, correlations, maxAcf 63 | } 64 | -------------------------------------------------------------------------------- /metrics.go: -------------------------------------------------------------------------------- 1 | package asap 2 | 3 | import "math" 4 | 5 | type metrics struct { 6 | mean float64 7 | std float64 8 | roughness float64 9 | kurtosis float64 10 | } 11 | 12 | func newMetrics(in []float64) *metrics { 13 | m := &metrics{} 14 | m.mean = mean(in) 15 | m.roughness = roughness(in) 16 | m.kurtosis = kurtosis(in, m.mean) 17 | return m 18 | } 19 | 20 | func mean(in []float64) float64 { 21 | var t float64 22 | for _, v := range in { 23 | t += v 24 | } 25 | return t / float64(len(in)) 26 | } 27 | 28 | func std(in []float64, mean float64) float64 { 29 | var t float64 30 | for _, v := range in { 31 | diff := v - mean 32 | t += diff * diff 33 | } 34 | return math.Sqrt(t / float64(len(in))) 35 | } 36 | 37 | func kurtosis(in []float64, mean float64) float64 { 38 | var variance, u4 float64 39 | for _, v := range in { 40 | diff := v - mean 41 | variance += diff * diff 42 | u4 += diff * diff * diff * diff 43 | } 44 | return float64(len(in)) * u4 / (variance * variance) 45 | } 46 | 47 | func roughness(in []float64) float64 { 48 | diff := make([]float64, len(in)-1) 49 | var t float64 50 | for i := 1; i < len(in); i++ { 51 | diff[i-1] = in[i] - in[i-1] 52 | t += diff[i-1] 53 | } 54 | dm := t / float64(len(diff)) 55 | return std(diff, dm) 56 | } 57 | --------------------------------------------------------------------------------