| // Copyright 2015 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 app | |
| import "math" | |
| // TODO: This all assumes that data is sampled at a regular interval | |
| // and there are no missing values. It could be generalized to accept | |
| // missing values (perhaps represented by NaN), or generalized much | |
| // further by accepting (t, x) pairs and a vector of times at which to | |
| // evaluate the filter (and an arbitrary window size). I would have to | |
| // figure out how that affects the difference array in KZA. | |
| // TODO: These can generate a lot of garbage. Perhaps the caller | |
| // should pass in the target slice? Or these should just overwrite the | |
| // input array and leave it to the caller to copy if necessary? | |
| // MovingAverage performs a moving average (MA) filter of xs with | |
| // window size m. m must be a positive odd integer. | |
| // | |
| // Note that this is filter is often described in terms of the half | |
| // length of the window (m-1)/2. | |
| func MovingAverage(xs []float64, m int) []float64 { | |
| if m <= 0 || m%2 != 1 { | |
| panic("m must be a positive, odd integer") | |
| } | |
| ys := make([]float64, len(xs)) | |
| sum, n := 0.0, 0 | |
| for l, i, r := -m, -(m-1)/2, 0; i < len(ys); l, i, r = l+1, i+1, r+1 { | |
| if l >= 0 { | |
| sum -= xs[l] | |
| n-- | |
| } | |
| if r < len(xs) { | |
| sum += xs[r] | |
| n++ | |
| } | |
| if i >= 0 { | |
| ys[i] = sum / float64(n) | |
| } | |
| } | |
| return ys | |
| } | |
| // KolmogorovZurbenko performs a Kolmogorov-Zurbenko (KZ) filter of xs | |
| // with window size m and k iterations. m must be a positive odd | |
| // integer. k must be positive. | |
| func KolmogorovZurbenko(xs []float64, m, k int) []float64 { | |
| // k is typically small, and MA is quite efficient, so just do | |
| // the iterated moving average rather than bothering to | |
| // compute the binomial coefficient kernel. | |
| for i := 0; i < k; i++ { | |
| // TODO: Generate less garbage. | |
| xs = MovingAverage(xs, m) | |
| } | |
| return xs | |
| } | |
| // AdaptiveKolmogorovZurbenko performs an adaptive Kolmogorov-Zurbenko | |
| // (KZA) filter of xs using an initial window size m and k iterations. | |
| // m must be a positive odd integer. k must be positive. | |
| // | |
| // See Zurbenko, et al. 1996: Detecting discontinuities in time series | |
| // of upper air data: Demonstration of an adaptive filter technique. | |
| // Journal of Climate, 9, 3548–3560. | |
| func AdaptiveKolmogorovZurbenko(xs []float64, m, k int) []float64 { | |
| // Perform initial KZ filter. | |
| z := KolmogorovZurbenko(xs, m, k) | |
| // Compute differenced values. | |
| q := (m - 1) / 2 | |
| d := make([]float64, len(z)+1) | |
| maxD := 0.0 | |
| for i := q; i < len(z)-q; i++ { | |
| d[i] = math.Abs(z[i+q] - z[i-q]) | |
| if d[i] > maxD { | |
| maxD = d[i] | |
| } | |
| } | |
| if maxD == 0 { | |
| // xs is constant, so no amount of filtering will do | |
| // anything. Avoid dividing 0/0 below. | |
| return xs | |
| } | |
| // Compute adaptive filter. | |
| ys := make([]float64, len(xs)) | |
| for t := range ys { | |
| dPrime := d[t+1] - d[t] | |
| f := 1 - d[t]/maxD | |
| qt := q | |
| if dPrime <= 0 { | |
| // Zurbenko doesn't specify what to do with | |
| // the fractional part of qt and qh, so we | |
| // interpret this as summing all points of xs | |
| // between qt and qh. | |
| qt = int(math.Ceil(float64(q) * f)) | |
| } | |
| if t-qt < 0 { | |
| qt = t | |
| } | |
| qh := q | |
| if dPrime >= 0 { | |
| qh = int(math.Floor(float64(q) * f)) | |
| } | |
| if t+qh >= len(xs) { | |
| qh = len(xs) - t - 1 | |
| } | |
| sum := 0.0 | |
| for i := t - qt; i <= t+qh; i++ { | |
| sum += xs[i] | |
| } | |
| // Zurbenko divides by qh+qt, but this undercounts the | |
| // number of terms in the sum by 1. | |
| ys[t] = sum / float64(qh+qt+1) | |
| } | |
| return ys | |
| } |