All files main.js

94.61% Statements 211/223
82.69% Branches 43/52
100% Functions 1/1
94.61% Lines 211/223

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 2241x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 61x 61x 61x 61x 61x 61x 61x 61x 61x 61x 61x 61x 61x 61x 61x 61x 61x 61x 61x 61x 61x 61x       61x 52x 52x 52x 61x 9x 9x 52x 52x 52x 52x 52x 52x 61x 550x 550x 2x 2x 548x 548x 548x 61x 1x 1x 49x 49x 49x 61x 15x 15x 5x 5x 10x 10x 10x 10x 10x 9x 9x 1x 1x 1x 1x 15x 300x 300x     300x 300x 300x 300x 1x 15x       61x 34x       34x 26x 26x 26x 34x 8x 8x 34x 2x 2x 24x 24x 34x 110x 110x 1x 1x 110x   110x 5x 109x 104x 104x 109x 109x 23x 34x 22x 22x 22x 34x 24x 24x 24x 61x 15x 15x 9x 9x 15x 15x 61x 2x 1x 1x 1x 2x 61x 13x 13x 13x 61x 61x 1x 1x 1x 1x 1x  
/**
* @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 isNonNegativeInteger = require( '@stdlib/assert/is-nonnegative-integer' ).isPrimitive;
var isCollection = require( '@stdlib/assert/is-collection' );
var isndarrayLike = require( '@stdlib/assert/is-ndarray-like' );
var isNumber = require( '@stdlib/assert/is-number' ).isPrimitive;
var isString = require( '@stdlib/assert/is-string' ).isPrimitive;
var absdiff = require( '@stdlib/math/base/utils/absolute-difference' );
var FLOAT64_SQRT_EPS = require( '@stdlib/constants/float64/sqrt-eps' );
var PINF = require( '@stdlib/constants/float64/pinf' );
var chisqCDF = require( '@stdlib/stats/base/dists/chisquare/cdf' );
var format = require( '@stdlib/string/format' );
var isnan = require( '@stdlib/assert/is-nan' );
var daxpy = require( '@stdlib/blas/base/daxpy' );
var dscal = require( '@stdlib/blas/base/dscal' );
var dsumpw = require( '@stdlib/blas/ext/base/dsumpw' );
var Float64Array = require( '@stdlib/array/float64' );
var defaults = require( './defaults.js' );
var validate = require( './validate.js' );
var getPMF = require( './get_pmf.js' );
var testStatistic = require( './statistic.js' );
var simulate = require( './simulate.js' );
var Results = require( './results.js' );
 
 
// MAIN //
 
/**
* Performs a chi-square goodness-of-fit test.
*
* @param {(Collection|VectorLike)} x - observation frequencies
* @param {(Collection|VectorLike|string)} y - expected frequencies or a discrete probability distribution name
* @param {...number} [args] - probability mass function (PMF) arguments
* @param {Options} [options] - function options
* @param {number} [options.alpha=0.05] - significance level
* @param {NonNegativeInteger} [options.ddof=0] - degrees of freedom adjustment
* @param {boolean} [options.simulate=false] - boolean indicating whether to compute p-values by Monte Carlo simulation
* @param {PositiveInteger} [options.iterations=500] - number of Monte Carlo iterations
* @throws {TypeError} first argument must be an array-like object or a 1-dimensional array containing nonnegative integers
* @throws {TypeError} second argument must be either an array-like object (or a 1-dimensional array) of nonnegative numbers, an array-like object (or a 1-dimensional array) of probabilities summing to one, or a discrete probability distribution name
* @throws {TypeError} options argument must be an object
* @throws {TypeError} must provide valid options
* @throws {RangeError} first and second arguments must have the same length
* @throws {Error} first argument must contain at least one element greater than zero
* @throws {RangeError} significance level must be a number on the interval `[0,1]`
* @throws {TypeError} probability mass function (PMF) arguments must be numbers
* @returns {Object} test results
*
* @example
* var x = [ 89, 37, 30, 28, 2 ];
* var p = [ 0.40, 0.20, 0.20, 0.15, 0.05 ];
*
* var out = chi2gof( x, p );
*
* var o = out.toJSON();
* // returns { 'pValue': ~0.0406, 'statistic': ~9.9901, ... }
*/
function chi2gof( x, y ) {
	var expected;
	var nargs;
	var args;
	var opts;
	var pval;
	var stat;
	var obs;
	var err;
	var pmf;
	var sum;
	var df;
	var N;
	var d;
	var s;
	var o;
	var n;
	var p;
	var v;
	var i;
 
	if ( isndarrayLike( x ) && x.ndims === 1 && x.strides.length === 1 ) { // is ndarray-like vector?
		d = x.data;
		s = x.strides[ 0 ];
		o = x.offset;
	} else if ( isCollection( x ) ) {
		d = x;
		s = 1;
		o = 0;
	} else {
		throw new TypeError( format( 'invalid argument. First argument must be either an array-like object or a one-dimensional ndarray. Value: `%s`.', x ) );
	}
	N = x.length;
 
	// Initialize an array for storing a copy of the observations array:
	obs = new Float64Array( N+1 ); // Note: `N+1` is intentional in the event that we need to add a remaining category for all values greater than or equal to `N`
 
	n = 0;
	for ( i = 0; i < N; i++ ) {
		v = d[ o+(s*i) ];
		if ( !isNonNegativeInteger( v ) ) {
			throw new TypeError( format( 'invalid argument. First argument must contain nonnegative integers. Index: `%u`. Value: `%s`.', i, v ) );
		}
		obs[ i ] = v;
		n += v;
	}
	if ( n === 0 ) {
		throw new Error( 'invalid argument. First argument must contain at least one element greater than zero (i.e., the total number of observations must be greater than zero).' );
	}
	// NOTE: `obs` is now a single-segment contiguous Float64Array
 
	nargs = 0;
	if ( isString( y ) ) {
		pmf = getPMF( y );
		if ( pmf instanceof Error ) {
			throw pmf;
		}
		nargs += pmf.length - 1; // WARNING: this relies on PMF functions having an explicit arity
		args = [ 0 ];
		for ( i = 0; i < nargs; i++ ) {
			v = arguments[ i+2 ];
			if ( !isNumber( v ) || isnan( v ) ) {
				throw new TypeError( format( 'invalid argument. Probability mass function (PMF) arguments must be numbers. Argument: `%u`. Value: `%s`.', i+2, v ) );
			}
			args.push( v );
		}
		expected = new Float64Array( N+1 );
		sum = 0.0;
		for ( i = 0; i < N; i++ ) {
			args[ 0 ] = i;
			if ( y === 'discrete-uniform' ) {
				args[ 0 ] += args[ 1 ]; // scales the value at which to evaluate the PMF based on the minimum support of the distribution (which should have been provided as the first distribution parameter)
			}
			v = pmf.apply( null, args );
			sum += v;
			expected[ i ] = v * n;
		}
		// Check whether we need to add a remaining category for all values greater than or equal to `N`...
		if ( sum < 1.0 ) {
			expected[ N ] = (1.0-sum) * n;
			N += 1;
		}
	} else {
		if ( isndarrayLike( y ) && y.ndims === 1 && y.strides.length === 1 ) { // is ndarray-like vector?
			d = y.data;
			s = y.strides[ 0 ];
			o = y.offset;
		} else if ( isCollection( y ) ) {
			d = y;
			s = 1;
			o = 0;
		} else {
			throw new TypeError( format( 'invalid argument. Second argument must be either an array-like object (or one-dimensional ndarray) of probabilities summing to one, an array-like object (or one-dimensional ndarray) of expected frequencies, or a discrete probability distribution name. Value: `%s`.', y ) );
		}
		if ( y.length !== N ) {
			throw new RangeError( 'invalid arguments. First and second arguments must have the same length.' );
		}
		expected = new Float64Array( N );
		sum = 0.0;
		for ( i = 0; i < N; i++ ) {
			v = d[ o+(s*i) ];
			if ( !isNumber( v ) ) {
				throw new TypeError( format( 'invalid argument. Second argument must only contain numbers. Index: `%u`. Value: `%s`.', i, v ) );
			}
			if ( v < 0.0 ) {
				throw new TypeError( format( 'invalid argument. Second argument must only contain nonnegative numbers. Index: `%u`. Value: `%d`.', i, v ) );
			} else if ( v > 1.0 ) {
				sum += PINF;
			} else {
				sum += v;
			}
			expected[ i ] = v;
		}
		// Check if provided a unity probability array (otherwise, assume provided an expected frequencies array)...
		if ( absdiff( sum, 1.0 ) <= FLOAT64_SQRT_EPS ) {
			p = y; // NOTE: `y` may not be a Float64Array
			expected = dscal( N, n, expected, 1 );
		}
	}
	// NOTE: `expected` is now a single-segment contiguous Float64Array
 
	opts = defaults();
	if ( arguments.length > 2+nargs ) {
		err = validate( opts, arguments[ 2+nargs ] );
		if ( err ) {
			throw err;
		}
	}
	stat = testStatistic( N, obs, 1, expected, 1 ); // TODO: consider replacing with low-level double-precision strided interface
	if ( opts.simulate ) {
		if ( p === void 0 ) {
			v = dsumpw( N, expected, 1 );
			p = daxpy( N, 1.0/v, expected, 1, new Float64Array( N ), 1 );
		}
		pval = simulate( N, expected, p, stat, n, opts.iterations );
	} else {
		df = N - 1 - opts.ddof;
		pval = 1.0 - chisqCDF( stat, df );
	}
	return new Results( pval, opts.alpha, stat, ( df === void 0 ) ? null : df );
}
 
 
// EXPORTS //
 
module.exports = chi2gof;