simple-statistics
Version:
1,543 lines (1,393 loc) • 145 kB
JavaScript
/**
* When adding a new value to a list, one does not have to necessary
* recompute the mean of the list in linear time. They can instead use
* this function to compute the new mean by providing the current mean,
* the number of elements in the list that produced it and the new
* value to add.
*
* @since 2.5.0
* @param {number} mean current mean
* @param {number} n number of items in the list
* @param {number} newValue the added value
* @returns {number} the new mean
*
* @example
* addToMean(14, 5, 53); // => 20.5
*/
function addToMean(mean, n, newValue) {
return mean + (newValue - mean) / (n + 1);
}
/**
* Split an array into chunks of a specified size. This function
* has the same behavior as [PHP's array_chunk](http://php.net/manual/en/function.array-chunk.php)
* function, and thus will insert smaller-sized chunks at the end if
* the input size is not divisible by the chunk size.
*
* `x` is expected to be an array, and `chunkSize` a number.
* The `x` array can contain any kind of data.
*
* @param {Array} x a sample
* @param {number} chunkSize size of each output array. must be a positive integer
* @returns {Array<Array>} a chunked array
* @throws {Error} if chunk size is less than 1 or not an integer
* @example
* chunk([1, 2, 3, 4, 5, 6], 2);
* // => [[1, 2], [3, 4], [5, 6]]
*/
function chunk(x, chunkSize) {
// a list of result chunks, as arrays in an array
var output = [];
// `chunkSize` must be zero or higher - otherwise the loop below,
// in which we call `start += chunkSize`, will loop infinitely.
// So, we'll detect and throw in that case to indicate
// invalid input.
if (chunkSize < 1) {
throw new Error("chunk size must be a positive number");
}
if (Math.floor(chunkSize) !== chunkSize) {
throw new Error("chunk size must be an integer");
}
// `start` is the index at which `.slice` will start selecting
// new array elements
for (var start = 0; start < x.length; start += chunkSize) {
// for each chunk, slice that part of the array and add it
// to the output. The `.slice` function does not change
// the original array.
output.push(x.slice(start, start + chunkSize));
}
return output;
}
/**
* Create a new column x row matrix.
*
* @private
* @param {number} columns
* @param {number} rows
* @return {Array<Array<number>>} matrix
* @example
* makeMatrix(10, 10);
*/
function makeMatrix(columns, rows) {
var matrix = [];
for (var i = 0; i < columns; i++) {
var column = [];
for (var j = 0; j < rows; j++) {
column.push(0);
}
matrix.push(column);
}
return matrix;
}
/**
* Sort an array of numbers by their numeric value, ensuring that the
* array is not changed in place.
*
* This is necessary because the default behavior of .sort
* in JavaScript is to sort arrays as string values
*
* [1, 10, 12, 102, 20].sort()
* // output
* [1, 10, 102, 12, 20]
*
* @param {Array<number>} x input array
* @return {Array<number>} sorted array
* @private
* @example
* numericSort([3, 2, 1]) // => [1, 2, 3]
*/
function numericSort(x) {
return (
x
// ensure the array is not changed in-place
.slice()
// comparator function that treats input as numeric
.sort(function (a, b) {
return a - b;
})
);
}
/**
* For a sorted input, counting the number of unique values
* is possible in constant time and constant memory. This is
* a simple implementation of the algorithm.
*
* Values are compared with `===`, so objects and non-primitive objects
* are not handled in any special way.
*
* @param {Array<*>} x an array of any kind of value
* @returns {number} count of unique values
* @example
* uniqueCountSorted([1, 2, 3]); // => 3
* uniqueCountSorted([1, 1, 1]); // => 1
*/
function uniqueCountSorted(x) {
var uniqueValueCount = 0;
var lastSeenValue;
for (var i = 0; i < x.length; i++) {
if (i === 0 || x[i] !== lastSeenValue) {
lastSeenValue = x[i];
uniqueValueCount++;
}
}
return uniqueValueCount;
}
/**
* Generates incrementally computed values based on the sums and sums of
* squares for the data array
*
* @private
* @param {number} j
* @param {number} i
* @param {Array<number>} sums
* @param {Array<number>} sumsOfSquares
* @return {number}
* @example
* ssq(0, 1, [-1, 0, 2], [1, 1, 5]);
*/
function ssq(j, i, sums, sumsOfSquares) {
var sji; // s(j, i)
if (j > 0) {
var muji = (sums[i] - sums[j - 1]) / (i - j + 1); // mu(j, i)
sji =
sumsOfSquares[i] - sumsOfSquares[j - 1] - (i - j + 1) * muji * muji;
} else {
sji = sumsOfSquares[i] - (sums[i] * sums[i]) / (i + 1);
}
if (sji < 0) {
return 0;
}
return sji;
}
/**
* Function that recursively divides and conquers computations
* for cluster j
*
* @private
* @param {number} iMin Minimum index in cluster to be computed
* @param {number} iMax Maximum index in cluster to be computed
* @param {number} cluster Index of the cluster currently being computed
* @param {Array<Array<number>>} matrix
* @param {Array<Array<number>>} backtrackMatrix
* @param {Array<number>} sums
* @param {Array<number>} sumsOfSquares
*/
function fillMatrixColumn(
iMin,
iMax,
cluster,
matrix,
backtrackMatrix,
sums,
sumsOfSquares
) {
if (iMin > iMax) {
return;
}
// Start at midpoint between iMin and iMax
var i = Math.floor((iMin + iMax) / 2);
matrix[cluster][i] = matrix[cluster - 1][i - 1];
backtrackMatrix[cluster][i] = i;
var jlow = cluster; // the lower end for j
if (iMin > cluster) {
jlow = Math.max(jlow, backtrackMatrix[cluster][iMin - 1] || 0);
}
jlow = Math.max(jlow, backtrackMatrix[cluster - 1][i] || 0);
var jhigh = i - 1; // the upper end for j
if (iMax < matrix[0].length - 1) {
/* c8 ignore start */
jhigh = Math.min(jhigh, backtrackMatrix[cluster][iMax + 1] || 0);
/* c8 ignore end */
}
var sji;
var sjlowi;
var ssqjlow;
var ssqj;
for (var j = jhigh; j >= jlow; --j) {
sji = ssq(j, i, sums, sumsOfSquares);
if (sji + matrix[cluster - 1][jlow - 1] >= matrix[cluster][i]) {
break;
}
// Examine the lower bound of the cluster border
sjlowi = ssq(jlow, i, sums, sumsOfSquares);
ssqjlow = sjlowi + matrix[cluster - 1][jlow - 1];
if (ssqjlow < matrix[cluster][i]) {
// Shrink the lower bound
matrix[cluster][i] = ssqjlow;
backtrackMatrix[cluster][i] = jlow;
}
jlow++;
ssqj = sji + matrix[cluster - 1][j - 1];
if (ssqj < matrix[cluster][i]) {
matrix[cluster][i] = ssqj;
backtrackMatrix[cluster][i] = j;
}
}
fillMatrixColumn(
iMin,
i - 1,
cluster,
matrix,
backtrackMatrix,
sums,
sumsOfSquares
);
fillMatrixColumn(
i + 1,
iMax,
cluster,
matrix,
backtrackMatrix,
sums,
sumsOfSquares
);
}
/**
* Initializes the main matrices used in Ckmeans and kicks
* off the divide and conquer cluster computation strategy
*
* @private
* @param {Array<number>} data sorted array of values
* @param {Array<Array<number>>} matrix
* @param {Array<Array<number>>} backtrackMatrix
*/
function fillMatrices(data, matrix, backtrackMatrix) {
var nValues = matrix[0].length;
// Shift values by the median to improve numeric stability
var shift = data[Math.floor(nValues / 2)];
// Cumulative sum and cumulative sum of squares for all values in data array
var sums = [];
var sumsOfSquares = [];
// Initialize first column in matrix & backtrackMatrix
for (var i = 0, shiftedValue = (void 0); i < nValues; ++i) {
shiftedValue = data[i] - shift;
if (i === 0) {
sums.push(shiftedValue);
sumsOfSquares.push(shiftedValue * shiftedValue);
} else {
sums.push(sums[i - 1] + shiftedValue);
sumsOfSquares.push(
sumsOfSquares[i - 1] + shiftedValue * shiftedValue
);
}
// Initialize for cluster = 0
matrix[0][i] = ssq(0, i, sums, sumsOfSquares);
backtrackMatrix[0][i] = 0;
}
// Initialize the rest of the columns
var iMin;
for (var cluster = 1; cluster < matrix.length; ++cluster) {
if (cluster < matrix.length - 1) {
iMin = cluster;
} else {
// No need to compute matrix[K-1][0] ... matrix[K-1][N-2]
iMin = nValues - 1;
}
fillMatrixColumn(
iMin,
nValues - 1,
cluster,
matrix,
backtrackMatrix,
sums,
sumsOfSquares
);
}
}
/**
* Ckmeans clustering is an improvement on heuristic-based clustering
* approaches like Jenks. The algorithm was developed in
* [Haizhou Wang and Mingzhou Song](http://journal.r-project.org/archive/2011-2/RJournal_2011-2_Wang+Song.pdf)
* as a [dynamic programming](https://en.wikipedia.org/wiki/Dynamic_programming) approach
* to the problem of clustering numeric data into groups with the least
* within-group sum-of-squared-deviations.
*
* Minimizing the difference within groups - what Wang & Song refer to as
* `withinss`, or within sum-of-squares, means that groups are optimally
* homogenous within and the data is split into representative groups.
* This is very useful for visualization, where you may want to represent
* a continuous variable in discrete color or style groups. This function
* can provide groups that emphasize differences between data.
*
* Being a dynamic approach, this algorithm is based on two matrices that
* store incrementally-computed values for squared deviations and backtracking
* indexes.
*
* This implementation is based on Ckmeans 3.4.6, which introduced a new divide
* and conquer approach that improved runtime from O(kn^2) to O(kn log(n)).
*
* Unlike the [original implementation](https://cran.r-project.org/web/packages/Ckmeans.1d.dp/index.html),
* this implementation does not include any code to automatically determine
* the optimal number of clusters: this information needs to be explicitly
* provided.
*
* ### References
* _Ckmeans.1d.dp: Optimal k-means Clustering in One Dimension by Dynamic
* Programming_ Haizhou Wang and Mingzhou Song ISSN 2073-4859
*
* from The R Journal Vol. 3/2, December 2011
* @param {Array<number>} x input data, as an array of number values
* @param {number} nClusters number of desired classes. This cannot be
* greater than the number of values in the data array.
* @returns {Array<Array<number>>} clustered input
* @throws {Error} if the number of requested clusters is higher than the size of the data
* @example
* ckmeans([-1, 2, -1, 2, 4, 5, 6, -1, 2, -1], 3);
* // The input, clustered into groups of similar numbers.
* //= [[-1, -1, -1, -1], [2, 2, 2], [4, 5, 6]]);
*/
function ckmeans(x, nClusters) {
if (nClusters > x.length) {
throw new Error(
"cannot generate more classes than there are data values"
);
}
var sorted = numericSort(x);
// we'll use this as the maximum number of clusters
var uniqueCount = uniqueCountSorted(sorted);
// if all of the input values are identical, there's one cluster
// with all of the input in it.
if (uniqueCount === 1) {
return [sorted];
}
// named 'S' originally
var matrix = makeMatrix(nClusters, sorted.length);
// named 'J' originally
var backtrackMatrix = makeMatrix(nClusters, sorted.length);
// This is a dynamic programming way to solve the problem of minimizing
// within-cluster sum of squares. It's similar to linear regression
// in this way, and this calculation incrementally computes the
// sum of squares that are later read.
fillMatrices(sorted, matrix, backtrackMatrix);
// The real work of Ckmeans clustering happens in the matrix generation:
// the generated matrices encode all possible clustering combinations, and
// once they're generated we can solve for the best clustering groups
// very quickly.
var clusters = [];
var clusterRight = backtrackMatrix[0].length - 1;
// Backtrack the clusters from the dynamic programming matrix. This
// starts at the bottom-right corner of the matrix (if the top-left is 0, 0),
// and moves the cluster target with the loop.
for (var cluster = backtrackMatrix.length - 1; cluster >= 0; cluster--) {
var clusterLeft = backtrackMatrix[cluster][clusterRight];
// fill the cluster from the sorted input by taking a slice of the
// array. the backtrack matrix makes this easy - it stores the
// indexes where the cluster should start and end.
clusters[cluster] = sorted.slice(clusterLeft, clusterRight + 1);
if (cluster > 0) {
clusterRight = clusterLeft - 1;
}
}
return clusters;
}
/**
* Our default sum is the [Kahan-Babuska algorithm](https://pdfs.semanticscholar.org/1760/7d467cda1d0277ad272deb2113533131dc09.pdf).
* This method is an improvement over the classical
* [Kahan summation algorithm](https://en.wikipedia.org/wiki/Kahan_summation_algorithm).
* It aims at computing the sum of a list of numbers while correcting for
* floating-point errors. Traditionally, sums are calculated as many
* successive additions, each one with its own floating-point roundoff. These
* losses in precision add up as the number of numbers increases. This alternative
* algorithm is more accurate than the simple way of calculating sums by simple
* addition.
*
* This runs in `O(n)`, linear time, with respect to the length of the array.
*
* @param {Array<number>} x input
* @return {number} sum of all input numbers
* @example
* sum([1, 2, 3]); // => 6
*/
function sum(x) {
// If the array is empty, we needn't bother computing its sum
if (x.length === 0) {
return 0;
}
// Initializing the sum as the first number in the array
var sum = x[0];
// Keeping track of the floating-point error correction
var correction = 0;
var transition;
if (typeof sum !== "number") {
return Number.NaN;
}
for (var i = 1; i < x.length; i++) {
if (typeof x[i] !== "number") {
return Number.NaN;
}
transition = sum + x[i];
// Here we need to update the correction in a different fashion
// if the new absolute value is greater than the absolute sum
if (Math.abs(sum) >= Math.abs(x[i])) {
correction += sum - transition + x[i];
} else {
correction += x[i] - transition + sum;
}
sum = transition;
}
// Returning the corrected sum
return sum + correction;
}
/**
* The mean, _also known as average_,
* is the sum of all values over the number of values.
* This is a [measure of central tendency](https://en.wikipedia.org/wiki/Central_tendency):
* a method of finding a typical or central value of a set of numbers.
*
* This runs in `O(n)`, linear time, with respect to the length of the array.
*
* @param {Array<number>} x sample of one or more data points
* @throws {Error} if the length of x is less than one
* @returns {number} mean
* @example
* mean([0, 10]); // => 5
*/
function mean(x) {
if (x.length === 0) {
throw new Error("mean requires at least one data point");
}
return sum(x) / x.length;
}
/**
* The sum of deviations to the Nth power.
* When n=2 it's the sum of squared deviations.
* When n=3 it's the sum of cubed deviations.
*
* @param {Array<number>} x
* @param {number} n power
* @returns {number} sum of nth power deviations
*
* @example
* var input = [1, 2, 3];
* // since the variance of a set is the mean squared
* // deviations, we can calculate that with sumNthPowerDeviations:
* sumNthPowerDeviations(input, 2) / input.length;
*/
function sumNthPowerDeviations(x, n) {
var meanValue = mean(x);
var sum = 0;
var tempValue;
var i;
// This is an optimization: when n is 2 (we're computing a number squared),
// multiplying the number by itself is significantly faster than using
// the Math.pow method.
if (n === 2) {
for (i = 0; i < x.length; i++) {
tempValue = x[i] - meanValue;
sum += tempValue * tempValue;
}
} else {
for (i = 0; i < x.length; i++) {
sum += Math.pow(x[i] - meanValue, n);
}
}
return sum;
}
/**
* The [sample variance](https://en.wikipedia.org/wiki/Variance#Sample_variance)
* is the sum of squared deviations from the mean. The sample variance
* is distinguished from the variance by the usage of [Bessel's Correction](https://en.wikipedia.org/wiki/Bessel's_correction):
* instead of dividing the sum of squared deviations by the length of the input,
* it is divided by the length minus one. This corrects the bias in estimating
* a value from a set that you don't know if full.
*
* References:
* * [Wolfram MathWorld on Sample Variance](http://mathworld.wolfram.com/SampleVariance.html)
*
* @param {Array<number>} x a sample of two or more data points
* @throws {Error} if the length of x is less than 2
* @return {number} sample variance
* @example
* sampleVariance([1, 2, 3, 4, 5]); // => 2.5
*/
function sampleVariance(x) {
if (x.length < 2) {
throw new Error("sampleVariance requires at least two data points");
}
var sumSquaredDeviationsValue = sumNthPowerDeviations(x, 2);
// this is Bessels' Correction: an adjustment made to sample statistics
// that allows for the reduced degree of freedom entailed in calculating
// values from samples rather than complete populations.
var besselsCorrection = x.length - 1;
// Find the mean value of that list
return sumSquaredDeviationsValue / besselsCorrection;
}
/**
* The [sample standard deviation](http://en.wikipedia.org/wiki/Standard_deviation#Sample_standard_deviation)
* is the square root of the sample variance.
*
* @param {Array<number>} x input array
* @returns {number} sample standard deviation
* @example
* sampleStandardDeviation([2, 4, 4, 4, 5, 5, 7, 9]).toFixed(2);
* // => '2.14'
*/
function sampleStandardDeviation(x) {
var sampleVarianceX = sampleVariance(x);
return Math.sqrt(sampleVarianceX);
}
/**
* The`coefficient of variation`_ is the ratio of the standard deviation to the mean.
* .._`coefficient of variation`: https://en.wikipedia.org/wiki/Coefficient_of_variation
*
*
* @param {Array} x input
* @returns {number} coefficient of variation
* @example
* coefficientOfVariation([1, 2, 3, 4]).toFixed(3); // => 0.516
* coefficientOfVariation([1, 2, 3, 4, 5]).toFixed(3); // => 0.527
* coefficientOfVariation([-1, 0, 1, 2, 3, 4]).toFixed(3); // => 1.247
*/
function coefficientOfVariation(x) {
return sampleStandardDeviation(x) / mean(x);
}
/**
* Implementation of Combinations
* Combinations are unique subsets of a collection - in this case, k x from a collection at a time.
* https://en.wikipedia.org/wiki/Combination
* @param {Array} x any type of data
* @param {int} k the number of objects in each group (without replacement)
* @returns {Array<Array>} array of permutations
* @example
* combinations([1, 2, 3], 2); // => [[1,2], [1,3], [2,3]]
*/
function combinations(x, k) {
var i;
var subI;
var combinationList = [];
var subsetCombinations;
var next;
for (i = 0; i < x.length; i++) {
if (k === 1) {
combinationList.push([x[i]]);
} else {
subsetCombinations = combinations(x.slice(i + 1, x.length), k - 1);
for (subI = 0; subI < subsetCombinations.length; subI++) {
next = subsetCombinations[subI];
next.unshift(x[i]);
combinationList.push(next);
}
}
}
return combinationList;
}
/**
* Implementation of [Combinations](https://en.wikipedia.org/wiki/Combination) with replacement
* Combinations are unique subsets of a collection - in this case, k x from a collection at a time.
* 'With replacement' means that a given element can be chosen multiple times.
* Unlike permutation, order doesn't matter for combinations.
*
* @param {Array} x any type of data
* @param {int} k the number of objects in each group (without replacement)
* @returns {Array<Array>} array of permutations
* @example
* combinationsReplacement([1, 2], 2); // => [[1, 1], [1, 2], [2, 2]]
*/
function combinationsReplacement(x, k) {
var combinationList = [];
for (var i = 0; i < x.length; i++) {
if (k === 1) {
// If we're requested to find only one element, we don't need
// to recurse: just push `x[i]` onto the list of combinations.
combinationList.push([x[i]]);
} else {
// Otherwise, recursively find combinations, given `k - 1`. Note that
// we request `k - 1`, so if you were looking for k=3 combinations, we're
// requesting k=2. This -1 gets reversed in the for loop right after this
// code, since we concatenate `x[i]` onto the selected combinations,
// bringing `k` back up to your requested level.
// This recursion may go many levels deep, since it only stops once
// k=1.
var subsetCombinations = combinationsReplacement(
x.slice(i, x.length),
k - 1
);
for (var j = 0; j < subsetCombinations.length; j++) {
combinationList.push([x[i]].concat(subsetCombinations[j]));
}
}
}
return combinationList;
}
/**
* When combining two lists of values for which one already knows the means,
* one does not have to necessary recompute the mean of the combined lists in
* linear time. They can instead use this function to compute the combined
* mean by providing the mean & number of values of the first list and the mean
* & number of values of the second list.
*
* @since 3.0.0
* @param {number} mean1 mean of the first list
* @param {number} n1 number of items in the first list
* @param {number} mean2 mean of the second list
* @param {number} n2 number of items in the second list
* @returns {number} the combined mean
*
* @example
* combineMeans(5, 3, 4, 3); // => 4.5
*/
function combineMeans(mean1, n1, mean2, n2) {
return (mean1 * n1 + mean2 * n2) / (n1 + n2);
}
/**
* When combining two lists of values for which one already knows the variances,
* one does not have to necessary recompute the variance of the combined lists
* in linear time. They can instead use this function to compute the combined
* variance by providing the variance, mean & number of values of the first list
* and the variance, mean & number of values of the second list.
*
* @since 3.0.0
* @param {number} variance1 variance of the first list
* @param {number} mean1 mean of the first list
* @param {number} n1 number of items in the first list
* @param {number} variance2 variance of the second list
* @param {number} mean2 mean of the second list
* @param {number} n2 number of items in the second list
* @returns {number} the combined mean
*
* @example
* combineVariances(14 / 3, 5, 3, 8 / 3, 4, 3); // => 47 / 12
*/
function combineVariances(variance1, mean1, n1, variance2, mean2, n2) {
var newMean = combineMeans(mean1, n1, mean2, n2);
return (
(n1 * (variance1 + Math.pow(mean1 - newMean, 2)) +
n2 * (variance2 + Math.pow(mean2 - newMean, 2))) /
(n1 + n2)
);
}
/**
* This computes the maximum number in an array.
*
* This runs in `O(n)`, linear time, with respect to the length of the array.
*
* @param {Array<number>} x sample of one or more data points
* @returns {number} maximum value
* @throws {Error} if the length of x is less than one
* @example
* max([1, 2, 3, 4]);
* // => 4
*/
function max(x) {
if (x.length === 0) {
throw new Error("max requires at least one data point");
}
var value = x[0];
for (var i = 1; i < x.length; i++) {
if (x[i] > value) {
value = x[i];
}
}
return value;
}
/**
* The min is the lowest number in the array.
* This runs in `O(n)`, linear time, with respect to the length of the array.
*
* @param {Array<number>} x sample of one or more data points
* @throws {Error} if the length of x is less than one
* @returns {number} minimum value
* @example
* min([1, 5, -10, 100, 2]); // => -10
*/
function min(x) {
if (x.length === 0) {
throw new Error("min requires at least one data point");
}
var value = x[0];
for (var i = 1; i < x.length; i++) {
if (x[i] < value) {
value = x[i];
}
}
return value;
}
/**
* Given an array of x, this will find the extent of the
* x and return an array of breaks that can be used
* to categorize the x into a number of classes. The
* returned array will always be 1 longer than the number of
* classes because it includes the minimum value.
*
* @param {Array<number>} x an array of number values
* @param {number} nClasses number of desired classes
* @returns {Array<number>} array of class break positions
* @example
* equalIntervalBreaks([1, 2, 3, 4, 5, 6], 4); // => [1, 2.25, 3.5, 4.75, 6]
*/
function equalIntervalBreaks(x, nClasses) {
if (x.length < 2) {
return x;
}
var theMin = min(x);
var theMax = max(x);
// the first break will always be the minimum value
// in the xset
var breaks = [theMin];
// The size of each break is the full range of the x
// divided by the number of classes requested
var breakSize = (theMax - theMin) / nClasses;
// In the case of nClasses = 1, this loop won't run
// and the returned breaks will be [min, max]
for (var i = 1; i < nClasses; i++) {
breaks.push(breaks[0] + breakSize * i);
}
// the last break will always be the
// maximum.
breaks.push(theMax);
return breaks;
}
/**
* This computes the minimum & maximum number in an array.
*
* This runs in `O(n)`, linear time, with respect to the length of the array.
*
* @param {Array<number>} x sample of one or more data points
* @returns {Array<number>} minimum & maximum value
* @throws {Error} if the length of x is less than one
* @example
* extent([1, 2, 3, 4]);
* // => [1, 4]
*/
function extent(x) {
if (x.length === 0) {
throw new Error("extent requires at least one data point");
}
var min = x[0];
var max = x[0];
for (var i = 1; i < x.length; i++) {
if (x[i] > max) {
max = x[i];
}
if (x[i] < min) {
min = x[i];
}
}
return [min, max];
}
/**
* The extent is the lowest & highest number in the array. With a sorted array,
* the first element in the array is always the lowest while the last element is always the largest, so this calculation
* can be done in one step, or constant time.
*
* @param {Array<number>} x input
* @returns {Array<number>} minimum & maximum value
* @example
* extentSorted([-100, -10, 1, 2, 5]); // => [-100, 5]
*/
function extentSorted(x) {
return [x[0], x[x.length - 1]];
}
/**
* The [Geometric Mean](https://en.wikipedia.org/wiki/Geometric_mean) is
* a mean function that is more useful for numbers in different
* ranges.
*
* This is the nth root of the input numbers multiplied by each other.
*
* The geometric mean is often useful for
* **[proportional growth](https://en.wikipedia.org/wiki/Geometric_mean#Proportional_growth)**: given
* growth rates for multiple years, like _80%, 16.66% and 42.85%_, a simple
* mean will incorrectly estimate an average growth rate, whereas a geometric
* mean will correctly estimate a growth rate that, over those years,
* will yield the same end value.
*
* This runs in `O(n)`, linear time, with respect to the length of the array.
*
* @param {Array<number>} x sample of one or more data points
* @returns {number} geometric mean
* @throws {Error} if x is empty
* @throws {Error} if x contains a negative number
* @example
* var growthRates = [1.80, 1.166666, 1.428571];
* var averageGrowth = ss.geometricMean(growthRates);
* var averageGrowthRates = [averageGrowth, averageGrowth, averageGrowth];
* var startingValue = 10;
* var startingValueMean = 10;
* growthRates.forEach(function(rate) {
* startingValue *= rate;
* });
* averageGrowthRates.forEach(function(rate) {
* startingValueMean *= rate;
* });
* startingValueMean === startingValue;
*/
function geometricMean(x) {
if (x.length === 0) {
throw new Error("geometricMean requires at least one data point");
}
// the starting value.
var value = 1;
for (var i = 0; i < x.length; i++) {
// the geometric mean is only valid for positive numbers
if (x[i] < 0) {
throw new Error(
"geometricMean requires only non-negative numbers as input"
);
}
// repeatedly multiply the value by each number
value *= x[i];
}
return Math.pow(value, 1 / x.length);
}
/**
* The [Harmonic Mean](https://en.wikipedia.org/wiki/Harmonic_mean) is
* a mean function typically used to find the average of rates.
* This mean is calculated by taking the reciprocal of the arithmetic mean
* of the reciprocals of the input numbers.
*
* This is a [measure of central tendency](https://en.wikipedia.org/wiki/Central_tendency):
* a method of finding a typical or central value of a set of numbers.
*
* This runs in `O(n)`, linear time, with respect to the length of the array.
*
* @param {Array<number>} x sample of one or more data points
* @returns {number} harmonic mean
* @throws {Error} if x is empty
* @throws {Error} if x contains a negative number
* @example
* harmonicMean([2, 3]).toFixed(2) // => '2.40'
*/
function harmonicMean(x) {
if (x.length === 0) {
throw new Error("harmonicMean requires at least one data point");
}
var reciprocalSum = 0;
for (var i = 0; i < x.length; i++) {
// the harmonic mean is only valid for positive numbers
if (x[i] <= 0) {
throw new Error(
"harmonicMean requires only positive numbers as input"
);
}
reciprocalSum += 1 / x[i];
}
// divide n by the reciprocal sum
return x.length / reciprocalSum;
}
/**
* This is the internal implementation of quantiles: when you know
* that the order is sorted, you don't need to re-sort it, and the computations
* are faster.
*
* This implements the linear interpolation method (type=7 in R/numpy),
* which is the default in numpy.percentile and R's quantile.
*
* @param {Array<number>} x sample of one or more data points
* @param {number} p desired quantile: a number between 0 to 1, inclusive
* @returns {number} quantile value
* @throws {Error} if p ix outside of the range from 0 to 1
* @throws {Error} if x is empty
* @example
* quantileSorted([3, 6, 7, 8, 8, 9, 10, 13, 15, 16, 20], 0.5); // => 9
*/
function quantileSorted(x, p) {
// Use (n-1) * p for index, matching numpy's linear method (type=7)
var idx = (x.length - 1) * p;
if (x.length === 0) {
throw new Error("quantile requires at least one data point.");
} else if (p < 0 || p > 1) {
throw new Error("quantiles must be between 0 and 1");
} else if (p === 1) {
// If p is 1, directly return the last element
return x[x.length - 1];
} else if (p === 0) {
// If p is 0, directly return the first element
return x[0];
} else if (idx % 1 !== 0) {
// If idx is not integer, interpolate linearly between floor and ceil
var lower = Math.floor(idx);
var upper = Math.ceil(idx);
var fraction = idx - lower;
return x[lower] + fraction * (x[upper] - x[lower]);
} else {
// If idx is integer, type=7 returns the value at that index
return x[idx];
}
}
/**
* Rearrange items in `arr` so that all items in `[left, k]` range are the smallest.
* The `k`-th element will have the `(k - left + 1)`-th smallest value in `[left, right]`.
*
* Implements Floyd-Rivest selection algorithm https://en.wikipedia.org/wiki/Floyd-Rivest_algorithm
*
* @param {Array<number>} arr input array
* @param {number} k pivot index
* @param {number} [left] left index
* @param {number} [right] right index
* @returns {void} mutates input array
* @example
* var arr = [65, 28, 59, 33, 21, 56, 22, 95, 50, 12, 90, 53, 28, 77, 39];
* quickselect(arr, 8);
* // = [39, 28, 28, 33, 21, 12, 22, 50, 53, 56, 59, 65, 90, 77, 95]
*/
function quickselect(arr, k, left, right) {
left = left || 0;
right = right || arr.length - 1;
while (right > left) {
// 600 and 0.5 are arbitrary constants chosen in the original paper to minimize execution time
if (right - left > 600) {
var n = right - left + 1;
var m = k - left + 1;
var z = Math.log(n);
var s = 0.5 * Math.exp((2 * z) / 3);
var sd = 0.5 * Math.sqrt((z * s * (n - s)) / n);
if (m - n / 2 < 0) { sd *= -1; }
var newLeft = Math.max(left, Math.floor(k - (m * s) / n + sd));
var newRight = Math.min(
right,
Math.floor(k + ((n - m) * s) / n + sd)
);
quickselect(arr, k, newLeft, newRight);
}
var t = arr[k];
var i = left;
var j = right;
swap(arr, left, k);
if (arr[right] > t) { swap(arr, left, right); }
while (i < j) {
swap(arr, i, j);
i++;
j--;
while (arr[i] < t) { i++; }
while (arr[j] > t) { j--; }
}
if (arr[left] === t) { swap(arr, left, j); }
else {
j++;
swap(arr, j, right);
}
if (j <= k) { left = j + 1; }
if (k <= j) { right = j - 1; }
}
}
function swap(arr, i, j) {
var tmp = arr[i];
arr[i] = arr[j];
arr[j] = tmp;
}
/**
* The [quantile](https://en.wikipedia.org/wiki/Quantile):
* this is a population quantile, since we assume to know the entire
* dataset in this library. This implementation uses linear interpolation,
* equivalent to R's type=7 and numpy's default percentile method.
*
* Sample is a one-dimensional array of numbers,
* and p is either a decimal number from 0 to 1 or an array of decimal
* numbers from 0 to 1.
* In terms of a k/q quantile, p = k/q - it's just dealing with fractions or dealing
* with decimal values.
* When p is an array, the result of the function is also an array containing the appropriate
* quantiles in input order
*
* @param {Array<number>} x sample of one or more numbers
* @param {Array<number> | number} p the desired quantile, as a number between 0 and 1
* @returns {number} quantile
* @example
* quantile([3, 6, 7, 8, 8, 9, 10, 13, 15, 16, 20], 0.5); // => 9
*/
function quantile(x, p) {
var copy = x.slice();
if (Array.isArray(p)) {
// rearrange elements so that each element corresponding to a requested
// quantile is on a place it would be if the array was fully sorted
multiQuantileSelect(copy, p);
// Initialize the result array
var results = [];
// For each requested quantile
for (var i = 0; i < p.length; i++) {
results[i] = quantileSorted(copy, p[i]);
}
return results;
} else {
var idx = quantileIndex(copy.length, p);
quantileSelect(copy, idx, 0, copy.length - 1);
return quantileSorted(copy, p);
}
}
function quantileSelect(arr, k, left, right) {
if (k % 1 === 0) {
quickselect(arr, k, left, right);
} else {
k = Math.floor(k);
quickselect(arr, k, left, right);
quickselect(arr, k + 1, k + 1, right);
}
}
function multiQuantileSelect(arr, p) {
var indices = [0];
for (var i = 0; i < p.length; i++) {
indices.push(quantileIndex(arr.length, p[i]));
}
indices.push(arr.length - 1);
indices.sort(compare);
var stack = [0, indices.length - 1];
while (stack.length) {
var r = Math.ceil(stack.pop());
var l = Math.floor(stack.pop());
if (r - l <= 1) { continue; }
var m = Math.floor((l + r) / 2);
quantileSelect(
arr,
indices[m],
Math.floor(indices[l]),
Math.ceil(indices[r])
);
stack.push(l, m, m, r);
}
}
function compare(a, b) {
return a - b;
}
function quantileIndex(len, p) {
// Use (n-1) * p to match numpy's linear method (type=7)
var idx = (len - 1) * p;
if (p === 1) {
// If p is 1, directly return the last index
return len - 1;
} else if (p === 0) {
// If p is 0, directly return the first index
return 0;
} else if (idx % 1 !== 0) {
// If index is not integer, keep the fractional position so we can
// select both surrounding order statistics for interpolation.
return idx;
} else {
// If index is integer, return that exact position.
return idx;
}
}
/**
* The [Interquartile range](http://en.wikipedia.org/wiki/Interquartile_range) is
* a measure of statistical dispersion, or how scattered, spread, or
* concentrated a distribution is. It's computed as the difference between
* the third quartile and first quartile.
*
* @param {Array<number>} x sample of one or more numbers
* @returns {number} interquartile range: the span between lower and upper quartile,
* 0.25 and 0.75
* @example
* interquartileRange([0, 1, 2, 3]); // => 2
*/
function interquartileRange(x) {
// Interquartile range is the span between the upper quartile,
// at `0.75`, and lower quartile, `0.25`
var q1 = quantile(x, 0.75);
var q2 = quantile(x, 0.25);
if (typeof q1 === "number" && typeof q2 === "number") {
return q1 - q2;
}
}
/*
* Pull Breaks Values for Jenks
*
* the second part of the jenks recipe: take the calculated matrices
* and derive an array of n breaks.
*
* @private
*/
function jenksBreaks(data, lowerClassLimits, nClasses) {
var k = data.length;
var kclass = [];
var countNum = nClasses;
// the calculation of classes will never include the upper
// bound, so we need to explicitly set it
kclass[nClasses] = data[data.length - 1];
// the lowerClassLimits matrix is used as indices into itself
// here: the `k` variable is reused in each iteration.
while (countNum > 0) {
kclass[countNum - 1] = data[lowerClassLimits[k][countNum] - 1];
k = lowerClassLimits[k][countNum] - 1;
countNum--;
}
return kclass;
}
/*
* Compute Matrices for Jenks
*
* Compute the matrices required for Jenks breaks. These matrices
* can be used for any classing of data with `classes <= nClasses`
*
* @private
*/
function jenksMatrices(data, nClasses) {
// in the original implementation, these matrices are referred to
// as `LC` and `OP`
//
// * lowerClassLimits (LC): optimal lower class limits
// * varianceCombinations (OP): optimal variance combinations for all classes
var lowerClassLimits = [];
var varianceCombinations = [];
// loop counters
var i;
var j;
// the variance, as computed at each step in the calculation
var variance = 0;
// Initialize and fill each matrix with zeroes
for (i = 0; i < data.length + 1; i++) {
var tmp1 = [];
var tmp2 = [];
// despite these arrays having the same values, we need
// to keep them separate so that changing one does not change
// the other
for (j = 0; j < nClasses + 1; j++) {
tmp1.push(0);
tmp2.push(0);
}
lowerClassLimits.push(tmp1);
varianceCombinations.push(tmp2);
}
for (i = 1; i < nClasses + 1; i++) {
lowerClassLimits[1][i] = 1;
varianceCombinations[1][i] = 0;
// in the original implementation, 9999999 is used but
// since Javascript has `Infinity`, we use that.
for (j = 2; j < data.length + 1; j++) {
varianceCombinations[j][i] = Number.POSITIVE_INFINITY;
}
}
for (var l = 2; l < data.length + 1; l++) {
// `SZ` originally. this is the sum of the values seen thus
// far when calculating variance.
var sum = 0;
// `ZSQ` originally. the sum of squares of values seen
// thus far
var sumSquares = 0;
// `WT` originally. This is the number of
var w = 0;
// `IV` originally
var i4 = 0;
// in several instances, you could say `Math.pow(x, 2)`
// instead of `x * x`, but this is slower in some browsers
// introduces an unnecessary concept.
for (var m = 1; m < l + 1; m++) {
// `III` originally
var lowerClassLimit = l - m + 1;
var val = data[lowerClassLimit - 1];
// here we're estimating variance for each potential classing
// of the data, for each potential number of classes. `w`
// is the number of data points considered so far.
w++;
// increase the current sum and sum-of-squares
sum += val;
sumSquares += val * val;
// the variance at this point in the sequence is the difference
// between the sum of squares and the total x 2, over the number
// of samples.
variance = sumSquares - (sum * sum) / w;
i4 = lowerClassLimit - 1;
if (i4 !== 0) {
for (j = 2; j < nClasses + 1; j++) {
// if adding this element to an existing class
// will increase its variance beyond the limit, break
// the class at this point, setting the `lowerClassLimit`
// at this point.
if (
varianceCombinations[l][j] >=
variance + varianceCombinations[i4][j - 1]
) {
lowerClassLimits[l][j] = lowerClassLimit;
varianceCombinations[l][j] =
variance + varianceCombinations[i4][j - 1];
}
}
}
}
lowerClassLimits[l][1] = 1;
varianceCombinations[l][1] = variance;
}
// return the two matrices. for just providing breaks, only
// `lowerClassLimits` is needed, but variances can be useful to
// evaluate goodness of fit.
return {
lowerClassLimits: lowerClassLimits,
varianceCombinations: varianceCombinations
};
}
/**
* The **[jenks natural breaks optimization](http://en.wikipedia.org/wiki/Jenks_natural_breaks_optimization)**
* is an algorithm commonly used in cartography and visualization to decide
* upon groupings of data values that minimize variance within themselves
* and maximize variation between themselves.
*
* For instance, cartographers often use jenks in order to choose which
* values are assigned to which colors in a [choropleth](https://en.wikipedia.org/wiki/Choropleth_map)
* map.
*
* @param {Array<number>} data input data, as an array of number values
* @param {number} nClasses number of desired classes
* @returns {Array<number>} array of class break positions
* // split data into 3 break points
* jenks([1, 2, 4, 5, 7, 9, 10, 20], 3) // = [1, 7, 20, 20]
*/
function jenks(data, nClasses) {
if (nClasses > data.length) {
return null;
}
// sort data in numerical order, since this is expected
// by the matrices function
data = data.slice().sort(function (a, b) {
return a - b;
});
// get our basic matrices
var matrices = jenksMatrices(data, nClasses);
// we only need lower class limits here
var lowerClassLimits = matrices.lowerClassLimits;
// extract nClasses out of the computed matrices
return jenksBreaks(data, lowerClassLimits, nClasses);
}
/**
* [Simple linear regression](http://en.wikipedia.org/wiki/Simple_linear_regression)
* is a simple way to find a fitted line
* between a set of coordinates. This algorithm finds the slope and y-intercept of a regression line
* using the least sum of squares.
*
* @param {Array<Array<number>>} data an array of two-element of arrays,
* like `[[0, 1], [2, 3]]`
* @returns {Object} object containing slope and intersect of regression line
* @example
* linearRegression([[0, 0], [1, 1]]); // => { m: 1, b: 0 }
*/
function linearRegression(data) {
var m;
var b;
// Store data length in a local variable to reduce
// repeated object property lookups
var dataLength = data.length;
//if there's only one point, arbitrarily choose a slope of 0
//and a y-intercept of whatever the y of the initial point is
if (dataLength === 1) {
m = 0;
b = data[0][1];
} else {
// Initialize our sums and scope the `m` and `b`
// variables that define the line.
var sumX = 0;
var sumY = 0;
var sumXX = 0;
var sumXY = 0;
// Use local variables to grab point values
// with minimal object property lookups
var point;
var x;
var y;
// Gather the sum of all x values, the sum of all
// y values, and the sum of x^2 and (x*y) for each
// value.
//
// In math notation, these would be SS_x, SS_y, SS_xx, and SS_xy
for (var i = 0; i < dataLength; i++) {
point = data[i];
x = point[0];
y = point[1];
sumX += x;
sumY += y;
sumXX += x * x;
sumXY += x * y;
}
// `m` is the slope of the regression line
m =
(dataLength * sumXY - sumX * sumY) /
(dataLength * sumXX - sumX * sumX);
// `b` is the y-intercept of the line.
b = sumY / dataLength - (m * sumX) / dataLength;
}
// Return both values as an object.
return {
m: m,
b: b
};
}
/**
* Given the output of `linearRegression`: an object
* with `m` and `b` values indicating slope and intercept,
* respectively, generate a line function that translates
* x values into y values.
*
* @param {Object} mb object with `m` and `b` members, representing
* slope and intersect of desired line
* @returns {Function} method that computes y-value at any given
* x-value on the line.
* @example
* var l = linearRegressionLine(linearRegression([[0, 0], [1, 1]]));
* l(0) // = 0
* l(2) // = 2
* linearRegressionLine({ b: 0, m: 1 })(1); // => 1
* linearRegressionLine({ b: 1, m: 1 })(1); // => 2
*/
function linearRegressionLine(mb /*: { b: number, m: number }*/) {
// Return a function that computes a `y` value for each
// x value it is given, based on the values of `b` and `a`
// that we just computed.
return function (x) {
return mb.b + mb.m * x;
};
}
/**
* The [log average](https://en.wikipedia.org/wiki/https://en.wikipedia.org/wiki/Geometric_mean#Relationship_with_logarithms)
* is an equivalent way of computing the geometric mean of an array suitable for large or small products.
*
* It's found by calculating the average logarithm of the elements and exponentiating.
*
* @param {Array<number>} x sample of one or more data points
* @returns {number} geometric mean
* @throws {Error} if x is empty
* @throws {Error} if x contains a negative number
*/
function logAverage(x) {
if (x.length === 0) {
throw new Error("logAverage requires at least one data point");
}
var value = 0;
for (var i = 0; i < x.length; i++) {
if (x[i] < 0) {
throw new Error(
"logAverage requires only non-negative numbers as input"
);
}
value += Math.log(x[i]);
}
return Math.exp(value / x.length);
}
/**
* The maximum is the highest number in the array. With a sorted array,
* the last element in the array is always the largest, so this calculation
* can be done in one step, or constant time.
*
* @param {Array<number>} x input
* @returns {number} maximum value
* @example
* maxSorted([-100, -10, 1, 2, 5]); // => 5
*/
function maxSorted(x) {
return x[x.length - 1];
}
/**
* The simple [sum](https://en.wikipedia.org/wiki/Summation) of an array
* is the result of adding all numbers together, starting from zero.
*
* This runs in `O(n)`, linear time, with respect to the length of the array.
*
* @param {Array<number>} x input
* @return {number} sum of all input numbers
* @example
* sumSimple([1, 2, 3]); // => 6
*/
function sumSimple(x) {
var value = 0;
for (var i = 0; i < x.length; i++) {
if (typeof x[i] !== "number") {
return Number.NaN;
}
value += x[i];
}
return value;
}
/**
* The mean, _also known as average_,
* is the sum of all values over the number of values.
* This is a [measure of central tendency](https://en.wikipedia.org/wiki/Central_tendency):
* a method of finding a typical or central value of a set of numbers.
*
* The simple mean uses the successive addition method internally
* to calculate it's result. Errors in floating-point addition are
* not accounted for, s