github.com/aclements/go-misc@v0.0.0-20240129233631-2f6ede80790c/benchplot/kza.go (about) 1 // Copyright 2015 The Go Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style 3 // license that can be found in the LICENSE file. 4 5 package main 6 7 import "math" 8 9 // TODO: This all assumes that data is sampled at a regular interval 10 // and there are no missing values. It could be generalized to accept 11 // missing values (perhaps represented by NaN), or generalized much 12 // further by accepting (t, x) pairs and a vector of times at which to 13 // evaluate the filter (and an arbitrary window size). I would have to 14 // figure out how that affects the difference array in KZA. 15 16 // TODO: These can generate a lot of garbage. Perhaps the caller 17 // should pass in the target slice? Or these should just overwrite the 18 // input array and leave it to the caller to copy if necessary? 19 20 // MovingAverage performs a moving average (MA) filter of xs with 21 // window size m. m must be a positive odd integer. 22 // 23 // Note that this is filter is often described in terms of the half 24 // length of the window (m-1)/2. 25 func MovingAverage(xs []float64, m int) []float64 { 26 if m <= 0 || m%2 != 1 { 27 panic("m must be a positive, odd integer") 28 } 29 ys := make([]float64, len(xs)) 30 sum, n := 0.0, 0 31 for l, i, r := -m, -(m-1)/2, 0; i < len(ys); l, i, r = l+1, i+1, r+1 { 32 if l >= 0 { 33 sum -= xs[l] 34 n-- 35 } 36 if r < len(xs) { 37 sum += xs[r] 38 n++ 39 } 40 if i >= 0 { 41 ys[i] = sum / float64(n) 42 } 43 } 44 return ys 45 } 46 47 // KolmogorovZurbenko performs a Kolmogorov-Zurbenko (KZ) filter of xs 48 // with window size m and k iterations. m must be a positive odd 49 // integer. k must be positive. 50 func KolmogorovZurbenko(xs []float64, m, k int) []float64 { 51 // k is typically small, and MA is quite efficient, so just do 52 // the iterated moving average rather than bothering to 53 // compute the binomial coefficient kernel. 54 for i := 0; i < k; i++ { 55 // TODO: Generate less garbage. 56 xs = MovingAverage(xs, m) 57 } 58 return xs 59 } 60 61 // AdaptiveKolmogorovZurbenko performs an adaptive Kolmogorov-Zurbenko 62 // (KZA) filter of xs using an initial window size m and k iterations. 63 // m must be a positive odd integer. k must be positive. 64 // 65 // See Zurbenko, et al. 1996: Detecting discontinuities in time series 66 // of upper air data: Demonstration of an adaptive filter technique. 67 // Journal of Climate, 9, 3548–3560. 68 func AdaptiveKolmogorovZurbenko(xs []float64, m, k int) []float64 { 69 // Perform initial KZ filter. 70 z := KolmogorovZurbenko(xs, m, k) 71 72 // Compute differenced values. 73 q := (m - 1) / 2 74 d := make([]float64, len(z)+1) 75 maxD := 0.0 76 for i := q; i < len(z)-q; i++ { 77 d[i] = math.Abs(z[i+q] - z[i-q]) 78 if d[i] > maxD { 79 maxD = d[i] 80 } 81 } 82 83 if maxD == 0 { 84 // xs is constant, so no amount of filtering will do 85 // anything. Avoid dividing 0/0 below. 86 return xs 87 } 88 89 // Compute adaptive filter. 90 ys := make([]float64, len(xs)) 91 for t := range ys { 92 dPrime := d[t+1] - d[t] 93 f := 1 - d[t]/maxD 94 95 qt := q 96 if dPrime <= 0 { 97 // Zurbenko doesn't specify what to do with 98 // the fractional part of qt and qh, so we 99 // interpret this as summing all points of xs 100 // between qt and qh. 101 qt = int(math.Ceil(float64(q) * f)) 102 } 103 if t-qt < 0 { 104 qt = t 105 } 106 107 qh := q 108 if dPrime >= 0 { 109 qh = int(math.Floor(float64(q) * f)) 110 } 111 if t+qh >= len(xs) { 112 qh = len(xs) - t - 1 113 } 114 115 sum := 0.0 116 for i := t - qt; i <= t+qh; i++ { 117 sum += xs[i] 118 } 119 // Zurbenko divides by qh+qt, but this undercounts the 120 // number of terms in the sum by 1. 121 ys[t] = sum / float64(qh+qt+1) 122 } 123 124 return ys 125 }