main.js (12961B)
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 var Float64Array = require( '@stdlib/array/float64' ); 27 28 29 // MAIN // 30 31 /** 32 * Returns an accumulator function which incrementally computes a moving unbiased sample covariance. 33 * 34 * ## Method 35 * 36 * - Let \\(W\\) be a window of \\(N\\) elements over which we want to compute an unbiased sample covariance. 37 * 38 * - We begin by defining the covariance \\( \operatorname{cov}_n(x,y) \\) for a window \\(n\\) as follows 39 * 40 * ```tex 41 * \operatorname{cov}_n(x,y) &= \frac{C_n}{n} 42 * ``` 43 * 44 * where \\(C_n\\) is the co-moment, which is defined as 45 * 46 * ```tex 47 * C_n = \sum_{i=1}^{N} ( x_i - \bar{x}_n ) ( y_i - \bar{y}_n ) 48 * ``` 49 * 50 * and where \\(\bar{x}_n\\) and \\(\bar{y}_n\\) are the sample means for \\(x\\) and \\(y\\), respectively, and \\(i=1\\) specifies the first element in a window. 51 * 52 * - The sample mean is computed using the canonical formula 53 * 54 * ```tex 55 * \bar{x}_n = \frac{1}{N} \sum_{i=1}^{N} x_i 56 * ``` 57 * 58 * which, taking into account a previous window, can be expressed 59 * 60 * ```tex 61 * \begin{align*} 62 * \bar{x}_n &= \frac{1}{N} \biggl( \sum_{i=0}^{N-1} x_i - x_0 + x_N \biggr) \\ 63 * &= \bar{x}_{n-1} + \frac{x_N - x_0}{N} 64 * \end{align*} 65 * ``` 66 * 67 * where \\(x_0\\) is the first value in the previous window. 68 * 69 * - We can substitute into the co-moment equation 70 * 71 * ```tex 72 * \begin{align*} 73 * C_n &= \sum_{i=1}^{N} ( x_i - \bar{x}_n ) ( y_i - \bar{y}_n ) \\ 74 * &= \sum_{i=1}^{N} \biggl( x_i - \bar{x}_{n-1} - \frac{x_N - x_0}{N} \biggr) \biggl( y_i - \bar{y}_{n-1} - \frac{y_N - y_0}{N} \biggr) \\ 75 * &= \sum_{i=1}^{N} \biggl( \Delta x_{i,n-1} - \frac{x_N - x_0}{N} \biggr) \biggl( \Delta y_{i,n-1} - \frac{y_N - y_0}{N} \biggr) 76 * \end{align*} 77 * ``` 78 * 79 * where 80 * 81 * ```tex 82 * \Delta x_{i,k} = x_i - \bar{x}_{k} 83 * ``` 84 * 85 * - We can subsequently expand terms and apply a summation identity 86 * 87 * ```tex 88 * \begin{align*} 89 * C_n &= \sum_{i=1}^{N} \Delta x_{i,n-1} \Delta y_{i,n-1} - \sum_{i=1}^{N} \Delta x_{i,n-1} \biggl( \frac{y_N - y_0}{N} \biggr) - \sum_{i=1}^{N} \Delta y_{i,n-1} \biggl( \frac{x_N - x_0}{N} \biggr) + \sum_{i=1}^{N} \biggl( \frac{x_N - x_0}{N} \biggr) \biggl( \frac{y_N - y_0}{N} \biggr) \\ 90 * &= \sum_{i=1}^{N} \Delta x_{i,n-1} \Delta y_{i,n-1} - \biggl( \frac{y_N - y_0}{N} \biggr) \sum_{i=1}^{N} \Delta x_{i,n-1} - \biggl( \frac{x_N - x_0}{N} \biggr) \sum_{i=1}^{N} \Delta y_{i,n-1} + \frac{(x_N - x_0)(y_N - y_0)}{N} 91 * \end{align*} 92 * ``` 93 * 94 * - Let us first consider the second term which we can reorganize as follows 95 * 96 * ```tex 97 * \begin{align*} 98 * \biggl( \frac{y_N - y_0}{N} \biggr) \sum_{i=1}^{N} \Delta x_{i,n-1} &= \biggl( \frac{y_N - y_0}{N} \biggr) \sum_{i=1}{N} ( x_i - \bar{x}_{n-1}) \\ 99 * &= \biggl( \frac{y_N - y_0}{N} \biggr) \sum_{i=1}^{N} x_i - \biggl( \frac{y_N - y_0}{N} \biggr) \sum_{i=1}^{N} \bar{x}_{n-1} \\ 100 * &= (y_N - y_0) \bar{x}_{n} - (y_N - y_0)\bar{x}_{n-1} \\ 101 * &= (y_N - y_0) (\bar{x}_{n} - \bar{x}_{n-1}) \\ 102 * &= \frac{(x_N - x_0)(y_N - y_0)}{N} 103 * \end{align*} 104 * ``` 105 * 106 * - The third term can be reorganized in a manner similar to the second term such that 107 * 108 * ```tex 109 * \biggl( \frac{x_N - x_0}{N} \biggr) \sum_{i=1}^{N} \Delta y_{i,n-1} = \frac{(x_N - x_0)(y_N - y_0)}{N} 110 * ``` 111 * 112 * - Substituting back into the equation for the co-moment 113 * 114 * ```tex 115 * \begin{align*} 116 * C_n &= \sum_{i=1}^{N} \Delta x_{i,n-1} \Delta y_{i,n-1} - \frac{(x_N - x_0)(y_N - y_0)}{N} - \frac{(x_N - x_0)(y_N - y_0)}{N} + \frac{(x_N - x_0)(y_N - y_0)}{N} \\ 117 * &= \sum_{i=1}^{N} \Delta x_{i,n-1} \Delta y_{i,n-1} - \frac{(x_N - x_0)(y_N - y_0)}{N} 118 * \end{align*} 119 * ``` 120 * 121 * - Let us now consider the first term which we can modify as follows 122 * 123 * ```tex 124 * \begin{align*} 125 * \sum_{i=1}^{N} \Delta x_{i,n-1} \Delta y_{i,n-1} &= \sum_{i=1}^{N} (x_i - \bar{x}_{n-1})(y_i - \bar{y}_{n-1}) \\ 126 * &= \sum_{i=1}^{N-1} (x_i - \bar{x}_{n-1})(y_i - \bar{y}_{n-1}) + (x_N - \bar{x}_{n-1})(y_N - \bar{y}_{n-1}) \\ 127 * &= \sum_{i=1}^{N-1} (x_i - \bar{x}_{n-1})(y_i - \bar{y}_{n-1}) + (x_N - \bar{x}_{n-1})(y_N - \bar{y}_{n-1}) + (x_0 - \bar{x}_{n-1})(y_0 - \bar{y}_{n-1}) - (x_0 - \bar{x}_{n-1})(y_0 - \bar{y}_{n-1}) \\ 128 * &= \sum_{i=0}^{N-1} (x_i - \bar{x}_{n-1})(y_i - \bar{y}_{n-1}) + (x_N - \bar{x}_{n-1})(y_N - \bar{y}_{n-1}) - (x_0 - \bar{x}_{n-1})(y_0 - \bar{y}_{n-1}) 129 * \end{align*} 130 * ``` 131 * 132 * where we recognize that the first term equals the co-moment for the previous window 133 * 134 * ```tex 135 * C_{n-1} = \sum_{i=0}^{N-1} (x_i - \bar{x}_{n-1})(y_i - \bar{y}_{n-1}) 136 * ``` 137 * 138 * In which case, 139 * 140 * ```tex 141 * \begin{align*} 142 * \sum_{i=1}^{N} \Delta x_{i,n-1} \Delta y_{i,n-1} &= C_{n-1} + (x_N - \bar{x}_{n-1})(y_N - \bar{y}_{n-1}) - (x_0 - \bar{x}_{n-1})(y_0 - \bar{y}_{n-1}) \\ 143 * &= C_{n-1} + \Delta x_{N,n-1} \Delta y_{N,n-1} - \Delta x_{0,n-1} \Delta y_{0,n-1} 144 * \end{align*} 145 * ``` 146 * 147 * - Substituting into the equation for the co-moment 148 * 149 * ```tex 150 * C_n = C_{n-1} + \Delta x_{N,n-1} \Delta y_{N,n-1} - \Delta x_{0,n-1} \Delta y_{0,n-1} - \frac{(x_N - x_0)(y_N - y_0)}{N} 151 * ``` 152 * 153 * - We can make one further modification to the last term 154 * 155 * ```tex 156 * \begin{align*} 157 * \frac{(x_N - x_0)(y_N - y_0)}{N} &= \frac{(x_N - \bar{x}_{n-1} - x_0 + \bar{x}_{n-1})(y_N - \bar{y}_{n-1} - y_0 + \bar{y}_{n-1})}{N} \\ 158 * &= \frac{(\Delta x_{N,n-1} - \Delta x_{0,n-1})(\Delta y_{N,n-1} - \Delta y_{0,n-1})}{N} 159 * \end{align*} 160 * ``` 161 * 162 * which, upon substitution into the equation for the co-moment, yields 163 * 164 * ```tex 165 * C_n = C_{n-1} + \Delta x_{N,n-1} \Delta y_{N,n-1} - \Delta x_{0,n-1} \Delta y_{0,n-1} - \frac{(\Delta x_{N,n-1} - \Delta x_{0,n-1})(\Delta y_{N,n-1} - \Delta y_{0,n-1})}{N} 166 * ``` 167 * 168 * 169 * @param {PositiveInteger} W - window size 170 * @param {number} [meanx] - mean value 171 * @param {number} [meany] - mean value 172 * @throws {TypeError} first argument must be a positive integer 173 * @throws {TypeError} second argument must be a number primitive 174 * @throws {TypeError} third argument must be a number primitive 175 * @returns {Function} accumulator function 176 * 177 * @example 178 * var accumulator = incrmcovariance( 3 ); 179 * 180 * var v = accumulator(); 181 * // returns null 182 * 183 * v = accumulator( 2.0, 1.0 ); 184 * // returns 0.0 185 * 186 * v = accumulator( -5.0, 3.14 ); 187 * // returns ~-7.49 188 * 189 * v = accumulator( 3.0, -1.0 ); 190 * // returns -8.35 191 * 192 * v = accumulator( 5.0, -9.5 ); 193 * // returns -29.42 194 * 195 * v = accumulator(); 196 * // returns -29.42 197 * 198 * @example 199 * var accumulator = incrmcovariance( 3, -2.0, 10.0 ); 200 */ 201 function incrmcovariance( W, meanx, meany ) { 202 var buf; 203 var dx0; 204 var dxN; 205 var dy0; 206 var dyN; 207 var mx; 208 var my; 209 var wi; 210 var C; 211 var N; 212 var n; 213 var i; 214 if ( !isPositiveInteger( W ) ) { 215 throw new TypeError( 'invalid argument. First argument must be a positive integer. Value: `' + W + '`.' ); 216 } 217 buf = new Float64Array( 2*W ); // strided array 218 n = W - 1; 219 C = 0.0; 220 wi = -1; 221 N = 0; 222 if ( arguments.length > 1 ) { 223 if ( !isNumber( meanx ) ) { 224 throw new TypeError( 'invalid argument. Second argument must be a number primitive. Value: `' + meanx + '`.' ); 225 } 226 if ( !isNumber( meany ) ) { 227 throw new TypeError( 'invalid argument. Third argument must be a number primitive. Value: `' + meany + '`.' ); 228 } 229 mx = meanx; 230 my = meany; 231 return accumulator2; 232 } 233 mx = 0.0; 234 my = 0.0; 235 return accumulator1; 236 237 /** 238 * If provided a value, the accumulator function returns an updated unbiased sample covariance. If not provided a value, the accumulator function returns the current unbiased sample covariance. 239 * 240 * @private 241 * @param {number} [x] - input value 242 * @param {number} [y] - input value 243 * @returns {(number|null)} unbiased sample covariance or null 244 */ 245 function accumulator1( x, y ) { 246 var v1; 247 var v2; 248 var k; 249 var j; 250 if ( arguments.length === 0 ) { 251 if ( N === 0 ) { 252 return null; 253 } 254 if ( N === 1 ) { 255 return 0.0; 256 } 257 if ( N < W ) { 258 return C / (N-1); 259 } 260 return C / n; 261 } 262 // Update the window and strided array indices for managing the circular buffer: 263 wi = (wi+1) % W; 264 i = 2 * wi; 265 266 // Case: an incoming value is NaN, the sliding co-moment is automatically NaN... 267 if ( isnan( x ) || isnan( y ) ) { 268 N = W; // explicitly set to avoid `N < W` branch 269 C = NaN; 270 } 271 // Case: initial window... 272 else if ( N < W ) { 273 buf[ i ] = x; // update buffer 274 buf[ i+1 ] = y; 275 276 N += 1; 277 dxN = x - mx; 278 mx += dxN / N; 279 my += ( y-my ) / N; 280 C += dxN * ( y-my ); // Note: repeated `y-my` is intentional, as `my` is updated when used here 281 if ( N === 1 ) { 282 return 0.0; 283 } 284 return C / (N-1); 285 } 286 // Case: N = W = 1 287 else if ( N === 1 ) { 288 return 0.0; 289 } 290 // Case: an outgoing value is NaN, and, thus, we need to compute the accumulated values... 291 else if ( isnan( buf[ i ] ) || isnan( buf[ i+1 ] ) ) { 292 N = 1; 293 mx = x; 294 my = y; 295 C = 0.0; 296 for ( k = 0; k < W; k++ ) { 297 j = 2 * k; // convert to a strided array index 298 if ( j !== i ) { 299 v1 = buf[ j ]; 300 v2 = buf[ j+1 ]; 301 if ( isnan( v1 ) || isnan( v2 ) ) { 302 N = W; // explicitly set to avoid `N < W` branch 303 C = NaN; 304 break; // co-moment is automatically NaN, so no need to continue 305 } 306 N += 1; 307 dxN = v1 - mx; 308 mx += dxN / N; 309 my += ( v2-my ) / N; 310 C += dxN * ( v2-my ); // Note: repeated `y-my` is intentional, as `my` is updated when used here 311 } 312 } 313 } 314 // Case: neither the current co-moment nor the incoming values are NaN, so we need to update the accumulated values... 315 else if ( isnan( C ) === false ) { 316 dx0 = buf[ i ] - mx; 317 dy0 = buf[ i+1 ] - my; 318 dxN = x - mx; 319 dyN = y - my; 320 C += (dxN*dyN) - (dx0*dy0) - ( (dxN-dx0)*(dyN-dy0)/W ); 321 mx += ( dxN-dx0 ) / W; 322 my += ( dyN-dy0 ) / W; 323 } 324 // Case: the current co-moment is NaN, so nothing to do until the buffer no longer contains NaN values... 325 buf[ i ] = x; 326 buf[ i+1 ] = y; 327 328 return C / n; 329 } 330 331 /** 332 * If provided a value, the accumulator function returns an updated unbiased sample covariance. If not provided a value, the accumulator function returns the current unbiased sample covariance. 333 * 334 * @private 335 * @param {number} [x] - input value 336 * @param {number} [y] - input value 337 * @returns {(number|null)} unbiased sample covariance or null 338 */ 339 function accumulator2( x, y ) { 340 var k; 341 var j; 342 if ( arguments.length === 0 ) { 343 if ( N === 0 ) { 344 return null; 345 } 346 if ( N < W ) { 347 return C / N; 348 } 349 return C / W; 350 } 351 // Update the window and strided array indices for managing the circular buffer: 352 wi = (wi+1) % W; 353 i = 2 * wi; 354 355 // Case: an incoming value is NaN, the sliding co-moment is automatically NaN... 356 if ( isnan( x ) || isnan( y ) ) { 357 N = W; // explicitly set to avoid `N < W` branch 358 C = NaN; 359 } 360 // Case: initial window... 361 else if ( N < W ) { 362 buf[ i ] = x; // update buffer 363 buf[ i+1 ] = y; 364 365 N += 1; 366 C += ( x-mx ) * ( y-my ); 367 return C / N; 368 } 369 // Case: an outgoing value is NaN, and, thus, we need to compute the accumulated values... 370 else if ( isnan( buf[ i ] ) || isnan( buf[ i+1 ] ) ) { 371 C = 0.0; 372 for ( k = 0; k < W; k++ ) { 373 j = 2 * k; // convert to a strided array index 374 if ( j !== i ) { 375 if ( isnan( buf[ j ] ) || isnan( buf[ j+1 ] ) ) { 376 N = W; // explicitly set to avoid `N < W` branch 377 C = NaN; 378 break; // co-moment is automatically NaN, so no need to continue 379 } 380 C += ( buf[j]-mx ) * ( buf[j+1]-my ); 381 } 382 } 383 } 384 // Case: neither the current co-moment nor the incoming values are NaN, so we need to update the accumulated values... 385 else if ( isnan( C ) === false ) { 386 // Use textbook formula along with simplification arising from difference of sums: 387 C += ( (x-mx)*(y-my) ) - ( (buf[i]-mx)*(buf[i+1]-my) ); 388 } 389 // Case: the current co-moment is NaN, so nothing to do until the buffer no longer contains NaN values... 390 buf[ i ] = x; 391 buf[ i+1 ] = y; 392 393 return C / W; 394 } 395 } 396 397 398 // EXPORTS // 399 400 module.exports = incrmcovariance;