Skip to content

Commit

Permalink
Merge pull request #388 from rcpch/eatyourpeas/issue302
Browse files Browse the repository at this point in the history
centiles-for-measurements
  • Loading branch information
eatyourpeas authored Nov 19, 2024
2 parents d057bdc + af9a8c1 commit a9ac3ca
Show file tree
Hide file tree
Showing 12 changed files with 657 additions and 117 deletions.
2 changes: 2 additions & 0 deletions project/npda/forms/external_patient_validators.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import logging
from dataclasses import dataclass
from datetime import date

import asyncio
from asgiref.sync import async_to_sync
Expand All @@ -12,6 +13,7 @@
gp_ods_code_for_postcode,
validate_postcode,
imd_for_postcode,
calculate_centiles_z_scores,
)


Expand Down
40 changes: 40 additions & 0 deletions project/npda/forms/visit_form.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,14 @@ class Meta:
"hospital_admission_reason",
"dka_additional_therapies",
"hospital_admission_other",
# calculated fields
"height_centile",
"height_sds",
"weight_centile",
"weight_sds",
"bmi",
"bmi_centile",
"bmi_sds",
]

widgets = {
Expand Down Expand Up @@ -100,8 +108,24 @@ class Meta:
"hospital_admission_reason": forms.Select(),
"dka_additional_therapies": forms.Select(),
"hospital_admission_other": forms.TextInput(attrs={"class": TEXT_INPUT}),
# calculated fields as hidden inputs
"height_centile": forms.HiddenInput(),
"height_sds": forms.HiddenInput(),
"weight_centile": forms.HiddenInput(),
"weight_sds": forms.HiddenInput(),
"bmi": forms.HiddenInput(),
"bmi_centile": forms.HiddenInput(),
"bmi_sds": forms.HiddenInput(),
}

height_centile = forms.CharField(widget=forms.HiddenInput(), required=False)
height_sds = forms.CharField(widget=forms.HiddenInput(), required=False)
weight_centile = forms.CharField(widget=forms.HiddenInput(), required=False)
weight_sds = forms.CharField(widget=forms.HiddenInput(), required=False)
bmi = forms.CharField(widget=forms.HiddenInput(), required=False)
bmi_centile = forms.CharField(widget=forms.HiddenInput(), required=False)
bmi_sds = forms.CharField(widget=forms.HiddenInput(), required=False)

categories = [
"Measurements",
"HBA1c",
Expand Down Expand Up @@ -708,6 +732,22 @@ def clean_hospital_discharge_date(self):
def clean(self):
cleaned_data = super().clean()

# prevent calculated fields from being saved as empty strings
if cleaned_data["height_centile"] == "":
cleaned_data["height_centile"] = None
if cleaned_data["height_sds"] == "":
cleaned_data["height_sds"] = None
if cleaned_data["weight_centile"] == "":
cleaned_data["weight_centile"] = None
if cleaned_data["weight_sds"] == "":
cleaned_data["weight_sds"] = None
if cleaned_data["bmi"] == "":
cleaned_data["bmi"] = None
if cleaned_data["bmi_centile"] == "":
cleaned_data["bmi_centile"] = None
if cleaned_data["bmi_sds"] == "":
cleaned_data["bmi_sds"] = None

hba1c_value = cleaned_data["hba1c"]
hba1c_format = cleaned_data["hba1c_format"]

Expand Down
1 change: 1 addition & 0 deletions project/npda/general_functions/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from .dgc_centile_calculations import *
from .email import *
from .group_for_group import *
from .index_multiple_deprivation import *
Expand Down
70 changes: 69 additions & 1 deletion project/npda/general_functions/csv/csv_upload.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,12 @@
from project.npda.forms.visit_form import VisitForm
from project.npda.forms.external_patient_validators import validate_patient_async

from project.npda.general_functions.dgc_centile_calculations import (
calculate_bmi,
calculate_centiles_z_scores,
)


async def csv_upload(user, dataframe, csv_file, pdu_pz_code):
"""
Processes standardised NPDA csv file and persists results in NPDA tables
Expand Down Expand Up @@ -281,15 +287,77 @@ async def validate_rows_in_parallel(rows_by_patient, async_client):
# We don't know what field caused the error so add to __all__
errors_to_return[patient_row_index]["__all__"].append(error)

no_errors_preventing_centile_calcuation = True
for visit_form, visit_row_index in parsed_visits:
# Errors validating the Visit fields
for field, error in visit_form.errors.as_data().items():
errors_to_return[visit_row_index][field].append(error)
if field in [
"height",
"weight",
"height_weight_observation_date",
"sex",
"date_of_birth",
]:
no_errors_preventing_centile_calcuation = False

try:
visit = create_instance(Visit, visit_form)
visit.patient = patient
await visit.asave()
# retrieve centiles and sds from RCPCH dGC API only if a measurement is supplied with a date and no errors
if (
(visit.height or visit.weight)
and visit.height_weight_observation_date
and visit.patient
and visit.patient.date_of_birth
and visit.patient.sex
and no_errors_preventing_centile_calcuation
):
if visit.height:
measurement_method = "height"
observation_value = visit.height
centile, sds = calculate_centiles_z_scores(
birth_date=visit.patient.date_of_birth,
observation_date=visit.height_weight_observation_date,
measurement_method=measurement_method,
observation_value=observation_value,
sex=visit.patient.sex,
)
visit.height_centile = centile
visit.height_sds = sds
if visit.weight:
measurement_method = "weight"
observation_value = visit.weight
centile, sds = calculate_centiles_z_scores(
birth_date=visit.patient.date_of_birth,
observation_date=visit.height_weight_observation_date,
measurement_method=measurement_method,
observation_value=observation_value,
sex=visit.patient.sex,
)
visit.weight_centile = centile
visit.weight_sds = sds
if visit.height and visit.weight:
measurement_method = "bmi"
visit.bmi = calculate_bmi(
height=visit.height, weight=visit.weight
)
observation_value = visit.bmi
centile, sds = calculate_centiles_z_scores(
birth_date=visit.patient.date_of_birth,
observation_date=visit.height_weight_observation_date,
measurement_method=measurement_method,
observation_value=observation_value,
sex=visit.patient.sex,
)
visit.bmi_centile = centile
visit.bmi_sds = sds
try:
await visit.asave()
except Exception as error:
print(
f"Error saving visit: {error}, height: {visit.height} (centile: {visit.height_centile, visit.height_sds}) {visit.weight} ({visit.weight_centile}, {visit.weight_sds}), {visit.bmi} ({visit.bmi_centile}, {visit.bmi_sds}), visit.patient: {visit.patient}"
)
except Exception as error:
errors_to_return[visit_row_index]["__all__"].append(error)

Expand Down
5 changes: 5 additions & 0 deletions project/npda/general_functions/data_generator_extended.py
Original file line number Diff line number Diff line change
Expand Up @@ -325,6 +325,11 @@ def _clinic_measures(
"height": height,
"weight": weight,
"height_weight_observation_date": height_weight_observation_date,
"height_centile": None,
"weight_centile": None,
"bmi": None,
"bmi_centile": None,
"bmi_sds": None,
"hba1c": hba1c,
"hba1c_format": hba1c_format,
"hba1c_date": hba1c_date,
Expand Down
103 changes: 103 additions & 0 deletions project/npda/general_functions/dgc_centile_calculations.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import datetime
import json
import logging
import requests
from requests.exceptions import HTTPError
from django.conf import settings

logger = logging.getLogger(__name__)


def calculate_centiles_z_scores(
birth_date, observation_date, sex, observation_value, measurement_method
):
"""
Calculate the centiles and z-scores for height and weight for a given patient.
:param height: The height of the patient.
:param weight: The weight of the patient.
:param birth_date: The birth date of the patient.
:param observation_date: The observation date of the patient.
:return: A tuple containing the centiles and z-scores for the requested measurement.
"""

url = f"{settings.RCPCH_DGC_API_URL}/uk-who/calculation"

# test if any of the parameters are none
if not all(
[
birth_date,
observation_date,
sex,
observation_value,
measurement_method,
]
):
logger.warning(f"Missing parameters in calculate_centiles_z_scores")
return None, None

if sex == 1:
sex = "male"
elif sex == 2:
sex = "female"
elif sex == 9 or sex == 0:
logger.warning(
"Sex is not known or not specified. Cannot calculate centiles and z-scores."
)
return None, None

body = {
"measurement_method": measurement_method,
"birth_date": birth_date.strftime("%Y-%m-%d"),
"observation_date": observation_date.strftime("%Y-%m-%d"),
"observation_value": float(observation_value),
"sex": sex,
"gestation_weeks": 40,
"gestation_days": 0,
}

ERROR_STRING = "An error occurred while fetching centile and z score details."
try:
response = requests.post(url=url, json=body, timeout=10)
response.raise_for_status()
except HTTPError as http_err:
logger.error(f"{ERROR_STRING} Error: http error {http_err.response.text}")
return None, None
except Exception as err:
logger.error(f"{ERROR_STRING} Error: {err}")
return None, None

if (
response.json()["measurement_calculated_values"]["corrected_centile"]
is not None
):
centile = round(
response.json()["measurement_calculated_values"]["corrected_centile"], 1
)
if response.json()["measurement_calculated_values"]["corrected_sds"] is not None:
z_score = round(
response.json()["measurement_calculated_values"]["corrected_sds"], 1
)

if centile is not None and centile > 99.9:
centile = 99.9

return (centile, z_score)


def calculate_bmi(height, weight):
"""
Calculate the BMI of a patient.
:param height: The height of the patient in cm.
:param weight: The weight of the patient in kg.
:return: The BMI of the patient.
"""
if height < 2:
raise ValueError("Height must be in cm.")
if weight > 250:
raise ValueError("Weight must be in kg.")
bmi = round(weight / (height / 100) ** 2, 1)
if bmi > 99:
return None
return bmi
Loading

0 comments on commit a9ac3ca

Please sign in to comment.