Skip to content

Commit

Permalink
Implementing statistics calculations. Still needs lots of testing.
Browse files Browse the repository at this point in the history
  • Loading branch information
Breno RdV committed Feb 15, 2024
1 parent 2e2bdcb commit 915a2ff
Show file tree
Hide file tree
Showing 21 changed files with 743 additions and 142 deletions.
16 changes: 12 additions & 4 deletions Raccoon.Ninja.AzFn.ScheduledTasks/HbA1cCalcFunc.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
using System.Linq;
using Microsoft.Azure.Functions.Worker;
using Microsoft.Extensions.Logging;
using Raccoon.Ninja.Domain.Core.Calculators;
using Raccoon.Ninja.Domain.Core.Calculators.Abstractions;
using Raccoon.Ninja.Domain.Core.Entities;
using Raccoon.Ninja.Domain.Core.ExtensionMethods;

Expand Down Expand Up @@ -50,12 +52,18 @@ public HbA1CCalculation Run(
{
var previousCalculation = previousCalculations.FirstOrDefault();

var hbA1C = readings.CalculateHbA1C(referenceDate);
var sortedGlucoseValues = readings.ToSortedValueArray();

if (previousCalculation is not null)
hbA1C = hbA1C with { Delta = hbA1C.Value - previousCalculation.Value };
var chain = BaseCalculatorHandler.BuildChain();

return hbA1C;
var calculatedData = chain.Handle(new CalculationData
{
GlucoseValues = sortedGlucoseValues,
PreviousHbA1C = previousCalculation
});

// TODO: Convert the calculated data into the appropriate CosmosDb documents.
return null;
}
catch (Exception e)
{
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
using Raccoon.Ninja.Domain.Core.Calculators.Abstractions;
using Raccoon.Ninja.Domain.Core.Calculators.Handlers;

namespace Raccoon.Ninja.Domain.Core.Tests.Calculators.Abstractions;

public class BaseCalculatorHandlerTests
{
[Fact]
public void BuildChain_ShouldReturnChain()
{
// Arrange
// Act
var result = BaseCalculatorHandler.BuildChain();

// Assert
result.Should().NotBeNull();
result.Should().BeOfType<AverageCalculator>();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
using Raccoon.Ninja.Domain.Core.Calculators.Handlers;
using Raccoon.Ninja.Domain.Core.Constants;
using Raccoon.Ninja.Domain.Core.Enums;
using Raccoon.Ninja.TestHelpers;

namespace Raccoon.Ninja.Domain.Core.Tests.Calculators.Handlers;

public class HbA1CCalculatorTests
{
[Theory]
[MemberData(nameof(TheoryGenerator.InvalidFloatListsWithNull), MemberType = typeof(TheoryGenerator))]
public void Handle_WhenListIsInvalid_ShouldReturnError(IList<float> glucoseValues)
{
// Arrange
var calculator = new HbA1CCalculator(null);

// Act
var result = calculator.Handle(Generators.CalculationDataMockSingle(glucoseValues));

// Assert
var status = result.Status;
status.Success.Should().BeFalse();
status.FirstFailedStep.Should().Be(nameof(HbA1CCalculator));
status.Message.Should().Be("This calculation requires a valid average glucose value.");
}

[Fact]
public void Handle_WhenListHasMoreThanExpected_ShouldReturnError()
{
// Arrange
var calculator = new HbA1CCalculator(null);

const int actualReadingCount = HbA1CConstants.ReadingsIn115Days + 1;

var glucoseValues = Generators.ListWithNumbers(actualReadingCount, 100f).ToList();

// Act
var result = calculator.Handle(Generators.CalculationDataMockSingle(glucoseValues));

// Assert
var status = result.Status;
status.Success.Should().BeFalse();
status.FirstFailedStep.Should().Be(nameof(HbA1CCalculator));
status.Message.Should().Be($"Too many readings to calculate HbA1c reliably. Expected (max) 33120 but got {actualReadingCount}");
}

[Theory]
[MemberData(nameof(TheoryGenerator.PartiallyValidHb1AcDataSets), MemberType = typeof(TheoryGenerator))]
public void Handle_WhenListHasLessThanExpectedReadings_ShouldReturnPartialSuccess(IList<float> glucoseValues, float expectedResult)
{
// Arrange
var calculator = new HbA1CCalculator(null);

// Act
var result = calculator.Handle(Generators.CalculationDataMockSingle(glucoseValues));

// Assert
result.Status.Success.Should().BeTrue();
result.CurrentHbA1C.Should().NotBeNull();
result.CurrentHbA1C.Value.Should().Be(expectedResult);
result.CurrentHbA1C.Status.Should().Be(HbA1CCalculationStatus.SuccessPartial);
}

[Theory]
[MemberData(nameof(TheoryGenerator.ValidHb1AcDataSets), MemberType = typeof(TheoryGenerator))]
public void CalculateHbA1c_WhenListHasExactNumberOfReadings_ShouldReturnSuccess(IList<float> glucoseValues, float expectedResult)
{
// Arrange
var calculator = new HbA1CCalculator(null);

// Act
var result = calculator.Handle(Generators.CalculationDataMockSingle(glucoseValues));

// Assert
result.Status.Success.Should().BeTrue();
result.CurrentHbA1C.Should().NotBeNull();
result.CurrentHbA1C.Value.Should().Be(expectedResult);
result.CurrentHbA1C.Status.Should().Be(HbA1CCalculationStatus.Success);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,6 @@ namespace Raccoon.Ninja.Domain.Core.Tests.ExtensionMethods;

public class ListExtensionsTests
{
private static readonly DateOnly ReferenceDate = DateOnly.FromDateTime(DateTime.UtcNow);

[Fact]
public void HasElements_Should_Return_True_When_List_Has_Elements()
{
Expand Down Expand Up @@ -48,71 +46,4 @@ public void HasElements_Should_Return_False_When_List_Is_Empty()
//Assert
result.Should().BeFalse();
}

[Fact]
public void CalculateHbA1c_WhenListIsNull_ShouldReturnError()
{
// Arrange
IEnumerable<GlucoseReading> list = null;

// Act
var result = list.CalculateHbA1C(ReferenceDate);

// Assert
result.Status.Should().Be(HbA1CCalculationStatus.Error);
result.Error.Should().Be("No readings to calculate HbA1c");
}

[Fact]
public void CalculateHbA1c_WhenListIsEmpty_ShouldReturnError()
{
// Arrange
var list = new List<GlucoseReading>();

// Act
var result = list.CalculateHbA1C(ReferenceDate);

// Assert
result.Status.Should().Be(HbA1CCalculationStatus.Error);
result.Error.Should().Be("No readings returned from Db to calculate HbA1c");
}

[Fact]
public void CalculateHbA1c_WhenListHasMoreThanExpected_ShouldReturnError()
{
// Arrange
var actualReadingCount = Constants.ReadingsIn115Days + 1;
var readings = Generators.GlucoseReadingMockList(actualReadingCount, 100);

// Act
var result = readings.CalculateHbA1C(ReferenceDate);

// Assert
result.Status.Should().Be(HbA1CCalculationStatus.Error);
result.Error.Should().Be($"Too many readings to calculate HbA1c reliably. Expected (max) 33120 but got {actualReadingCount}");
}

[Theory]
[MemberData(nameof(TheoryGenerator.PartiallyValidHb1AcDataSets), MemberType = typeof(TheoryGenerator))]
public void CalculateHbA1c_WhenListHasOneReading_ShouldReturnPartialSuccess(IList<GlucoseReading> readings, float expectedResult)
{
// Arrange & Act
var result = readings.CalculateHbA1C(ReferenceDate);

// Assert
result.Status.Should().Be(HbA1CCalculationStatus.SuccessPartial);
result.Value.Should().Be(expectedResult);
}

[Theory]
[MemberData(nameof(TheoryGenerator.ValidHb1AcDataSets), MemberType = typeof(TheoryGenerator))]
public void CalculateHbA1c_WhenListHasExactNumberOfReadings_ShouldReturnSuccess(IList<GlucoseReading> readings, float expectedResult)
{
// Arrange & Act
var result = readings.CalculateHbA1C(ReferenceDate);

// Assert
result.Status.Should().Be(HbA1CCalculationStatus.Success);
result.Value.Should().Be(expectedResult);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
using Raccoon.Ninja.Domain.Core.Calculators.Handlers;

namespace Raccoon.Ninja.Domain.Core.Calculators.Abstractions;

/// <summary>
/// Base class to handle the calculation of a specific metric.
/// The overall result will be converted do a CosmosDb document
/// and stored in the aggregation collection.
/// </summary>
/// <remarks>
/// Why not use a simple function that calculates everything at once?
/// - The idea is to have a pipeline of handlers, each one responsible for a specific calculation.
/// - This way, we can easily add new calculations without changing the main class.
/// - Also, we can easily test each calculation separately.
/// </remarks>
public abstract class BaseCalculatorHandler
{
private readonly BaseCalculatorHandler _nextHandler;

protected BaseCalculatorHandler(BaseCalculatorHandler nextHandler)
{
_nextHandler = nextHandler;
}

protected virtual bool CanHandle(CalculationData data)
{
return data.GlucoseValues is not null && data.GlucoseValues.Count > 0;
}

protected virtual CalculationData HandleError(CalculationData data)
{
return data with
{
Status = new CalculationDataStatus
{
Message = "No glucose values were provided.",
Success = false,
FirstFailedStep = GetType().Name
}
};
}

protected CalculationData HandleNext(CalculationData data)
{
return _nextHandler is null
? data
: _nextHandler.Handle(data);
}

public abstract CalculationData Handle(CalculationData data);

/// <summary>
/// Build the default chain of calculations.
/// </summary>
/// <returns>First link in the chain.</returns>
public static BaseCalculatorHandler BuildChain()
{
// Last step of the chain
var mageCalculator = new MageCalculator(null);
var tirCalculator = new TimeInRangeCalculator(mageCalculator);
var hbA1CCalculator = new HbA1CCalculator(tirCalculator);
var glucoseVariabilityCalculator = new RangeCalculator(hbA1CCalculator);
var medianCalculator = new MedianCalculator(glucoseVariabilityCalculator);
var percentileCalculator = new PercentileCalculator(medianCalculator);
var sdCalculator = new StandardDeviationCalculator(percentileCalculator);
var cvCalculator = new CoefficientOfVariationCalculator(sdCalculator);
var avgCalculator = new AverageCalculator(cvCalculator);
//First step of the chain

return avgCalculator;
}
}
46 changes: 46 additions & 0 deletions Raccoon.Ninja.Domain.Core/Calculators/CalculationData.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
using Raccoon.Ninja.Domain.Core.Entities;

namespace Raccoon.Ninja.Domain.Core.Calculators;

public record CalculationData
{
public IList<float> GlucoseValues { get; init; }
public float Average { get; init; }
public int Count => GlucoseValues?.Count ?? 0;
public float Median { get; init; }
public float Min { get; init; }
public float Max { get; init; }
public float Mage { get; init; }
public float StandardDeviation { get; init; }
public float CoefficientOfVariation { get; init; }
public HbA1CCalculation CurrentHbA1C { get; init; }
public HbA1CCalculation PreviousHbA1C { get; init; }

public CalculationDataTimeInRange TimeInRange { get; init; } = new ();
public CalculationDataPercentile Percentile { get; init; } = new ();
public CalculationDataStatus Status { get; init; } = new ();
}

public record CalculationDataTimeInRange
{
public float Low { get; init; }
public float Normal { get; init; }
public float High { get; init; }
public float VeryHigh { get; init; }
}

public record CalculationDataPercentile
{
public float P10 { get; init; }
public float P25 { get; init; }
public float P75 { get; init; }
public float P90 { get; init; }
public float Iqr { get; init; }
}

public record CalculationDataStatus
{
public bool Success { get; init; } = true;
public string FirstFailedStep { get; set; }
public string Message { get; init; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
using Raccoon.Ninja.Domain.Core.Calculators.Abstractions;

namespace Raccoon.Ninja.Domain.Core.Calculators.Handlers;

/// <summary>
/// Average Glucose: The average glucose level is a measure of central tendency that can be used to assess
/// overall glucose control.
/// </summary>
public class AverageCalculator: BaseCalculatorHandler
{
public AverageCalculator(BaseCalculatorHandler nextHandler) : base(nextHandler)
{
}

public override CalculationData Handle(CalculationData data)
{
if (!CanHandle(data))
{
return HandleError(data);
}

var average = data.GlucoseValues.Average();

return HandleNext(data with
{
Average = average,
});
}
}
Loading

0 comments on commit 915a2ff

Please sign in to comment.