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  }