diff --git a/src/OSPSuite.Core/Extensions/MathExtensions.cs b/src/OSPSuite.Core/Extensions/MathExtensions.cs index b3a56b0ca..b93f02a89 100644 --- a/src/OSPSuite.Core/Extensions/MathExtensions.cs +++ b/src/OSPSuite.Core/Extensions/MathExtensions.cs @@ -92,36 +92,5 @@ public static float ArithmeticStandardDeviation(this IReadOnlyList array) return Convert.ToSingle(Math.Pow(sum, 0.5)); } - - public static float Percentile(this IReadOnlyList sortedArray, double percentile) - { - return sortedArray.Quantile(percentile / 100); - } - - public static float Median(this IReadOnlyList sortedArray) - { - return sortedArray.Percentile(50); - } - - public static float Quantile(this IReadOnlyList sortedArray, double quantile) - { - int N = sortedArray.Count; - - if (N == 0) - return float.NaN; - - double n = (N - 1) * quantile + 1; - - if (n == 1d) - return sortedArray[0]; - - if (n == N) - return sortedArray[N - 1]; - - int k = (int) n; - var d = n - k; - double value = sortedArray[k - 1] + d * (sortedArray[k] - sortedArray[k - 1]); - return (float) value; - } } } \ No newline at end of file diff --git a/src/OSPSuite.Core/Maths/SortedFloatArray.cs b/src/OSPSuite.Core/Maths/SortedFloatArray.cs new file mode 100644 index 000000000..fd056bcd0 --- /dev/null +++ b/src/OSPSuite.Core/Maths/SortedFloatArray.cs @@ -0,0 +1,54 @@ +using System.Collections.Generic; +using OSPSuite.Core.Extensions; + +namespace OSPSuite.Core.Maths +{ + public class SortedFloatArray + { + private readonly IReadOnlyList _sortedArray; + + /// + /// Takes a list of floats which will be used to preform quantile operations + /// + /// The list of floats used to calculate quantiles + /// If the list is already sorted, then specify as false + /// If the list needs to be sorted, specify true + public SortedFloatArray(IReadOnlyList floatArray, bool alreadySorted) + { + _sortedArray = floatArray; + if (!alreadySorted) + _sortedArray = _sortedArray.OrderedAndPurified(); + } + + public float Percentile(double percentile) + { + return Quantile(percentile / 100); + } + + public float Median() + { + return Percentile(50); + } + + public float Quantile(double quantile) + { + int N = _sortedArray.Count; + + if (N == 0) + return float.NaN; + + double n = (N - 1) * quantile + 1; + + if (n == 1d) + return _sortedArray[0]; + + if (n == N) + return _sortedArray[N - 1]; + + int k = (int)n; + var d = n - k; + double value = _sortedArray[k - 1] + d * (_sortedArray[k] - _sortedArray[k - 1]); + return (float)value; + } + } +} diff --git a/tests/OSPSuite.Core.Tests/Domain/MathExtensionsSpecs.cs b/tests/OSPSuite.Core.Tests/Domain/MathExtensionsSpecs.cs index a6f2eefa0..0b02e70be 100644 --- a/tests/OSPSuite.Core.Tests/Domain/MathExtensionsSpecs.cs +++ b/tests/OSPSuite.Core.Tests/Domain/MathExtensionsSpecs.cs @@ -1,6 +1,7 @@ using OSPSuite.BDDHelper; using OSPSuite.BDDHelper.Extensions; using OSPSuite.Core.Extensions; +using OSPSuite.Core.Maths; namespace OSPSuite.Core.Domain { @@ -20,25 +21,6 @@ public void should_return_the_expected_value() } } - public class When_calculating_the_median_of_an_array : StaticContextSpecification - { - private float[] _values1; - private float[] _values2; - - protected override void Context() - { - _values1 = new float[] {1, 2, 5, 8, 7}; - _values2 = new float[] {1, 2, 2, 6, 8, 7}; - } - - [Observation] - public void should_return_the_expected_value() - { - ((double) _values1.Median()).ShouldBeEqualTo(5); - ((double) _values2.Median()).ShouldBeEqualTo(4); - } - } - public class When_calculating_the_mean_of_an_array : StaticContextSpecification { private float[] _values; @@ -151,105 +133,5 @@ public void should_return_false_otherwise() } } - public class When_calculating_the_percentile : StaticContextSpecification - { - private float[] _values; - - protected override void Context() - { - _values = new float[] {0f, 1f, 2f, 3f, 4f, 5f, 6f, 7f, 8f, 9f, 10f}; - } - - [Observation] - public void should_return_the_expected_value() - { - ((double) _values.Percentile(50)).ShouldBeEqualTo(_values.Median(), 1e-5); - } - } - - public class When_calculating_the_percentile_with_doubled_values : StaticContextSpecification - { - private float[] _values; - - protected override void Context() - { - _values = new float[] {0f, 1f, 1f, 2f, 3f, 4f, 5f, 6f, 7f, 8f, 9f, 10f}; - } - - [Observation] - public void should_return_the_expected_value() - { - ((double) _values.Percentile(50)).ShouldBeEqualTo(_values.Median(), 1e-5); - } - } - - public class When_calculating_the_quantile_with_doubled_values : StaticContextSpecification - { - private float[] _values; - private float[] _values2; - - protected override void Context() - { - _values = new[] {0f, 1f, 2f, 3f, 4f, 5f, 6f, 7f, 8f, 9f, 10f}; - _values2 = new[] {2f, 3f, 3f, 4f, 5f, 6f}; - } - - [Observation] - public void should_return_the_expected_median() - { - ((double) _values.Quantile(0.5)).ShouldBeEqualTo(5f); - } - - [Observation] - public void should_calculate_Median() - { - _values.Quantile(0.5).ShouldBeEqualTo(5f); - } - - [Observation] - public void should_calculate_2_5Percentile() - { - _values.Quantile(0.025).ShouldBeEqualTo(0.25f); - } - - [Observation] - public void should_calculate_5Percentile() - { - _values.Quantile(0.05).ShouldBeEqualTo(0.5f); - } - - [Observation] - public void should_calculate_25Percentile() - { - _values.Quantile(0.25).ShouldBeEqualTo(2.5f); - } - - [Observation] - public void should_calculate_75Percentile() - { - _values.Quantile(0.75).ShouldBeEqualTo(7.5f); - } - - [Observation] - public void should_calculate_Percentiles() - { - _values.Quantile(0.83).ShouldBeEqualTo(8.3f); - _values.Quantile(0.8).ShouldBeEqualTo(8f); - - _values2.Quantile(0.24).ShouldBeEqualTo(3f); - _values2.Quantile(0).ShouldBeEqualTo(2f); - _values2.Quantile(0.05).ShouldBeEqualTo(2.25f); - _values2.Quantile(0.95).ShouldBeEqualTo(5.75f); - } - } - - public class When_calculating_the_quantile_for_an_array_with_only_one_value : StaticContextSpecification - { - [Observation] - public void should_return_the_expected_values() - { - new[] {1f}.Quantile(0.5).ShouldBeEqualTo(1f); - new[] {5f}.Quantile(0.5).ShouldBeEqualTo(5f); - } - } + } \ No newline at end of file diff --git a/tests/OSPSuite.Core.Tests/Domain/SortedFloatArraySpecs.cs b/tests/OSPSuite.Core.Tests/Domain/SortedFloatArraySpecs.cs new file mode 100644 index 000000000..d5ad3ece5 --- /dev/null +++ b/tests/OSPSuite.Core.Tests/Domain/SortedFloatArraySpecs.cs @@ -0,0 +1,142 @@ +using OSPSuite.BDDHelper; +using OSPSuite.Core.Maths; +using OSPSuite.BDDHelper.Extensions; + +namespace OSPSuite.Core.Domain +{ + public class concern_for_SortedFloatArray : ContextSpecification + { + + } + + public class When_calculating_the_median_of_an_unsorted_array : concern_for_SortedFloatArray + { + protected override void Context() + { + sut = new SortedFloatArray(new float[] { 1, 2, 7, 8, 5 }, false); + } + + [Observation] + public void should_return_the_expected_value() + { + ((double)sut.Median()).ShouldBeEqualTo(5); + } + } + + public class When_calculating_the_median_of_an_array : concern_for_SortedFloatArray + { + private SortedFloatArray _values1; + private SortedFloatArray _values2; + + protected override void Context() + { + _values1 = new SortedFloatArray(new float[] { 1, 2, 5, 8, 7 }, true); + _values2 = new SortedFloatArray(new float[] { 1, 2, 2, 6, 8, 7 }, true); + } + + [Observation] + public void should_return_the_expected_value() + { + ((double)_values1.Median()).ShouldBeEqualTo(5); + ((double)_values2.Median()).ShouldBeEqualTo(4); + } + } + + public class When_calculating_the_percentile : concern_for_SortedFloatArray + { + protected override void Context() + { + sut = new SortedFloatArray(new[] { 0f, 1f, 2f, 3f, 4f, 5f, 6f, 7f, 8f, 9f, 10f }, true); + } + + [Observation] + public void should_return_the_expected_value() + { + ((double)sut.Percentile(50)).ShouldBeEqualTo(sut.Median(), 1e-5); + } + } + + public class When_calculating_the_percentile_with_doubled_values : concern_for_SortedFloatArray + { + protected override void Context() + { + sut = new SortedFloatArray(new[] { 0f, 1f, 1f, 2f, 3f, 4f, 5f, 6f, 7f, 8f, 9f, 10f }, true); + } + + [Observation] + public void should_return_the_expected_value() + { + ((double)sut.Percentile(50)).ShouldBeEqualTo(sut.Median(), 1e-5); + } + } + + public class When_calculating_the_quantile_with_doubled_values : concern_for_SortedFloatArray + { + private SortedFloatArray _values; + private SortedFloatArray _values2; + + protected override void Context() + { + _values = new SortedFloatArray(new[] { 0f, 1f, 2f, 3f, 4f, 5f, 6f, 7f, 8f, 9f, 10f }, true); + _values2 = new SortedFloatArray(new[] { 2f, 3f, 3f, 4f, 5f, 6f }, true); + } + + [Observation] + public void should_return_the_expected_median() + { + ((double)_values.Quantile(0.5)).ShouldBeEqualTo(5f); + } + + [Observation] + public void should_calculate_Median() + { + _values.Quantile(0.5).ShouldBeEqualTo(5f); + } + + [Observation] + public void should_calculate_2_5Percentile() + { + _values.Quantile(0.025).ShouldBeEqualTo(0.25f); + } + + [Observation] + public void should_calculate_5Percentile() + { + _values.Quantile(0.05).ShouldBeEqualTo(0.5f); + } + + [Observation] + public void should_calculate_25Percentile() + { + _values.Quantile(0.25).ShouldBeEqualTo(2.5f); + } + + [Observation] + public void should_calculate_75Percentile() + { + _values.Quantile(0.75).ShouldBeEqualTo(7.5f); + } + + [Observation] + public void should_calculate_Percentiles() + { + _values.Quantile(0.83).ShouldBeEqualTo(8.3f); + _values.Quantile(0.8).ShouldBeEqualTo(8f); + + _values2.Quantile(0.24).ShouldBeEqualTo(3f); + _values2.Quantile(0).ShouldBeEqualTo(2f); + _values2.Quantile(0.05).ShouldBeEqualTo(2.25f); + _values2.Quantile(0.95).ShouldBeEqualTo(5.75f); + } + } + + public class When_calculating_the_quantile_for_an_array_with_only_one_value : concern_for_SortedFloatArray + { + [Observation] + public void should_return_the_expected_values() + { + new SortedFloatArray(new[] { 1f }, true).Quantile(0.5).ShouldBeEqualTo(1f); + new SortedFloatArray(new[] { 5f }, true).Quantile(0.5).ShouldBeEqualTo(5f); + } + } +}