main.js (8629B)
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 isNumber = require( '@stdlib/assert/is-number' ).isPrimitive; 25 var isnan = require( '@stdlib/math/base/assert/is-nan' ); 26 27 28 // MAIN // 29 30 /** 31 * Returns an accumulator function which incrementally computes a moving variance-to-mean ratio (VMR). 32 * 33 * ## Method 34 * 35 * - Let \\(W\\) be a window of \\(N\\) elements over which we want to compute a variance-to-mean ratio (VMR). 36 * 37 * - 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 38 * 39 * ```tex 40 * \Delta s^2 = s_{i+1}^2 - s_{i}^2 41 * ``` 42 * 43 * - If we multiply both sides by \\(N-1\\), 44 * 45 * ```tex 46 * (N-1)(\Delta s^2) = (N-1)s_{i+1}^2 - (N-1)s_{i}^2 47 * ``` 48 * 49 * - If we substitute the definition of the unbiased sample variance having the form 50 * 51 * ```tex 52 * \begin{align*} 53 * s^2 &= \frac{1}{N-1} \biggl( \sum_{i=1}^{N} (x_i - \bar{x})^2 \biggr) \\ 54 * &= \frac{1}{N-1} \biggl( \sum_{i=1}^{N} (x_i^2 - 2\bar{x}x_i + \bar{x}^2) \biggr) \\ 55 * &= \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) \\ 56 * &= \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) \\ 57 * &= \frac{1}{N-1} \biggl( \sum_{i=1}^{N} x_i^2 - 2N\bar{x}^2 + N\bar{x}^2 \biggr) \\ 58 * &= \frac{1}{N-1} \biggl( \sum_{i=1}^{N} x_i^2 - N\bar{x}^2 \biggr) 59 * \end{align*} 60 * ``` 61 * 62 * we return 63 * 64 * ```tex 65 * (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) 66 * ``` 67 * 68 * - This can be further simplified by recognizing that subtracting the sums reduces to \\(x_N^2 - x_0^2\\); in which case, 69 * 70 * ```tex 71 * \begin{align*} 72 * (N-1)(\Delta s^2) &= x_N^2 - x_0^2 - N\bar{x}_{i+1}^2 + N\bar{x}_{i}^2 \\ 73 * &= x_N^2 - x_0^2 - N(\bar{x}_{i+1}^2 - \bar{x}_{i}^2) \\ 74 * &= x_N^2 - x_0^2 - N(\bar{x}_{i+1} - \bar{x}_{i})(\bar{x}_{i+1} + \bar{x}_{i}) 75 * \end{align*} 76 * ``` 77 * 78 * - Recognizing that the difference of means can be expressed 79 * 80 * ```tex 81 * \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} 82 * ``` 83 * 84 * and substituting into the equation above 85 * 86 * ```tex 87 * (N-1)(\Delta s^2) = x_N^2 - x_0^2 - (x_N - x_0)(\bar{x}_{i+1} + \bar{x}_{i}) 88 * ``` 89 * 90 * - Rearranging terms gives us the update equation for the unbiased sample variance 91 * 92 * ```tex 93 * \begin{align*} 94 * (N-1)(\Delta s^2) &= (x_N - x_0)(x_N + x_0) - (x_N - x_0)(\bar{x}_{i+1} + \bar{x}_{i}) 95 * &= (x_N - x_0)(x_N + x_0 - \bar{x}_{i+1} - \bar{x}_{i}) \\ 96 * &= (x_N - x_0)(x_N - \bar{x}_{i+1} + x_0 - \bar{x}_{i}) 97 * \end{align*} 98 * ``` 99 * 100 * @param {PositiveInteger} W - window size 101 * @param {number} [mean] - mean value 102 * @throws {TypeError} first argument must be a positive integer 103 * @throws {TypeError} second argument must be a number primitive 104 * @returns {Function} accumulator function 105 * 106 * @example 107 * var accumulator = incrmvmr( 3 ); 108 * 109 * var F = accumulator(); 110 * // returns null 111 * 112 * F = accumulator( 2.0 ); 113 * // returns 0.0 114 * 115 * F = accumulator( 1.0 ); 116 * // returns ~0.33 117 * 118 * F = accumulator( 3.0 ); 119 * // returns 0.5 120 * 121 * F = accumulator( 7.0 ); 122 * // returns ~2.55 123 * 124 * F = accumulator(); 125 * // returns ~2.55 126 * 127 * @example 128 * var accumulator = incrmvmr( 3, 2.0 ); 129 */ 130 function incrmvmr( W, mean ) { 131 var delta; 132 var buf; 133 var tmp; 134 var M2; 135 var mu; 136 var d1; 137 var d2; 138 var N; 139 var n; 140 var i; 141 if ( !isPositiveInteger( W ) ) { 142 throw new TypeError( 'invalid argument. Must provide a positive integer. Value: `' + W + '`.' ); 143 } 144 buf = new Array( W ); 145 n = W - 1; 146 M2 = 0.0; 147 i = -1; 148 N = 0; 149 if ( arguments.length > 1 ) { 150 if ( !isNumber( mean ) ) { 151 throw new TypeError( 'invalid argument. Must provide a number primitive. Value: `' + mean + '`.' ); 152 } 153 mu = mean; 154 return accumulator2; 155 } 156 mu = 0.0; 157 return accumulator1; 158 159 /** 160 * If provided a value, the accumulator function returns an updated accumulated value. If not provided a value, the accumulator function returns the current accumulated value. 161 * 162 * @private 163 * @param {number} [x] - input value 164 * @returns {(number|null)} accumulated value or null 165 */ 166 function accumulator1( x ) { 167 var k; 168 var v; 169 if ( arguments.length === 0 ) { 170 if ( N === 0 ) { 171 return null; 172 } 173 if ( N === 1 ) { 174 return ( isnan( M2 ) ) ? NaN : 0.0/mu; 175 } 176 if ( N < W ) { 177 return ( M2/(N-1) ) / mu; 178 } 179 return ( M2/n ) / mu; 180 } 181 // Update the index for managing the circular buffer: 182 i = (i+1) % W; 183 184 // Case: incoming value is NaN, the sliding second moment is automatically NaN... 185 if ( isnan( x ) ) { 186 N = W; // explicitly set to avoid `N < W` branch 187 mu = NaN; 188 M2 = NaN; 189 } 190 // Case: initial window... 191 else if ( N < W ) { 192 buf[ i ] = x; // update buffer 193 N += 1; 194 delta = x - mu; 195 mu += delta / N; 196 M2 += delta * (x - mu); 197 if ( N === 1 ) { 198 return 0.0 / mu; 199 } 200 return ( M2/(N-1) ) / mu; 201 } 202 // Case: N = W = 1 203 else if ( N === 1 ) { 204 mu = x; 205 M2 = 0.0; 206 return M2 / mu; 207 } 208 // Case: outgoing value is NaN, and, thus, we need to compute the accumulated values... 209 else if ( isnan( buf[ i ] ) ) { 210 N = 1; 211 mu = x; 212 M2 = 0.0; 213 for ( k = 0; k < W; k++ ) { 214 if ( k !== i ) { 215 v = buf[ k ]; 216 if ( isnan( v ) ) { 217 N = W; // explicitly set to avoid `N < W` branch 218 mu = NaN; 219 M2 = NaN; 220 break; // second moment is automatically NaN, so no need to continue 221 } 222 N += 1; 223 delta = v - mu; 224 mu += delta / N; 225 M2 += delta * (v - mu); 226 } 227 } 228 } 229 // Case: neither the current second moment nor the incoming value are NaN, so we need to update the accumulated values... 230 else if ( isnan( M2 ) === false ) { 231 tmp = buf[ i ]; 232 delta = x - tmp; 233 d1 = tmp - mu; 234 mu += delta / W; 235 d2 = x - mu; 236 M2 += delta * (d1 + d2); 237 } 238 // Case: the current second moment is NaN, so nothing to do until the buffer no longer contains NaN values... 239 buf[ i ] = x; 240 241 return ( M2/n ) / mu; 242 } 243 244 /** 245 * If provided a value, the accumulator function returns an updated accumulated value. If not provided a value, the accumulator function returns the current accumulated value. 246 * 247 * @private 248 * @param {number} [x] - input value 249 * @returns {(number|null)} accumulated value or null 250 */ 251 function accumulator2( x ) { 252 var k; 253 if ( arguments.length === 0 ) { 254 if ( N === 0 ) { 255 return null; 256 } 257 if ( N < W ) { 258 return ( M2/N ) / mu; 259 } 260 return ( M2/W ) / mu; 261 } 262 // Update the index for managing the circular buffer: 263 i = (i+1) % W; 264 265 // Case: incoming value is NaN, the sliding second moment is automatically NaN... 266 if ( isnan( x ) ) { 267 N = W; // explicitly set to avoid `N < W` branch 268 M2 = NaN; 269 } 270 // Case: initial window... 271 else if ( N < W ) { 272 buf[ i ] = x; // update buffer 273 N += 1; 274 delta = x - mu; 275 M2 += delta * delta; 276 return ( M2/N ) / mu; 277 } 278 // Case: outgoing value is NaN, and, thus, we need to compute the accumulated values... 279 else if ( isnan( buf[ i ] ) ) { 280 M2 = 0.0; 281 for ( k = 0; k < W; k++ ) { 282 if ( k !== i ) { 283 if ( isnan( buf[ k ] ) ) { 284 N = W; // explicitly set to avoid `N < W` branch 285 M2 = NaN; 286 break; // second moment is automatically NaN, so no need to continue 287 } 288 delta = buf[ k ] - mu; 289 M2 += delta * delta; 290 } 291 } 292 } 293 // Case: neither the current second moment nor the incoming value are NaN, so we need to update the accumulated values... 294 else if ( isnan( M2 ) === false ) { 295 tmp = buf[ i ]; 296 M2 += ( x-tmp ) * ( x-mu + tmp-mu ); 297 } 298 // Case: the current second moment is NaN, so nothing to do until the buffer no longer contains NaN values... 299 buf[ i ] = x; 300 301 return ( M2/W ) / mu; 302 } 303 } 304 305 306 // EXPORTS // 307 308 module.exports = incrmvmr;