Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions rcpchgrowth/constants/validation_constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,12 @@
# 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`)

MAXIMUM_PARENTAL_HEIGHT_CM = 260
# The tallest person for whom there is irrefutable evidence is
# Robert Pershing Wadlow who was 2.72m (272 cm) tall at his death in 1940
# https://en.wikipedia.org/wiki/Robert_Wadlow
# Setting maximum at 260 cm to allow for measurement errors while still catching impossible values

# 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
Expand Down
61 changes: 60 additions & 1 deletion rcpchgrowth/mid_parental_height.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from .constants import HEIGHT, MALE, FEMALE, UK_WHO, WHO
from .constants import HEIGHT, MALE, FEMALE, UK_WHO, WHO, MINIMUM_PARENTAL_HEIGHT_CM, MAXIMUM_PARENTAL_HEIGHT_CM, REFERENCES, SEXES
from .global_functions import sds_for_measurement
"""
Functions to calculate mid-parental height
Expand All @@ -11,7 +11,37 @@
def mid_parental_height(maternal_height, paternal_height, sex):
"""
Calculate mid-parental height

maternal_height: Maternal height in cm (float)
paternal_height: Paternal height in cm (float)
sex: Sex of the child ('male' or 'female')
return: Mid-parental height in cm (float)
raises ValueError: If any input is invalid
"""
# Validate sex
if sex not in SEXES:
raise ValueError(f"Sex must be '{MALE}' or '{FEMALE}', got '{sex}'")

# Validate maternal_height
if maternal_height is None:
raise ValueError("Maternal height cannot be None. Please provide a height in cm.")
if not isinstance(maternal_height, (int, float)):
raise ValueError(f"Maternal height must be a number, got {type(maternal_height).__name__}")
if maternal_height < MINIMUM_PARENTAL_HEIGHT_CM:
raise ValueError(f"Maternal height of {maternal_height} cm is below the minimum of {MINIMUM_PARENTAL_HEIGHT_CM} cm and is considered to be an error.")
if maternal_height > MAXIMUM_PARENTAL_HEIGHT_CM:
raise ValueError(f"Maternal height of {maternal_height} cm is above the maximum of {MAXIMUM_PARENTAL_HEIGHT_CM} cm and is considered to be an error.")

# Validate paternal_height
if paternal_height is None:
raise ValueError("Paternal height cannot be None. Please provide a height in cm.")
if not isinstance(paternal_height, (int, float)):
raise ValueError(f"Paternal height must be a number, got {type(paternal_height).__name__}")
if paternal_height < MINIMUM_PARENTAL_HEIGHT_CM:
raise ValueError(f"Paternal height of {paternal_height} cm is below the minimum of {MINIMUM_PARENTAL_HEIGHT_CM} cm and is considered to be an error.")
if paternal_height > MAXIMUM_PARENTAL_HEIGHT_CM:
raise ValueError(f"Paternal height of {paternal_height} cm is above the maximum of {MAXIMUM_PARENTAL_HEIGHT_CM} cm and is considered to be an error.")

if sex == MALE:
return (maternal_height + paternal_height + 13) / 2
else:
Expand All @@ -20,7 +50,36 @@ def mid_parental_height(maternal_height, paternal_height, sex):
def mid_parental_height_z(maternal_height, paternal_height, reference=UK_WHO):
"""
Calculate mid-parental height standard deviation

:param maternal_height: Maternal height in cm (float)
:param paternal_height: Paternal height in cm (float)
:param reference: Reference dataset to use (default: 'uk-who')
:return: Mid-parental height z-score (float)
:raises ValueError: If any input is invalid
"""
# Validate reference
if reference not in REFERENCES:
raise ValueError(f"Reference must be one of {REFERENCES}, got '{reference}'")

# Validate maternal_height
if maternal_height is None:
raise ValueError("Maternal height cannot be None. Please provide a height in cm.")
if not isinstance(maternal_height, (int, float)):
raise ValueError(f"Maternal height must be a number, got {type(maternal_height).__name__}")
if maternal_height < MINIMUM_PARENTAL_HEIGHT_CM:
raise ValueError(f"Maternal height of {maternal_height} cm is below the minimum of {MINIMUM_PARENTAL_HEIGHT_CM} cm and is considered to be an error.")
if maternal_height > MAXIMUM_PARENTAL_HEIGHT_CM:
raise ValueError(f"Maternal height of {maternal_height} cm is above the maximum of {MAXIMUM_PARENTAL_HEIGHT_CM} cm and is considered to be an error.")

# Validate paternal_height
if paternal_height is None:
raise ValueError("Paternal height cannot be None. Please provide a height in cm.")
if not isinstance(paternal_height, (int, float)):
raise ValueError(f"Paternal height must be a number, got {type(paternal_height).__name__}")
if paternal_height < MINIMUM_PARENTAL_HEIGHT_CM:
raise ValueError(f"Paternal height of {paternal_height} cm is below the minimum of {MINIMUM_PARENTAL_HEIGHT_CM} cm and is considered to be an error.")
if paternal_height > MAXIMUM_PARENTAL_HEIGHT_CM:
raise ValueError(f"Paternal height of {paternal_height} cm is above the maximum of {MAXIMUM_PARENTAL_HEIGHT_CM} cm and is considered to be an error.")

# convert parental heights to z-scores
adult_age = 20.0
Expand Down
105 changes: 103 additions & 2 deletions rcpchgrowth/tests/test_mid_parental_height.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import pytest

from rcpchgrowth.constants import MALE, FEMALE, UK_WHO, CDC
from rcpchgrowth.constants import MALE, FEMALE, UK_WHO, CDC, MINIMUM_PARENTAL_HEIGHT_CM, MAXIMUM_PARENTAL_HEIGHT_CM
from rcpchgrowth.mid_parental_height import mid_parental_height, mid_parental_height_z, expected_height_z_from_mid_parental_height_z

maternal_height = 151
Expand All @@ -13,4 +13,105 @@ def test_midparental_height():
assert mid_parental_height(maternal_height=maternal_height, paternal_height=paternal_height, sex=FEMALE) == 152.5
assert mid_parental_height_z(maternal_height=maternal_height, paternal_height=paternal_height, reference=UK_WHO) == pytest.approx(-0.8943229, ACCURACY)
assert mid_parental_height_z(maternal_height=maternal_height, paternal_height=paternal_height, reference=CDC) == pytest.approx(-0.8177165233046019, ACCURACY)
assert expected_height_z_from_mid_parental_height_z(mid_parental_height_z=-0.8943229) == pytest.approx(-0.44716145, ACCURACY)
assert expected_height_z_from_mid_parental_height_z(mid_parental_height_z=-0.8943229) == pytest.approx(-0.44716145, ACCURACY)


def test_midparental_height_validation_none_values():
"""Test that None values raise appropriate errors"""
with pytest.raises(ValueError, match="Maternal height cannot be None"):
mid_parental_height(maternal_height=None, paternal_height=paternal_height, sex=MALE)

with pytest.raises(ValueError, match="Paternal height cannot be None"):
mid_parental_height(maternal_height=maternal_height, paternal_height=None, sex=MALE)

with pytest.raises(ValueError, match="Maternal height cannot be None"):
mid_parental_height_z(maternal_height=None, paternal_height=paternal_height, reference=UK_WHO)

with pytest.raises(ValueError, match="Paternal height cannot be None"):
mid_parental_height_z(maternal_height=maternal_height, paternal_height=None, reference=UK_WHO)


def test_midparental_height_validation_non_numeric():
"""Test that non-numeric values raise appropriate errors"""
with pytest.raises(ValueError, match="Maternal height must be a number"):
mid_parental_height(maternal_height="151", paternal_height=paternal_height, sex=MALE)

with pytest.raises(ValueError, match="Paternal height must be a number"):
mid_parental_height(maternal_height=maternal_height, paternal_height="167", sex=MALE)

with pytest.raises(ValueError, match="Maternal height must be a number"):
mid_parental_height_z(maternal_height=[151], paternal_height=paternal_height, reference=UK_WHO)

with pytest.raises(ValueError, match="Paternal height must be a number"):
mid_parental_height_z(maternal_height=maternal_height, paternal_height={"height": 167}, reference=UK_WHO)


def test_midparental_height_validation_below_minimum():
"""Test that heights below minimum raise appropriate errors"""
too_short = MINIMUM_PARENTAL_HEIGHT_CM - 1

with pytest.raises(ValueError, match=f"Maternal height of {too_short} cm is below the minimum"):
mid_parental_height(maternal_height=too_short, paternal_height=paternal_height, sex=MALE)

with pytest.raises(ValueError, match=f"Paternal height of {too_short} cm is below the minimum"):
mid_parental_height(maternal_height=maternal_height, paternal_height=too_short, sex=MALE)

with pytest.raises(ValueError, match=f"Maternal height of {too_short} cm is below the minimum"):
mid_parental_height_z(maternal_height=too_short, paternal_height=paternal_height, reference=UK_WHO)

with pytest.raises(ValueError, match=f"Paternal height of {too_short} cm is below the minimum"):
mid_parental_height_z(maternal_height=maternal_height, paternal_height=too_short, reference=UK_WHO)


def test_midparental_height_validation_above_maximum():
"""Test that heights above maximum raise appropriate errors"""
too_tall = MAXIMUM_PARENTAL_HEIGHT_CM + 1

with pytest.raises(ValueError, match=f"Maternal height of {too_tall} cm is above the maximum"):
mid_parental_height(maternal_height=too_tall, paternal_height=paternal_height, sex=MALE)

with pytest.raises(ValueError, match=f"Paternal height of {too_tall} cm is above the maximum"):
mid_parental_height(maternal_height=maternal_height, paternal_height=too_tall, sex=MALE)

with pytest.raises(ValueError, match=f"Maternal height of {too_tall} cm is above the maximum"):
mid_parental_height_z(maternal_height=too_tall, paternal_height=paternal_height, reference=UK_WHO)

with pytest.raises(ValueError, match=f"Paternal height of {too_tall} cm is above the maximum"):
mid_parental_height_z(maternal_height=maternal_height, paternal_height=too_tall, reference=UK_WHO)


def test_midparental_height_validation_invalid_sex():
"""Test that invalid sex values raise appropriate errors"""
with pytest.raises(ValueError, match="Sex must be 'male' or 'female'"):
mid_parental_height(maternal_height=maternal_height, paternal_height=paternal_height, sex="invalid")

with pytest.raises(ValueError, match="Sex must be 'male' or 'female'"):
mid_parental_height(maternal_height=maternal_height, paternal_height=paternal_height, sex="MALE") # wrong case

with pytest.raises(ValueError, match="Sex must be 'male' or 'female'"):
mid_parental_height(maternal_height=maternal_height, paternal_height=paternal_height, sex=None)


def test_midparental_height_z_validation_invalid_reference():
"""Test that invalid reference values raise appropriate errors"""
with pytest.raises(ValueError, match="Reference must be one of"):
mid_parental_height_z(maternal_height=maternal_height, paternal_height=paternal_height, reference="invalid")

with pytest.raises(ValueError, match="Reference must be one of"):
mid_parental_height_z(maternal_height=maternal_height, paternal_height=paternal_height, reference=None)


def test_midparental_height_validation_boundary_values():
"""Test that boundary values (minimum and maximum) are accepted"""
# Test minimum boundary
assert mid_parental_height(maternal_height=MINIMUM_PARENTAL_HEIGHT_CM, paternal_height=MINIMUM_PARENTAL_HEIGHT_CM, sex=MALE) == (MINIMUM_PARENTAL_HEIGHT_CM + MINIMUM_PARENTAL_HEIGHT_CM + 13) / 2

# Test maximum boundary
assert mid_parental_height(maternal_height=MAXIMUM_PARENTAL_HEIGHT_CM, paternal_height=MAXIMUM_PARENTAL_HEIGHT_CM, sex=FEMALE) == (MAXIMUM_PARENTAL_HEIGHT_CM + MAXIMUM_PARENTAL_HEIGHT_CM - 13) / 2

# Test that z-score calculation works with boundary values
result = mid_parental_height_z(maternal_height=MINIMUM_PARENTAL_HEIGHT_CM, paternal_height=MINIMUM_PARENTAL_HEIGHT_CM, reference=UK_WHO)
assert isinstance(result, float)

result = mid_parental_height_z(maternal_height=MAXIMUM_PARENTAL_HEIGHT_CM, paternal_height=MAXIMUM_PARENTAL_HEIGHT_CM, reference=UK_WHO)
assert isinstance(result, float)