main.js (7295B)
1 /** 2 * @license Apache-2.0 3 * 4 * Copyright (c) 2018 The Stdlib Authors. 5 * 6 * Licensed under the Apache License, Version 2.0 (the "License"); 7 * you may not use this file except in compliance with the License. 8 * You may obtain a copy of the License at 9 * 10 * http://www.apache.org/licenses/LICENSE-2.0 11 * 12 * Unless required by applicable law or agreed to in writing, software 13 * distributed under the License is distributed on an "AS IS" BASIS, 14 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 * See the License for the specific language governing permissions and 16 * limitations under the License. 17 */ 18 19 'use strict'; 20 21 // MODULES // 22 23 var isPositiveInteger = require( '@stdlib/assert/is-positive-integer' ).isPrimitive; 24 var isArrayLike = require( '@stdlib/assert/is-array-like-object' ); 25 var isnan = require( '@stdlib/math/base/assert/is-nan' ); 26 var Float64Array = require( '@stdlib/array/float64' ); 27 28 29 // MAIN // 30 31 /** 32 * Returns an accumulator function which incrementally computes a moving arithmetic mean and unbiased sample variance. 33 * 34 * ## Method 35 * 36 * - Let \\(W\\) be a window of \\(N\\) elements over which we want to compute an unbiased sample variance. 37 * 38 * - The difference between the unbiased sample variance in a window \\(W_i\\) and the unbiased sample variance in a window \\(W_{i+1})\\) is given by 39 * 40 * ```tex 41 * \Delta s^2 = s_{i+1}^2 - s_{i}^2 42 * ``` 43 * 44 * - If we multiply both sides by \\(N-1\\), 45 * 46 * ```tex 47 * (N-1)(\Delta s^2) = (N-1)s_{i+1}^2 - (N-1)s_{i}^2 48 * ``` 49 * 50 * - If we substitute the definition of the unbiased sample variance having the form 51 * 52 * ```tex 53 * \begin{align*} 54 * s^2 &= \frac{1}{N-1} \biggl( \sum_{i=1}^{N} (x_i - \bar{x})^2 \biggr) \\ 55 * &= \frac{1}{N-1} \biggl( \sum_{i=1}^{N} (x_i^2 - 2\bar{x}x_i + \bar{x}^2) \biggr) \\ 56 * &= \frac{1}{N-1} \biggl( \sum_{i=1}^{N} x_i^2 - 2\bar{x} \sum_{i=1}^{N} x_i + \sum_{i=1}^{N} \bar{x}^2) \biggr) \\ 57 * &= \frac{1}{N-1} \biggl( \sum_{i=1}^{N} x_i^2 - \frac{2N\bar{x}\sum_{i=1}^{N} x_i}{N} + N\bar{x}^2 \biggr) \\ 58 * &= \frac{1}{N-1} \biggl( \sum_{i=1}^{N} x_i^2 - 2N\bar{x}^2 + N\bar{x}^2 \biggr) \\ 59 * &= \frac{1}{N-1} \biggl( \sum_{i=1}^{N} x_i^2 - N\bar{x}^2 \biggr) 60 * \end{align*} 61 * ``` 62 * 63 * we return 64 * 65 * ```tex 66 * (N-1)(\Delta s^2) = \biggl(\sum_{k=1}^N x_k^2 - N\bar{x}_{i+1}^2 \biggr) - \biggl(\sum_{k=0}^{N-1} x_k^2 - N\bar{x}_{i}^2 \biggr) 67 * ``` 68 * 69 * - This can be further simplified by recognizing that subtracting the sums reduces to \\(x_N^2 - x_0^2\\); in which case, 70 * 71 * ```tex 72 * \begin{align*} 73 * (N-1)(\Delta s^2) &= x_N^2 - x_0^2 - N\bar{x}_{i+1}^2 + N\bar{x}_{i}^2 \\ 74 * &= x_N^2 - x_0^2 - N(\bar{x}_{i+1}^2 - \bar{x}_{i}^2) \\ 75 * &= x_N^2 - x_0^2 - N(\bar{x}_{i+1} - \bar{x}_{i})(\bar{x}_{i+1} + \bar{x}_{i}) 76 * \end{align*} 77 * ``` 78 * 79 * - Recognizing that the difference of means can be expressed 80 * 81 * ```tex 82 * \bar{x}_{i+1} - \bar{x}_i = \frac{1}{N} \biggl( \sum_{k=1}^N x_k - \sum_{k=0}^{N-1} x_k \biggr) = \frac{x_N - x_0}{N} 83 * ``` 84 * 85 * and substituting into the equation above 86 * 87 * ```tex 88 * (N-1)(\Delta s^2) = x_N^2 - x_0^2 - (x_N - x_0)(\bar{x}_{i+1} + \bar{x}_{i}) 89 * ``` 90 * 91 * - Rearranging terms gives us the update equation 92 * 93 * ```tex 94 * \begin{align*} 95 * (N-1)(\Delta s^2) &= (x_N - x_0)(x_N + x_0) - (x_N - x_0)(\bar{x}_{i+1} + \bar{x}_{i}) 96 * &= (x_N - x_0)(x_N + x_0 - \bar{x}_{i+1} - \bar{x}_{i}) \\ 97 * &= (x_N - x_0)(x_N - \bar{x}_{i+1} + x_0 - \bar{x}_{i}) 98 * \end{align*} 99 * ``` 100 * 101 * @param {Collection} [out] - output array 102 * @param {PositiveInteger} window - window size 103 * @throws {TypeError} output argument must be array-like 104 * @throws {TypeError} window size must be a positive integer 105 * @returns {Function} accumulator function 106 * 107 * @example 108 * var accumulator = incrmmeanvar( 3 ); 109 * 110 * var v = accumulator(); 111 * // returns null 112 * 113 * v = accumulator( 2.0 ); 114 * // returns [ 2.0, 0.0 ] 115 * 116 * v = accumulator( -5.0 ); 117 * // returns [ -1.5, 24.5 ] 118 * 119 * v = accumulator( 3.0 ); 120 * // returns [ 0.0, 19.0 ] 121 * 122 * v = accumulator( 5.0 ); 123 * // returns [ 1.0, 28.0 ] 124 * 125 * v = accumulator(); 126 * // returns [ 1.0, 28.0 ] 127 */ 128 function incrmmeanvar( out, window ) { 129 var meanvar; 130 var delta; 131 var buf; 132 var tmp; 133 var M2; 134 var mu; 135 var d1; 136 var d2; 137 var W; 138 var N; 139 var n; 140 var i; 141 if ( arguments.length === 1 ) { 142 meanvar = [ 0.0, 0.0 ]; 143 W = out; 144 } else { 145 if ( !isArrayLike( out ) ) { 146 throw new TypeError( 'invalid argument. Output argument must be an array-like object. Value: `' + out + '`.' ); 147 } 148 meanvar = out; 149 W = window; 150 } 151 if ( !isPositiveInteger( W ) ) { 152 throw new TypeError( 'invalid argument. Window size must be a positive integer. Value: `' + W + '`.' ); 153 } 154 buf = new Float64Array( W ); 155 n = W - 1; 156 M2 = 0.0; 157 mu = 0.0; 158 i = -1; 159 N = 0; 160 161 return accumulator; 162 163 /** 164 * If provided a value, the accumulator function returns updated accumulated values. If not provided a value, the accumulator function returns the current accumulated values. 165 * 166 * @private 167 * @param {number} [x] - input value 168 * @returns {(ArrayLikeObject|null)} output array or null 169 */ 170 function accumulator( x ) { 171 var k; 172 var v; 173 if ( arguments.length === 0 ) { 174 if ( N === 0 ) { 175 return null; 176 } 177 meanvar[ 0 ] = mu; 178 if ( N === 1 ) { 179 if ( isnan( M2 ) ) { 180 meanvar[ 1 ] = NaN; 181 } else { 182 meanvar[ 1 ] = 0.0; 183 } 184 } else if ( N < W ) { 185 meanvar[ 1 ] = M2 / (N-1); 186 } else { 187 meanvar[ 1 ] = M2 / n; 188 } 189 return meanvar; 190 } 191 // Update the index for managing the circular buffer: 192 i = (i+1) % W; 193 194 // Case: incoming value is NaN, the sliding second moment is automatically NaN... 195 if ( isnan( x ) ) { 196 N = W; // explicitly set to avoid `N < W` branch 197 mu = NaN; 198 M2 = NaN; 199 } 200 // Case: initial window... 201 else if ( N < W ) { 202 buf[ i ] = x; // update buffer 203 N += 1; 204 delta = x - mu; 205 mu += delta / N; 206 M2 += delta * (x - mu); 207 208 meanvar[ 0 ] = mu; 209 if ( N === 1 ) { 210 meanvar[ 1 ] = 0.0; 211 } else { 212 meanvar[ 1 ] = M2 / (N-1); 213 } 214 return meanvar; 215 } 216 // Case: N = W = 1 217 else if ( N === 1 ) { 218 mu = x; 219 M2 = 0.0; 220 meanvar[ 0 ] = x; 221 meanvar[ 1 ] = 0.0; 222 return meanvar; 223 } 224 // Case: outgoing value is NaN, and, thus, we need to compute the accumulated values... 225 else if ( isnan( buf[ i ] ) ) { 226 N = 1; 227 mu = x; 228 M2 = 0.0; 229 for ( k = 0; k < W; k++ ) { 230 if ( k !== i ) { 231 v = buf[ k ]; 232 if ( isnan( v ) ) { 233 N = W; // explicitly set to avoid `N < W` branch 234 mu = NaN; 235 M2 = NaN; 236 break; // second moment is automatically NaN, so no need to continue 237 } 238 N += 1; 239 delta = v - mu; 240 mu += delta / N; 241 M2 += delta * (v - mu); 242 } 243 } 244 } 245 // Case: neither the current second moment nor the incoming value are NaN, so we need to update the accumulated values... 246 else if ( isnan( M2 ) === false ) { 247 tmp = buf[ i ]; 248 delta = x - tmp; 249 d1 = tmp - mu; 250 mu += delta / W; 251 d2 = x - mu; 252 M2 += delta * (d1 + d2); 253 } 254 // Case: the current second moment is NaN, so nothing to do until the buffer no longer contains NaN values... 255 buf[ i ] = x; 256 257 meanvar[ 0 ] = mu; 258 meanvar[ 1 ] = M2 / n; 259 return meanvar; 260 } 261 } 262 263 264 // EXPORTS // 265 266 module.exports = incrmmeanvar;