main.js (8401B)
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 hasOwnProp = require( '@stdlib/assert/has-own-property' ); 24 var isObject = require( '@stdlib/assert/is-plain-object' ); 25 var isPositiveInteger = require( '@stdlib/assert/is-positive-integer' ).isPrimitive; 26 var isBoolean = require( '@stdlib/assert/is-boolean' ).isPrimitive; 27 var incrminmax = require( './../../../incr/minmax' ); 28 var incrmeanstdev = require( './../../../incr/meanstdev' ); 29 var copy = require( '@stdlib/utils/copy' ); 30 var setReadOnly = require( '@stdlib/utils/define-read-only-property' ); 31 var setReadOnlyAccessor = require( '@stdlib/utils/define-read-only-accessor' ); 32 var max = require( '@stdlib/math/base/special/max' ); 33 var sqrt = require( '@stdlib/math/base/special/sqrt' ); 34 var roundn = require( '@stdlib/math/base/special/roundn' ); 35 var tQuantile = require( './../../../base/dists/t/quantile' ); 36 var validate = require( './validate.js' ); 37 var defaults = require( './defaults.json' ); 38 39 40 // MAIN // 41 42 /** 43 * Returns an accumulator function which incrementally performs Grubbs' test for detecting outliers. 44 * 45 * @param {Options} [options] - function options 46 * @param {number} [options.alpha=0.05] - significance level 47 * @param {string} [options.alternative='two-sided'] - alternative hypothesis ('two-sided', 'min', 'max') 48 * @param {NonNegativeInteger} [options.init=100] - number of data points used to compute initial statistics 49 * @throws {TypeError} options argument must be an object 50 * @throws {TypeError} must provide valid options 51 * @throws {RangeError} `alpha` option must be on the interval `[0,1]` 52 * @returns {Function} accumulator function 53 * 54 * @example 55 * var rnorm = require( '@stdlib/random/base/normal' ); 56 * 57 * var accumulator; 58 * var opts; 59 * var res; 60 * var i; 61 * 62 * opts = { 63 * 'init': 100 64 * }; 65 * 66 * accumulator = incrgrubbs( opts ); 67 * 68 * for ( i = 0; i < 200; i++ ) { 69 * res = accumulator( rnorm( 10.0, 5.0 ) ); 70 * } 71 */ 72 function incrgrubbs() { 73 var meanstdev; 74 var results; 75 var minmax; 76 var opts; 77 var err; 78 var mm; 79 var ms; 80 var gc; 81 var df; 82 var N; 83 var G; 84 85 opts = copy( defaults ); 86 if ( arguments.length ) { 87 err = validate( opts, arguments[ 0 ] ); 88 if ( err ) { 89 throw err; 90 } 91 } 92 // Initialize the results object: 93 results = {}; 94 setReadOnlyAccessor( results, 'rejected', getRejected ); 95 setReadOnly( results, 'alpha', opts.alpha ); 96 setReadOnlyAccessor( results, 'criticalValue', getCriticalValue ); 97 setReadOnlyAccessor( results, 'statistic', getStatistic ); 98 setReadOnlyAccessor( results, 'df', getDOF ); 99 setReadOnlyAccessor( results, 'mean', getMean ); 100 setReadOnlyAccessor( results, 'sd', getStDev ); 101 setReadOnlyAccessor( results, 'min', getMin ); 102 setReadOnlyAccessor( results, 'max', getMax ); 103 setReadOnly( results, 'alt', opts.alternative ); 104 setReadOnly( results, 'method', 'Grubbs\' Test' ); 105 setReadOnly( results, 'print', print ); 106 107 N = 0; 108 df = 0; 109 G = 0.0; 110 gc = 0.0; 111 112 // Initialize statistics accumulators: 113 mm = [ 0.0, 0.0 ]; 114 minmax = incrminmax( mm ); 115 116 ms = [ 0.0, 0.0 ]; 117 meanstdev = incrmeanstdev( ms ); 118 119 return accumulator; 120 121 /** 122 * If provided a value, the accumulator function returns updated Grubbs' test results. If not provided a value, the accumulator function returns the current Grubbs' test results. 123 * 124 * @private 125 * @param {number} [x] - new value 126 * @returns {(Object|null)} test results or null 127 */ 128 function accumulator( x ) { 129 var sig; 130 var md; 131 var tc; 132 if ( arguments.length === 0 ) { 133 if ( N < opts.init || df <= 0 ) { 134 return null; 135 } 136 return results; 137 } 138 N += 1; 139 140 // Update model statistics: 141 meanstdev( x ); 142 minmax( x ); 143 144 // Compute the degrees of freedom: 145 df = N - 2; 146 147 if ( N < opts.init || df <= 0 ) { 148 return null; 149 } 150 // Compute the test statistic and significance level... 151 if ( opts.alternative === 'min' ) { 152 G = ( ms[0]-mm[0] ) / ms[ 1 ]; 153 sig = opts.alpha / N; 154 } else if ( opts.alternative === 'max' ) { 155 G = ( mm[1]-ms[0] ) / ms[ 1 ]; 156 sig = opts.alpha / N; 157 } else { // two-sided 158 md = max( ms[0]-mm[0], mm[1]-ms[0] ); // maximum absolute deviation 159 G = md / ms[ 1 ]; 160 sig = opts.alpha / (2*N); 161 } 162 // Compute the critical values: 163 tc = tQuantile( 1.0-sig, df ); 164 gc = (N-1)*tc / sqrt( N*(df+(tc*tc)) ); 165 166 return results; 167 } 168 169 /** 170 * Returns a `boolean` indicating whether the null hypothesis should be rejected. 171 * 172 * @private 173 * @returns {boolean} boolean indicating whether the null hypothesis should be rejected 174 */ 175 function getRejected() { 176 return ( G > gc ); 177 } 178 179 /** 180 * Returns the critical value. 181 * 182 * @private 183 * @returns {number} critical value 184 */ 185 function getCriticalValue() { 186 return gc; 187 } 188 189 /** 190 * Returns the test statistic. 191 * 192 * @private 193 * @returns {number} test statistic 194 */ 195 function getStatistic() { 196 return G; 197 } 198 199 /** 200 * Returns the degrees of freedom (DOF). 201 * 202 * @private 203 * @returns {PositiveInteger} degrees of freedom 204 */ 205 function getDOF() { 206 return df; 207 } 208 209 /** 210 * Returns the sample mean. 211 * 212 * @private 213 * @returns {number} sample mean 214 */ 215 function getMean() { 216 return ms[ 0 ]; 217 } 218 219 /** 220 * Returns the corrected sample standard deviation. 221 * 222 * @private 223 * @returns {number} corrected sample standard deviation 224 */ 225 function getStDev() { 226 return ms[ 1 ]; 227 } 228 229 /** 230 * Returns the sample minimum. 231 * 232 * @private 233 * @returns {number} sample minimum 234 */ 235 function getMin() { 236 return mm[ 0 ]; 237 } 238 239 /** 240 * Returns the sample maximum. 241 * 242 * @private 243 * @returns {number} sample maximum 244 */ 245 function getMax() { 246 return mm[ 1 ]; 247 } 248 249 /** 250 * Pretty-print test results. 251 * 252 * @private 253 * @param {Object} [options] - options object 254 * @param {PositiveInteger} [options.digits=4] - number of digits after the decimal point 255 * @param {boolean} [options.decision=true] - boolean indicating whether to print the test decision 256 * @throws {TypeError} options argument must be an object 257 * @throws {TypeError} must provide valid options 258 * @returns {string} formatted output 259 */ 260 function print( options ) { 261 var decision; 262 var digits; 263 var str; 264 265 digits = opts.digits; 266 decision = opts.decision; 267 if ( arguments.length > 0 ) { 268 if ( !isObject( options ) ) { 269 throw new TypeError( 'invalid argument. Must provide an object. Value: `' + options + '`.' ); 270 } 271 if ( hasOwnProp( options, 'digits' ) ) { 272 if ( !isPositiveInteger( options.digits ) ) { 273 throw new TypeError( 'invalid option. `digits` option must be a positive integer. Option: `' + options.digits + '`.' ); 274 } 275 digits = options.digits; 276 } 277 if ( hasOwnProp( options, 'decision' ) ) { 278 if ( !isBoolean( options.decision ) ) { 279 throw new TypeError( 'invalid option. `decision` option must be boolean. Option: `' + options.decision + '`.' ); 280 } 281 decision = options.decision; 282 } 283 } 284 str = ''; 285 str += results.method; 286 str += '\n\n'; 287 str += 'Alternative hypothesis: '; 288 if ( opts.alternative === 'max' ) { 289 str += 'The maximum value (' + mm[ 1 ] + ') is an outlier'; 290 } else if ( opts.alternative === 'min' ) { 291 str += 'The minimum value (' + mm[ 0 ] + ') is an outlier'; 292 } else { // two-sided 293 str += 'The '; 294 if ( ms[0]-mm[0] > mm[1]-ms[0] ) { 295 str += 'minimum value (' + mm[ 0 ] + ')'; 296 } else { 297 str += 'maximum value (' + mm[ 1 ] + ')'; 298 } 299 str += ' is an outlier'; 300 } 301 str += '\n\n'; 302 str += ' criticalValue: ' + roundn( gc, -digits ) + '\n'; 303 str += ' statistic: ' + roundn( G, -digits ) + '\n'; 304 str += ' df: ' + df + '\n'; 305 str += '\n'; 306 if ( decision ) { 307 str += 'Test Decision: '; 308 if ( G > gc ) { 309 str += 'Reject null in favor of alternative at ' + (opts.alpha*100.0) + '% significance level'; 310 } else { 311 str += 'Fail to reject null in favor of alternative at ' + (opts.alpha*100.0) + '% significance level'; 312 } 313 str += '\n'; 314 } 315 return str; 316 } 317 } 318 319 320 // EXPORTS // 321 322 module.exports = incrgrubbs;