318 lines
8.4 KiB
JavaScript
318 lines
8.4 KiB
JavaScript
/**
|
|
* @license Apache-2.0
|
|
*
|
|
* Copyright (c) 2018 The Stdlib Authors.
|
|
*
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
* you may not use this file except in compliance with the License.
|
|
* You may obtain a copy of the License at
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
* See the License for the specific language governing permissions and
|
|
* limitations under the License.
|
|
*/
|
|
|
|
'use strict';
|
|
|
|
// MODULES //
|
|
|
|
var hasOwnProp = require( '@stdlib/assert/has-own-property' );
|
|
var isObject = require( '@stdlib/assert/is-plain-object' );
|
|
var isPositiveInteger = require( '@stdlib/assert/is-positive-integer' ).isPrimitive;
|
|
var isBoolean = require( '@stdlib/assert/is-boolean' ).isPrimitive;
|
|
var copy = require( '@stdlib/utils/copy' );
|
|
var setReadOnly = require( '@stdlib/utils/define-read-only-property' );
|
|
var setReadOnlyAccessor = require( '@stdlib/utils/define-read-only-accessor' );
|
|
var max = require( '@stdlib/math/base/special/max' );
|
|
var sqrt = require( '@stdlib/math/base/special/sqrt' );
|
|
var roundn = require( '@stdlib/math/base/special/roundn' );
|
|
var tQuantile = require( './../../../base/dists/t/quantile' );
|
|
var Float64Array = require( '@stdlib/array/float64' );
|
|
var validate = require( './validate.js' );
|
|
var defaults = require( './defaults.json' );
|
|
var incrmminmax = require( './minmax.js' );
|
|
var incrmmeanstdev = require( './meanstdev.js' );
|
|
|
|
|
|
// MAIN //
|
|
|
|
/**
|
|
* Returns an accumulator function which incrementally performs a moving Grubbs' test for detecting outliers.
|
|
*
|
|
* @param {PositiveInteger} W - window size
|
|
* @param {Options} [options] - function options
|
|
* @param {number} [options.alpha=0.05] - significance level
|
|
* @param {string} [options.alternative='two-sided'] - alternative hypothesis ('two-sided', 'min', 'max')
|
|
* @throws {TypeError} first argument must be a positive integer
|
|
* @throws {RangeError} first argument must be greater than or equal to 3
|
|
* @throws {TypeError} options argument must be an object
|
|
* @throws {TypeError} must provide valid options
|
|
* @throws {RangeError} `alpha` option must be on the interval `[0,1]`
|
|
* @returns {Function} accumulator function
|
|
*
|
|
* @example
|
|
* var rnorm = require( '@stdlib/random/base/normal' );
|
|
*
|
|
* var accumulator;
|
|
* var opts;
|
|
* var i;
|
|
*
|
|
* accumulator = incrmgrubbs( 20, opts );
|
|
*
|
|
* for ( i = 0; i < 200; i++ ) {
|
|
* res = accumulator( rnorm( 10.0, 5.0 ) );
|
|
* }
|
|
*/
|
|
function incrmgrubbs( W ) {
|
|
var meanstdev;
|
|
var results;
|
|
var minmax;
|
|
var opts;
|
|
var err;
|
|
var buf;
|
|
var sig;
|
|
var mm;
|
|
var ms;
|
|
var tc;
|
|
var gc;
|
|
var df;
|
|
var N;
|
|
var G;
|
|
var i;
|
|
|
|
if ( !isPositiveInteger( W ) ) {
|
|
throw new TypeError( 'invalid argument. Window size must be a positive integer. Value: `' + W + '`.' );
|
|
}
|
|
if ( W < 3 ) {
|
|
throw new RangeError( 'invalid argument. Window size must be greater than or equal to 3. Value: `' + W + '`.' );
|
|
}
|
|
opts = copy( defaults );
|
|
if ( arguments.length > 1 ) {
|
|
err = validate( opts, arguments[ 1 ] );
|
|
if ( err ) {
|
|
throw err;
|
|
}
|
|
}
|
|
buf = new Float64Array( W );
|
|
df = W - 2;
|
|
gc = 0.0;
|
|
G = 0.0;
|
|
N = 0;
|
|
i = -1;
|
|
|
|
// Compute the critical values:
|
|
if ( opts.alternative === 'min' ) {
|
|
sig = opts.alpha / W;
|
|
} else if ( opts.alternative === 'max' ) {
|
|
sig = opts.alpha / W;
|
|
} else { // two-sided
|
|
sig = opts.alpha / (2*W);
|
|
}
|
|
tc = tQuantile( 1.0-sig, df );
|
|
gc = (W-1)*tc / sqrt( W*(df+(tc*tc)) );
|
|
|
|
// Initialize statistics accumulators:
|
|
mm = [ 0.0, 0.0 ];
|
|
minmax = incrmminmax( mm, W, buf );
|
|
|
|
ms = [ 0.0, 0.0 ];
|
|
meanstdev = incrmmeanstdev( ms, W, buf );
|
|
|
|
// Initialize the results object:
|
|
results = {};
|
|
setReadOnlyAccessor( results, 'rejected', getRejected );
|
|
setReadOnly( results, 'alpha', opts.alpha );
|
|
setReadOnly( results, 'criticalValue', gc );
|
|
setReadOnlyAccessor( results, 'statistic', getStatistic );
|
|
setReadOnly( results, 'df', df );
|
|
setReadOnlyAccessor( results, 'mean', getMean );
|
|
setReadOnlyAccessor( results, 'sd', getStDev );
|
|
setReadOnlyAccessor( results, 'min', getMin );
|
|
setReadOnlyAccessor( results, 'max', getMax );
|
|
setReadOnly( results, 'alt', opts.alternative );
|
|
setReadOnly( results, 'method', 'Grubbs\' Test' );
|
|
setReadOnly( results, 'print', print );
|
|
|
|
return accumulator;
|
|
|
|
/**
|
|
* 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.
|
|
*
|
|
* @private
|
|
* @param {number} [x] - new value
|
|
* @returns {(Object|null)} test results or null
|
|
*/
|
|
function accumulator( x ) {
|
|
var md;
|
|
if ( arguments.length === 0 ) {
|
|
if ( N < W ) {
|
|
return null;
|
|
}
|
|
return results;
|
|
}
|
|
N += 1;
|
|
|
|
// Update the index for managing the circular buffer:
|
|
i = (i+1) % W;
|
|
|
|
// Update model statistics:
|
|
meanstdev( x, i );
|
|
minmax( x, i );
|
|
|
|
// Insert the value into the buffer:
|
|
buf[ i ] = x;
|
|
|
|
if ( N < W ) {
|
|
return null;
|
|
}
|
|
// Compute the test statistic...
|
|
if ( opts.alternative === 'min' ) {
|
|
G = ( ms[0]-mm[0] ) / ms[ 1 ];
|
|
} else if ( opts.alternative === 'max' ) {
|
|
G = ( mm[1]-ms[0] ) / ms[ 1 ];
|
|
} else { // two-sided
|
|
md = max( ms[0]-mm[0], mm[1]-ms[0] ); // maximum absolute deviation
|
|
G = md / ms[ 1 ];
|
|
}
|
|
return results;
|
|
}
|
|
|
|
/**
|
|
* Returns a `boolean` indicating whether the null hypothesis should be rejected.
|
|
*
|
|
* @private
|
|
* @returns {boolean} boolean indicating whether the null hypothesis should be rejected
|
|
*/
|
|
function getRejected() {
|
|
return ( G > gc );
|
|
}
|
|
|
|
/**
|
|
* Returns the test statistic.
|
|
*
|
|
* @private
|
|
* @returns {number} test statistic
|
|
*/
|
|
function getStatistic() {
|
|
return G;
|
|
}
|
|
|
|
/**
|
|
* Returns the sample mean.
|
|
*
|
|
* @private
|
|
* @returns {number} sample mean
|
|
*/
|
|
function getMean() {
|
|
return ms[ 0 ];
|
|
}
|
|
|
|
/**
|
|
* Returns the corrected sample standard deviation.
|
|
*
|
|
* @private
|
|
* @returns {number} corrected sample standard deviation
|
|
*/
|
|
function getStDev() {
|
|
return ms[ 1 ];
|
|
}
|
|
|
|
/**
|
|
* Returns the sample minimum.
|
|
*
|
|
* @private
|
|
* @returns {number} sample minimum
|
|
*/
|
|
function getMin() {
|
|
return mm[ 0 ];
|
|
}
|
|
|
|
/**
|
|
* Returns the sample maximum.
|
|
*
|
|
* @private
|
|
* @returns {number} sample maximum
|
|
*/
|
|
function getMax() {
|
|
return mm[ 1 ];
|
|
}
|
|
|
|
/**
|
|
* Pretty-print test results.
|
|
*
|
|
* @private
|
|
* @param {Object} [options] - options object
|
|
* @param {PositiveInteger} [options.digits=4] - number of digits after the decimal point
|
|
* @param {boolean} [options.decision=true] - boolean indicating whether to print the test decision
|
|
* @throws {TypeError} options argument must be an object
|
|
* @throws {TypeError} must provide valid options
|
|
* @returns {string} formatted output
|
|
*/
|
|
function print( options ) {
|
|
var decision;
|
|
var digits;
|
|
var str;
|
|
|
|
digits = opts.digits;
|
|
decision = opts.decision;
|
|
if ( arguments.length > 0 ) {
|
|
if ( !isObject( options ) ) {
|
|
throw new TypeError( 'invalid argument. Must provide an object. Value: `' + options + '`.' );
|
|
}
|
|
if ( hasOwnProp( options, 'digits' ) ) {
|
|
if ( !isPositiveInteger( options.digits ) ) {
|
|
throw new TypeError( 'invalid option. `digits` option must be a positive integer. Option: `' + options.digits + '`.' );
|
|
}
|
|
digits = options.digits;
|
|
}
|
|
if ( hasOwnProp( options, 'decision' ) ) {
|
|
if ( !isBoolean( options.decision ) ) {
|
|
throw new TypeError( 'invalid option. `decision` option must be boolean. Option: `' + options.decision + '`.' );
|
|
}
|
|
decision = options.decision;
|
|
}
|
|
}
|
|
str = '';
|
|
str += results.method;
|
|
str += '\n\n';
|
|
str += 'Alternative hypothesis: ';
|
|
if ( opts.alternative === 'max' ) {
|
|
str += 'The maximum value (' + mm[ 1 ] + ') is an outlier';
|
|
} else if ( opts.alternative === 'min' ) {
|
|
str += 'The minimum value (' + mm[ 0 ] + ') is an outlier';
|
|
} else { // two-sided
|
|
str += 'The ';
|
|
if ( ms[0]-mm[0] > mm[1]-ms[0] ) {
|
|
str += 'minimum value (' + mm[ 0 ] + ')';
|
|
} else {
|
|
str += 'maximum value (' + mm[ 1 ] + ')';
|
|
}
|
|
str += ' is an outlier';
|
|
}
|
|
str += '\n\n';
|
|
str += ' criticalValue: ' + roundn( gc, -digits ) + '\n';
|
|
str += ' statistic: ' + roundn( G, -digits ) + '\n';
|
|
str += ' df: ' + df + '\n';
|
|
str += '\n';
|
|
if ( decision ) {
|
|
str += 'Test Decision: ';
|
|
if ( G > gc ) {
|
|
str += 'Reject null in favor of alternative at ' + (opts.alpha*100.0) + '% significance level';
|
|
} else {
|
|
str += 'Fail to reject null in favor of alternative at ' + (opts.alpha*100.0) + '% significance level';
|
|
}
|
|
str += '\n';
|
|
}
|
|
return str;
|
|
}
|
|
}
|
|
|
|
|
|
// EXPORTS //
|
|
|
|
module.exports = incrmgrubbs;
|