Improved error messages for statistical functions

This commit is contained in:
jos 2018-01-24 11:52:49 +01:00
parent b80911d4d6
commit f26e5cea03
21 changed files with 240 additions and 75 deletions

View File

@ -25,6 +25,10 @@ Breaking changes:
- `null` is no longer implicitly casted to a number `0`, so input like
`math.add(2, null)` is no longer supported. See #830, #353.
Non breaking changes:
- Improved error messages for statistical functions.
## 2018-01-17, version 3.20.1

View File

@ -7,6 +7,7 @@ function factory (type, config, load, typed) {
var map = load(require('../matrix/map'));
var median = load(require('../statistics/median'));
var subtract = load(require('../arithmetic/subtract'));
var improveErrorMessage = load(require('./utils/improveErrorMessage'));
/**
* Compute the median absolute deviation of a matrix or a list with values.
@ -50,13 +51,23 @@ function factory (type, config, load, typed) {
array = flatten(array.valueOf());
if (array.length === 0) {
throw new Error('Cannot calculate median absolute deviation of an empty array');
throw new Error('Cannot calculate median absolute deviation (mad) of an empty array');
}
var med = median(array);
return median(map(array, function (value) {
return abs(subtract(value, med));
}));
try {
var med = median(array);
return median(map(array, function (value) {
return abs(subtract(value, med));
}));
}
catch (err) {
if (err instanceof TypeError && err.message.indexOf('median') !== -1) {
throw new TypeError(err.message.replace('median', 'mad'));
}
else {
throw improveErrorMessage(err, 'mad');
}
}
}
}

View File

@ -6,6 +6,7 @@ var containsCollections = require('../../utils/collection/containsCollections');
function factory (type, config, load, typed) {
var larger = load(require('../relational/larger'));
var improveErrorMessage = load(require('./utils/improveErrorMessage'));
/**
* Compute the maximum value of a matrix or a list with values.
@ -68,8 +69,13 @@ function factory (type, config, load, typed) {
* @returns {*} Returns x when x is largest, or y when y is largest
* @private
*/
function _largest(x, y){
return larger(x, y) ? x : y;
function _largest(x, y) {
try {
return larger(x, y) ? x : y;
}
catch (err) {
throw improveErrorMessage(err, 'max', y);
}
}
/**
@ -82,8 +88,13 @@ function factory (type, config, load, typed) {
var max = undefined;
deepForEach(array, function (value) {
if (max === undefined || larger(value, max)) {
max = value;
try {
if (max === undefined || larger(value, max)) {
max = value;
}
}
catch (err) {
throw improveErrorMessage(err, 'max', value);
}
});
@ -93,6 +104,7 @@ function factory (type, config, load, typed) {
return max;
}
}
exports.name = 'max';

View File

@ -8,6 +8,7 @@ var containsCollections = require('../../utils/collection/containsCollections');
function factory (type, config, load, typed) {
var add = load(require('../arithmetic/add'));
var divide = load(require('../arithmetic/divide'));
var improveErrorMessage = load(require('./utils/improveErrorMessage'));
/**
* Compute the mean value of matrix or a list with values.
@ -41,7 +42,7 @@ function factory (type, config, load, typed) {
'Array | Matrix': _mean,
// mean([a, b, c, d, ...], dim)
'Array | Matrix, number | BigNumber': _nmean,
'Array | Matrix, number | BigNumber': _nmeanDim,
// mean(a, b, c, d, ...)
'...': function (args) {
@ -65,10 +66,15 @@ function factory (type, config, load, typed) {
* @return {number} mean
* @private
*/
function _nmean(array, dim){
var sum = reduce(array, dim, add);
var s = Array.isArray(array) ? size(array) : array.size();
return divide(sum, s[dim]);
function _nmeanDim(array, dim) {
try {
var sum = reduce(array, dim, add);
var s = Array.isArray(array) ? size(array) : array.size();
return divide(sum, s[dim]);
}
catch (err) {
throw improveErrorMessage(err, 'mean');
}
}
/**
@ -82,8 +88,13 @@ function factory (type, config, load, typed) {
var num = 0;
deepForEach(array, function (value) {
sum = add(sum, value);
num++;
try {
sum = add(sum, value);
num++;
}
catch (err) {
throw improveErrorMessage(err, 'mean', value);
}
});
if (num === 0) {

View File

@ -8,6 +8,7 @@ function factory (type, config, load, typed) {
var divide = load(require('../arithmetic/divideScalar'));
var compare = load(require('../relational/compare'));
var partitionSelect = load(require('../matrix/partitionSelect'));
var improveErrorMessage = load(require('./utils/improveErrorMessage'));
/**
* Compute the median of a matrix or a list with values. The values are
@ -64,33 +65,38 @@ function factory (type, config, load, typed) {
* @private
*/
function _median(array) {
array = flatten(array.valueOf());
try {
array = flatten(array.valueOf());
var num = array.length;
if (num == 0) {
throw new Error('Cannot calculate median of an empty array');
}
if (num % 2 == 0) {
// even: return the average of the two middle values
var mid = num / 2 - 1;
var right = partitionSelect(array, mid + 1);
// array now partitioned at mid + 1, take max of left part
var left = array[mid];
for (var i = 0; i < mid; ++i) {
if (compare(array[i], left) > 0) {
left = array[i];
}
var num = array.length;
if (num == 0) {
throw new Error('Cannot calculate median of an empty array');
}
return middle2(left, right);
}
else {
// odd: return the middle value
var m = partitionSelect(array, (num - 1) / 2);
if (num % 2 == 0) {
// even: return the average of the two middle values
var mid = num / 2 - 1;
var right = partitionSelect(array, mid + 1);
return middle(m);
// array now partitioned at mid + 1, take max of left part
var left = array[mid];
for (var i = 0; i < mid; ++i) {
if (compare(array[i], left) > 0) {
left = array[i];
}
}
return middle2(left, right);
}
else {
// odd: return the middle value
var m = partitionSelect(array, (num - 1) / 2);
return middle(m);
}
}
catch (err) {
throw improveErrorMessage(err, 'median');
}
}

View File

@ -6,6 +6,7 @@ var containsCollections = require('../../utils/collection/containsCollections');
function factory (type, config, load, typed) {
var smaller = load(require('../relational/smaller'));
var improveErrorMessage = load(require('./utils/improveErrorMessage'));
/**
* Compute the maximum value of a matrix or a list of values.
@ -69,7 +70,12 @@ function factory (type, config, load, typed) {
* @private
*/
function _smallest(x, y) {
return smaller(x, y) ? x : y;
try {
return smaller(x, y) ? x : y;
}
catch (err) {
throw improveErrorMessage(err, 'min', y);
}
}
/**
@ -82,8 +88,13 @@ function factory (type, config, load, typed) {
var min = undefined;
deepForEach(array, function (value) {
if (min === undefined || smaller(value, min)) {
min = value;
try {
if (min === undefined || smaller(value, min)) {
min = value;
}
}
catch (err) {
throw improveErrorMessage(err, 'min', value);
}
});

View File

@ -4,6 +4,7 @@ var deepForEach = require('../../utils/collection/deepForEach');
function factory (type, config, load, typed) {
var multiply = load(require('../arithmetic/multiplyScalar'));
var improveErrorMessage = load(require('./utils/improveErrorMessage'));
/**
* Compute the product of a matrix or a list with values.
@ -61,7 +62,12 @@ function factory (type, config, load, typed) {
var prod = undefined;
deepForEach(array, function (value) {
prod = (prod === undefined) ? value : multiply(prod, value);
try {
prod = (prod === undefined) ? value : multiply(prod, value);
}
catch (err) {
throw improveErrorMessage(err, 'prod', value);
}
});
if (prod === undefined) {

View File

@ -66,7 +66,17 @@ function factory (type, config, load, typed) {
throw new SyntaxError('Function std requires one or more parameters (0 provided)');
}
return sqrt(variance.apply(null, arguments));
try {
return sqrt(variance.apply(null, arguments));
}
catch (err) {
if (err instanceof TypeError && err.message.indexOf(' var') !== -1) {
throw new TypeError(err.message.replace(' var', ' std'));
}
else {
throw err;
}
}
}
}

View File

@ -4,6 +4,7 @@ var deepForEach = require('../../utils/collection/deepForEach');
function factory (type, config, load, typed) {
var add = load(require('../arithmetic/addScalar'));
var improveErrorMessage = load(require('./utils/improveErrorMessage'));
/**
* Compute the sum of a matrix or a list with values.
@ -60,7 +61,12 @@ function factory (type, config, load, typed) {
var sum = undefined;
deepForEach(array, function (value) {
sum = (sum === undefined) ? value : add(sum, value);
try {
sum = (sum === undefined) ? value : add(sum, value);
}
catch (err) {
throw improveErrorMessage(err, 'sum', value);
}
});
if (sum === undefined) {

View File

@ -0,0 +1,40 @@
function factory (type, config, load, typed) {
var getType = load(require('../../utils/typeof'));
/**
* Improve error messages for statistics functions. Errors are typically
* thrown in an internally used function like larger, causing the error
* not to mention the function (like max) which is actually used by the user.
*
* @param {Error} err
* @param {String} fnName
* @param {*} [value]
* @return {Error}
*/
return function improveErrorMessage(err, fnName, value) {
// TODO: add information with the index (also needs transform in expression parser)
var details;
if (String(err).indexOf('Unexpected type') !== -1) {
details = arguments.length > 2
? ' (type: ' + getType(value) + ', value: ' + JSON.stringify(value) + ')'
: ' (type: ' + err.data.actual + ')';
return new TypeError('Cannot calculate ' + fnName + ', unexpected type of argument' + details);
}
if (String(err).indexOf('complex numbers') !== -1) {
details = arguments.length > 2
? ' (type: ' + getType(value) + ', value: ' + JSON.stringify(value) + ')'
: '';
return new TypeError('Cannot calculate ' + fnName + ', no ordering relation is defined for complex numbers' + details);
}
return err;
}
}
exports.factory = factory;

View File

@ -9,6 +9,7 @@ function factory (type, config, load, typed) {
var subtract = load(require('../arithmetic/subtract'));
var multiply = load(require('../arithmetic/multiplyScalar'));
var divide = load(require('../arithmetic/divideScalar'));
var improveErrorMessage = load(require('./utils/improveErrorMessage'));
/**
* Compute the variance of a matrix or a list with values.
@ -92,8 +93,13 @@ function factory (type, config, load, typed) {
// calculate the mean and number of elements
deepForEach(array, function (value) {
sum = add(sum, value);
num++;
try {
sum = add(sum, value);
num++;
}
catch (err) {
throw improveErrorMessage(err, 'var', value);
}
});
if (num === 0) throw new Error('Cannot calculate var of an empty array');

View File

@ -3,6 +3,7 @@ var approx = require('../../../tools/approx');
var math = require('../../../index');
var BigNumber = math.type.BigNumber;
var DenseMatrix = math.type.DenseMatrix;
var Complex = math.type.Complex;
var mad = math.mad;
describe('mad', function() {
@ -52,6 +53,15 @@ describe('mad', function() {
assert.throws(function() {mad([])});
});
it('should throw an error if called with invalid type of arguments', function() {
assert.throws(function () {mad(2, new Complex(2,5))}, /TypeError: Cannot calculate mad, no ordering relation is defined for complex numbers/);
assert.throws(function () {mad(new Complex(2,3), new Complex(2,1))}, /TypeError: Cannot calculate mad, no ordering relation is defined for complex numbers/);
assert.throws(function() {mad([2,new Date(), 4])}, /TypeError: Cannot calculate mad, unexpected type of argument/);
assert.throws(function() {mad([2,null, 4])}, /TypeError: Cannot calculate mad, unexpected type of argument/);
assert.throws(function() {mad([[2, 5], [4, null], [1, 7]], 0)}, /TypeError: Cannot calculate mad, unexpected type of argument/);
});
it('should LaTeX mad', function () {
var expression = math.parse('mad(1,2,3)');
assert.equal(expression.toTex(), '\\mathrm{mad}\\left(1,2,3\\right)');

View File

@ -68,16 +68,6 @@ describe('max', function() {
[[2, 4, 6], [7, 9, 11]]);
});
it('should throw an error when called with complex numbers', function() {
assert.throws(function () {max(new Complex(2,3), new Complex(2,1))}, TypeError);
assert.throws(function () {max(new Complex(2,3), new Complex(2,5))}, TypeError);
assert.throws(function () {max(new Complex(3,4), 4)}, TypeError);
assert.throws(function () {max(new Complex(3,4), 5)}, TypeError);
assert.throws(function () {max(5, new Complex(3,4))}, TypeError);
assert.throws(function () {max(new Complex(3,4), 6)}, TypeError);
});
it('should throw an error when called multiple arrays or matrices', function() {
assert.throws(function () {max([1,2], [3,4])}, /Scalar values expected/);
assert.throws(function () {max(math.matrix([1,2]), math.matrix([3,4]))}, /Scalar values expected/);
@ -93,6 +83,16 @@ describe('max', function() {
assert.throws(function() {max([], 2, 3)});
});
it('should throw an error if called with invalid type of arguments', function() {
assert.throws(function () {max(2, new Complex(2,5))}, /TypeError: Cannot calculate max, no ordering relation is defined for complex numbers/);
assert.throws(function () {max(new Complex(2,3), new Complex(2,1))}, /TypeError: Cannot calculate max, no ordering relation is defined for complex numbers/);
assert.throws(function() {max([[2,undefined, 4]])}, /TypeError: Cannot calculate max, unexpected type of argument/);
assert.throws(function() {max([[2,new Date(), 4]])}, /TypeError: Cannot calculate max, unexpected type of argument/);
assert.throws(function() {max([2,null, 4])}, /TypeError: Cannot calculate max, unexpected type of argument/);
assert.throws(function() {max([[2, 5], [4, null], [1, 7]], 0)}, /TypeError: Cannot calculate max, unexpected type of argument/);
});
it('should return undefined if called with an empty array', function() {
assert.throws(function() {max([])});
});

View File

@ -92,6 +92,13 @@ describe('mean', function() {
assert.throws(function() {mean([])});
});
it('should throw an error if called with invalid type of arguments', function() {
assert.throws(function() {mean([[2,undefined, 4]])}, /TypeError: Cannot calculate mean, unexpected type of argument/);
assert.throws(function() {mean([[2,new Date(), 4]])}, /TypeError: Cannot calculate mean, unexpected type of argument/);
assert.throws(function() {mean([2,null, 4])}, /TypeError: Cannot calculate mean, unexpected type of argument/);
assert.throws(function() {mean([[2, 5], [4, null], [1, 7]], 0)}, /TypeError: Cannot calculate mean, unexpected type of argument/);
});
it('should LaTeX mean', function () {
var expression = math.parse('mean(1,2,3,4)');
assert.equal(expression.toTex(), '\\mathrm{mean}\\left(1,2,3,4\\right)');

View File

@ -82,9 +82,13 @@ describe('median', function() {
assert.throws(function() {median([], 2)}, /not yet supported/);
});
it('should throw an error if called with unsupported type of arguments', function() {
assert.throws(function () {median(new Date(), 2, 3)}, /TypeError: Unexpected type of argument/);
assert.throws(function () {median(new Complex(2,3), new Complex(-1,2))}, /TypeError: No ordering relation is defined for complex numbers/);
it('should throw an error if called with invalid type of arguments', function() {
assert.throws(function () {median(2, new Complex(2,5))}, /TypeError: Cannot calculate median, no ordering relation is defined for complex numbers/);
assert.throws(function () {median(new Complex(2,3), new Complex(2,1))}, /TypeError: Cannot calculate median, no ordering relation is defined for complex numbers/);
assert.throws(function() {median([[2,undefined, 4]])}, /TypeError: Cannot calculate median, unexpected type of argument/);
assert.throws(function() {median([[2,new Date(), 4]])}, /TypeError: Cannot calculate median, unexpected type of argument/);
assert.throws(function() {median([2,null, 4])}, /TypeError: Cannot calculate median, unexpected type of argument/);
});
it('should throw an error if called with an empty array', function() {

View File

@ -77,16 +77,6 @@ describe('min', function() {
[[1, 2], [3,4], [5,6]]);
});
it('should throw an error when called with complex numbers', function() {
assert.throws(function () {min(new Complex(2,3), new Complex(2,1))}, TypeError);
assert.throws(function () {min(new Complex(2,3), new Complex(2,5))}, TypeError);
assert.throws(function () {min(new Complex(3,4), 4)}, TypeError);
assert.throws(function () {min(new Complex(3,4), 5)}, TypeError);
assert.throws(function () {min(5, new Complex(3,4))}, TypeError);
assert.throws(function () {min(new Complex(3,4), 6)}, TypeError);
});
it('should throw an error when called multiple arrays or matrices', function() {
assert.throws(function () {min([1,2], [3,4])}, /Scalar values expected/);
assert.throws(function () {min(math.matrix([1,2]), math.matrix([3,4]))}, /Scalar values expected/);
@ -106,6 +96,16 @@ describe('min', function() {
assert.throws(function() {min([])});
});
it('should throw an error if called with invalid type of arguments', function() {
assert.throws(function () {min(2, new Complex(2,5))}, /TypeError: Cannot calculate min, no ordering relation is defined for complex numbers/);
assert.throws(function () {min(new Complex(2,3), new Complex(2,1))}, /TypeError: Cannot calculate min, no ordering relation is defined for complex numbers/);
assert.throws(function() {min([[2,undefined, 4]])}, /TypeError: Cannot calculate min, unexpected type of argument/);
assert.throws(function() {min([[2,new Date(), 4]])}, /TypeError: Cannot calculate min, unexpected type of argument/);
assert.throws(function() {min([2,null, 4])}, /TypeError: Cannot calculate min, unexpected type of argument/);
assert.throws(function() {min([[2, 5], [4, null], [1, 7]], 0)}, /TypeError: Cannot calculate min, unexpected type of argument/);
});
it('should LaTeX min', function () {
var expression = math.parse('min(1,2,3)');
assert.equal(expression.toTex(), '\\min\\left(1,2,3\\right)');

View File

@ -63,6 +63,12 @@ describe('prod', function() {
assert.throws(function() {prod([])});
});
it('should throw an error if called with invalid type of arguments', function() {
assert.throws(function() {prod([[2,undefined, 4]])}, /TypeError: Cannot calculate prod, unexpected type of argument/);
assert.throws(function() {prod([[2,new Date(), 4]])}, /TypeError: Cannot calculate prod, unexpected type of argument/);
assert.throws(function() {prod([2,null, 4])}, /TypeError: Cannot calculate prod, unexpected type of argument/);
});
it('should LaTeX prod', function () {
var expression = math.parse('prod(1,2,3)');
assert.equal(expression.toTex(), '\\mathrm{prod}\\left(1,2,3\\right)');

View File

@ -156,7 +156,12 @@ describe('quantileSeq', function() {
assert.throws(function () {quantileSeq('A', 'C', 'B')}, math.error.UnsupportedTypeError);
assert.throws(function () {quantileSeq(true, false, true)}, math.error.UnsupportedTypeError);
assert.throws(function () {quantileSeq(0, 'B')}, math.error.UnsupportedTypeError);
assert.throws(function () {quantileSeq(math.complex(2,3), math.complex(-1,2))}, TypeError);
assert.throws(function () {quantileSeq(math.complex(2,3), math.complex(-1,2))}, /TypeError: Unexpected type of argument in function quantileSeq/);
assert.throws(function () {quantileSeq(2, null)}, /TypeError: Unexpected type of argument in function quantileSeq/);
// TODO: improve error messages of quantileSeq
assert.throws(function () {quantileSeq([2, null], 2)}, /TypeError: Unexpected type of argument in function compare/);
});
it('should throw error for bad probabilities and splits', function() {

View File

@ -71,8 +71,10 @@ describe('std', function() {
});
it('should throw an error if called with invalid type of arguments', function() {
assert.throws(function() {std(new Date(), 2)}, TypeError);
assert.throws(function() {std(new Unit('5cm'), new Unit('10cm'))}, TypeError);
assert.throws(function() {std(new Date(), 2)}, /Cannot calculate std, unexpected type of argument/);
assert.throws(function() {std(new Unit(5, 'cm'), new Unit(10, 'cm'))}, /Cannot calculate std, unexpected type of argument/);
assert.throws(function() {std(2, 3, null)}, /Cannot calculate std, unexpected type of argument/);
assert.throws(function() {std([2, 3, null])}, /Cannot calculate std, unexpected type of argument/);
assert.throws(function() {std([2,3,4], 5)}, /Unknown normalization "5"/);
});

View File

@ -84,6 +84,12 @@ describe('sum', function() {
assert.equal(math.equal(fracMath.sum([]), new fracMath.type.Fraction(0)), true);
});
it('should throw an error if called with invalid type of arguments', function() {
assert.throws(function() {sum(new Date(), 2)}, /Cannot calculate sum, unexpected type of argument/);
assert.throws(function() {sum(2, 3, null)}, /Cannot calculate sum, unexpected type of argument/);
assert.throws(function() {sum([2, 3, null])}, /Cannot calculate sum, unexpected type of argument/);
});
it('should LaTeX sum', function () {
var expression = math.parse('sum(1,2,3)');
assert.equal(expression.toTex(), '\\mathrm{sum}\\left(1,2,3\\right)');

View File

@ -69,8 +69,10 @@ describe('variance', function() {
});
it('should throw an error if called with invalid type of arguments', function() {
assert.throws(function() {variance(new Date(), 2)}, /TypeError/);
assert.throws(function() {variance(new Unit('5cm'), new Unit('10cm'))}, /TypeError/);
assert.throws(function() {variance(new Date(), 2)}, /Cannot calculate var, unexpected type of argument/);
assert.throws(function() {variance(new Unit(5, 'cm'), new Unit(10, 'cm'))}, /Cannot calculate var, unexpected type of argument/);
assert.throws(function() {variance(2, 3, null)}, /Cannot calculate var, unexpected type of argument/);
assert.throws(function() {variance([2, 3, null])}, /Cannot calculate var, unexpected type of argument/);
assert.throws(function() {variance([2,3,4], 5)}, /Unknown normalization "5"/);
});