Skip to content

Commit ae8eb04

Browse files
authored
Merge pull request #30 from opensafely/viv3ckj/denominators
Modify denominators to be specific for each clinical condition
2 parents 0733b26 + 20f51f5 commit ae8eb04

8 files changed

+229
-29
lines changed

analysis/codelists.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,3 +13,10 @@
1313
column="snomedcode",
1414
category_column="Grouping_6",
1515
)
16+
17+
# Import pregnancy codelist
18+
pregnancy_codelist = codelist_from_csv(
19+
"codelists/nhsd-primary-care-domain-refsets-preg_cod.csv",
20+
column="code",
21+
category_column="term",
22+
)

analysis/measures_definition_pf_codes_conditions.py

Lines changed: 23 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -6,26 +6,19 @@
66
addresses,
77
ethnicity_from_sus,
88
)
9-
from codelists import pharmacy_first_conditions_codelist, ethnicity_codelist
9+
from codelists import (
10+
pharmacy_first_conditions_codelist,
11+
ethnicity_codelist,
12+
)
13+
14+
from pf_dataset import pharmacy_first_event_codes
1015

1116
measures = create_measures()
1217
measures.configure_dummy_data(population_size=1000)
1318

1419
start_date = "2023-11-01"
1520
monthly_intervals = 9
1621

17-
# Create dictionary of pharmacy first codes
18-
pharmacy_first_event_codes = {
19-
# Community Pharmacy (CP) Blood Pressure (BP) Check Service (procedure)
20-
"blood_pressure_service": ["1659111000000107"],
21-
# Community Pharmacy (CP) Contraception Service (procedure)
22-
"contraception_service": ["1659121000000101"],
23-
# Community Pharmacist (CP) Consultation Service for minor illness (procedure)
24-
"consultation_service": ["1577041000000109"],
25-
# Pharmacy First service (qualifier value)
26-
"pharmacy_first_service": ["983341000000102"],
27-
}
28-
2922
registration = practice_registrations.for_patient_on(INTERVAL.end_date)
3023

3124
latest_ethnicity_from_codes_category_num = (
@@ -70,7 +63,6 @@
7063
otherwise="Missing",
7164
)
7265

73-
7466
# Age bands for age breakdown
7567
age = patients.age_on(INTERVAL.start_date)
7668
age_band = case(
@@ -95,12 +87,13 @@
9587
)
9688

9789
latest_region = case(
98-
when(
99-
registration.practice_nuts1_region_name.is_not_null()
100-
).then(registration.practice_nuts1_region_name),
90+
when(registration.practice_nuts1_region_name.is_not_null()).then(
91+
registration.practice_nuts1_region_name
92+
),
10193
otherwise="Missing",
10294
)
10395

96+
10497
# Select clinical events in interval date range
10598
selected_events = clinical_events.where(
10699
clinical_events.date.is_on_or_between(INTERVAL.start_date, INTERVAL.end_date)
@@ -145,6 +138,17 @@
145138
intervals=months(monthly_intervals).starting_on(start_date),
146139
)
147140

141+
# Create dictionary for clinical condition denominators
142+
pf_condition_denominators = {
143+
"uncomplicated_urinary_tract_infection": denominator,
144+
"herpes_zoster": denominator,
145+
"impetigo": denominator,
146+
"infected_insect_bite": denominator,
147+
"acute_pharyngitis": denominator,
148+
"acute_sinusitis": denominator,
149+
"acute_otitis_media": denominator,
150+
}
151+
148152
# Create measures for pharmacy first conditions
149153
pharmacy_first_conditions_codes = {}
150154
for codes, term in pharmacy_first_conditions_codelist.items():
@@ -164,7 +168,7 @@
164168
measures.define_measure(
165169
name=f"count_{condition_name}",
166170
numerator=numerator,
167-
denominator=denominator,
171+
denominator=pf_condition_denominators[condition_name],
168172
intervals=months(monthly_intervals).starting_on(start_date),
169173
)
170174

@@ -173,7 +177,7 @@
173177
measures.define_measure(
174178
name=f"count_{condition_name}_by_{breakdown}",
175179
numerator=numerator,
176-
denominator=denominator,
180+
denominator=pf_condition_denominators[condition_name],
177181
group_by={breakdown: variable},
178182
intervals=months(monthly_intervals).starting_on(start_date),
179183
)

analysis/pf_dataset.py

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
from pf_variables_library import check_pregnancy_status, count_past_events
2+
3+
# Create dictionary of pharmacy first codes
4+
pharmacy_first_event_codes = {
5+
# # Community Pharmacy (CP) Blood Pressure (BP) Check Service (procedure)
6+
# "blood_pressure_service": ["1659111000000107"],
7+
# # Community Pharmacy (CP) Contraception Service (procedure)
8+
# "contraception_service": ["1659121000000101"],
9+
# Community Pharmacist (CP) Consultation Service for minor illness (procedure)
10+
"consultation_service": ["1577041000000109"],
11+
# Pharmacy First service (qualifier value)
12+
"pharmacy_first_service": ["983341000000102"],
13+
"combined_service": ["1577041000000109", "983341000000102"],
14+
}
15+
16+
17+
# Create denominator variables for each clinical condition
18+
# These are based on NHS England rules using sex, age, pregnancy status and repeated diagnoses
19+
# NOTE: The following exclusions have not been added:
20+
# - urinary catheter for URT,
21+
# - bullous impetigo,
22+
# - chronic sinusitis and immunosuppressed individuals for acute sinusitis
23+
def get_uncomplicated_uti_denominator(index_date, selected_events, pregnancy_codelist):
24+
urt_code = ["1090711000000102"]
25+
count_urt_6m = count_past_events(index_date, selected_events, urt_code, 6)
26+
count_urt_12m = count_past_events(index_date, selected_events, urt_code, 12)
27+
28+
age = patients.age_on(index_date)
29+
pregnancy_status = check_pregnancy_status(
30+
index_date, selected_events, pregnancy_codelist
31+
)
32+
33+
return (
34+
(age >= 16)
35+
& (age <= 64)
36+
& (patients.sex.is_in(["female"]) & pregnancy_status.is_null())
37+
| (count_urt_6m < 2)
38+
| (count_urt_12m < 3)
39+
)
40+
41+
42+
def get_shingles_denominator(index_date, selected_events, pregnancy_codelist):
43+
age = patients.age_on(index_date)
44+
pregnancy_status = check_pregnancy_status(
45+
index_date, selected_events, pregnancy_codelist
46+
)
47+
48+
return (age >= 18) & pregnancy_status.is_null()
49+
50+
51+
def get_impetigo_denominator(index_date, selected_events, pregnancy_codelist):
52+
impetigo_code = ["48277006"]
53+
count_impetigo_12m = count_past_events(
54+
index_date, selected_events, impetigo_code, 12
55+
)
56+
57+
age = patients.age_on(index_date)
58+
pregnancy_status = check_pregnancy_status(
59+
index_date, selected_events, pregnancy_codelist
60+
)
61+
62+
return (
63+
(age >= 1)
64+
| (pregnancy_status.is_not_null() & (age >= 16))
65+
| (count_impetigo_12m < 2)
66+
)
67+
68+
69+
def get_infected_insect_bites_denominator(
70+
index_date, selected_events, pregnancy_codelist
71+
):
72+
age = patients.age_on(index_date)
73+
pregnancy_status = check_pregnancy_status(
74+
index_date, selected_events, pregnancy_codelist
75+
)
76+
77+
return (age >= 1) | (pregnancy_status.is_not_null() & (age >= 16))
78+
79+
80+
def get_acute_sore_throat_denominator(index_date, selected_events, pregnancy_codelist):
81+
age = patients.age_on(index_date)
82+
pregnancy_status = check_pregnancy_status(
83+
index_date, selected_events, pregnancy_codelist
84+
)
85+
86+
return (age >= 5) | (pregnancy_status.is_not_null() & (age >= 16))
87+
88+
89+
def get_acute_sinusitis_denominator(index_date, selected_events, pregnancy_codelist):
90+
age = patients.age_on(index_date)
91+
pregnancy_status = check_pregnancy_status(
92+
index_date, selected_events, pregnancy_codelist
93+
)
94+
95+
return (age >= 12) | (pregnancy_status.is_not_null() & (age >= 16))
96+
97+
98+
def get_acute_otitis_media_denominator(index_date, selected_events, pregnancy_codelist):
99+
acute_otitis_code = ["3110003"]
100+
count_acute_otitis_6m = count_past_events(
101+
index_date, selected_events, acute_otitis_code, 6
102+
)
103+
count_acute_otitis_12m = count_past_events(
104+
index_date, selected_events, acute_otitis_code, 12
105+
)
106+
107+
age = patients.age_on(index_date)
108+
pregnancy_status = check_pregnancy_status(
109+
index_date, selected_events, pregnancy_codelist
110+
)
111+
112+
return (
113+
(age >= 1) & (age <= 17)
114+
| (pregnancy_status.is_not_null() & (age >= 16))
115+
| (count_acute_otitis_6m < 3)
116+
| (count_acute_otitis_12m < 4)
117+
)

analysis/pf_variables_library.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
# Function to check status of a condition within a specified time window
2+
def check_pregnancy_status(index_date, selected_events, codelist):
3+
return (
4+
selected_events.where(selected_events.snomedct_code.is_in(codelist))
5+
.where(
6+
selected_events.date.is_on_or_between(index_date - months(1), index_date)
7+
)
8+
.exists_for_patient()
9+
)
10+
11+
12+
# Function to count number of coded events within a specified time window
13+
def count_past_events(index_date, selected_events, codelist, num_months):
14+
return (
15+
selected_events.where(selected_events.snomedct_code.is_in(codelist))
16+
.where(
17+
selected_events.date.is_on_or_between(
18+
index_date - months(num_months), index_date
19+
)
20+
)
21+
.count_for_patient()
22+
)

codelists/codelists.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,12 @@
1111
"url": "https://www.opencodelists.org/codelist/opensafely/ethnicity-snomed-0removed/2e641f61/",
1212
"downloaded_at": "2024-10-03 11:34:59.878387Z",
1313
"sha": "8b849b5b785d80306e488d0109640c8c1d2da01b"
14+
},
15+
"nhsd-primary-care-domain-refsets-preg_cod.csv": {
16+
"id": "nhsd-primary-care-domain-refsets/preg_cod/20200812",
17+
"url": "https://www.opencodelists.org/codelist/nhsd-primary-care-domain-refsets/preg_cod/20200812/",
18+
"downloaded_at": "2024-10-10 13:27:57.825804Z",
19+
"sha": "4a1cbc5c12ce8589030b376905ac47f5a68b2503"
1420
}
1521
}
1622
}

codelists/codelists.txt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
user/chriswood/pharmacy-first-clinical-pathway-conditions/7ec97762
2-
opensafely/ethnicity-snomed-0removed/2e641f61
2+
opensafely/ethnicity-snomed-0removed/2e641f61
3+
nhsd-primary-care-domain-refsets/preg_cod/20200812
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
code,term
2+
10231000132102,In-vitro fertilization pregnancy
3+
1109951000000101,Pregnancy insufficiently advanced for reliable antenatal screening
4+
1109971000000105,Pregnancy too advanced for reliable antenatal screening
5+
1148801000000108,Monochorionic monoamniotic triplet pregnancy
6+
1148811000000105,Trichorionic triamniotic triplet pregnancy
7+
1148821000000104,Dichorionic triamniotic triplet pregnancy
8+
1148841000000106,Dichorionic diamniotic triplet pregnancy
9+
1149411000000103,Monochorionic diamniotic triplet pregnancy
10+
1149421000000109,Monochorionic triamniotic triplet pregnancy
11+
134781000119106,High risk pregnancy due to recurrent miscarriage
12+
16356006,Multiple pregnancy
13+
169488004,Contraceptive intrauterine device failure - pregnant
14+
169501005,"Pregnant, diaphragm failure"
15+
169508004,"Pregnant, sheath failure"
16+
169560008,Pregnant - urine test confirms
17+
169561007,Pregnant - blood test confirms
18+
169562000,Pregnant - vaginal examination confirms
19+
169563005,Pregnant - on history
20+
169564004,Pregnant - on abdominal palpation
21+
169565003,Pregnant - planned
22+
169566002,Pregnancy unplanned but wanted
23+
237238006,Pregnancy with uncertain dates
24+
237239003,Low risk pregnancy
25+
276367008,Wanted pregnancy
26+
314204000,Early stage of pregnancy
27+
439311009,Intends to continue pregnancy
28+
444661007,High risk pregnancy due to history of preterm labor
29+
459166009,Dichorionic diamniotic twin pregnancy
30+
459167000,Monochorionic twin pregnancy
31+
459168005,Monochorionic diamniotic twin pregnancy
32+
459171002,Monochorionic monoamniotic twin pregnancy
33+
47200007,High risk pregnancy
34+
60810003,Quadruplet pregnancy
35+
64254006,Triplet pregnancy
36+
65147003,Twin pregnancy
37+
713575004,Dizygotic twin pregnancy
38+
713576003,Monozygotic twin pregnancy
39+
72957006,Diamniotic-monochorionic twins
40+
77386006,Pregnant
41+
80997009,Quintuplet pregnancy

reports/pharmacy_first_report.Rmd

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -26,10 +26,11 @@ df_measures <- readr::read_csv(
2626
2727
# Define dictionaries with tidy names and mappings for measures
2828
pf_measures_name_dict <- list(
29-
blood_pressure_service = "Blood Pressure Service",
30-
contraception_service = "Contraception Service",
29+
# blood_pressure_service = "Blood Pressure Service",
30+
# contraception_service = "Contraception Service",
3131
consultation_service = "Consultation Service",
3232
pharmacy_first_service = "Pharmacy First Service",
33+
combined_service = "Pharmacy First Services",
3334
acute_otitis_media = "Acute Otitis Media",
3435
herpes_zoster = "Herpes Zoster",
3536
acute_sinusitis = "Acute Sinusitis",
@@ -40,10 +41,11 @@ pf_measures_name_dict <- list(
4041
)
4142
4243
pf_measures_name_mapping <- list(
43-
blood_pressure_service = "clinical_service",
44-
contraception_service = "clinical_service",
44+
# blood_pressure_service = "clinical_service",
45+
# contraception_service = "clinical_service",
4546
consultation_service = "clinical_service",
4647
pharmacy_first_service = "clinical_service",
48+
combined_service = "pharmacy_first_services",
4749
acute_otitis_media = "clinical_condition",
4850
herpes_zoster = "clinical_condition",
4951
acute_sinusitis = "clinical_condition",
@@ -146,7 +148,7 @@ plot_measures(
146148
```{r, message=FALSE, warning=FALSE, fig.height=8, fig.width=8}
147149
# Select measures and breakdown
148150
df_measures_selected <- df_measures %>%
149-
filter(measure_desc == "clinical_service") %>%
151+
filter(measure_desc == "pharmacy_first_services") %>%
150152
filter(group_by == "Age band")
151153
152154
# Create visualisation
@@ -168,7 +170,7 @@ plot_measures(
168170
```{r, message=FALSE, warning=FALSE, fig.height=8, fig.width=8}
169171
# Select measures and breakdown
170172
df_measures_selected <- df_measures %>%
171-
filter(measure_desc == "clinical_service") %>%
173+
filter(measure_desc == "pharmacy_first_services") %>%
172174
filter(group_by == "Sex")
173175
174176
# Create visualisation
@@ -190,7 +192,7 @@ plot_measures(
190192
```{r, message=FALSE, warning=FALSE, fig.height=8, fig.width=8}
191193
# Select measures and breakdown
192194
df_measures_selected <- df_measures %>%
193-
filter(measure_desc == "clinical_service") %>%
195+
filter(measure_desc == "pharmacy_first_services") %>%
194196
filter(group_by == "IMD")
195197
196198
# Create visualisation
@@ -212,7 +214,7 @@ plot_measures(
212214
```{r, message=FALSE, warning=FALSE, fig.height=8, fig.width=8}
213215
# Select measures and breakdown
214216
df_measures_selected <- df_measures %>%
215-
filter(measure_desc == "clinical_service") %>%
217+
filter(measure_desc == "pharmacy_first_services") %>%
216218
filter(group_by == "Region")
217219
218220
# Create visualisation
@@ -236,7 +238,7 @@ plot_measures(
236238
237239
# Select measures and breakdown
238240
df_measures_selected <- df_measures %>%
239-
filter(measure_desc == "clinical_service") %>%
241+
filter(measure_desc == "pharmacy_first_services") %>%
240242
filter(group_by == "Ethnicity")
241243
242244
# Create visualisation

0 commit comments

Comments
 (0)