using System; using System.Collections.Generic; namespace Unity.PerformanceTesting.Statistics { /// /// Provides basic measurement statistics calculated for a sample collection. /// readonly ref struct MeasurementsStatistics { /// /// Mean of the samples. /// public double Mean { get; } /// /// Margin of error within the requested confidence interval. /// public double MarginOfError { get; } MeasurementsStatistics(double mean, double marginOfError) { Mean = mean; MarginOfError = marginOfError; } /// /// Calculates basic measurement statistics for a sample collection. /// /// The sample collection. /// Outlier removal mode. /// Confidence interval for calculating the margin of error. /// A MeasurementStatistics instance calculated for the provided data. /// Thrown if the sample count is zero. public static MeasurementsStatistics Calculate(List measurements, OutlierMode outlierMode, ConfidenceLevel confidenceLevel) { var n = measurements.Count; if (n == 0) throw new InvalidOperationException("Requesting statistic measurements of a sequence which contains no elements."); double sum; double mean; double variance; double standardDeviation; double standardError; double marginOfError; if (outlierMode == OutlierMode.DontRemove) { sum = Sum(measurements); mean = sum / n; variance = Variance(measurements, n, mean); standardDeviation = Math.Sqrt(variance); standardError = standardDeviation / Math.Sqrt(n); marginOfError = n <= 2 ? double.NaN : standardError * confidenceLevel.GetZValue(n); return new MeasurementsStatistics(mean, marginOfError); } measurements.Sort(); double q1, q3; if (n == 1) q1 = q3 = measurements[0]; else { q1 = GetQuartile(measurements, measurements.Count / 2); q3 = GetQuartile(measurements, measurements.Count * 3 / 2); } var interquartileRange = q3 - q1; var lowerFence = q1 - 1.5 * interquartileRange; var upperFence = q3 + 1.5 * interquartileRange; SumWithoutOutliers(outlierMode, measurements, lowerFence, upperFence, out sum, out n); mean = sum / n; variance = VarianceWithoutOutliers(outlierMode, measurements, n, mean, lowerFence, upperFence); standardDeviation = Math.Sqrt(variance); standardError = standardDeviation / Math.Sqrt(n); marginOfError = n <= 2 ? double.NaN : standardError * confidenceLevel.GetZValue(n); return new MeasurementsStatistics(mean, marginOfError); } static double Sum(List measurements) { var sum = 0d; foreach (var m in measurements) { sum += m; } return sum; } static void SumWithoutOutliers(OutlierMode outlierMode, List measurements, double lowerFence, double upperFence, out double sum, out int n) { sum = 0; n = 0; foreach (var m in measurements) { if (!IsOutlier(outlierMode, m, lowerFence, upperFence)) { sum += m; ++n; } } } static double Variance(List measurements, int n, double mean) { if (n == 1) { return 0; } double variance = 0; foreach (var m in measurements) { variance += (m - mean) * (m - mean) / (n - 1); } return variance; } static double VarianceWithoutOutliers(OutlierMode outlierMode, List measurements, int n, double mean, double lowerFence, double upperFence) { if (n == 1) { return 0; } double variance = 0; foreach (var m in measurements) { if (!IsOutlier(outlierMode, m, lowerFence, upperFence)) { variance += (m - mean) * (m - mean) / (n - 1); } } return variance; } static double GetQuartile(List measurements, int count) { if (count % 2 == 0) { return (measurements[count / 2 - 1] + measurements[count / 2]) / 2; } return measurements[count / 2]; } static bool IsOutlier(OutlierMode outlierMode, double value, double lowerFence, double upperFence) { switch (outlierMode) { case OutlierMode.DontRemove: return false; case OutlierMode.Remove: return value < lowerFence || value > upperFence; default: throw new ArgumentOutOfRangeException(nameof(outlierMode), outlierMode, "Unknown OutlierMode value."); } } } }