/// <summary> /// Calculates a z-score for the given indicator, age in months, measurement value, and gender. /// </summary> /// <param name="indicator">The indicator to use for computing the z-score (e.g. BMI-for-age, Height-for-Age, Weight-for-Age, etc.)</param> /// <param name="measurement1"> /// The first measurement value. Must be in metric units. For example, if the indicator is Height-for-Age, /// then measurement1 represents the child's height in centimeters. /// </param> /// <param name="measurement2"> /// The second measurement. Typically age of the child in months. For example, if the indicator is /// 'Height-for-Age', then measurement2 represents the child's age. If the indicator is instead /// 'Weight-for-Length' or 'Weight-for-Height' then measurement2 represents the child's length or /// height (respectively) and must be a non-zero value provided in centimeters. Automatically /// rounded to 5 decimal values. /// </param> /// <param name="sex">Whether the child is male or female</param> /// <returns>double; the calculated z-score for the given inputs</return> internal double CalculateZScore(Indicator indicator, double measurement1, double measurement2, Sex sex) { if (measurement1 < 0) { throw new ArgumentOutOfRangeException(nameof(measurement1)); } if (!IsValidMeasurement(indicator, measurement2)) { throw new ArgumentOutOfRangeException(nameof(measurement2)); } measurement2 = Math.Round(measurement2, 5); IDictionary <int, Lookup> reference = null; switch (indicator) { case Indicator.BodyMassIndexForAge: reference = CDC2000_BMI; break; case Indicator.HeadCircumferenceForAge: reference = CDC2000_HeadCircumference; break; case Indicator.LengthForAge: reference = CDC2000_LengthForAge; break; case Indicator.HeightForAge: reference = CDC2000_HeightForAge; break; case Indicator.WeightForAge: reference = CDC2000_WeightForAge; break; case Indicator.WeightForLength: reference = CDC2000_WeightForLength; break; case Indicator.WeightForHeight: reference = CDC2000_WeightForHeight; break; default: throw new ArgumentOutOfRangeException(nameof(indicator)); } int key = BuildKey(sex, measurement2); Lookup lookup = null; bool found = reference.TryGetValue(key, out lookup); if (found) { return(StatisticsHelper.CalculateZScore(measurement1, lookup.L, lookup.M, lookup.S, false)); } else { var interpolatedValues = InterpolateLMS(sex, measurement2, reference); return(StatisticsHelper.CalculateZScore(measurement1, interpolatedValues.Item1, interpolatedValues.Item2, interpolatedValues.Item3, false)); } }
/// <summary> /// Interpolates the L, M, and S values for a given measurement using the closest neighbors to that measurement. For /// example, if the lookup table has LMS entries for 24.5 and 25.5, and the measurement provided is 24.7, then the /// lower LMS values will be multiplied by 0.8 and added to the upper LMS values, which will be multiplied by 0.2. /// The interpolated LMS values are then returned in a 3-tuple. /// </summary> /// <remarks> /// The CDC 2000 Growth Chart data points are mostly spaced at 1.0 intervals, e.g. 24.5 to 25.5 to 26.5, etc. /// However, a small handful of points are spaced 0.5 apart such as the BMI-for-age data values at 24.0 and /// 24.5. Some extra logic is included to find the proper neighbor in this case (since one cannot assume that /// the neighbors will always be +/- 1 away) and then to adjust the modifiers applied to the upper and lower /// LMS values. This means a 24.1 measurement for age will see the lower LMS values multiplied by 0.8, since /// 0.1 is 20% of the distance to 0.5. If the measurement were instead 25.1, then the lower LMS values would /// be multiplied by 0.1 since 0.1 is 10% of the distance to 1.0. /// </remarks> /// <param name="sex">Whether the child is male or female</param> /// <param name="measurement">The measurement in metric units</param> /// <param name="reference">The lookup table to use to find the closest neighbors to the measurement value</param> /// <returns>3-tuple of double representing the interpolated L, M, and S values</return> internal Tuple <double, double, double> InterpolateLMS(Sex sex, double measurement, IDictionary <int, Lookup> reference) { double rounded = Math.Round(measurement); double nextUpper = -1; double nextLower = -1; Lookup lookupUpper = null; Lookup lookupLower = null; int initialKeyAttempt = BuildKey(sex, rounded); if (reference.ContainsKey(initialKeyAttempt)) { if (rounded > measurement) { nextUpper = rounded; lookupUpper = reference[initialKeyAttempt]; } else { nextLower = rounded; lookupLower = reference[initialKeyAttempt]; } } var keyUpper = -1; var keyLower = -1; if (nextUpper == -1) { nextUpper = Math.Round(measurement, 0) + 0.5; keyUpper = BuildKey(sex, nextUpper); } if (nextLower == -1) { nextLower = Math.Round(measurement, 0) - 0.5; keyLower = BuildKey(sex, nextLower); } if (reference.ContainsKey(keyUpper)) { lookupUpper = reference[keyUpper]; nextUpper = lookupUpper.Measurement; } if (reference.ContainsKey(keyLower)) { lookupLower = reference[keyLower]; nextLower = lookupUpper.Measurement; } if (nextUpper == -1) { nextUpper = Math.Round(measurement, 0) + 1; keyUpper = BuildKey(sex, nextUpper); if (reference.ContainsKey(keyUpper)) { lookupUpper = reference[keyUpper]; nextUpper = lookupUpper.Measurement; } } if (nextLower == -1) { nextLower = Math.Round(measurement, 0) - 1; keyLower = BuildKey(sex, nextLower); if (reference.ContainsKey(keyLower)) { lookupLower = reference[keyLower]; nextLower = lookupUpper.Measurement; } } double upperL = lookupUpper.L; double upperM = lookupUpper.M; double upperS = lookupUpper.S; double lowerL = lookupLower.L; double lowerM = lookupLower.M; double lowerS = lookupLower.S; double diff = lookupUpper.Measurement - lookupLower.Measurement; double globalModifier = 100 / (100 * diff); double lowerModifier = Math.Round((lookupUpper.Measurement - measurement), 5); double upperModifier = (diff - lowerModifier); lowerModifier *= globalModifier; upperModifier *= globalModifier; double L = ((lookupLower.L * lowerModifier) + (lookupUpper.L * upperModifier)); double M = ((lookupLower.M * lowerModifier) + (lookupUpper.M * upperModifier)); double S = ((lookupLower.S * lowerModifier) + (lookupUpper.S * upperModifier)); return(new Tuple <double, double, double>(L, M, S)); }
/// <summary> /// Calculates a z-score for a given indicator, pair of measurements (measurement1-for-measurement2, as /// in "BMI-for-Age"), and gender. /// </summary> /// <param name="indicator">The indicator to use for computing the z-score (e.g. BMI, Height-for-Age, Weight-for-Age, etc.)</param> /// <param name="measurement1"> /// The first measurement value. Must be in metric units and must be greater than or equal to zero. For /// example, if the indicator is 'Height-for-Age', then measurement1 represents the child's height in /// centimeters. Note that subscapular skinfold and triceps skinfold require measurement1 be provided /// in millimeters. /// </param> /// <param name="measurement2"> /// The second measurement. Typically age of the child in days. For example, if the indicator is /// 'Height-for-Age', then measurement2 represents the child's age. If the indicator is instead /// 'Weight-for-Length' or 'Weight-for-Height' then measurement2 represents the child's length or /// height (respectively) and must be a non-zero value provided in centimeters. Automatically /// rounded to 5 decimal values if measuring height or length and automatically rounded to a whole /// number if measuring age in days. /// </param> /// <param name="sex">Whether the child is male or female</param> /// <returns>double; the z-score for the given inputs</return> internal double CalculateZScore(Indicator indicator, double measurement1, double measurement2, Sex sex) { if (measurement1 < 0) { throw new ArgumentOutOfRangeException(nameof(measurement1)); } if (!IsValidMeasurement(indicator, measurement2)) { throw new ArgumentOutOfRangeException(nameof(measurement2)); } measurement2 = Math.Round(measurement2, 5); Dictionary <int, Lookup> reference = null; bool shouldRound = true; switch (indicator) { case Indicator.BodyMassIndexForAge: reference = WHO2006_BMI; break; case Indicator.WeightForLength: reference = WHO2006_WeightForLength; shouldRound = false; break; case Indicator.WeightForHeight: reference = WHO2006_WeightForHeight; shouldRound = false; break; case Indicator.WeightForAge: reference = WHO2006_WeightForAge; break; case Indicator.ArmCircumferenceForAge: reference = WHO2006_ArmCircumference; break; case Indicator.HeadCircumferenceForAge: reference = WHO2006_HeadCircumference; break; case Indicator.HeightForAge: case Indicator.LengthForAge: reference = WHO2006_LengthHeightForAge; break; case Indicator.SubscapularSkinfoldForAge: reference = WHO2006_SubscapularSkinfoldForAge; break; case Indicator.TricepsSkinfoldForAge: reference = WHO2006_TricepsSkinfoldForAge; break; default: throw new ArgumentOutOfRangeException(nameof(indicator)); } if (shouldRound && !StatisticsHelper.IsWholeNumber(measurement2)) { measurement2 = Math.Round(measurement2, 0); } int key = BuildKey(sex, measurement2); Lookup lookup = null; bool found = reference.TryGetValue(key, out lookup); if (found) { return(StatisticsHelper.CalculateZScore(measurement1, lookup.L, lookup.M, lookup.S, true)); } else if (indicator == Indicator.WeightForLength || indicator == Indicator.WeightForHeight) { var interpolatedLMS = InterpolateLMS(sex, measurement2, reference); return(StatisticsHelper.CalculateZScore(measurement1, interpolatedLMS.Item1, interpolatedLMS.Item2, interpolatedLMS.Item3, true)); } else { throw new InvalidOperationException($"Could not find a lookup match for value {Math.Round(measurement2, 2).ToString("N2")}"); } }