Skip to content

Commit

Permalink
Fixes #1746 Create typed way to use quantile extension methods (#1747)
Browse files Browse the repository at this point in the history
  • Loading branch information
rwmcintosh authored Nov 7, 2022
1 parent 1450813 commit 5f12a7d
Show file tree
Hide file tree
Showing 4 changed files with 198 additions and 151 deletions.
31 changes: 0 additions & 31 deletions src/OSPSuite.Core/Extensions/MathExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -92,36 +92,5 @@ public static float ArithmeticStandardDeviation(this IReadOnlyList<float> array)

return Convert.ToSingle(Math.Pow(sum, 0.5));
}

public static float Percentile(this IReadOnlyList<float> sortedArray, double percentile)
{
return sortedArray.Quantile(percentile / 100);
}

public static float Median(this IReadOnlyList<float> sortedArray)
{
return sortedArray.Percentile(50);
}

public static float Quantile(this IReadOnlyList<float> 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;
}
}
}
54 changes: 54 additions & 0 deletions src/OSPSuite.Core/Maths/SortedFloatArray.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
using System.Collections.Generic;
using OSPSuite.Core.Extensions;

namespace OSPSuite.Core.Maths
{
public class SortedFloatArray
{
private readonly IReadOnlyList<float> _sortedArray;

/// <summary>
/// Takes a list of floats which will be used to preform quantile operations
/// </summary>
/// <param name="floatArray">The list of floats used to calculate quantiles</param>
/// <param name="alreadySorted">If the list is already sorted, then specify <paramref name="alreadySorted"/> as false
/// If the list needs to be sorted, specify true</param>
public SortedFloatArray(IReadOnlyList<float> 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;
}
}
}
122 changes: 2 additions & 120 deletions tests/OSPSuite.Core.Tests/Domain/MathExtensionsSpecs.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using OSPSuite.BDDHelper;
using OSPSuite.BDDHelper.Extensions;
using OSPSuite.Core.Extensions;
using OSPSuite.Core.Maths;

namespace OSPSuite.Core.Domain
{
Expand All @@ -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;
Expand Down Expand Up @@ -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);
}
}

}
142 changes: 142 additions & 0 deletions tests/OSPSuite.Core.Tests/Domain/SortedFloatArraySpecs.cs
Original file line number Diff line number Diff line change
@@ -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<SortedFloatArray>
{

}

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

0 comments on commit 5f12a7d

Please sign in to comment.