diff --git a/project/constants/types/kpi_types.py b/project/constants/types/kpi_types.py index 2b14203d..40b586bc 100644 --- a/project/constants/types/kpi_types.py +++ b/project/constants/types/kpi_types.py @@ -1,5 +1,7 @@ # Object types from dataclasses import dataclass +from datetime import date, datetime +from typing import Dict, Union @dataclass @@ -7,4 +9,17 @@ class KPIResult: total_eligible: int total_ineligible: int total_passed: int - total_failed: int \ No newline at end of file + total_failed: int + + +@dataclass +class KPICalculationsObject: + pz_code: str + calculation_datetime: datetime + audit_start_date: date + audit_end_date: date + total_patients_count: int + calculated_kpi_values: Dict[ + str, + Union[KPIResult, str], + ] # looks like { 'kpi_name' : KPIResult OR "Not implemented"} diff --git a/project/npda/general_functions/kpis.py b/project/npda/general_functions/kpis.py index d2c37c08..04ee41e0 100644 --- a/project/npda/general_functions/kpis.py +++ b/project/npda/general_functions/kpis.py @@ -1,9 +1,4 @@ -"""Views for KPIs - -TODO: - - refactor all calculate_kpi methods which require kpi_1 base query set to use - _get_total_kpi_1_eligible_pts_base_query_set_and_total_count - - additionally, do same for any other reused attrs +"""Views for KPIs calculations """ # Python imports @@ -28,7 +23,7 @@ from project.constants.retinal_screening_results import \ RETINAL_SCREENING_RESULTS from project.constants.smoking_status import SMOKING_STATUS -from project.constants.types.kpi_types import KPIResult +from project.constants.types.kpi_types import KPICalculationsObject, KPIResult from project.constants.yes_no_unknown import YES_NO_UNKNOWN from project.npda.general_functions import get_audit_period_for_date from project.npda.models import Patient @@ -37,6 +32,7 @@ # Logging logger = logging.getLogger(__name__) + class CalculateKPIS: def __init__(self, pz_code: str, calculation_date: date = None): @@ -185,7 +181,7 @@ def _run_kpi_calculation_method( return kpi_result - def calculate_kpis_for_patients(self) -> dict: + def calculate_kpis_for_patients(self) -> KPICalculationsObject: """Calculate KPIs 1 - 49 for given self.pz_code and cohort range (self.audit_start_date and self.audit_end_date). @@ -257,13 +253,11 @@ def calculate_kpi_1_total_eligible(self) -> KPIResult: ) ) - eligible_patients = ( - self.total_kpi_1_eligible_pts_base_query_set.distinct() - ) - self.kpi_1_total_eligible = eligible_patients.count() - # Count eligible patients and set as attribute # to be used in subsequent KPI calculations + self.kpi_1_total_eligible = ( + self.total_kpi_1_eligible_pts_base_query_set.count() + ) total_eligible = self.kpi_1_total_eligible # Calculate ineligible patients @@ -293,16 +287,15 @@ def calculate_kpi_2_total_new_diagnoses(self) -> KPIResult: * Date of diagnosis within the audit period" """ - # If we have not already calculated KPI 1, do so now to set - # self.total_kpi_1_eligible_pts_base_query_set - if not hasattr(self, "total_kpi_1_eligible_pts_base_query_set"): - self.calculate_kpi_1_total_eligible() + base_eligible_patients, _ = ( + self._get_total_kpi_1_eligible_pts_base_query_set_and_total_count() + ) # This is same as KPI1 but with an additional filter for diagnosis date self.total_kpi_2_eligible_pts_base_query_set = ( - self.total_kpi_1_eligible_pts_base_query_set.filter( + base_eligible_patients.filter( Q(diagnosis_date__range=(self.AUDIT_DATE_RANGE)) - ).distinct() + ) ) # Count eligible patients @@ -339,10 +332,14 @@ def calculate_kpi_3_total_t1dm(self) -> KPIResult: (1, Type 1 Insulin-Dependent Diabetes Mellitus) """ - eligible_patients = self.total_kpi_1_eligible_pts_base_query_set.filter( + base_eligible_patients, _ = ( + self._get_total_kpi_1_eligible_pts_base_query_set_and_total_count() + ) + + eligible_patients = base_eligible_patients.filter( # is type 1 diabetes Q(diabetes_type=DIABETES_TYPES[0][0]) - ).distinct() + ) # Count eligible patients total_eligible = eligible_patients.count() @@ -375,7 +372,11 @@ def calculate_kpi_4_total_t1dm_gte_12yo(self) -> KPIResult: * Diagnosis of Type 1 diabetes" """ - eligible_patients = self.total_kpi_1_eligible_pts_base_query_set.filter( + base_eligible_patients, _ = ( + self._get_total_kpi_1_eligible_pts_base_query_set_and_total_count() + ) + + eligible_patients = base_eligible_patients.filter( # Diagnosis of Type 1 diabetes Q(diabetes_type=DIABETES_TYPES[0][0]) # Age 12 and above years at the start of the audit period @@ -383,7 +384,7 @@ def calculate_kpi_4_total_t1dm_gte_12yo(self) -> KPIResult: date_of_birth__lte=self.audit_start_date - relativedelta(years=12) ) - ).distinct() + ) # Count eligible patients total_eligible = eligible_patients.count() @@ -437,7 +438,7 @@ def calculate_kpi_5_total_t1dm_complete_year(self) -> KPIResult: ) # EXCLUDE Date of death within the audit period" | Q(death_date__range=(self.AUDIT_DATE_RANGE)) - ).distinct() + ) # Count eligible patients total_eligible = eligible_patients.count() @@ -500,7 +501,8 @@ def calculate_kpi_6_total_t1dm_complete_year_gte_12yo(self) -> dict: | Q(death_date__range=(self.AUDIT_DATE_RANGE)) ) - eligible_patients = eligible_patients_exclusions.filter( + + base_eligible_patients = eligible_patients_exclusions.filter( # Valid attributes Q(nhs_number__isnull=False) & Q(date_of_birth__isnull=False) @@ -511,58 +513,66 @@ def calculate_kpi_6_total_t1dm_complete_year_gte_12yo(self) -> dict: ) # Diagnosis of Type 1 diabetes & Q(diabetes_type=DIABETES_TYPES[0][0]) - & ( - # an observation within the audit period - # this requires checking for a date in any of the Visit model's - # observation fields (found simply by searching for date fields - # with the word 'observation' in the field verbose_name) + ) + + # Find patients with at least one observation within the audit period + # this requires checking for a date in any of the Visits for a given + # patient + valid_visit_subquery = Visit.objects.filter( + Q( Q( - visit__height_weight_observation_date__range=( + height_weight_observation_date__range=( self.AUDIT_DATE_RANGE ) ) - | Q(visit__hba1c_date__range=(self.AUDIT_DATE_RANGE)) + | Q(hba1c_date__range=(self.AUDIT_DATE_RANGE)) | Q( - visit__blood_pressure_observation_date__range=( - self.AUDIT_DATE_RANGE - ) - ) - | Q( - visit__foot_examination_observation_date__range=( + blood_pressure_observation_date__range=( self.AUDIT_DATE_RANGE ) ) | Q( - visit__retinal_screening_observation_date__range=( + foot_examination_observation_date__range=( self.AUDIT_DATE_RANGE ) ) | Q( - visit__albumin_creatinine_ratio_date__range=( + retinal_screening_observation_date__range=( self.AUDIT_DATE_RANGE ) ) | Q( - visit__total_cholesterol_date__range=( + albumin_creatinine_ratio_date__range=( self.AUDIT_DATE_RANGE ) ) + | Q(total_cholesterol_date__range=(self.AUDIT_DATE_RANGE)) + | Q(thyroid_function_date__range=(self.AUDIT_DATE_RANGE)) + | Q(coeliac_screen_date__range=(self.AUDIT_DATE_RANGE)) | Q( - visit__thyroid_function_date__range=(self.AUDIT_DATE_RANGE) - ) - | Q(visit__coeliac_screen_date__range=(self.AUDIT_DATE_RANGE)) - | Q( - visit__psychological_screening_assessment_date__range=( + psychological_screening_assessment_date__range=( self.AUDIT_DATE_RANGE ) ) - ) - ).distinct() + ), + patient=OuterRef("pk"), + visit_date__range=self.AUDIT_DATE_RANGE, + ) + + # Check any observation across all visits + eligible_pts_annotated_kpi_6_visits = base_eligible_patients.annotate( + valid_kpi_6_visits=Exists(valid_visit_subquery) + ) + + eligible_patients = eligible_pts_annotated_kpi_6_visits.filter( + valid_kpi_6_visits__gte=1 + ) # Count eligible patients total_eligible = eligible_patients.count() - # In case we need to use this as a base query set for subsequent KPIs + # We reuse this as a base query set for subsequent KPIs so set + # as an attribute self.total_kpi_6_eligible_pts_base_query_set = eligible_patients self.kpi_6_total_eligible = total_eligible @@ -592,8 +602,9 @@ def calculate_kpi_7_total_new_diagnoses_t1dm(self) -> dict: * Date of diagnosis within the audit period """ - # total_kpi_1_eligible_pts_base_query_set is slightly different (additionally specifies - # visit date). So we need to make a new query set + # total_kpi_1_eligible_pts_base_query_set is slightly different + # (additionally specifies visit date). So we need to make a new + # query set eligible_patients = self.patients.filter( # Valid attributes Q(nhs_number__isnull=False) @@ -652,7 +663,7 @@ def calculate_kpi_7_total_new_diagnoses_t1dm(self) -> dict: ) ) ) - ).distinct() + ) # Count eligible patients total_eligible = eligible_patients.count() @@ -682,10 +693,14 @@ def calculate_kpi_8_total_deaths(self) -> dict: Number of eligible patients (measure 1) with: * a death date in the audit period """ - eligible_patients = self.total_kpi_1_eligible_pts_base_query_set.filter( + base_eligible_patients, _ = ( + self._get_total_kpi_1_eligible_pts_base_query_set_and_total_count() + ) + + eligible_patients = base_eligible_patients.filter( # Date of death within the audit period" Q(death_date__range=(self.AUDIT_DATE_RANGE)) - ).distinct() + ) # Count eligible patients total_eligible = eligible_patients.count() @@ -712,14 +727,18 @@ def calculate_kpi_9_total_service_transitions(self) -> dict: Number of eligible patients (measure 1) with * a leaving date in the audit period """ - eligible_patients = self.total_kpi_1_eligible_pts_base_query_set.filter( + base_eligible_patients, _ = ( + self._get_total_kpi_1_eligible_pts_base_query_set_and_total_count() + ) + + eligible_patients = base_eligible_patients.filter( # a leaving date in the audit period Q( paediatric_diabetes_units__date_leaving_service__range=( self.AUDIT_DATE_RANGE ) ) - ).distinct() + ) # Count eligible patients total_eligible = eligible_patients.count() @@ -755,14 +774,15 @@ def calculate_kpi_10_total_coeliacs(self) -> dict: ) # Filter the Patient queryset based on the subquery - eligible_patients = ( - self.total_kpi_1_eligible_pts_base_query_set.filter( - Q( - id__in=Subquery( - Patient.objects.filter( - visit__in=latest_visit_subquery - ).values("id") - ) + base_query_set, _ = ( + self._get_total_kpi_1_eligible_pts_base_query_set_and_total_count() + ) + eligible_patients = base_query_set.filter( + Q( + id__in=Subquery( + Patient.objects.filter( + visit__in=latest_visit_subquery + ).values("id") ) ) ) @@ -803,14 +823,15 @@ def calculate_kpi_11_total_thyroids(self) -> dict: ) # Filter the Patient queryset based on the subquery - eligible_patients = ( - self.total_kpi_1_eligible_pts_base_query_set.filter( - Q( - id__in=Subquery( - Patient.objects.filter( - visit__in=latest_visit_subquery - ).values("id") - ) + base_query_set, _ = ( + self._get_total_kpi_1_eligible_pts_base_query_set_and_total_count() + ) + eligible_patients = base_query_set.filter( + Q( + id__in=Subquery( + Patient.objects.filter( + visit__in=latest_visit_subquery + ).values("id") ) ) ) @@ -851,14 +872,15 @@ def calculate_kpi_12_total_ketone_test_equipment(self) -> dict: ) # Filter the Patient queryset based on the subquery - eligible_patients = ( - self.total_kpi_1_eligible_pts_base_query_set.filter( - Q( - id__in=Subquery( - Patient.objects.filter( - visit__in=latest_visit_subquery - ).values("id") - ) + base_query_set, _ = ( + self._get_total_kpi_1_eligible_pts_base_query_set_and_total_count() + ) + eligible_patients = base_query_set.filter( + Q( + id__in=Subquery( + Patient.objects.filter( + visit__in=latest_visit_subquery + ).values("id") ) ) ) @@ -890,8 +912,10 @@ def calculate_kpi_13_one_to_three_injections_per_day(self) -> dict: Denominator: Total number of eligible patients (measure 1) """ - eligible_patients = self.total_kpi_1_eligible_pts_base_query_set - total_eligible = self.kpi_1_total_eligible + eligible_patients, total_eligible = ( + self._get_total_kpi_1_eligible_pts_base_query_set_and_total_count() + ) + total_ineligible = self.total_patients_count - total_eligible # Define the subquery to find the latest visit where treatment_regimen = 1 @@ -928,8 +952,10 @@ def calculate_kpi_14_four_or_more_injections_per_day(self) -> dict: Denominator: Total number of eligible patients (measure 1) """ - eligible_patients = self.total_kpi_1_eligible_pts_base_query_set - total_eligible = self.kpi_1_total_eligible + eligible_patients, total_eligible = ( + self._get_total_kpi_1_eligible_pts_base_query_set_and_total_count() + ) + total_ineligible = self.total_patients_count - total_eligible # Define the subquery to find the latest visit where treatment_regimen = 2 @@ -966,8 +992,10 @@ def calculate_kpi_15_insulin_pump(self) -> dict: Denominator: Total number of eligible patients (measure 1) """ - eligible_patients = self.total_kpi_1_eligible_pts_base_query_set - total_eligible = self.kpi_1_total_eligible + eligible_patients, total_eligible = ( + self._get_total_kpi_1_eligible_pts_base_query_set_and_total_count() + ) + total_ineligible = self.total_patients_count - total_eligible # Define the subquery to find the latest visit where treatment_regimen = 3 @@ -1005,8 +1033,10 @@ def calculate_kpi_16_one_to_three_injections_plus_other_medication( Denominator: Total number of eligible patients (measure 1) """ - eligible_patients = self.total_kpi_1_eligible_pts_base_query_set - total_eligible = self.kpi_1_total_eligible + eligible_patients, total_eligible = ( + self._get_total_kpi_1_eligible_pts_base_query_set_and_total_count() + ) + total_ineligible = self.total_patients_count - total_eligible # Define the subquery to find the latest visit where treatment_regimen = 4 @@ -1044,8 +1074,9 @@ def calculate_kpi_17_four_or_more_injections_plus_other_medication( Denominator: Total number of eligible patients (measure 1) """ - eligible_patients = self.total_kpi_1_eligible_pts_base_query_set - total_eligible = self.kpi_1_total_eligible + eligible_patients, total_eligible = ( + self._get_total_kpi_1_eligible_pts_base_query_set_and_total_count() + ) total_ineligible = self.total_patients_count - total_eligible # Define the subquery to find the latest visit where treatment_regimen = 5 @@ -1083,8 +1114,9 @@ def calculate_kpi_18_insulin_pump_plus_other_medication( Denominator: Total number of eligible patients (measure 1) """ - eligible_patients = self.total_kpi_1_eligible_pts_base_query_set - total_eligible = self.kpi_1_total_eligible + eligible_patients, total_eligible = ( + self._get_total_kpi_1_eligible_pts_base_query_set_and_total_count() + ) total_ineligible = self.total_patients_count - total_eligible # Define the subquery to find the latest visit where treatment_regimen = 6 @@ -1122,8 +1154,9 @@ def calculate_kpi_19_dietary_management_alone( Denominator: Total number of eligible patients (measure 1) """ - eligible_patients = self.total_kpi_1_eligible_pts_base_query_set - total_eligible = self.kpi_1_total_eligible + eligible_patients, total_eligible = ( + self._get_total_kpi_1_eligible_pts_base_query_set_and_total_count() + ) total_ineligible = self.total_patients_count - total_eligible # Define the subquery to find the latest visit where treatment_regimen = 7 @@ -1161,8 +1194,9 @@ def calculate_kpi_20_dietary_management_plus_other_medication( Denominator: Total number of eligible patients (measure 1) """ - eligible_patients = self.total_kpi_1_eligible_pts_base_query_set - total_eligible = self.kpi_1_total_eligible + eligible_patients, total_eligible = ( + self._get_total_kpi_1_eligible_pts_base_query_set_and_total_count() + ) total_ineligible = self.total_patients_count - total_eligible # Define the subquery to find the latest visit where treatment_regimen = 8 @@ -1200,8 +1234,9 @@ def calculate_kpi_21_flash_glucose_monitor( Denominator: Total number of eligible patients (measure 1) """ - eligible_patients = self.total_kpi_1_eligible_pts_base_query_set - total_eligible = self.kpi_1_total_eligible + eligible_patients, total_eligible = ( + self._get_total_kpi_1_eligible_pts_base_query_set_and_total_count() + ) total_ineligible = self.total_patients_count - total_eligible # Define the subquery to find the latest visit where blood glucose monitoring (item 22) is either 2 = Flash glucose monitor or 3 = Modified flash glucose monitor (e.g. with MiaoMiao, Blucon etc.) @@ -1241,8 +1276,9 @@ def calculate_kpi_22_real_time_cgm_with_alarms( Denominator: Total number of eligible patients (measure 1) """ - eligible_patients = self.total_kpi_1_eligible_pts_base_query_set - total_eligible = self.kpi_1_total_eligible + eligible_patients, total_eligible = ( + self._get_total_kpi_1_eligible_pts_base_query_set_and_total_count() + ) total_ineligible = self.total_patients_count - total_eligible # Define the subquery to find the latest visit where blood glucose monitoring (item 22) is 4 = Real time continuous glucose monitor with alarms @@ -1280,13 +1316,10 @@ def calculate_kpi_23_type1_real_time_cgm_with_alarms( Denominator: Number of eligible patients whose most recent entry (based on visit date) for blood glucose monitoring (item 22) is 4 = Real time continuous glucose monitor with alarms """ - # If running this method standalone, need to set self.kpi_2_total_eligible first - # by running its calculation method - if not hasattr(self, "kpi_2_total_eligible"): - self.calculate_kpi_2_total_new_diagnoses() + eligible_patients, total_eligible = ( + self._get_total_kpi_2_eligible_pts_base_query_set_and_total_count() + ) - eligible_patients = self.total_kpi_2_eligible_pts_base_query_set - total_eligible = self.kpi_2_total_eligible total_ineligible = self.total_patients_count - total_eligible # Define the subquery to find the latest visit where blood glucose monitoring (item 22) is 4 = Real time continuous glucose monitor with alarms @@ -1868,7 +1901,7 @@ def calculate_kpi_35_smoking_status_screened( valid_smoking_visits = Visit.objects.filter( patient=OuterRef("pk"), visit_date__range=self.AUDIT_DATE_RANGE, - smoking_status__in=[1, 2], + smoking_status__in=[SMOKING_STATUS[0][0], SMOKING_STATUS[1][0]], ) eligible_pts_annotated_smoke_screen_visits = eligible_patients.annotate( smoke_valid_visits=Exists( @@ -1887,6 +1920,7 @@ def calculate_kpi_35_smoking_status_screened( ) ) + total_passed = total_passed_query_set.count() total_failed = total_eligible - total_passed @@ -2549,15 +2583,14 @@ def calculate_kpi_49_albuminuria_present( total_failed=total_failed, ) - def _debug_helper_print_postcode_and_attrs(self, queryset, *attrs): + def _debug_helper_print_postcode_and_attrs(self, patient_queryset, *attrs): """Helper function to be used with tests which prints out the postcode - (`can add name to postcode as non-validated string field`) and specified attributes for each patient in the queryset """ - logger.debug(f"===QuerySet:{str(queryset)}===") + logger.debug(f"===QuerySet:{str(patient_queryset)}===") logger.debug(f"==={self.AUDIT_DATE_RANGE=}===\n") - for item in queryset.values("postcode", *attrs): + for item in patient_queryset.values("postcode", *attrs): logger.debug(f'Patient Name: {item["postcode"]}') del item["postcode"] logger.debug(pformat(item) + "\n") @@ -2566,7 +2599,7 @@ def _debug_helper_print_postcode_and_attrs(self, queryset, *attrs): def _get_total_kpi_1_eligible_pts_base_query_set_and_total_count( self, - ) -> Tuple[QuerySet, int]: + ) -> Tuple[QuerySet[Patient], int]: """Enables reuse of the base query set for KPI 1 If running calculation methods in order, this attribute will be set in calculate_kpi_1_total_eligible(). @@ -2586,6 +2619,28 @@ def _get_total_kpi_1_eligible_pts_base_query_set_and_total_count( self.kpi_1_total_eligible, ) + def _get_total_kpi_2_eligible_pts_base_query_set_and_total_count( + self, + ) -> Tuple[QuerySet[Patient], int]: + """Enables reuse of the base query set for KPI 2 + + If running calculation methods in order, this attribute will be set in calculate_kpi_2_total_new_diagnoses(). + + If running another kpi calculation standalone, need to run that method first to have the attribute set. + + Returns: + QuerySet: Base query set of eligible patients for KPI 2 + int: base query set count of total eligible patients for KPI 2 + """ + + if not hasattr(self, "total_kpi_2_eligible_pts_base_query_set"): + self.calculate_kpi_2_total_new_diagnoses() + + return ( + self.total_kpi_2_eligible_pts_base_query_set, + self.kpi_2_total_eligible, + ) + def _get_total_kpi_5_eligible_pts_base_query_set_and_total_count( self, ) -> Tuple[QuerySet, int]: diff --git a/project/npda/tests/kpi_calculations/test_kpis_13_20.py b/project/npda/tests/kpi_calculations/test_kpis_13_20.py index 4886096b..423592ae 100644 --- a/project/npda/tests/kpi_calculations/test_kpis_13_20.py +++ b/project/npda/tests/kpi_calculations/test_kpis_13_20.py @@ -81,10 +81,6 @@ def test_kpi_calculations_13_to_20( pz_code="PZ130", calculation_date=AUDIT_START_DATE ) - # First set self.total_kpi_1_eligible_pts_base_query_set result - # of total eligible - calc_kpis.calculate_kpi_1_total_eligible() - # Dynamically get the kpi calc method based on treatment type # `treatment` is an int between 1-8 # these kpi calulations start at 13 diff --git a/project/npda/tests/kpi_calculations/test_kpis_1_12.py b/project/npda/tests/kpi_calculations/test_kpis_1_12.py index 64c43aa3..a9b0f233 100644 --- a/project/npda/tests/kpi_calculations/test_kpis_1_12.py +++ b/project/npda/tests/kpi_calculations/test_kpis_1_12.py @@ -9,6 +9,7 @@ from project.npda.general_functions.kpis import CalculateKPIS, KPIResult from project.npda.models import Patient from project.npda.tests.factories.patient_factory import PatientFactory +from project.npda.tests.factories.visit_factory import VisitFactory from project.npda.tests.kpi_calculations.test_kpi_calculations import \ assert_kpi_result_equal @@ -118,9 +119,6 @@ def test_kpi_calculation_2(AUDIT_START_DATE): total_failed=N_PATIENTS_FAIL * 3, ) - # First set kpi1 result of total eligible - calc_kpis.calculate_kpi_1_total_eligible() - assert_kpi_result_equal( expected=EXPECTED_KPIRESULT, actual=calc_kpis.calculate_kpi_2_total_new_diagnoses(), @@ -181,10 +179,6 @@ def test_kpi_calculation_3(AUDIT_START_DATE): total_failed=N_PATIENTS_FAIL * 3, ) - # First set self.total_kpi_1_eligible_pts_base_query_set result - # of total eligible - calc_kpis.calculate_kpi_1_total_eligible() - assert_kpi_result_equal( expected=EXPECTED_KPIRESULT, actual=calc_kpis.calculate_kpi_3_total_t1dm(), @@ -253,10 +247,6 @@ def test_kpi_calculation_4(AUDIT_START_DATE): total_failed=N_PATIENTS_FAIL * 4, ) - # First set self.total_kpi_1_eligible_pts_base_query_set result - # of total eligible - calc_kpis.calculate_kpi_1_total_eligible() - assert_kpi_result_equal( expected=EXPECTED_KPIRESULT, actual=calc_kpis.calculate_kpi_4_total_t1dm_gte_12yo(), @@ -361,10 +351,6 @@ def test_kpi_calculation_5(AUDIT_START_DATE): total_failed=EXPECTED_TOTAL_INELIGIBLE, ) - # First set self.total_kpi_1_eligible_pts_base_query_set result - # of total eligible - calc_kpis.calculate_kpi_1_total_eligible() - assert_kpi_result_equal( expected=EXPECTED_KPIRESULT, actual=calc_kpis.calculate_kpi_5_total_t1dm_complete_year(), @@ -406,7 +392,7 @@ def test_kpi_calculation_6(AUDIT_START_DATE): for field_name in observation_field_names: eligible_patient_pt_obs = PatientFactory( # string field without validation, just using for debugging - # postcode=f"eligible_patient_{field_name}", + postcode=f"eligible_patient_{field_name}", # KPI1 eligible # Age 12 and above at the start of the audit period date_of_birth=AUDIT_START_DATE - relativedelta(years=12), @@ -415,10 +401,41 @@ def test_kpi_calculation_6(AUDIT_START_DATE): # an observation within the audit period **{ f"visit__{field_name}": AUDIT_START_DATE - + relativedelta(days=2) + + relativedelta(days=2), + f"visit__visit_date": AUDIT_START_DATE + relativedelta(days=2), }, ) + # Additionally create a patient where first visit observations are None + # but the second visit has an observation + eligible_patient_second_visit_observation = PatientFactory( + postcode="eligible_patient_second_visit_observation", + # KPI1 eligible + # Age 12 and above at the start of the audit period + date_of_birth=AUDIT_START_DATE - relativedelta(years=12), + # Diagnosis of Type 1 diabetes + diabetes_type=DIABETES_TYPES[0][0], + # observations all None + visit__visit_date=AUDIT_START_DATE + relativedelta(days=2), + visit__height_weight_observation_date=None, + visit__hba1c_date=None, + visit__blood_pressure_observation_date=None, + visit__albumin_creatinine_ratio_date=None, + visit__total_cholesterol_date=None, + visit__thyroid_function_date=None, + visit__coeliac_screen_date=None, + visit__psychological_screening_assessment_date=None, + ) + # 2nd visit has observations + VisitFactory( + patient=eligible_patient_second_visit_observation, + visit_date=AUDIT_START_DATE + relativedelta(months=2), + height_weight_observation_date=AUDIT_START_DATE + + relativedelta(months=2), + psychological_screening_assessment_date=AUDIT_START_DATE + + relativedelta(months=2), + ) + # Create Patients and Visits that should FAIL KPI3 ineligible_patient_diag_within_audit_period = PatientFactory( postcode="ineligible_patient_diag_within_audit_period", @@ -451,7 +468,7 @@ def test_kpi_calculation_6(AUDIT_START_DATE): pz_code="PZ130", calculation_date=AUDIT_START_DATE ) - EXPECTED_TOTAL_ELIGIBLE = len(observation_field_names) + EXPECTED_TOTAL_ELIGIBLE = len(observation_field_names) + 1 EXPECTED_TOTAL_INELIGIBLE = 3 EXPECTED_KPIRESULT = KPIResult( @@ -536,9 +553,6 @@ def test_kpi_calculation_7(AUDIT_START_DATE): calc_kpis = CalculateKPIS( pz_code="PZ130", calculation_date=AUDIT_START_DATE ) - # First set self.total_kpi_1_eligible_pts_base_query_set result - # of total eligible - calc_kpis.calculate_kpi_1_total_eligible() EXPECTED_TOTAL_ELIGIBLE = len(observation_field_names) EXPECTED_TOTAL_INELIGIBLE = 2 @@ -614,10 +628,6 @@ def test_kpi_calculation_8(AUDIT_START_DATE): total_failed=EXPECTED_TOTAL_INELIGIBLE, ) - # First set self.total_kpi_1_eligible_pts_base_query_set result - # of total eligible - calc_kpis.calculate_kpi_1_total_eligible() - assert_kpi_result_equal( expected=EXPECTED_KPIRESULT, actual=calc_kpis.calculate_kpi_8_total_deaths(), @@ -692,10 +702,6 @@ def test_kpi_calculation_9(AUDIT_START_DATE): total_failed=EXPECTED_TOTAL_INELIGIBLE, ) - # First set self.total_kpi_1_eligible_pts_base_query_set result - # of total eligible - calc_kpis.calculate_kpi_1_total_eligible() - assert_kpi_result_equal( expected=EXPECTED_KPIRESULT, actual=calc_kpis.calculate_kpi_9_total_service_transitions(), @@ -761,10 +767,6 @@ def test_kpi_calculation_10(AUDIT_START_DATE): total_failed=EXPECTED_TOTAL_INELIGIBLE, ) - # First set self.total_kpi_1_eligible_pts_base_query_set result - # of total eligible - calc_kpis.calculate_kpi_1_total_eligible() - assert_kpi_result_equal( expected=EXPECTED_KPIRESULT, actual=calc_kpis.calculate_kpi_10_total_coeliacs(), @@ -839,10 +841,6 @@ def test_kpi_calculation_11(AUDIT_START_DATE): total_failed=EXPECTED_TOTAL_INELIGIBLE, ) - # First set self.total_kpi_1_eligible_pts_base_query_set result - # of total eligible - calc_kpis.calculate_kpi_1_total_eligible() - assert_kpi_result_equal( expected=EXPECTED_KPIRESULT, actual=calc_kpis.calculate_kpi_11_total_thyroids(), @@ -908,10 +906,6 @@ def test_kpi_calculation_12(AUDIT_START_DATE): total_failed=EXPECTED_TOTAL_INELIGIBLE, ) - # First set self.total_kpi_1_eligible_pts_base_query_set result - # of total eligible - calc_kpis.calculate_kpi_1_total_eligible() - assert_kpi_result_equal( expected=EXPECTED_KPIRESULT, actual=calc_kpis.calculate_kpi_12_total_ketone_test_equipment(), diff --git a/project/npda/tests/kpi_calculations/test_kpis_21_23.py b/project/npda/tests/kpi_calculations/test_kpis_21_23.py index d0841501..555b6138 100644 --- a/project/npda/tests/kpi_calculations/test_kpis_21_23.py +++ b/project/npda/tests/kpi_calculations/test_kpis_21_23.py @@ -78,10 +78,6 @@ def test_kpi_calculation_21(AUDIT_START_DATE): total_failed=EXPECTED_TOTAL_FAILED, ) - # First set self.total_kpi_1_eligible_pts_base_query_set result - # of total eligible - calc_kpis.calculate_kpi_1_total_eligible() - assert_kpi_result_equal( expected=EXPECTED_KPIRESULT, actual=calc_kpis.calculate_kpi_21_flash_glucose_monitor(), @@ -151,10 +147,6 @@ def test_kpi_calculation_22(AUDIT_START_DATE): total_failed=EXPECTED_TOTAL_FAILED, ) - # First set self.total_kpi_1_eligible_pts_base_query_set result - # of total eligible - calc_kpis.calculate_kpi_1_total_eligible() - assert_kpi_result_equal( expected=EXPECTED_KPIRESULT, actual=calc_kpis.calculate_kpi_22_real_time_cgm_with_alarms(), diff --git a/project/npda/tests/kpi_calculations/test_kpis_25_32.py b/project/npda/tests/kpi_calculations/test_kpis_25_32.py index 285d55f4..179bd2cf 100644 --- a/project/npda/tests/kpi_calculations/test_kpis_25_32.py +++ b/project/npda/tests/kpi_calculations/test_kpis_25_32.py @@ -17,9 +17,12 @@ def test_kpi_calculation_25(AUDIT_START_DATE): """Tests that KPI25 is calculated correctly. - Numerator: Number of eligible patients with at least one valid entry for HbA1c value (item 17) with an observation date (item 19) within the audit period + Numerator: Number of eligible patients with at least one valid entry for + HbA1c value (item 17) with an observation date (item 19) within the audit + period - Denominator: Number of patients with Type 1 diabetes with a complete year of care in the audit period (measure 5) + Denominator: Number of patients with Type 1 diabetes with a complete year + of care in the audit period (measure 5) """ # Ensure starting with clean pts in test db @@ -415,6 +418,7 @@ def test_kpi_calculation_28(AUDIT_START_DATE): # Diagnosis of Type 1 diabetes "diabetes_type": DIABETES_TYPES[0][0], # KPI 6 specific = an observation within the audit period + "visit__visit_date": AUDIT_START_DATE + relativedelta(days=2), "visit__height_weight_observation_date": AUDIT_START_DATE + relativedelta(days=2), # Also has same exclusions as KPI 5 @@ -538,6 +542,7 @@ def test_kpi_calculation_29(AUDIT_START_DATE): # Diagnosis of Type 1 diabetes "diabetes_type": DIABETES_TYPES[0][0], # KPI 6 specific = an observation within the audit period + "visit__visit_date": AUDIT_START_DATE + relativedelta(days=2), "visit__height_weight_observation_date": AUDIT_START_DATE + relativedelta(days=2), # Also has same exclusions as KPI 5 @@ -661,6 +666,7 @@ def test_kpi_calculation_30(AUDIT_START_DATE): # Diagnosis of Type 1 diabetes "diabetes_type": DIABETES_TYPES[0][0], # KPI 6 specific = an observation within the audit period + "visit__visit_date": AUDIT_START_DATE + relativedelta(days=2), "visit__height_weight_observation_date": AUDIT_START_DATE + relativedelta(days=2), # Also has same exclusions as KPI 5 @@ -793,6 +799,7 @@ def test_kpi_calculation_31(AUDIT_START_DATE): # Diagnosis of Type 1 diabetes "diabetes_type": DIABETES_TYPES[0][0], # KPI 6 specific = an observation within the audit period + "visit__visit_date": AUDIT_START_DATE + relativedelta(days=2), "visit__height_weight_observation_date": AUDIT_START_DATE + relativedelta(days=2), # Also has same exclusions as KPI 5 diff --git a/project/npda/tests/kpi_calculations/test_kpis_33_40.py b/project/npda/tests/kpi_calculations/test_kpis_33_40.py index 9d7cb8d1..6d8ddbfa 100644 --- a/project/npda/tests/kpi_calculations/test_kpis_33_40.py +++ b/project/npda/tests/kpi_calculations/test_kpis_33_40.py @@ -1,4 +1,5 @@ """Tests for the 7 Key Processes KPIs.""" + from typing import List import pytest @@ -68,6 +69,21 @@ def test_kpi_calculation_33(AUDIT_START_DATE): hba1c=43, hba1c_date=AUDIT_START_DATE + relativedelta(days=6), ) + # 1 of the Visits has no HbA1c + passing_patient_3 = PatientFactory( + postcode="passing_patient_3", + # KPI5 eligible + **eligible_criteria, + visit__hba1c=None, + visit__hba1c_date=None, + ) + for i in range(4): + VisitFactory( + patient=passing_patient_3, + visit_date=AUDIT_START_DATE, + hba1c=46, + hba1c_date=AUDIT_START_DATE + relativedelta(days=i), + ) # Failing patients # < 4 hba1c @@ -140,9 +156,9 @@ def test_kpi_calculation_33(AUDIT_START_DATE): calculation_date=AUDIT_START_DATE, ) - EXPECTED_TOTAL_ELIGIBLE = 4 + EXPECTED_TOTAL_ELIGIBLE = 5 EXPECTED_TOTAL_INELIGIBLE = 5 - EXPECTED_TOTAL_PASSED = 2 + EXPECTED_TOTAL_PASSED = 3 EXPECTED_TOTAL_FAILED = 2 EXPECTED_KPIRESULT = KPIResult( @@ -327,7 +343,7 @@ def test_kpi_calculation_35(AUDIT_START_DATE): # Passing patients passing_patient_1 = PatientFactory( postcode="passing_patient_1", - # KPI5 eligible + # KPI6 eligible **eligible_criteria, # KPI 35 specific visit__visit_date=AUDIT_START_DATE + relativedelta(days=5), @@ -336,7 +352,7 @@ def test_kpi_calculation_35(AUDIT_START_DATE): # second visit has a valid smoking status passing_patient_2 = PatientFactory( postcode="passing_patient_2", - # KPI5 eligible + # KPI6 eligible **eligible_criteria, # KPI 35 specific visit__visit_date=None, @@ -345,6 +361,9 @@ def test_kpi_calculation_35(AUDIT_START_DATE): # create 2nd visit VisitFactory( patient=passing_patient_2, + # KPI 6 specific = an observation within the audit period + height_weight_observation_date=AUDIT_START_DATE + + relativedelta(days=5), visit_date=AUDIT_START_DATE + relativedelta(days=5), smoking_status=SMOKING_STATUS[1][0], ) @@ -356,6 +375,7 @@ def test_kpi_calculation_35(AUDIT_START_DATE): # KPI5 eligible **eligible_criteria, # KPI 35 specific + visit__visit_date=AUDIT_START_DATE + relativedelta(days=5), visit__smoking_status=SMOKING_STATUS[2][0], ) # No smoke screening @@ -364,6 +384,7 @@ def test_kpi_calculation_35(AUDIT_START_DATE): # KPI5 eligible **eligible_criteria, # KPI 35 specific + visit__visit_date=AUDIT_START_DATE + relativedelta(days=5), visit__smoking_status=None, ) @@ -568,7 +589,7 @@ def test_kpi_calculation_37(AUDIT_START_DATE): # KPI5 eligible **eligible_criteria, # KPI 37 specific - visit__dietician_additional_appointment_offered=1 + visit__dietician_additional_appointment_offered=1, ) # only second visit has a valid dietician appt offered passing_patient_2 = PatientFactory( @@ -657,6 +678,7 @@ def test_kpi_calculation_37(AUDIT_START_DATE): actual=calc_kpis.calculate_kpi_37_additional_dietetic_appointment_offered(), ) + @pytest.mark.django_db def test_kpi_calculation_38(AUDIT_START_DATE): """Tests that KPI38 is calculated correctly. @@ -690,7 +712,8 @@ def test_kpi_calculation_38(AUDIT_START_DATE): # KPI5 eligible **eligible_criteria, # KPI 38 specific - visit__dietician_additional_appointment_date=AUDIT_START_DATE+relativedelta(days=30) + visit__dietician_additional_appointment_date=AUDIT_START_DATE + + relativedelta(days=30), ) # only second visit has a valid additional dietician appt passing_patient_2 = PatientFactory( @@ -704,7 +727,8 @@ def test_kpi_calculation_38(AUDIT_START_DATE): VisitFactory( patient=passing_patient_2, visit_date=AUDIT_START_DATE + relativedelta(days=5), - dietician_additional_appointment_date=AUDIT_START_DATE+relativedelta(days=4) + dietician_additional_appointment_date=AUDIT_START_DATE + + relativedelta(days=4), ) # Failing patients @@ -779,6 +803,7 @@ def test_kpi_calculation_38(AUDIT_START_DATE): actual=calc_kpis.calculate_kpi_38_patients_attending_additional_dietetic_appointment(), ) + @pytest.mark.django_db def test_kpi_calculation_39(AUDIT_START_DATE): """Tests that KPI39 is calculated correctly. @@ -812,7 +837,8 @@ def test_kpi_calculation_39(AUDIT_START_DATE): # KPI5 eligible **eligible_criteria, # KPI 39 specific - visit__flu_immunisation_recommended_date=AUDIT_START_DATE+relativedelta(days=30) + visit__flu_immunisation_recommended_date=AUDIT_START_DATE + + relativedelta(days=30), ) # only second visit has a valid influenza immunisation recommended passing_patient_2 = PatientFactory( @@ -826,7 +852,8 @@ def test_kpi_calculation_39(AUDIT_START_DATE): VisitFactory( patient=passing_patient_2, visit_date=AUDIT_START_DATE + relativedelta(days=5), - flu_immunisation_recommended_date=AUDIT_START_DATE+relativedelta(days=4) + flu_immunisation_recommended_date=AUDIT_START_DATE + + relativedelta(days=4), ) # Failing patients @@ -901,6 +928,7 @@ def test_kpi_calculation_39(AUDIT_START_DATE): actual=calc_kpis.calculate_kpi_39_influenza_immunisation_recommended(), ) + @pytest.mark.django_db def test_kpi_calculation_40(AUDIT_START_DATE): """Tests that KPI40 is calculated correctly. @@ -926,7 +954,8 @@ def test_kpi_calculation_40(AUDIT_START_DATE): # KPI1 eligible **eligible_criteria, # KPI 40 specific - visit__sick_day_rules_training_date=AUDIT_START_DATE+relativedelta(days=30) + visit__sick_day_rules_training_date=AUDIT_START_DATE + + relativedelta(days=30), ) # only second visit has a valid sick day rule passing_patient_2 = PatientFactory( @@ -940,7 +969,7 @@ def test_kpi_calculation_40(AUDIT_START_DATE): VisitFactory( patient=passing_patient_2, visit_date=AUDIT_START_DATE + relativedelta(days=5), - sick_day_rules_training_date=AUDIT_START_DATE+relativedelta(days=4) + sick_day_rules_training_date=AUDIT_START_DATE + relativedelta(days=4), ) # Failing patients @@ -983,4 +1012,4 @@ def test_kpi_calculation_40(AUDIT_START_DATE): assert_kpi_result_equal( expected=EXPECTED_KPIRESULT, actual=calc_kpis.calculate_kpi_40_sick_day_rules_advice(), - ) \ No newline at end of file + )