time-to-botec

Benchmark sampling in different programming languages
Log | Files | Refs | README

main.js (8512B)


      1 /* eslint-disable max-statements, max-lines-per-function */
      2 
      3 /**
      4 * @license Apache-2.0
      5 *
      6 * Copyright (c) 2020 The Stdlib Authors.
      7 *
      8 * Licensed under the Apache License, Version 2.0 (the "License");
      9 * you may not use this file except in compliance with the License.
     10 * You may obtain a copy of the License at
     11 *
     12 *    http://www.apache.org/licenses/LICENSE-2.0
     13 *
     14 * Unless required by applicable law or agreed to in writing, software
     15 * distributed under the License is distributed on an "AS IS" BASIS,
     16 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     17 * See the License for the specific language governing permissions and
     18 * limitations under the License.
     19 */
     20 
     21 'use strict';
     22 
     23 // MODULES //
     24 
     25 var isNumberArray = require( '@stdlib/assert/is-number-array' ).primitives;
     26 var isTypedArrayLike = require( '@stdlib/assert/is-typed-array-like' );
     27 var setReadOnly = require( '@stdlib/utils/define-read-only-property' );
     28 var isObject = require( '@stdlib/assert/is-plain-object' );
     29 var ranks = require( './../../ranks' );
     30 var normalCDF = require( './../../base/dists/normal/cdf' ).factory;
     31 var signrankCDF = require( './../../base/dists/signrank/cdf' );
     32 var tabulate = require( '@stdlib/utils/tabulate' );
     33 var signum = require( '@stdlib/math/base/special/signum' );
     34 var sqrt = require( '@stdlib/math/base/special/sqrt' );
     35 var abs = require( '@stdlib/math/base/special/abs' );
     36 var Float64Array = require( '@stdlib/array/float64' );
     37 var validate = require( './validate.js' );
     38 var unique = require( './unique.js' );
     39 var print = require( './print.js' ); // eslint-disable-line stdlib/no-redeclare
     40 
     41 
     42 // VARIABLES //
     43 
     44 var pnorm = normalCDF( 0.0, 1.0 );
     45 
     46 
     47 // MAIN //
     48 
     49 /**
     50 * Computes a Wilcoxon signed rank test.
     51 *
     52 * @param {NumericArray} x - data array
     53 * @param {NumericArray} [y] - optional paired data array
     54 * @param {Options} [options] - function options
     55 * @param {number} [options.alpha=0.05] - significance level
     56 * @param {string} [options.alternative='two-sided'] - alternative hypothesis (`two-sided`, `less`, or `greater`)
     57 * @param {string} [options.zeroMethod='wilcox'] - method governing how zero-differences are handled (`pratt`, `wilcox`, or `zsplit`)
     58 * @param {boolean} [options.correction=true] - continuity correction adjusting the Wilcoxon rank statistic by 0.5 towards the mean
     59 * @param {boolean} [options.exact=false] - whether to force using the exact distribution instead of a normal approximation when there are more than fifty data points
     60 * @param {number} [options.mu=0] - location parameter under H0
     61 * @throws {TypeError} `x` must be a numeric array
     62 * @throws {TypeError} `y` must be a numeric array
     63 * @throws {TypeError} options has to be simple object
     64 * @throws {TypeError} `alpha` option has to be a number primitive
     65 * @throws {RangeError} `alpha` option has to be a number in the interval `[0,1]`
     66 * @throws {TypeError} `alternative` option has to be a string primitive
     67 * @throws {Error} `alternative` option must be `two-sided`, `less`, or `greater`
     68 * @throws {TypeError} `zeroMethod` option has to be a string primitive
     69 * @throws {Error} `zeroMethod` option must be `pratt`, `wilcox`, or `zsplit`
     70 * @throws {TypeError} `correction` option has to be a boolean primitive
     71 * @throws {TypeError} `exact` option has to be a boolean primitive
     72 * @throws {TypeError} `mu` option has to be a number primitive
     73 * @returns {Object} test result object
     74 *
     75 * @example
     76 * var x = [ 6, 8, 14, 16, 23, 24, 28, 29, 41, -48, 49, 56, 60, -67, 75 ];
     77 * var out = wilcoxon( x, {
     78 *     'mu': 2
     79 * });
     80 *
     81 * @example
     82 * var x = [ 6, 8, 14, 16, 23, 24, 28, 29, 41, -48, 49, 56, 60, -67, 75 ];
     83 * var out = wilcoxon( x, {
     84 *     'alternative': 'greater'
     85 * });
     86 */
     87 function wilcoxon() {
     88 	var correction;
     89 	var zeroMethod;
     90 	var options;
     91 	var hasTies;
     92 	var counts;
     93 	var repsum;
     94 	var rplus;
     95 	var nzero;
     96 	var rzero;
     97 	var alpha;
     98 	var pval;
     99 	var opts;
    100 	var stat;
    101 	var alt;
    102 	var err;
    103 	var len;
    104 	var tmp;
    105 	var out;
    106 	var ad;
    107 	var mu;
    108 	var mn;
    109 	var se;
    110 	var d;
    111 	var i;
    112 	var r;
    113 	var T;
    114 	var v;
    115 	var x;
    116 	var y;
    117 
    118 	x = arguments[ 0 ];
    119 	if ( !isTypedArrayLike( x ) && !isNumberArray( x ) ) {
    120 		throw new TypeError( 'invalid argument. First argument must be a numeric array. Value: `' + x + '`.' );
    121 	}
    122 	len = x.length;
    123 	if ( arguments.length > 1 ) {
    124 		if ( isObject( arguments[ 1 ] ) ) {
    125 			options = arguments[ 1 ];
    126 		} else {
    127 			y = arguments[ 1 ];
    128 			if ( !isTypedArrayLike( y ) && !isNumberArray( y ) ) {
    129 				throw new TypeError( 'invalid argument. `y` argument must be a numeric array. Value: `' + y + '`.' );
    130 			}
    131 			if ( len !== y.length ) {
    132 				throw new Error( 'invalid arguments. The first and second arguments must have the same length.' );
    133 			}
    134 			if ( arguments.length > 2 ) {
    135 				options = arguments[ 2 ];
    136 			}
    137 		}
    138 	}
    139 	opts = {};
    140 	if ( options ) {
    141 		err = validate( opts, options );
    142 		if ( err ) {
    143 			throw err;
    144 		}
    145 	}
    146 	mu = opts.mu || 0.0;
    147 	if ( opts.correction === void 0 ) {
    148 		correction = true;
    149 	} else {
    150 		correction = opts.correction;
    151 	}
    152 	if ( opts.alpha === void 0 ) {
    153 		alpha = 0.05;
    154 	} else {
    155 		alpha = opts.alpha;
    156 	}
    157 	if ( len < 2 ) {
    158 		throw new Error( 'invalid argument. First argument must contain at least two elements. Value: `' + x + '`' );
    159 	}
    160 	alt = opts.alternative || 'two-sided';
    161 	zeroMethod = opts.zeroMethod || 'wilcox';
    162 
    163 	if ( zeroMethod === 'wilcox' ) {
    164 		// Only keep all non-zero differences:
    165 		d = [];
    166 		if ( y ) {
    167 			for ( i = 0; i < len; i++ ) {
    168 				v = ( x[ i ] - y[ i ] ) - mu;
    169 				if ( v !== 0 ) {
    170 					d.push( v );
    171 				}
    172 			}
    173 		} else {
    174 			for ( i = 0; i < len; i++ ) {
    175 				if ( x[ i ] !== 0 ) {
    176 					d.push( x[ i ] - mu );
    177 				}
    178 			}
    179 		}
    180 		nzero = x.length - d.length;
    181 	} else {
    182 		d = new Float64Array( len );
    183 		nzero = 0;
    184 		if ( y ) {
    185 			for ( i = 0; i < len; i++ ) {
    186 				d[ i ] = ( x[ i ] - y[ i ] ) - mu;
    187 				if ( d[ i ] === 0 ) {
    188 					nzero += 1;
    189 				}
    190 			}
    191 		} else {
    192 			for ( i = 0; i < len; i++ ) {
    193 				d[ i ] = x[ i ] - mu;
    194 				if ( d[ i ] === 0 ) {
    195 					nzero += 1;
    196 				}
    197 			}
    198 		}
    199 	}
    200 	if ( nzero === len ) {
    201 		throw new Error( '`x` or `x - y` cannot be zero for all elements.' );
    202 	}
    203 	// Update length after potentially discarding zero values:
    204 	len = d.length;
    205 	ad = new Float64Array( len );
    206 	for ( i = 0; i < len; i++ ) {
    207 		ad[ i ] = abs( d[ i ] );
    208 	}
    209 	r = ranks( ad );
    210 	rplus = 0;
    211 	rzero = 0;
    212 	for ( i = 0; i < len; i++ ) {
    213 		if ( d[ i ] > 0 ) {
    214 			rplus += r[ i ];
    215 		}
    216 		else if ( d[ i ] === 0 ) {
    217 			rzero += r[ i ];
    218 		}
    219 	}
    220 	hasTies = unique( r ).length !== r.length;
    221 	if ( zeroMethod === 'zsplit' ) {
    222 		rplus += rzero / 2.0;
    223 	}
    224 	T = rplus;
    225 	mn = len * ( len + 1.0 ) * 0.25;
    226 	se = len * ( len + 1.0 ) * ( ( 2.0 * len ) + 1.0 );
    227 
    228 	if ( zeroMethod === 'pratt' ) {
    229 		tmp = [];
    230 		for ( i = 0; i < len; i++ ) {
    231 			if ( d[ i ] !== 0 ) {
    232 				tmp.push( r[ i ] );
    233 			}
    234 		}
    235 		r = tmp;
    236 		mn -= nzero * ( nzero + 1.0 ) * 0.25;
    237 		se -= nzero * ( nzero + 1.0 ) * ( ( 2.0 * nzero ) + 1.0 );
    238 	}
    239 	counts = tabulate( r );
    240 	repsum = 0;
    241 	for ( i = 0; i < counts.length; i++ ) {
    242 		if ( counts[ i ][ 1 ] > 1 ) {
    243 			v = counts[ i ][ 1 ];
    244 			repsum += v * ( (v*v) - 1 );
    245 		}
    246 	}
    247 	if ( repsum > 0 ) {
    248 		// Correction for repeated values:
    249 		se -= 0.5 * repsum;
    250 	}
    251 	se = sqrt( se / 24.0 );
    252 
    253 	if (
    254 		( len > 50 && !opts.exact ) ||
    255 		nzero > 0 ||
    256 		hasTies
    257 	) {
    258 		d = 0.0;
    259 		if ( correction ) {
    260 			switch ( alt ) {
    261 			case 'two-sided':
    262 				d = 0.5 * signum( T - mn );
    263 				break;
    264 			case 'less':
    265 				d = -0.5;
    266 				break;
    267 			default:
    268 				d = 0.5;
    269 				break;
    270 			}
    271 		}
    272 		// Compute test statistic and p-value using normal approximation:
    273 		stat = ( T - mn - d ) / se;
    274 		if ( alt === 'two-sided' ) {
    275 			pval = 2.0 * ( 1.0 - pnorm( abs( stat ) ) );
    276 		} else if ( alt === 'greater' ) {
    277 			pval = 1.0 - pnorm( stat );
    278 		} else {
    279 			pval = pnorm( stat );
    280 		}
    281 	} else {
    282 		// Compute test statistic and p-value using exact critical values:
    283 		stat = T;
    284 		if ( alt === 'two-sided' ) {
    285 			if ( stat > ( len * ( len+1 ) / 4 ) ) {
    286 				pval = 2.0 * ( 1 - signrankCDF( stat - 1, len ) );
    287 			} else {
    288 				pval = 2.0 * signrankCDF( stat, len );
    289 			}
    290 		} else if ( alt === 'greater' ) {
    291 			pval = 1.0 - signrankCDF( stat - 1, len );
    292 		} else {
    293 			pval = signrankCDF( stat, len );
    294 		}
    295 	}
    296 	out = {};
    297 	setReadOnly( out, 'rejected', pval <= alpha );
    298 	setReadOnly( out, 'alpha', alpha );
    299 	setReadOnly( out, 'pValue', pval );
    300 	setReadOnly( out, 'statistic', T );
    301 	setReadOnly( out, 'nullValue', mu );
    302 	setReadOnly( out, 'alternative', alt );
    303 	setReadOnly( out, 'method', ( ( y ) ? 'Paired' : 'One-Sample' ) + ' Wilcoxon signed rank test' );
    304 	setReadOnly( out, 'print', print );
    305 	return out;
    306 }
    307 
    308 
    309 // EXPORTS //
    310 
    311 module.exports = wilcoxon;