From aec0f7cb4201f1b027eb69593e4b4a44a4878129 Mon Sep 17 00:00:00 2001 From: Milan Wiedemann Date: Tue, 24 Sep 2024 13:03:15 +0100 Subject: [PATCH 1/4] Tidying up code --- analysis/report_measures.py | 28 ++++++++++++++++------------ 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/analysis/report_measures.py b/analysis/report_measures.py index 92656b1..3541917 100644 --- a/analysis/report_measures.py +++ b/analysis/report_measures.py @@ -4,6 +4,9 @@ measures = create_measures() measures.configure_dummy_data(population_size=1000) +start_date = "2023-11-01" +monthly_intervals = 8 + # Dictionary of pharmacy first codes pharmacy_first_event_codes = { # Community Pharmacy (CP) Blood Pressure (BP) Check Service (procedure) @@ -17,18 +20,12 @@ } # Import the codelist from CSV -pharmacy_first_codelist = codelist_from_csv( +pharmacy_first_conditions_codelist = codelist_from_csv( "codelists/user-chriswood-pharmacy-first-clinical-pathway-conditions.csv", - column="code", category_column = "term" + column="code", + category_column="term", ) -pharmacy_first_conditions_codes = {} -# Iterate through codelist, forming a dictionary -for codes, term in pharmacy_first_codelist.items(): - normalised_term = term.lower().replace(" ", "_") - codes = [codes] - pharmacy_first_conditions_codes[normalised_term] = codes - registration = practice_registrations.for_patient_on(INTERVAL.end_date) # Select clinical events in interval date range @@ -36,6 +33,13 @@ clinical_events.date.is_on_or_between(INTERVAL.start_date, INTERVAL.end_date) ) +# Iterate through codelist, forming a dictionary +pharmacy_first_conditions_codes = {} +for codes, term in pharmacy_first_conditions_codelist.items(): + normalised_term = term.lower().replace(" ", "_") + codes = [codes] + pharmacy_first_conditions_codes[normalised_term] = codes + # Loop through each CLINICAL SERVICE to create a measure for pharmacy_first_event, codelist in pharmacy_first_event_codes.items(): condition_events = selected_events.where( @@ -52,7 +56,7 @@ name=f"count_{pharmacy_first_event}", numerator=numerator, denominator=denominator, - intervals=months(8).starting_on("2023-11-01") + intervals=months(monthly_intervals).starting_on(start_date), ) # Loop through each CLINICAL CONDITION to create a measure @@ -71,5 +75,5 @@ name=f"count_{condition_name}", numerator=numerator, denominator=denominator, - intervals=months(8).starting_on("2023-11-01") - ) \ No newline at end of file + intervals=months(monthly_intervals).starting_on(start_date), + ) From 8b633b96cadb7bd830088b6f0634c4478466537d Mon Sep 17 00:00:00 2001 From: Milan Wiedemann Date: Tue, 24 Sep 2024 13:03:44 +0100 Subject: [PATCH 2/4] Rework measures definition and report --- ...easures_definition_pf_codes_conditions.py} | 23 +- project.yaml | 6 +- reports/pf_report.Rmd | 206 ------------------ reports/pharmacy_first_report.Rmd | 94 ++++++++ 4 files changed, 108 insertions(+), 221 deletions(-) rename analysis/{report_measures.py => measures_definition_pf_codes_conditions.py} (89%) delete mode 100644 reports/pf_report.Rmd create mode 100644 reports/pharmacy_first_report.Rmd diff --git a/analysis/report_measures.py b/analysis/measures_definition_pf_codes_conditions.py similarity index 89% rename from analysis/report_measures.py rename to analysis/measures_definition_pf_codes_conditions.py index 3541917..8393ef3 100644 --- a/analysis/report_measures.py +++ b/analysis/measures_definition_pf_codes_conditions.py @@ -1,5 +1,5 @@ from ehrql import INTERVAL, create_measures, months, codelist_from_csv -from ehrql.tables.tpp import clinical_events, patients, practice_registrations +from ehrql.tables.tpp import clinical_events, practice_registrations measures = create_measures() measures.configure_dummy_data(population_size=1000) @@ -7,7 +7,7 @@ start_date = "2023-11-01" monthly_intervals = 8 -# Dictionary of pharmacy first codes +# Create dictionary of pharmacy first codes pharmacy_first_event_codes = { # Community Pharmacy (CP) Blood Pressure (BP) Check Service (procedure) "blood_pressure_service": ["1659111000000107"], @@ -19,7 +19,7 @@ "pharmacy_first_service": ["983341000000102"], } -# Import the codelist from CSV +# Import pharmacy first conditions codelist pharmacy_first_conditions_codelist = codelist_from_csv( "codelists/user-chriswood-pharmacy-first-clinical-pathway-conditions.csv", column="code", @@ -33,14 +33,7 @@ clinical_events.date.is_on_or_between(INTERVAL.start_date, INTERVAL.end_date) ) -# Iterate through codelist, forming a dictionary -pharmacy_first_conditions_codes = {} -for codes, term in pharmacy_first_conditions_codelist.items(): - normalised_term = term.lower().replace(" ", "_") - codes = [codes] - pharmacy_first_conditions_codes[normalised_term] = codes - -# Loop through each CLINICAL SERVICE to create a measure +# Create measures for pharmacy first services for pharmacy_first_event, codelist in pharmacy_first_event_codes.items(): condition_events = selected_events.where( clinical_events.snomedct_code.is_in(codelist) @@ -59,7 +52,13 @@ intervals=months(monthly_intervals).starting_on(start_date), ) -# Loop through each CLINICAL CONDITION to create a measure +# Create measures for pharmacy first conditions +pharmacy_first_conditions_codes = {} +for codes, term in pharmacy_first_conditions_codelist.items(): + normalised_term = term.lower().replace(" ", "_") + codes = [codes] + pharmacy_first_conditions_codes[normalised_term] = codes + for condition_name, condition_code in pharmacy_first_conditions_codes.items(): condition_events = selected_events.where( clinical_events.snomedct_code.is_in(condition_code) diff --git a/project.yaml b/project.yaml index d7250b9..aa353e1 100644 --- a/project.yaml +++ b/project.yaml @@ -7,8 +7,8 @@ expectations: actions: generate_pf_measures: run: > - ehrql:v1 generate-measures analysis/report_measures.py - --output output/report/conditions_measures.csv + ehrql:v1 generate-measures analysis/measures_definition_pf_codes_conditions.py + --output output/measures/pf_codes_conditions_measures.csv outputs: moderately_sensitive: - measure: output/report/conditions_measures.csv \ No newline at end of file + measure: output/measures/pf_codes_conditions_measures.csv \ No newline at end of file diff --git a/reports/pf_report.Rmd b/reports/pf_report.Rmd deleted file mode 100644 index 774e7c2..0000000 --- a/reports/pf_report.Rmd +++ /dev/null @@ -1,206 +0,0 @@ ---- -title: "Pharmacy First" -output: - html_document: - toc: true - toc_depth: 4 - theme: journal -date: "01-01-2020 through 31-06-2024 by month" ---- -```{css, echo=FALSE} -pre { - max-height: 300px; - overflow-y: auto; -} -pre[class] { - max-height: 200px; -} -``` - - - - -```{r setup, include=FALSE} -knitr::opts_chunk$set(echo = FALSE) - -library(tidyverse) -library(here) -library(readr) - - -``` -
- -## Background - -Add background here. - -
- -## Methods - -This study used data from OpenSAFELY-TPP, which covers 40% of the population of England. For a description of the representativeness of this sample, please see our manuscript [here](https://wellcomeopenresearch.org/articles/7-191/v1). Individuals were included if they were alive and registered at a TPP practice each month, across the study period. Patients were excluded if their listed age was not between 0 and 120 years. - -Counts represent patients with at least one clinical code of relevance in that month. Patients with more than one of the same clinical code in a month were only counted once. Rates divide the count by the included study population and multiply by 1,000 to achieve a rate per 1,000 registered patients. - -Counts <=7 have been redacted and all numbers rounded to the nearest 5 to avoid potential re-identification of individuals. The rates displayed were computed with these rounded counts. - -Our data relies on a relevant Pharmacy First code being added to a patient's GP record. The Pharmacy First service relies on [GP Connect: Update Record](https://digital.nhs.uk/services/gp-connect/gp-connect-in-your-organisation/gp-connect-update-record) to update a patient's GP record with consultation information from the community pharmacy. Following the launch of the Pharmacy First service, there has been a [gradual roll-out of GP Connect: Update Record](https://cpe.org.uk/our-news/gp-connect-update-record-rollout-and-flow-of-information/) across the approved community pharmacy IT system suppliers. - -*Need to complete this* - -Links to the codelist for each analysis can be found beneath the relevant section. - -
- -## Clinical Services - -
- -```{r, message=FALSE, warning=FALSE} -# Load plotting function -source(here::here("lib", "functions", "function_plot_measures.R")) -# Load data -df_measures <- readr::read_csv( - here::here("output", "report", "conditions_measures.csv") -) - -# Define the custom labels for clinical conditions -clinical_measure_labels <- c( - "count_acute_otitis_media" = "Acute Otitis Media", - "count_herpes_zoster" = "Herpes Zoster", - "count_acute_sinusitis" = "Acute Sinusitis", - "count_impetigo" = "Impetigo", - "count_infected_insect_bite" = "Infected Insect Bite", - "count_acute_pharyngitis" = "Acute Pharyngitis", - "count_uncomplicated_urinary_tract_infection" = "UTI" -) - -# Define the custom labels for clinical services -clinical_service_labels <- c( - "count_blood_pressure_service" = "Blood Pressure Service", - "count_contraception_service" = "Contraception Service", - "count_consultation_service" = "Consultation Service", - "count_pharmacy_first_service" = "Pharmacy First Service" -) - -``` - -```{r, message=FALSE, warning=FALSE} -plot_measures(df_measures, -title = "Number of consultations for each clinical service per month", -measure_names = names(clinical_service_labels), -custom_labels = clinical_service_labels, -y_label = "Number of codes for consultations", -) -``` - -## Clinical Pathways - -This section focuses on the Clinical Pathways element of that Pharmacy First service. - - -### Clinical Condition - -This section focuses on the clinical conditions within the Clinical Pathways element of that Pharmacy First service: - -#### Counts by clinical condition -Here we show the number of consultations for each of the Pharmacy First Clinical Pathways Clinical Conditions - -```{r, message=FALSE, warning=FALSE} -plot_measures(df_measures, -title = "Number of consultations for each clinical condition per month", -measure_names = names(clinical_measure_labels), -custom_labels = clinical_measure_labels, -y_label = "Number of codes for consultations", -) -``` - -### Counts by day of the week -Here we show the number of consultations for the Pharmacy First Clinical Pathways by day of the week the consultation was conducted. (Mainly of interest to show what happens at weekends etc e.g. when GP practices are closed). - -> BAR CHART WITH COUNT BY DAY OF THE WEEK - -### Rates by region -Here we show the rates of consultations for the Pharmacy First Clinical Pathways by the NHS Region in which the patient is registered. (Might not be the region where PF consultation conducted). - -> RATES OF ANY PF CLINICAL PATHWAY BY REGION - -### Rates by ethnicity -Here we show the rates of consultations for the Pharmacy First Clinical Pathways by patient ethnicity - -> RATES OF ANY PF CLINICAL PATHWAY BY ETHNICITY - -### Rates by IMD -Here we show the rates of consultations for the Pharmacy First Clinical Pathways by IMD (based on patient address) - -> RATES OF ANY PF CLINICAL PATHWAY BY IMD - -### Shingles -Here we show the rates specifically for consultations conducted under the Shingles clinical pathway: - -#### Rates by sex -Here we show the rates of consultations for the Pharmacy First Shingles Clinical Pathways by sex - -> RATES OF SHINGLES CLINICAL PATHWAY BY SEX - -#### Rates by age -Here we show the rates of consultations for the Pharmacy First Shingles Clinical Pathways by age. (Would need to consider age bands as different for each pathway) - -> RATES OF SHINGLES CLINICAL PATHWAY BY AGE - -#### Rates by ethnicity -Here we show the rates of consultations for the Pharmacy First Shingles Clinical Pathways by ethnicity - -> RATES OF SHINGLES CLINICAL PATHWAY BY ETHNICITY - -#### Rates by IMD -Here we show the rates of consultations for the Pharmacy First Shingles Clinical Pathways by IMD - -> RATES OF SHINGLES CLINICAL PATHWAY BY IMD - -#### Rates by region -Here we show the rates of consultations for the Pharmacy First Shingles Clinical Pathways by NHS region - -> RATES OF SHINGLES CLINICAL PATHWAY BY REGION - -#### Rates by medication supply -Here we show the percentage of consultations for the Pharmacy First Shingles Clinical Pathways with vs without medication supply. (Could also potentially show details of which medication issued) - -> BAR PLOT OF PERCENTAGE OF SHINGLES CLINICAL PATHWAY WITH/WITHOUT MEDICATION SUPPLIED - -### Urinary tract infection etc. - -> *placeholder* - -## Hypertension - -*description* - -### Age - -> *placeholder* - -### Sex - -> *placeholder* - -### Region - -> *placeholder* - -## Contraception - -*description* - -### Age - -> *placeholder* - -### Region - -> *placeholder* \ No newline at end of file diff --git a/reports/pharmacy_first_report.Rmd b/reports/pharmacy_first_report.Rmd new file mode 100644 index 0000000..a2ba37a --- /dev/null +++ b/reports/pharmacy_first_report.Rmd @@ -0,0 +1,94 @@ +--- +title: "Pharmacy First" +output: + html_document: + toc: true + toc_depth: 4 +date: "`r format(Sys.time(), '%d %B, %Y')`" +--- + +```{r setup, include=FALSE} +knitr::opts_chunk$set(echo = FALSE) +library(tidyverse) +library(here) +library(readr) +``` + +```{r load-data, message=FALSE, warning=FALSE} +# Load plotting function +source(here::here("lib", "functions", "function_plot_measures.R")) +# Load data +df_measures <- readr::read_csv( + here::here("output", "measures", "pf_codes_conditions_measures.csv") +) +``` + +```{r setup-dicts, message=FALSE, warning=FALSE} +# Define the custom labels for clinical conditions +pf_clinical_condition_dict <- c( + "count_acute_otitis_media" = "Acute Otitis Media", + "count_herpes_zoster" = "Herpes Zoster", + "count_acute_sinusitis" = "Acute Sinusitis", + "count_impetigo" = "Impetigo", + "count_infected_insect_bite" = "Infected Insect Bite", + "count_acute_pharyngitis" = "Acute Pharyngitis", + "count_uncomplicated_urinary_tract_infection" = "UTI" +) + +# Define the custom labels for clinical services +pf_clinical_service_dict <- c( + "count_blood_pressure_service" = "Blood Pressure Service", + "count_contraception_service" = "Contraception Service", + "count_consultation_service" = "Consultation Service", + "count_pharmacy_first_service" = "Pharmacy First Service" +) +``` + +# Background + +Add background here. + +# Methods + +This study used data from OpenSAFELY-TPP, which covers 40% of the population of England. For a description of the representativeness of this sample, please see our manuscript [here](https://wellcomeopenresearch.org/articles/7-191/v1). +Individuals were included if they were alive and registered at a TPP practice each month, across the study period. +Patients were excluded if their listed age was not between 0 and 120 years. +Counts represent patients with at least one clinical code of relevance in that month. +Patients with more than one of the same clinical code in a month were only counted once. Rates divide the count by the included study population and multiply by 1,000 to achieve a rate per 1,000 registered patients. +Counts <=7 have been redacted and all numbers rounded to the nearest 5 to avoid potential re-identification of individuals. The rates displayed were computed with these rounded counts. +Our data relies on a relevant Pharmacy First code being added to a patient's GP record. +The Pharmacy First service relies on [GP Connect - Update Record](https://digital.nhs.uk/services/gp-connect/gp-connect-in-your-organisation/gp-connect-update-record) to update a patient's GP record with consultation information from the community pharmacy. +Following the launch of the Pharmacy First service, there has been a [Gradual roll-out of GP Connect - Update Record](https://cpe.org.uk/our-news/gp-connect-update-record-rollout-and-flow-of-information/) across the approved community pharmacy IT system suppliers. + +Links to the codelist for each analysis can be found beneath the relevant section. + +# Results + +## Clinical Services + +```{r, message=FALSE, warning=FALSE} +plot_measures(df_measures, + title = "Number of consultations for each clinical service per month", + measure_names = names(pf_clinical_service_dict), + custom_labels = pf_clinical_service_dict, + y_label = "Number of codes for consultations", +) +``` + +## Clinical Pathways + +This section focuses on the Clinical Pathways element of that Pharmacy First service. + +## Clinical Condition + +This section focuses on the clinical conditions within the Clinical Pathways element of that Pharmacy First service: +Here we show the number of consultations for each of the Pharmacy First Clinical Pathways Clinical Conditions. + +```{r, message=FALSE, warning=FALSE} +plot_measures(df_measures, + title = "Number of consultations for each clinical condition per month", + measure_names = names(pf_clinical_condition_dict), + custom_labels = pf_clinical_condition_dict, + y_label = "Number of codes for consultations", +) +``` From b5e1c8395fc823555ad5728163c6dbb1837e3081 Mon Sep 17 00:00:00 2001 From: Milan Wiedemann Date: Tue, 24 Sep 2024 13:17:42 +0100 Subject: [PATCH 3/4] Update `.gitignore` --- .gitignore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 99b781b..d645d6d 100644 --- a/.gitignore +++ b/.gitignore @@ -9,4 +9,4 @@ venv/ .DS_Store .Rhistory .Rproj.user/ -reports/pf_report.html +reports/pharmacy_first_report.html From 7b20586f9d4120465b51e4b424095bac6f6f5cf3 Mon Sep 17 00:00:00 2001 From: Milan Wiedemann Date: Wed, 25 Sep 2024 09:53:00 +0100 Subject: [PATCH 4/4] Format R code of plotting function --- lib/functions/function_plot_measures.R | 40 +++++++++++++------------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/lib/functions/function_plot_measures.R b/lib/functions/function_plot_measures.R index 234f085..c8d2c68 100644 --- a/lib/functions/function_plot_measures.R +++ b/lib/functions/function_plot_measures.R @@ -1,21 +1,19 @@ #' Plot Measures Over Time -#' +#' #' Creates a line plot of measures over time, with customisable labels and colours. -#' +#' #' @param data A dataframe containing the data to plot. #' @param measure_names Strings specifiying the names of measure columns to be plotted. -#' @param custom_labels Strings specifying the names of legend labels. -#' @param title A string specifying the title of the plot. Default is NULL. +#' @param custom_labels Strings specifying the names of legend labels. +#' @param title A string specifying the title of the plot. Default is NULL. #' @param x_label A string specifying the label for the x-axis. Default is NULL. -#' @param y_label A string specifying the label for the y-axis. Default is NULL. +#' @param y_label A string specifying the label for the y-axis. Default is NULL. #' @param color_label A string specifying the label for the color legend. Default is NULL. #' @param value_col The name of the dataframe column which contains the y-axis values. Default is "numerator". #' @param measure_col The name of the dataframe column which contains the categorical variable. Default is "measure". -#' -#' +#' #' @return A ggplot object. -# Define the function plot_measures <- function( data, measure_names, @@ -28,7 +26,6 @@ plot_measures <- function( y_label = NULL, color_label = NULL, axis_x_text_size = 7) { - # Check if the necessary columns exist in the data if (date_col %in% names(data) == FALSE) { stop("Data does not have a column with the name '", date_col, "'") @@ -80,24 +77,27 @@ plot_measures <- function( limits = c(0, NA), ) + theme_minimal() + - theme(axis.text.x = element_text(size = axis_x_text_size), - legend.position="bottom", - legend.key.size = unit(0.5, "cm"), - legend.text = element_text(size = 8), - legend.title = element_text(size = 8)) + + theme( + axis.text.x = element_text(size = axis_x_text_size), + legend.position = "bottom", + legend.key.size = unit(0.5, "cm"), + legend.text = element_text(size = 8), + legend.title = element_text(size = 8) + ) + + # Adjust number of rows in the legend guides( - color = guide_legend(nrow = 2) # Adjust number of rows in the legend -) + + color = guide_legend(nrow = 2) + ) + geom_vline( xintercept = lubridate::as_date("2024-02-01"), linetype = "dotted", colour = "orange", - linewidth = .7) + + linewidth = .7 + ) + scale_x_date( - date_breaks = "1 month", + date_breaks = "1 month", date_labels = "%b %Y" ) - plot1 -} \ No newline at end of file +}