/*! * Stats.js (https://github.com/angusgibbs/stats.js) * Copyright 2012 Angus Gibbs */ (function(root) { // Create the top level stats object // ================================= // Wrapper to create a chainable stats object. // // arr - The array to work with. // // Returns a new chainable object. var stats = function(arr) { return new stats.init(arguments.length > 1 ? Array.prototype.slice.call(arguments, 0) : arr); }; // Creates a new chainable array object. // // arr - The array to work with. // // Returns a new chainable object. stats.init = function(arr) { this.arr = arr; this.length = arr.length; }; // Define the methods for the stats object stats.init.prototype = { // Calls a function on each element in an array or JSON object. // // fn - The function to call on each element. // el - The array or object element // index - The index or key of the array element // arr - The array or object // // Returns nothing. each: function(fn) { if (this.arr.length === undefined) { // The wrapped array is a JSON object for (var key in arr) { fn.call(this.arr[key], key, this.arr[key], this.arr); } } else { // The wrapper array is an array for (var i = 0, l = this.arr.length; i < l; i++) { fn.call(this.arr[i], this.arr[i], i, this.arr); } } return this; }, // Replaces each element in an array or JSON object with the result of // the function that is called against each element. // // fn - The function to call on each element // el - The array or object element // index - The index or key of the array element // // Returns nothing. map: function(fn) { var arr = this.arr; if (arr.length === undefined) { // The wrapped array is a JSON object for (var key in arr) { this.arr[key] = fn.call(arr[key], arr[key], key, arr); } } else { // The wrapped array is an array for (var i = 0, l = this.arr.length; i < l; i++) { this.arr[i] = fn.call(arr[i], arr[i], i, arr); } } return this; }, // Replaces each element of the array with the attribute of that given // element. // // attr - The attribute to pluck. // // Returns nothing. pluck: function(attr) { var newArr = []; if (this.arr.length === undefined) { // The wrapped array is a JSON object for (var key in arr) { newArr.push(this.arr[key][attr]); } } else { // The wrapped array is an array for (var i = 0, l = this.arr.length; i < l; i++) { newArr.push(this.arr[i][attr]); } } return stats(newArr); }, // Finds the smallest number. // // attr - Optional. If passed, the elemnt with the minimum value for the // given attribute will be returned. // // Returns the minimum. min: function(attr) { // Get the numbers var arr = this.arr; // Go through each of the numbers and find the minimum var minimum = attr == null ? arr[0] : arr[0][attr]; var minimumEl = attr == null ? arr[0] : arr[0]; stats(arr).each(function(num, index) { if ((attr == null ? num : num[attr]) < minimum) { minimum = attr == null ? num : num[attr]; minimumEl = num; } }); return minimumEl; }, // Finds the largest number. // // attr - Optional. If passed, the elemnt with the maximum value for the // given attribute will be returned. // // Returns the maximum. max: function(attr) { // Get the numbers var arr = this.arr; // Go through each of the numbers and find the maximum var maximum = attr == null ? arr[0] : arr[0][attr]; var maximumEl = attr == null ? arr[0] : arr[0]; stats(arr).each(function(num, index) { if ((attr == null ? num : num[attr]) > maximum) { maximum = attr == null ? num : num[attr]; maximumEl = num; } }); return maximumEl; }, // Finds the median of the numbers. // // Returns the median. median: function() { // Sort the numbers var arr = this.clone().sort().toArray(); if (arr.length % 2 === 0) { // There are an even number of elements in the array; the median // is the average of the middle two return (arr[arr.length / 2 - 1] + arr[arr.length / 2]) / 2; } else { // There are an odd number of elements in the array; the median // is the middle one return arr[(arr.length - 1) / 2]; } }, // Finds the first quartile of the numbers. // // Returns the first quartile. q1: function() { // Handle the single element case if (this.length == 1) { return this.arr[0]; } // Sort the numbers var nums = this.clone().sort(); // The first quartile is the median of the lower half of the numbers return nums.slice(0, Math.floor(nums.size() / 2)).median(); }, // Finds the third quartile of the numbers. // // Returns the third quartile. q3: function() { // Handle the single element case if (this.length == 1) { return this.arr[0]; } // Sort the numbers var nums = this.clone().sort(); // The third quartile is the median of the upper half of the numbers return nums.slice(Math.ceil(nums.size() / 2)).median(); }, // Finds the interquartile range of the data set. // // Returns the IQR. iqr: function() { return this.q3() - this.q1(); }, // Finds all outliers in the data set, using the 1.5 * IQR away from // the median test. // // Returns a new stats object with the outliers. findOutliers: function() { // Get the median and the range that the number must fall within var median = this.median(); var range = this.iqr() * 1.5; // Create a new stats object to hold the outliers var outliers = stats([]); // Go through each element in the data set and test to see if it // is an outlier this.each(function(num) { if (Math.abs(num - median) > range) { // The number is an outlier outliers.push(num); } }); return outliers; }, // Tests if the given number would be an outlier in the data set. // // num - The number to test. // // Returns a boolean. testOutlier: function(num) { return (Math.abs(num - this.median()) > this.iqr() * 1.5); }, // Removes all the outliers from the data set. // // Returns nothing. removeOutliers: function() { // Get the median and the range that the number must fall within var median = this.median(); var range = this.iqr() * 1.5; // Create a new stats object that will hold all the non-outliers var notOutliers = stats([]); // Go through each element in the data set and test to see if it // is an outlier this.each(function(num) { if (Math.abs(num - median) <= range) { // The number is not an outlier notOutliers.push(num); } }); return notOutliers; }, // Finds the mean of the numbers. // // Returns the mean. mean: function() { return this.sum() / this.size(); }, // Finds the sum of the numbers. // // Returns the sum. sum: function() { var result = 0; this.each(function(num) { result += num; }); return result; }, // Finds the standard deviation of the numbers. // // Returns the standard deviation. stdDev: function() { // Get the mean var mean = this.mean(); // Get a new stats object to work with var nums = this.clone(); // Map each element of nums to the square of the element minus the // mean nums.map(function(num) { return Math.pow(num - mean, 2); }); // Return the standard deviation return Math.sqrt(nums.sum() / (nums.size() - 1)); }, // Calculates the correlation coefficient for the data set. // // Returns the value of r. r: function() { // Get the x and y coordinates var xCoords = this.pluck('x'); var yCoords = this.pluck('y'); // Get the means for the x and y coordinates var meanX = xCoords.mean(); var meanY = yCoords.mean(); // Get the standard deviations for the x and y coordinates var stdDevX = xCoords.stdDev(); var stdDevY = yCoords.stdDev(); // Map each element to the difference of the element and the mean // divided by the standard deviation xCoords.map(function(num) { return (num - meanX) / stdDevX; }); yCoords.map(function(num) { return (num - meanY) / stdDevY; }); // Multiply each element in the x by the corresponding value in // the y var nums = this.clone().map(function(num, index) { return xCoords.get(index) * yCoords.get(index); }); // r is the sum of xCoords over the number of points minus 1 return nums.sum() / (nums.size() - 1); }, // Calculates the Least Squares Regression line for the data set. // // Returns an object with the slope and y intercept. linReg: function() { // Get the x and y coordinates var xCoords = this.pluck('x'); var yCoords = this.pluck('y'); // Get the means for the x and y coordinates var meanX = xCoords.mean(); var meanY = yCoords.mean(); // Get the standard deviations for the x and y coordinates var stdDevX = xCoords.stdDev(); var stdDevY = yCoords.stdDev(); // Calculate the correlation coefficient var r = this.r(); // Calculate the slope var slope = r * (stdDevY / stdDevX); // Calculate the y-intercept var yIntercept = meanY - slope * meanX; return { slope: slope, yIntercept: yIntercept, r: r }; }, // Calculates the exponential regression line for the data set. // // Returns an object with the coefficient, base, and correlation // coefficient for the linearized data. expReg: function() { // Get y coordinates var yCoords = this.pluck('y'); // Do a semi-log transformation of the coordinates yCoords.map(function(num) { return Math.log(num); }); // Get a new stats object to work with that has the transformed data var nums = this.clone().map(function(coord, index) { return { x: coord.x, y: yCoords.get(index) }; }); // Calculate the linear regression for the linearized data var linReg = nums.linReg(); // Calculate the coefficient for the exponential equation var coefficient = Math.pow(Math.E, linReg.yIntercept); // Calculate the base for the exponential equation var base = Math.pow(Math.E, linReg.slope); return { coefficient: coefficient, base: base, r: linReg.r }; }, // Calculates the power regression line for the data set. // // Returns an object with the coefficient, base, and correlation // coefficient for the linearized data. powReg: function() { // Get y coordinates var xCoords = this.pluck('x'); var yCoords = this.pluck('y'); // Do a log-log transformation of the coordinates xCoords.map(function(num) { return Math.log(num); }); yCoords.map(function(num) { return Math.log(num); }); // Get a new stats object to work with that has the transformed data var nums = this.clone().map(function(coord, index) { return { x: xCoords.get(index), y: yCoords.get(index) }; }); // Calculate the linear regression for the linearized data var linReg = nums.linReg(); // Calculate the coefficient for the power equation var coefficient = Math.pow(Math.E, linReg.yIntercept); // Calculate the exponent for the power equation var exponent = linReg.slope; return { coefficient: coefficient, exponent: exponent, r: linReg.r }; }, // Returns the number of elements. size: function() { return this.arr.length; }, // Clones the current stats object, providing a new stats object which // can be changed without modifying the original object. // // Returns a new stats object. clone: function() { return stats(this.arr.slice(0)); }, // Sorts the internal array, optionally by an attribute. // // attr - The attribute of the JSON object to sort by. (Optional.) // // Returns nothing. sort: function(attr) { // Create the sort function var sortFn; // CASE: Simple ascending sort if (attr == null) { sortFn = function(a, b) { return a - b; }; } // CASE: Simple descending sort else if (attr === true) { sortFn = function(a, b) { return b - a; }; } // CASE: Sort by an attribute else if (typeof attr === 'string') { sortFn = function(a, b) { return a[attr] - b[attr]; }; } // CASE: Sort by a function else { sortFn = attr; } this.arr = this.arr.sort(sortFn); return this; }, // Gets an element from the object. // // i - The index to retrieve. // // Returns the element at that index. get: function(i) { return this.arr[i]; }, // Sets an element on the object. // // i - The index to set. // val - The value to set the index to. // // Returns nothing. set: function(i, val) { this.arr[i] = val; return this; }, // Calculates the greatest common divisor of the set. // // Returns a Number, the gcd. gcd: function() { // Create a new stats object to work with var nums = this.clone(); // Go through each element and make the element the gcd of it // and the element to its left for (var i = 1; i < nums.size(); i++) { nums.set(i, gcd(nums.get(i - 1), nums.get(i))); } // The gcd of all the numbers is now in the final element return nums.get(nums.size() - 1); }, // Calculates the least common multiple of the set. // // Returns a Number, the lcm. lcm: function() { // Create a new stats object to work with var nums = this.clone(); // Go through each element and make the element the lcm of it // and the element to its left for (var i = 1; i < nums.size(); i++) { nums.set(i, lcm(nums.get(i - 1), nums.get(i))); } // The lcm of all the numbers if now in the final element return nums.get(nums.size() - 1); } }; // Private. Calculates the gcd of two numbers using Euclid's method. // // Returns a Number. function gcd(a, b) { if (b === 0) { return a; } return gcd(b, a - b * Math.floor(a / b)); } // Private. Calculates the lcm of two numbers. // // Returns a Number. function lcm(a, b) { // The least common multiple is the absolute value of the product of // the numbers divided by the greatest common denominator return Math.abs(a * b) / gcd(a, b); } // Provide built in JavaScript array mutator methods for the data list var mutators = ['pop', 'push', 'shift', 'splice', 'unshift']; for (var i = 0; i < mutators.length; i++) { stats.init.prototype[mutators[i]] = (function(method) { return function() { return Array.prototype[method].apply( this.arr, Array.prototype.slice.call(arguments, 0) ); }; })(mutators[i]); } // Provide built in JavaScript array accessor methods for the data list var accessors = ['concat', 'join', 'slice', 'reverse']; for (var i = 0; i < accessors.length; i++) { stats.init.prototype[accessors[i]] = (function(method) { return function() { this.arr = Array.prototype[method].apply( this.arr, Array.prototype.slice.call(arguments, 0) ); return this; }; })(accessors[i]); } // Override the built-in #toJSON() and #toArray() methods stats.init.prototype.toJSON = stats.init.prototype.toArray = function() { return this.arr; }; // Creates a list from the specified lower bound to the specified upper // bound. // // lower - The lower bound. // upper - The upper bound. // step - The amount to count by. // // Returns a new stats object. stats.list = function(lower, upper, step) { // Create the array var arr = []; for (var i = lower; i <= upper; i += step || 1) { arr[i - lower] = i; } return stats(arr); }; // Computes the factorial of a number. // // num - The number. // // Returns the factorial. stats.factorial = function(num) { // Handle the special case of 0 if (num === 0) { return 1; } // Otherwise compute the factorial for (var i = num - 1; i > 1; i--) { num *= i; } return num; }; // Computes a permutation. // // n - The length of the set. // r - The number of elements in the subset. // // Returns the permutation. stats.permutation = stats.nPr = function(n, r) { return stats.factorial(n) / stats.factorial(n - r); }; // Computes a combination. // // n - The length of the set. // r - The number of elements in the subset. // // Returns the combination. stats.combination = stats.nCr = function(n, r) { return stats.factorial(n) / (stats.factorial(r) * stats.factorial(n - r)); }; // Computes the probability of a binomial event. // // trials - The number of trials. // p - The probability of success. // x - The event number (optional). // // If x is not passed, an array with all the probabilities // will be returned. // // Returns a number or an array. stats.binompdf = function(trials, p, x) { // CASE: Specific event was passed if (x != null) { // Return 0 if the event does not exist if (x > trials || x < 0) { return 0; } // Return the probability otherwise return stats.nCr(trials, x) * Math.pow(p, x) * Math.pow(1 - p, trials - x); } // CASE: No specific event was passed else { // Compute the probabilities return stats.list(0, trials).map(function(num) { return stats.binompdf(trials, p, num); }).toArray(); } }; // Computes the cumulative probability of a binomial event. // // trials - The number of trials. // p - The probability of success. // x - The upper bound (inclusive). // // Returns the probability. stats.binomcdf = function(trials, p, x) { return stats.list(0, x).map(function(num) { return stats.binompdf(trials, p, num); }).sum(); }; // Computes the probability of a geometric event. // // p - The probability of success. // x - The event number. // // Returns the probability. stats.geompdf = function(p, x) { return Math.pow(1 - p, x - 1) * p; }; // Computes the cumulative probability of a geometric event. // // p - The probability of success. // x - The event number. // // Returns the probability. stats.geomcdf = function(p, x) { return stats.list(1, x).map(function(num) { return stats.geompdf(p, num); }).sum(); }; // Computes the normal probability of an event. // // x - The event number. // mu - The mean. // sigma - The standard deviation. // // Returns the probability. stats.normalpdf = function(x, mu, sigma) { return (1 / (sigma * Math.sqrt(2 * Math.PI))) * Math.exp(-1 * (x - mu) * (x - mu) / (2 * sigma * sigma)); }; // Computes the cumulative normal probability of an event. // // x - The event number. // mu - The mean. // sigma - The standard deviation. // // If four parameters are passed the first two are taken as the // low and high. // // Returns the probability. stats.normalcdf = function(x, mu, sigma) { // If four parameters were passed, return the difference of // the probabilities of the upper and lower if (arguments.length === 4) { // The cumulative probability is the difference of the // probabilities of the upper and the lower return stats.normalcdf(arguments[1], arguments[2], arguments[3]) - stats.normalcdf(arguments[0], arguments[2], arguments[3]); } // Convert the event to a z score var z = (x - mu) / sigma; // The probability will be stored here var p; // Coefficients var p0 = 220.2068679123761; var p1 = 221.2135961699311; var p2 = 112.0792914978709; var p3 = 33.91286607838300; var p4 = 6.373962203531650; var p5 = .7003830644436881; var p6 = .03526249659989109; var q0 = 440.4137358247522; var q1 = 793.8265125199484; var q2 = 637.3336333788311; var q3 = 296.5642487796737; var q4 = 86.78073220294608; var q5 = 16.06417757920695; var q6 = 1.755667163182642; var q7 = .08838834764831844; // Another coefficient (10/sqrt(2)) var cutoff = 7.071; // Cache the absolute value of the z score var zabs = Math.abs(z); // If you're more than 37 z scores away just return 1 or 0 if (z > 37) { return 1; } if (z < -37) { return 0.0; } // Compute the normalpdf of this event var exp = Math.exp(-.5 * zabs * zabs); var pdf = exp / Math.sqrt(2 * Math.PI); // Compute the probability if (zabs < cutoff) { p = exp*((((((p6*zabs + p5)*zabs + p4)*zabs + p3)*zabs + p2)*zabs + p1)*zabs + p0)/(((((((q7*zabs + q6)*zabs + q5)*zabs + q4)*zabs + q3)*zabs + q2)*zabs + q1)*zabs + q0); } else { p = pdf/(zabs + 1.0/(zabs + 2.0/(zabs + 3.0/(zabs + 4.0/ (zabs + 0.65))))); } if (z < 0.0) { return p; } else { return 1 - p; } }; // Computes the inverse normal. // // p - The probability of x being less than or equal to *x*, the value we // are looking for. // // Returns the x value. stats.invNorm = function(p) { // Coefficients for the rational approximation var a = [ -3.969683028665376e+01, 2.209460984245205e+02, -2.759285104469687e+02, 1.383577518672690e+02, -3.066479806614716e+01, 2.506628277459239e+00 ]; var b = [ -5.447609879822406e+01, 1.615858368580409e+02, -1.556989798598866e+02, 6.680131188771972e+01, -1.328068155288572e+01 ]; var c = [ -7.784894002430293e-03, -3.223964580411365e-01, -2.400758277161838e+00, -2.549732539343734e+00, 4.374664141464968e+00, 2.938163982698783e+00 ]; var d = [ 7.784695709041462e-03, 3.224671290700398e-01, 2.445134137142996e+00, 3.754408661907416e+00 ]; // Breakpoints var pLow = .02425; var pHigh = 1 - pLow; // Rational appoximation for the lower region if (0 < p && p < pLow) { var q = Math.sqrt(-2 * Math.log(p)); return (((((c[0] * q + c[1]) * q + c[2]) * q + c[3]) * q + c[4]) * q + c[5]) / ((((d[0] * q + d[1]) * q + d[2]) * q + d[3]) *q + 1); } else if (pLow <= p && p <= pHigh) { var q = p - 0.5; var r = q * q; return (((((a[0] * r + a[1]) * r + a[2]) * r + a[3]) * r + a[4]) * r + a[5]) * q / (((((b[0] * r + b[1]) * r + b[2]) * r + b[3]) * r + b[4]) * r + 1); } else if (pHigh < p && p < 1) { var q = Math.sqrt(-2 * Math.log(1 - p)); return -(((((c[0] * q + c[1]) * q + c[2]) * q + c[3]) * q + c[4]) * q + c[5]) / ((((d[0] * q + d[1]) * q + d[2]) * q + d[3]) * q + 1); } }; // Runs a 1-mean z-test. // // muZero - The proposed population mean. // sigma - The standard deviation of the population. // xBar - The sample mean. // n - The sample size. // inequality - One of "notequal", "lessthan", or "greaterthan", depending // on what you're testing // // Returns an object with the z-test statistic (z) and P-value (p). stats.ZTest = function(muZero, sigma, xBar, n, inequality) { // Calculate the z-test statistic var z = (xBar - muZero) / (sigma / Math.sqrt(n)); // Calculate the P-value var p; if (z < 0) { // Use normalcdf from -infinity to the z statistic p = stats.normalcdf(-Infinity, z, 0, 1); } else { // Use normalcdf from the z statistic to +infinity p = stats.normalcdf(z, Infinity, 0, 1); } // Multiply the P-value by two if you're doing a not equal test if (inequality === 'notequal') { p *= 2; } return { z: z, p: p }; }; // Computes a 1-mean z-interval. // // sigma - The population standard deviation. // xBar - The sample mean. // n - The sample size. // level - The confidence level (between 0 and 1, exclusive). // // Returns an object with the low, high, and margin of error (moe). stats.ZInterval = function(sigma, xBar, n, level) { // Compute the margin of error var moe = -stats.invNorm((1 - level) / 2) * sigma / Math.sqrt(n); return { low: xBar - moe, high: xBar + moe, moe: moe }; }; // Export the stats object // ================================================= if (typeof define === 'function') { // Expose to AMD loaders define(function() { return stats; }); } else if (typeof module !== 'undefined' && module.exports) { // Expose to Node and similar environments module.exports = stats; } else { // Just write to window (or whatever is the root object) root.stats = stats; } }(this));