Skip to content

Commit

Permalink
Merge pull request #45 from rcpch/eatyourpeas/issue32
Browse files Browse the repository at this point in the history
Eatyourpeas/issue32
  • Loading branch information
eatyourpeas authored Nov 3, 2024
2 parents 4627fcf + 6f13206 commit b7ba07c
Show file tree
Hide file tree
Showing 11 changed files with 126 additions and 57 deletions.
2 changes: 1 addition & 1 deletion .bumpversion.cfg
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
[bumpversion]
current_version = 4.2.3
current_version = 4.2.4
tag = False
commit = True

Expand Down
4 changes: 2 additions & 2 deletions rcpchgrowth/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,10 @@
from .constants import *
from .date_calculations import chronological_decimal_age, corrected_decimal_age, chronological_calendar_age, estimated_date_delivery, corrected_gestational_age
from .dynamic_growth import create_thrive_line, return_correlation, create_thrive_lines
from .global_functions import centile, sds_for_measurement, measurement_from_sds, percentage_median_bmi, mid_parental_height, measurement_for_z, cubic_interpolation, linear_interpolation
from .global_functions import centile, sds_for_measurement, measurement_from_sds, percentage_median_bmi, measurement_for_z, cubic_interpolation, linear_interpolation
from .fictional_child import generate_fictional_child_data
from .measurement import Measurement
from .mid_parental_height import *
from .mid_parental_height import mid_parental_height, mid_parental_height_z, expected_height_z_from_mid_parental_height_z, lower_and_upper_limits_of_expected_height_z
from .trisomy_21 import select_reference_data_for_trisomy_21
from .trisomy_21_aap import select_reference_data_for_trisomy_21_aap
from .turner import select_reference_data_for_turner
Expand Down
16 changes: 11 additions & 5 deletions rcpchgrowth/centile_bands.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# imports from rcpchgrowth
from rcpchgrowth.constants.reference_constants import COLE_TWO_THIRDS_SDS_NINE_CENTILES, THREE_PERCENT_CENTILES, UK_WHO, CDC
from .constants import BMI, HEAD_CIRCUMFERENCE,THREE_PERCENT_CENTILE_COLLECTION,COLE_TWO_THIRDS_SDS_NINE_CENTILE_COLLECTION ,FIVE_PERCENT_CENTILES, FIVE_PERCENT_CENTILE_COLLECTION, EIGHTY_FIVE_PERCENT_CENTILES, EIGHTY_FIVE_PERCENT_CENTILE_COLLECTION
from .constants import BMI, HEAD_CIRCUMFERENCE,THREE_PERCENT_CENTILE_COLLECTION,COLE_TWO_THIRDS_SDS_NINE_CENTILE_COLLECTION ,FIVE_PERCENT_CENTILES, FIVE_PERCENT_CENTILE_COLLECTION, EIGHTY_FIVE_PERCENT_CENTILES, EIGHTY_FIVE_PERCENT_CENTILE_COLLECTION, MAXIMUM_HEIGHT_WEIGHT_OFC_ADVISORY_SDS, MINIMUM_HEIGHT_WEIGHT_OFC_ADVISORY_SDS, MAXIMUM_BMI_ADVISORY_SDS, MINIMUM_BMI_ADVISORY_SDS, HEIGHT, WEIGHT, HEAD_CIRCUMFERENCE, BMI
from .global_functions import rounded_sds_for_centile, sds_for_centile

# Recommendations from Project board for reporting Centiles
Expand Down Expand Up @@ -107,6 +107,8 @@ def centile_band_for_centile(sds: float, measurement_method: str, centile_format
params: accepts a sds: float
params: accepts a measurement_method as string
params: accepts array of centiles representing the centile lines
These advice messages appear in the tooltips of the growth charts in the RCPCH Growth Chart and are advisory only. They do not reject data entry.
"""

centile_collection = []
Expand All @@ -121,15 +123,19 @@ def centile_band_for_centile(sds: float, measurement_method: str, centile_format

centile_band_ranges = generate_centile_band_ranges(centile_collection)

upper_threshold = MAXIMUM_HEIGHT_WEIGHT_OFC_ADVISORY_SDS
lower_threshold = MINIMUM_HEIGHT_WEIGHT_OFC_ADVISORY_SDS
if measurement_method == BMI:
measurement_method = "body mass index"
upper_threshold = MAXIMUM_BMI_ADVISORY_SDS
lower_threshold = MINIMUM_BMI_ADVISORY_SDS
elif measurement_method == HEAD_CIRCUMFERENCE:
measurement_method = "head circumference"

if sds <= -6:
return f"This {measurement_method} measurement is well below the normal range. Please review its accuracy."
elif sds > 6:
return f"This {measurement_method} measurement is well above the normal range. Please review its accuracy."
if sds < lower_threshold:
return f"This {measurement_method} measurement is well outside the normal range. Please check its accuracy."
elif sds > upper_threshold:
return f"This {measurement_method} measurement is well outside the normal range. Please check its accuracy."
elif sds <= centile_band_ranges[0][0]:
return f"This {measurement_method} measurement is below the normal range."
elif sds > centile_band_ranges[-1][1]:
Expand Down
22 changes: 14 additions & 8 deletions rcpchgrowth/constants/validation_constants.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
"""
These constants are intended to be used throughout both the rcpchgrowth module
and the API for validation of input values and rejection of very high or low values
The constants used previously drew on the Guiness Book of Records and other sources to set hard limits.
These were replaced with SDS sds_advisory and sds_error values in the rcpchgrowth-python repository as per issue #32
"""

# HEIGHT CONSTANTS
MINIMUM_LENGTH_CM = 20
MAXIMUM_HEIGHT_CM = 300
# The smallest baby was report by the BBC on 9/8/2021 (https://www.bbc.co.uk/news/world-asia-58141756.amp)
# to have a length of just 24cm and was born at 25 weeks gestation, discharged at 13 mths of age (chronological)
# from Singapore's National University Hospital
Expand All @@ -14,8 +15,6 @@
# https://en.wikipedia.org/wiki/Robert_Wadlow

# WEIGHT CONSTANTS
MINIMUM_WEIGHT_KG = 0.15
MAXIMUM_WEIGHT_KG = 500
# The smallest baby was report by the BBC on 9/8/2021 (https://www.bbc.co.uk/news/world-asia-58141756.amp)
# to weigh just 212g and was born at 25 weeks gestation, discharged at 13 mths of age (chronological)
# from Singapore's National University Hospital
Expand All @@ -25,16 +24,12 @@
# https://en.wikipedia.org/wiki/Jon_Brower_Minnoch

# OFC CONSTANTS
MINIMUM_OFC_CM = 5
MAXIMUM_OFC_CM = 100
# We couldn't find a good answer for the 'largest human head' that we could use
# Instead have presumed that 100 is a good maximum, given that pathological states
# causing very large head size (eg hydrocephalus) render the OFC meaningless for
# growth measurement and assessment purposes.

# BMI CONSTANTS
MINIMUM_BMI_KGM2 = 7.5
MAXIMUM_BMI_KGM2 = 105
# Again, John Brower Minnoch comes to our rescue here. HIs body mass index
# at the time of his death was 105 kg/m2. In terms of the lowest body mass index ever recorded
# there was nly limited web search information that suggested 7.5 kg.m2 as the lowest ever.
Expand All @@ -51,3 +46,14 @@
# the shortest woman in the world is Jyoti Kishanji Amge at 62.8 cm
# lower limit to paternal and maternal height here therefore set arbitrarily at 50 cm
# (this constant was added from the API server `schemas/request_validation_classes.py`)

# These constants are used to determine the range of SDS values that are considered - see discussion in issue #32 in the rcpchgrowth-python repository
# All previous hard-coded values have been replaced with these constants
MINIMUM_HEIGHT_WEIGHT_OFC_ADVISORY_SDS = -4
MAXIMUM_HEIGHT_WEIGHT_OFC_ADVISORY_SDS = 4
MINIMUM_BMI_ADVISORY_SDS = -4
MAXIMUM_BMI_ADVISORY_SDS = 4
MINIMUM_HEIGHT_WEIGHT_OFC_ERROR_SDS = -8
MAXIMUM_HEIGHT_WEIGHT_OFC_ERROR_SDS = 8
MINIMUM_BMI_ERROR_SDS = -15
MAXIMUM_BMI_ERROR_SDS = 15
14 changes: 0 additions & 14 deletions rcpchgrowth/global_functions.py
Original file line number Diff line number Diff line change
Expand Up @@ -210,20 +210,6 @@ def generate_centile(

return centile_measurements


def mid_parental_height(
height_paternal: float, height_maternal: float, sex: str
) -> float:
"""
Calculates mid-parental height for the child.
All units are in cm
"""
if sex == MALE:
return (height_paternal + height_maternal + 13) / 2
else:
return (height_paternal + height_maternal - 13) / 2


"""
*** PUBLIC FUNCTIONS THAT CONVERT BETWEEN CENTILE AND SDS
"""
Expand Down
60 changes: 37 additions & 23 deletions rcpchgrowth/measurement.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# standard imports
from datetime import date
from typing import Literal

# rcpch imports
from .centile_bands import centile_band_for_centile
Expand Down Expand Up @@ -71,13 +72,6 @@ def __init__(
# self.height_prediction_reference = height_prediction_reference
self.events_text = events_text

# validate the measurement method to ensure that the observation value is within the expected range - TODO this will be changed to SDS-based cutoffs
try:
self.__validate_measurement_method(
measurement_method=measurement_method, observation_value=observation_value)
observation_value_error = None
except Exception as err:
observation_value_error = f"{err}"

# the ages_object receives birth_data and measurement_dates objects
self.ages_object = self.__calculate_ages(
Expand All @@ -87,6 +81,13 @@ def __init__(
gestation_weeks=self.gestation_weeks,
gestation_days=self.gestation_days)

# validate the measurement method to ensure that the observation value is within the expected range - changed to SDS-based cutoffs - issue #32
try:
self.__validate_measurement_method(
measurement_method=measurement_method, observation_value=observation_value, corrected_decimal_age=self.ages_object['measurement_dates']['corrected_decimal_age'], reference=reference, sex=sex)
observation_value_error = None
except Exception as err:
observation_value_error = f"{err}"

# the calculate_measurements_object receives the child_observation_value and measurement_calculated_values objects
self.calculated_measurements_object = self.sds_and_centile_for_measurement_method(
Expand Down Expand Up @@ -598,17 +599,31 @@ def __create_measurement_object(
def __validate_measurement_method(
self,
measurement_method: str,
observation_value: float):
observation_value: float,
corrected_decimal_age: float,
sex: Literal["male", "female"],
reference: Literal['uk-who', 'turners-syndrome', 'trisomy-21', 'trisomy-21-aap', 'cdc'] = 'uk-who'):

# Private method which accepts a measurement_method (height, weight, bmi or ofc) and
# Private method which accepts a measurement_method (height, weight, bmi or ofc), reference and age as well as observation value
# and returns True if valid

is_valid = False

observation_value_z_score = None
if observation_value is not None:
observation_value_z_score = sds_for_measurement(
reference=reference, age=corrected_decimal_age, measurement_method=measurement_method, observation_value=observation_value, sex=sex)

if measurement_method == 'bmi':
if observation_value is None:
raise ValueError(
'Missing observation_value for Body Mass Index. Please pass a Body Mass Index in kilograms per metre squared (kg/m2)')
'Missing observation_value for Body Mass Index. Please pass a Body Mass Index in kilograms per metre squared (kg/m²)')
elif observation_value_z_score < MINIMUM_BMI_ERROR_SDS:
raise ValueError(
f'The Body Mass Index measurement of {observation_value} kg/m² is below -15 SD and considered to be an error.')
elif observation_value_z_score > MAXIMUM_BMI_KGM2:
raise ValueError(
f'The Body Mass Index measurement of {observation_value} kg/m² is above +15 SD and considered to be an error.')
else:
is_valid = True

Expand All @@ -620,40 +635,39 @@ def __validate_measurement_method(
# most likely metres passed instead of cm.
raise ValueError(
'Height/length must be passed in cm, not metres')
elif observation_value < MINIMUM_LENGTH_CM:
# a baby is unlikely to be < 30 cm long - probably a data entry error
elif observation_value_z_score < MINIMUM_HEIGHT_WEIGHT_OFC_ERROR_SDS:
raise ValueError(
f'The height/length you have entered is very low and likely to be an error. Are you sure you meant a height of {observation_value} centimetres?')
elif observation_value > MAXIMUM_HEIGHT_CM:
f'The height/length of {observation_value} cm is below -8 SD and considered to be an error.')
elif observation_value_z_score > MAXIMUM_HEIGHT_WEIGHT_OFC_ERROR_SDS:
raise ValueError(
f'The height/length you have entered is very high and likely to be an error. Are you sure you meant a height of {observation_value} centimetres?')
f'The height/length of {observation_value} cm is above +8 SD and considered to be an error.')
else:
is_valid = True

elif measurement_method == 'weight':
if observation_value is None:
raise ValueError(
'Missing observation_value for weight. Please pass a weight in kilograms.')
elif observation_value < MINIMUM_WEIGHT_KG:
elif observation_value_z_score < MINIMUM_HEIGHT_WEIGHT_OFC_ERROR_SDS:
raise ValueError(
f'Error. {observation_value} kilograms is very low. Please pass an accurate weight in kilograms')
elif observation_value > MAXIMUM_WEIGHT_KG:
f'The weight of {observation_value} kg is below -8 SD and considered to be an error.')
elif observation_value_z_score > MAXIMUM_HEIGHT_WEIGHT_OFC_ERROR_SDS:
# it is likely the weight is passed in grams, not kg.
raise ValueError(
f'{observation_value} kilograms is very high. Weight must be passed in kilograms.')
f'The weight of {observation_value} kg is above +8 SD and considered to be an error. Note that the weight should be supplied in kilograms.')
else:
is_valid = True

elif measurement_method == 'ofc':
if observation_value is None:
raise ValueError(
'Missing observation_value for head circumference. Please pass a head circumference in centimetres.')
elif observation_value < MINIMUM_OFC_CM:
elif observation_value_z_score < MINIMUM_HEIGHT_WEIGHT_OFC_ERROR_SDS:
raise ValueError(
f'Please check this value: {observation_value}. A head circumference less than 5 centimetres is likely an error. Please pass an accurate head circumference in centimetres.')
elif observation_value > MAXIMUM_OFC_CM:
f'The head circumference of {observation_value} cm is below -8 SD and considered to be an error.')
elif observation_value_z_score > MAXIMUM_HEIGHT_WEIGHT_OFC_ERROR_SDS:
raise ValueError(
f'Please check this value: {observation_value}. A head circumference > 150 centimetres is likely an error. Please pass an accurate head circumference in cm.')
f'The head circumference of {observation_value} cm is above +8 SD and considered to be an error.')
else:
is_valid = True

Expand Down
2 changes: 1 addition & 1 deletion rcpchgrowth/mid_parental_height.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ def mid_parental_height_z(maternal_height, paternal_height, reference=UK_WHO):
maternal_height_z = sds_for_measurement(reference=reference, age=20.0, measurement_method=HEIGHT, observation_value=maternal_height, sex=FEMALE)
paternal_height_z = sds_for_measurement(reference=reference, age=20.0, measurement_method=HEIGHT, observation_value=paternal_height, sex=MALE)

# take the measn of the z-scores and apply the regression coefficient of 0.5 - simplifed: (MatHtz +PatHtz)/4
# take the means of the z-scores and apply the regression coefficient of 0.5 - simplifed: (MatHtz +PatHtz)/4
mid_parental_height_z_score = (maternal_height_z + paternal_height_z) / 4.0

return mid_parental_height_z_score
Expand Down
26 changes: 26 additions & 0 deletions rcpchgrowth/tests/sds_age_validation_2021.json
Original file line number Diff line number Diff line change
Expand Up @@ -51998,5 +51998,31 @@
"observation_value": 53.9,
"corrected_sds": -0.577332795,
"chronological_sds": -0.628501058
},
{
"birth_date": "26/10/2015",
"observation_date": "01/09/2018",
"gestation_weeks": 40,
"gestation_days": 0,
"sex": "male",
"measurement_method": "height",
"observation_value": 180.0,
"chronological_age": 2.8501026694045173,
"corrected_sds": 23.499259393398773,
"corrected_age": 2.8501026694045173,
"chronological_sds": 23.499259393398773
},
{
"birth_date": "26/10/2015",
"observation_date": "01/09/2030",
"gestation_weeks": 40,
"gestation_days": 0,
"sex": "male",
"measurement_method": "bmi",
"observation_value": 7.0,
"chronological_age": 14.850102669404517,
"corrected_sds": -22.21786749424378,
"corrected_age": 14.850102669404517,
"chronological_sds": -22.21786749424378
}
]
10 changes: 9 additions & 1 deletion rcpchgrowth/tests/test_centile_band_for_centile.py
Original file line number Diff line number Diff line change
Expand Up @@ -373,4 +373,12 @@ def test_centile_band_for_centile_eighty_five_percent():
#on eighty-five-percent-centiles (85th, 98th, 99.9th, 99.99th)
for centile in on_eighty_five_percent_centiles_bmi:
sds = sds_for_centile(centile['centile'])
assert centile_band_for_centile(sds=sds, measurement_method="bmi", centile_format=EIGHTY_FIVE_PERCENT_CENTILES) == f"This body mass index measurement is on or near the {centile['text']} centile."
assert centile_band_for_centile(sds=sds, measurement_method="bmi", centile_format=EIGHTY_FIVE_PERCENT_CENTILES) == f"This body mass index measurement is on or near the {centile['text']} centile."

def test_advisory_thresholds():
for measurement in measurements:
for centile in beyond_eighty_five_percent_thresholds:
sds = 5 # above 4 and less than 8
if measurement == BMI:
sds = 10
assert centile_band_for_centile(sds=sds, measurement_method=measurement, centile_format=COLE_TWO_THIRDS_SDS_NINE_CENTILES) == f"This {measurement_texts[measurements.index(measurement)]} measurement is well outside the normal range. Please check its accuracy."
Loading

0 comments on commit b7ba07c

Please sign in to comment.