main.js (10375B)
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 isSquareMatrix = require( '@stdlib/assert/is-square-matrix' ); 25 var isVectorLike = require( '@stdlib/assert/is-vector-like' ); 26 var Float64Array = require( '@stdlib/array/float64' ); 27 var ctor = require( '@stdlib/ndarray/ctor' ); 28 var bctor = require( '@stdlib/ndarray/base/ctor' ); 29 var numel = require( '@stdlib/ndarray/base/numel' ); 30 31 32 // FUNCTIONS // 33 34 /** 35 * Returns a matrix. 36 * 37 * @private 38 * @param {PositiveInteger} n - matrix order 39 * @param {boolean} bool - boolean indicating whether to create a low-level ndarray 40 * @returns {ndarray} matrix 41 */ 42 function createMatrix( n, bool ) { 43 var strides; 44 var buffer; 45 var shape; 46 var f; 47 48 if ( bool ) { 49 f = bctor; 50 } else { 51 f = ctor; 52 } 53 buffer = new Float64Array( n*n ); 54 shape = [ n, n ]; 55 strides = [ n, 1 ]; 56 return f( 'float64', buffer, shape, strides, 0, 'row-major' ); 57 } 58 59 /** 60 * Returns a vector. 61 * 62 * @private 63 * @param {PositiveInteger} N - number of elements 64 * @returns {ndarray} vector 65 */ 66 function createVector( N ) { 67 var strides; 68 var buffer; 69 var shape; 70 71 buffer = new Float64Array( N ); 72 shape = [ N ]; 73 strides = [ 1 ]; 74 75 return bctor( 'float64', buffer, shape, strides, 0, 'row-major' ); 76 } 77 78 79 // MAIN // 80 81 /** 82 * Returns an accumulator function which incrementally computes an unbiased sample covariance matrix. 83 * 84 * ## Method 85 * 86 * - For each unbiased sample covariance, we begin by defining the co-moment \\(C_{jn}\\) 87 * 88 * ```tex 89 * C_n = \sum_{i=1}^{n} ( x_i - \bar{x}_n ) ( y_i - \bar{y}_n ) 90 * ``` 91 * 92 * where \\(\bar{x}_n\\) and \\(\bar{y}_n\\) are the sample means for \\(x\\) and \\(y\\), respectively. 93 * 94 * - Based on Welford's method, we know the update formulas for the sample means are given by 95 * 96 * ```tex 97 * \bar{x}_n = \bar{x}_{n-1} + \frac{x_n - \bar{x}_{n-1}}{n} 98 * ``` 99 * 100 * and 101 * 102 * ```tex 103 * \bar{y}_n = \bar{y}_{n-1} + \frac{y_n - \bar{y}_{n-1}}{n} 104 * ``` 105 * 106 * - Substituting into the equation for \\(C_n\\) and rearranging terms 107 * 108 * ```tex 109 * C_n = C_{n-1} + (x_n - \bar{x}_n) (y_n - \bar{y}_{n-1}) 110 * ``` 111 * 112 * where the apparent asymmetry arises from 113 * 114 * ```tex 115 * x_n - \bar{x}_n = \frac{n-1}{n} (x_n - \bar{x}_{n-1}) 116 * ``` 117 * 118 * and, hence, the update term can be equivalently expressed 119 * 120 * ```tex 121 * \frac{n-1}{n} (x_n - \bar{x}_{n-1}) (y_n - \bar{y}_{n-1}) 122 * ``` 123 * 124 * - The covariance can be defined 125 * 126 * ```tex 127 * \begin{align*} 128 * \operatorname{cov}_n(x,y) &= \frac{C_n}{n} \\ 129 * &= \frac{C_{n-1} + \frac{n-1}{n} (x_n - \bar{x}_{n-1}) (y_n - \bar{y}_{n-1})}{n} \\ 130 * &= \frac{(n-1)\operatorname{cov}_{n-1}(x,y) + \frac{n-1}{n} (x_n - \bar{x}_{n-1}) (y_n - \bar{y}_{n-1})}{n} 131 * \end{align*} 132 * ``` 133 * 134 * - Applying Bessel's correction, we arrive at an update formula for calculating an unbiased sample covariance 135 * 136 * ```tex 137 * \begin{align*} 138 * \operatorname{cov}_n(x,y) &= \frac{n}{n-1}\cdot\frac{(n-1)\operatorname{cov}_{n-1}(x,y) + \frac{n-1}{n} (x_n - \bar{x}_{n-1}) (y_n - \bar{y}_{n-1})}{n} \\ 139 * &= \operatorname{cov}_{n-1}(x,y) + \frac{(x_n - \bar{x}_{n-1}) (y_n - \bar{y}_{n-1})}{n} \\ 140 * &= \frac{C_{n-1}}{n-1} + \frac{(x_n - \bar{x}_{n-1}) (y_n - \bar{y}_{n-1})}{n} 141 * &= \frac{C_{n-1} + \frac{n-1}{n} (x_n - \bar{x}_{n-1}) (y_n - \bar{y}_{n-1})}{n-1} 142 * \end{align*} 143 * ``` 144 * 145 * @param {(PositiveInteger|ndarray)} out - order of the covariance matrix or a square 2-dimensional output ndarray for storing the covariance matrix 146 * @param {ndarray} [means] - mean values 147 * @throws {TypeError} first argument must be either a positive integer or a 2-dimensional ndarray having equal dimensions 148 * @throws {TypeError} second argument must be a 1-dimensional ndarray 149 * @throws {Error} number of means must match covariance matrix dimensions 150 * @returns {Function} accumulator function 151 * 152 * @example 153 * var Float64Array = require( '@stdlib/array/float64' ); 154 * var ndarray = require( '@stdlib/ndarray/ctor' ); 155 * 156 * // Create an output covariance matrix: 157 * var buffer = new Float64Array( 4 ); 158 * var shape = [ 2, 2 ]; 159 * var strides = [ 2, 1 ]; 160 * var offset = 0; 161 * var order = 'row-major'; 162 * 163 * var cov = ndarray( 'float64', buffer, shape, strides, offset, order ); 164 * 165 * // Create a covariance matrix accumulator: 166 * var accumulator = incrcovmat( cov ); 167 * 168 * var out = accumulator(); 169 * // returns null 170 * 171 * // Create a data vector: 172 * buffer = new Float64Array( 2 ); 173 * shape = [ 2 ]; 174 * strides = [ 1 ]; 175 * 176 * var vec = ndarray( 'float64', buffer, shape, strides, offset, order ); 177 * 178 * // Provide data to the accumulator: 179 * vec.set( 0, 2.0 ); 180 * vec.set( 1, 1.0 ); 181 * 182 * out = accumulator( vec ); 183 * // returns <ndarray> 184 * 185 * var bool = ( out === cov ); 186 * // returns true 187 * 188 * vec.set( 0, -5.0 ); 189 * vec.set( 1, 3.14 ); 190 * 191 * out = accumulator( vec ); 192 * // returns <ndarray> 193 * 194 * // Retrieve the covariance matrix: 195 * out = accumulator(); 196 * // returns <ndarray> 197 */ 198 function incrcovmat( out, means ) { 199 var order; 200 var cov; 201 var mu; 202 var C; 203 var d; 204 var N; 205 206 N = 0; 207 if ( isPositiveInteger( out ) ) { 208 order = out; 209 cov = createMatrix( order, false ); 210 } else if ( isSquareMatrix( out ) ) { 211 order = out.shape[ 0 ]; 212 cov = out; 213 } else { 214 throw new TypeError( 'invalid argument. First argument must either specify the order of the covariance matrix or be a square 2-dimensional ndarray for storing the covariance matrix. Value: `' + out + '`.' ); 215 } 216 // Create a scratch array for storing residuals (i.e., `x_i - xbar_{i-1}`): 217 d = new Float64Array( order ); 218 219 // Create a low-level scratch matrix for storing co-moments: 220 C = createMatrix( order, true ); 221 222 if ( arguments.length > 1 ) { 223 if ( !isVectorLike( means ) ) { 224 throw new TypeError( 'invalid argument. Second argument must be a 1-dimensional ndarray. Value: `' + means + '`.' ); 225 } 226 if ( numel( means.shape ) !== order ) { 227 throw new Error( 'invalid argument. The number of elements (means) in the second argument must match covariance matrix dimensions. Expected: '+order+'. Actual: '+numel( means.shape )+'.' ); 228 } 229 mu = means; // TODO: should we copy this? Otherwise, internal state could be "corrupted" due to mutation outside the accumulator 230 return accumulator2; 231 } 232 // Create an ndarray vector for storing sample means (note: an ndarray interface is not necessary, but it reduces implementation complexity by ensuring a consistent abstraction for accessing and updating sample means): 233 mu = createVector( order ); 234 235 return accumulator1; 236 237 /** 238 * If provided a data vector, the accumulator function returns an updated unbiased sample covariance matrix. If not provided a data vector, the accumulator function returns the current unbiased sample covariance matrix. 239 * 240 * @private 241 * @param {ndarray} [v] - data vector 242 * @throws {TypeError} must provide a 1-dimensional ndarray 243 * @throws {Error} vector length must match covariance matrix dimensions 244 * @returns {(ndarray|null)} unbiased sample covariance matrix or null 245 */ 246 function accumulator1( v ) { 247 var covij; 248 var denom; 249 var rdx; 250 var cij; 251 var m; 252 var n; 253 var r; 254 var i; 255 var j; 256 if ( arguments.length === 0 ) { 257 if ( N === 0 ) { 258 return null; 259 } 260 return cov; 261 } 262 if ( !isVectorLike( v ) ) { 263 throw new TypeError( 'invalid argument. Must provide a 1-dimensional ndarray. Value: `' + v + '`.' ); 264 } 265 if ( v.shape[ 0 ] !== order ) { 266 throw new Error( 'invalid argument. Vector length must match covariance matrix dimensions. Expected: '+order+'. Actual: '+v.shape[ 0 ]+'.' ); 267 } 268 n = N; 269 N += 1; 270 r = n / N; 271 272 denom = n || 1; // Bessel's correction (avoiding divide-by-zero below) 273 274 for ( i = 0; i < order; i++ ) { 275 m = mu.get( i ); 276 277 // Compute the residual: 278 d[ i ] = v.get( i ) - m; 279 280 // Update the sample mean: 281 m += d[ i ] / N; 282 mu.set( i, m ); 283 284 // Update the co-moments and covariance matrix, recognizing that the covariance matrix is symmetric... 285 rdx = r * d[ i ]; // if `n=0`, `r=0.0` 286 for ( j = 0; j <= i; j++ ) { 287 cij = C.get( i, j ) + ( rdx*d[j] ); 288 C.set( i, j, cij ); 289 C.set( j, i, cij ); // via symmetry 290 291 covij = cij / denom; 292 cov.set( i, j, covij ); 293 cov.set( j, i, covij ); // via symmetry 294 } 295 } 296 return cov; 297 } 298 299 /** 300 * If provided a data vector, the accumulator function returns an updated unbiased sample covariance matrix. If not provided a data vector, the accumulator function returns the current unbiased sample covariance matrix. 301 * 302 * @private 303 * @param {ndarray} [v] - data vector 304 * @throws {TypeError} must provide a 1-dimensional ndarray 305 * @throws {Error} vector length must match covariance matrix dimensions 306 * @returns {(ndarray|null)} unbiased sample covariance matrix or null 307 */ 308 function accumulator2( v ) { 309 var covij; 310 var cij; 311 var di; 312 var i; 313 var j; 314 if ( arguments.length === 0 ) { 315 if ( N === 0 ) { 316 return null; 317 } 318 return cov; 319 } 320 if ( !isVectorLike( v ) ) { 321 throw new TypeError( 'invalid argument. Must provide a 1-dimensional ndarray. Value: `' + v + '`.' ); 322 } 323 if ( v.shape[ 0 ] !== order ) { 324 throw new Error( 'invalid argument. Vector length must match covariance matrix dimensions. Expected: '+order+'. Actual: '+v.shape[ 0 ]+'.' ); 325 } 326 N += 1; 327 for ( i = 0; i < order; i++ ) { 328 // Compute the residual: 329 d[ i ] = v.get( i ) - mu.get( i ); 330 331 // Update the co-moments and covariance matrix, recognizing that the covariance matrix is symmetric... 332 di = d[ i ]; 333 for ( j = 0; j <= i; j++ ) { 334 cij = C.get( i, j ) + ( di*d[j] ); 335 C.set( i, j, cij ); 336 C.set( j, i, cij ); // via symmetry 337 338 covij = cij / N; 339 cov.set( i, j, covij ); 340 cov.set( j, i, covij ); // via symmetry 341 } 342 } 343 return cov; 344 } 345 } 346 347 348 // EXPORTS // 349 350 module.exports = incrcovmat;