// // Math.NET Numerics, part of the Math.NET Project // http://numerics.mathdotnet.com // http://github.com/mathnet/mathnet-numerics // http://mathnetnumerics.codeplex.com // // Copyright (c) 2009-2013 Math.NET // // Permission is hereby granted, free of charge, to any person // obtaining a copy of this software and associated documentation // files (the "Software"), to deal in the Software without // restriction, including without limitation the rights to use, // copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the // Software is furnished to do so, subject to the following // conditions: // // The above copyright notice and this permission notice shall be // included in all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES // OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT // HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, // WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR // OTHER DEALINGS IN THE SOFTWARE. // using System; namespace MathNet.Numerics.Statistics { /// /// Statistics operating on arrays assumed to be unsorted. /// WARNING: Methods with the Inplace-suffix may modify the data array by reordering its entries. /// /// /// /// public static class ArrayStatistics { // TODO: Benchmark various options to find out the best approach (-> branch prediction) // TODO: consider leveraging MKL /// /// Returns the smallest value from the unsorted data array. /// Returns NaN if data is empty or any entry is NaN. /// /// Sample array, no sorting is assumed. public static double Minimum(double[] data) { if (data == null) throw new ArgumentNullException("data"); if (data.Length == 0) return double.NaN; var min = double.PositiveInfinity; for (int i = 0; i < data.Length; i++) { if (data[i] < min || double.IsNaN(data[i])) { min = data[i]; } } return min; } /// /// Returns the smallest value from the unsorted data array. /// Returns NaN if data is empty or any entry is NaN. /// /// Sample array, no sorting is assumed. public static double Maximum(double[] data) { if (data == null) throw new ArgumentNullException("data"); if (data.Length == 0) return double.NaN; var max = double.NegativeInfinity; for (int i = 0; i < data.Length; i++) { if (data[i] > max || double.IsNaN(data[i])) { max = data[i]; } } return max; } /// /// Estimates the arithmetic sample mean from the unsorted data array. /// Returns NaN if data is empty or any entry is NaN. /// /// Sample array, no sorting is assumed. public static double Mean(double[] data) { if (data == null) throw new ArgumentNullException("data"); if (data.Length == 0) return double.NaN; double mean = 0; ulong m = 0; for (int i = 0; i < data.Length; i++) { mean += (data[i] - mean)/++m; } return mean; } /// /// Estimates the unbiased population variance from the provided samples as unsorted array. /// On a dataset of size N will use an N-1 normalizer. /// Returns NaN if data has less than two entries or if any entry is NaN. /// /// Sample array, no sorting is assumed. public static double Variance(double[] samples) { if (samples == null) throw new ArgumentNullException("samples"); if (samples.Length <= 1) return double.NaN; double variance = 0; double t = samples[0]; for (int i = 1; i < samples.Length; i++) { t += samples[i]; double diff = ((i + 1)*samples[i]) - t; variance += (diff*diff)/((i + 1)*i); } return variance/(samples.Length - 1); } /// /// Estimates the unbiased population standard deviation from the provided samples as unsorted array. /// On a dataset of size N will use an N-1 normalizer. /// Returns NaN if data has less than two entries or if any entry is NaN. /// /// Sample array, no sorting is assumed. public static double StandardDeviation(double[] samples) { return Math.Sqrt(Variance(samples)); } /// /// Evaluates the biased population variance from the provided full population as unsorted array. /// On a dataset of size N will use an N normalizer. /// Returns NaN if data is empty or if any entry is NaN. /// /// Sample array, no sorting is assumed. public static double PopulationVariance(double[] population) { if (population == null) throw new ArgumentNullException("population"); if (population.Length == 0) return double.NaN; double variance = 0; double t = population[0]; for (int i = 1; i < population.Length; i++) { t += population[i]; double diff = ((i + 1)*population[i]) - t; variance += (diff*diff)/((i + 1)*i); } return variance/population.Length; } /// /// Evaluates the biased population standard deviation from the provided full population as unsorted array. /// On a dataset of size N will use an N normalizer. /// Returns NaN if data is empty or if any entry is NaN. /// /// Sample array, no sorting is assumed. public static double PopulationStandardDeviation(double[] population) { return Math.Sqrt(PopulationVariance(population)); } /// /// Returns the order statistic (order 1..N) from the unsorted data array. /// WARNING: Works inplace and can thus causes the data array to be reordered. /// /// Sample array, no sorting is assumed. Will be reordered. /// One-based order of the statistic, must be between 1 and N (inclusive). public static double OrderStatisticInplace(double[] data, int order) { if (data == null) throw new ArgumentNullException("data"); if (order < 1 || order > data.Length) return double.NaN; if (order == 1) return Minimum(data); if (order == data.Length) return Maximum(data); return SelectInplace(data, order - 1); } /// /// Estimates the median value from the unsorted data array. /// Approximately median-unbiased regardless of the sample distribution (R8). /// WARNING: Works inplace and can thus causes the data array to be reordered. /// /// Sample array, no sorting is assumed. Will be reordered. public static double MedianInplace(double[] data) { return QuantileInplace(data, 0.5d); } /// /// Estimates the p-Percentile value from the unsorted data array. /// If a non-integer Percentile is needed, use Quantile instead. /// Approximately median-unbiased regardless of the sample distribution (R8). /// WARNING: Works inplace and can thus causes the data array to be reordered. /// /// Sample array, no sorting is assumed. Will be reordered. /// Percentile selector, between 0 and 100 (inclusive). public static double PercentileInplace(double[] data, int p) { return QuantileInplace(data, p / 100d); } /// /// Estimates the first quartile value from the unsorted data array. /// Approximately median-unbiased regardless of the sample distribution (R8). /// WARNING: Works inplace and can thus causes the data array to be reordered. /// /// Sample array, no sorting is assumed. Will be reordered. public static double LowerQuartileInplace(double[] data) { return QuantileInplace(data, 0.25d); } /// /// Estimates the third quartile value from the unsorted data array. /// Approximately median-unbiased regardless of the sample distribution (R8). /// WARNING: Works inplace and can thus causes the data array to be reordered. /// /// Sample array, no sorting is assumed. Will be reordered. public static double UpperQuartileInplace(double[] data) { return QuantileInplace(data, 0.75d); } /// /// Estimates the inter-quartile range from the unsorted data array. /// Approximately median-unbiased regardless of the sample distribution (R8). /// WARNING: Works inplace and can thus causes the data array to be reordered. /// /// Sample array, no sorting is assumed. Will be reordered. public static double InterquartileRangeInplace(double[] data) { return QuantileInplace(data, 0.75d) - QuantileInplace(data, 0.25d); } /// /// Estimates {min, lower-quantile, median, upper-quantile, max} from the unsorted data array. /// Approximately median-unbiased regardless of the sample distribution (R8). /// WARNING: Works inplace and can thus causes the data array to be reordered. /// /// Sample array, no sorting is assumed. Will be reordered. public static double[] FiveNumberSummaryInplace(double[] data) { if (data == null) throw new ArgumentNullException("data"); if (data.Length == 0) return new[] { double.NaN, double.NaN, double.NaN, double.NaN, double.NaN }; // TODO: Benchmark: is this still faster than sorting the array then using SortedArrayStatistics instead? return new[] { Minimum(data), QuantileInplace(data, 0.25), QuantileInplace(data, 0.50), QuantileInplace(data, 0.75), Maximum(data) }; } /// /// Estimates the tau-th quantile from the unsorted data array. /// The tau-th quantile is the data value where the cumulative distribution /// function crosses tau. /// Approximately median-unbiased regardless of the sample distribution (R8). /// WARNING: Works inplace and can thus causes the data array to be reordered. /// /// Sample array, no sorting is assumed. Will be reordered. /// Quantile selector, between 0.0 and 1.0 (inclusive). /// /// R-8, SciPy-(1/3,1/3): /// Linear interpolation of the approximate medians for order statistics. /// When tau < (2/3) / (N + 1/3), use x1. When tau >= (N - 1/3) / (N + 1/3), use xN. /// public static double QuantileInplace(double[] data, double tau) { if (data == null) throw new ArgumentNullException("data"); if (tau < 0d || tau > 1d || data.Length == 0) return double.NaN; double h = (data.Length + 1d/3d)*tau + 1d/3d; var hf = (int) h; if (hf <= 0 || tau == 0d) { return Minimum(data); } if (hf >= data.Length || tau == 1d) { return Maximum(data); } var a = SelectInplace(data, hf - 1); var b = SelectInplace(data, hf); return a + (h - hf)*(b - a); } /// /// Estimates the tau-th quantile from the unsorted data array. /// The tau-th quantile is the data value where the cumulative distribution /// function crosses tau. The quantile defintion can be specified /// by 4 parameters a, b, c and d, consistent with Mathematica. /// WARNING: Works inplace and can thus causes the data array to be reordered. /// /// Sample array, no sorting is assumed. Will be reordered. /// Quantile selector, between 0.0 and 1.0 (inclusive) public static double QuantileCustomInplace(double[] data, double tau, double a, double b, double c, double d) { if (data == null) throw new ArgumentNullException("data"); if (tau < 0d || tau > 1d || data.Length == 0) return double.NaN; var x = a + (data.Length + b) * tau - 1; #if PORTABLE var ip = (int)x; #else var ip = Math.Truncate(x); #endif var fp = x - ip; if (Math.Abs(fp) < 1e-9) { return SelectInplace(data, (int) ip); } var lower = SelectInplace(data, (int) Math.Floor(x)); var upper = SelectInplace(data, (int) Math.Ceiling(x)); return lower + (upper - lower) * (c + d * fp); } /// /// Estimates the tau-th quantile from the unsorted data array. /// The tau-th quantile is the data value where the cumulative distribution /// function crosses tau. The quantile definition can be specificed to be compatible /// with an existing system. /// WARNING: Works inplace and can thus causes the data array to be reordered. /// /// Sample array, no sorting is assumed. Will be reordered. /// Quantile selector, between 0.0 and 1.0 (inclusive) /// Quantile definition, to choose what product/definition it should be consistent with public static double QuantileCustomInplace(double[] data, double tau, QuantileDefinition definition) { if (data == null) throw new ArgumentNullException("data"); if (tau < 0d || tau > 1d || data.Length == 0) return double.NaN; if (tau == 0d || data.Length == 1) return Minimum(data); if (tau == 1d) return Maximum(data); switch (definition) { case QuantileDefinition.R1: { double h = data.Length * tau + 0.5d; return SelectInplace(data, (int)Math.Ceiling(h - 0.5d) - 1); } case QuantileDefinition.R2: { double h = data.Length * tau + 0.5d; return (SelectInplace(data, (int) Math.Ceiling(h - 0.5d) - 1) + SelectInplace(data, (int) (h + 0.5d) - 1))*0.5d; } case QuantileDefinition.R3: { double h = data.Length * tau; return SelectInplace(data, (int)Math.Round(h) - 1); } case QuantileDefinition.R4: { double h = data.Length * tau; var hf = (int)h; var lower = SelectInplace(data, hf - 1); var upper = SelectInplace(data, hf); return lower + (h - hf) * (upper - lower); } case QuantileDefinition.R5: { double h = data.Length * tau + 0.5d; var hf = (int)h; var lower = SelectInplace(data, hf - 1); var upper = SelectInplace(data, hf); return lower + (h - hf) * (upper - lower); } case QuantileDefinition.R6: { double h = (data.Length + 1) * tau; var hf = (int)h; var lower = SelectInplace(data, hf - 1); var upper = SelectInplace(data, hf); return lower + (h - hf) * (upper - lower); } case QuantileDefinition.R7: { double h = (data.Length - 1) * tau + 1d; var hf = (int)h; var lower = SelectInplace(data, hf - 1); var upper = SelectInplace(data, hf); return lower + (h - hf) * (upper - lower); } case QuantileDefinition.R8: { double h = (data.Length + 1 / 3d) * tau + 1 / 3d; var hf = (int)h; var lower = SelectInplace(data, hf - 1); var upper = SelectInplace(data, hf); return lower + (h - hf) * (upper - lower); } case QuantileDefinition.R9: { double h = (data.Length + 0.25d) * tau + 0.375d; var hf = (int)h; var lower = SelectInplace(data, hf - 1); var upper = SelectInplace(data, hf); return lower + (h - hf) * (upper - lower); } default: throw new NotSupportedException(); } } static double SelectInplace(double[] workingData, int rank) { // Numerical Recipes: select // http://en.wikipedia.org/wiki/Selection_algorithm if (rank <= 0) return Minimum(workingData); if (rank >= workingData.Length - 1) return Maximum(workingData); var a = workingData; int low = 0; int high = a.Length - 1; while (true) { if (high <= low + 1) { if (high == low + 1 && a[high] < a[low]) { var tmp = a[low]; a[low] = a[high]; a[high] = tmp; } return a[rank]; } int middle = (low + high) >> 1; var tmp1 = a[middle]; a[middle] = a[low + 1]; a[low + 1] = tmp1; if (a[low] > a[high]) { var tmp = a[low]; a[low] = a[high]; a[high] = tmp; } if (a[low + 1] > a[high]) { var tmp = a[low + 1]; a[low + 1] = a[high]; a[high] = tmp; } if (a[low] > a[low + 1]) { var tmp = a[low]; a[low] = a[low + 1]; a[low + 1] = tmp; } int begin = low + 1; int end = high; double pivot = a[begin]; while (true) { do begin++; while (a[begin] < pivot); do end--; while (a[end] > pivot); if (end < begin) break; var tmp = a[begin]; a[begin] = a[end]; a[end] = tmp; } a[low + 1] = a[end]; a[end] = pivot; if (end >= rank) high = end - 1; if (end <= rank) low = begin; } } } }