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.");
}
}
}
}