UNPKG

simple-statistics

Version:
1,543 lines (1,393 loc) 145 kB
/** * 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