Last active
September 10, 2025 18:45
-
-
Save francisbarton/b9af0ea60d65db83f9811cf817013f84 to your computer and use it in GitHub Desktop.
Mapping NHS England Acute Trusts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| --- | |
| title: NHS England Acute trust league table rankings map 2025 | |
| code-fold: true | |
| knitr: | |
| opts_chunk: | |
| dev: "ragg_png" | |
| fig-dpi: 144 | |
| fig-width: 8.2 | |
| fig-height: 11.6 | |
| --- | |
| ```{r read in data} | |
| #| eval: false | |
| url <- file.path( | |
| "https://www.england.nhs.uk", | |
| "wp-content", | |
| "uploads", | |
| "2025", | |
| "09", | |
| "nhs-oversight-framework-acute-trust-league-table.csv" | |
| ) | |
| t <- "lt_data.rds" | |
| lt_data <- readr::read_csv(url, col_types = "--ccc-d-d-d-") |> | |
| readr::write_rds(t) | |
| ``` | |
| ```{r pull out table} | |
| tbl <- readr::read_rds(t) |> | |
| janitor::clean_names() |> | |
| dplyr::rename_with(\(x) sub("^(trust|average)_", "", x)) |> | |
| dplyr::mutate( | |
| dplyr::across("subtype", \(x) sub("^Acute \\- ", "", x)), | |
| decile = dplyr::ntile(.data[["score"]], 10L) | |
| ) |> | |
| dplyr::mutate(dplyr::across(c("segment", "decile"), forcats::as_factor)) | |
| ``` | |
| ```{r talk to ods api} | |
| api_req <- httr2::request("https://sandbox.api.service.nhs.uk") | |
| get_org_data <- function(org_code, req = api_req) { | |
| req |> | |
| httr2::req_url_path_append("organisation-data-terminology-api") |> | |
| httr2::req_url_path_append("fhir") |> | |
| httr2::req_url_path_append("Organization") |> | |
| httr2::req_url_path_append(org_code) |> | |
| httr2::req_perform() |> | |
| httr2::resp_check_status() |> | |
| httr2::resp_body_json() | |
| } | |
| get_postcode <- \(lst) purrr::pluck(lst, "address", 1, "postalCode") | |
| ``` | |
| ```{r get trust postcodes} | |
| #| eval: false | |
| tbl_full <- tbl |> | |
| dplyr::mutate(ods_data = purrr::map(.data[["code"]], get_org_data)) |> | |
| dplyr::mutate( | |
| postcode = purrr::map_chr(.data[["ods_data"]], get_postcode), | |
| .keep = "unused" | |
| ) |> | |
| # https://codeberg.org/francisbarton/myrmidon | |
| myrmidon::postcode_data_join() |> | |
| dplyr::select( | |
| dplyr::all_of(c(colnames(tbl), "postcode", "eastings", "northings")) | |
| ) |> | |
| readr::write_rds("tbl_full.rds") | |
| ``` | |
| ```{r get shape data} | |
| #| eval: false | |
| # https://codeberg.org/francisbarton/boundr | |
| england <- boundr::bounds("rgn", opts = boundr::opts("BUC", crs = 27700)) |> | |
| readr::write_rds("england.rds") | |
| ``` | |
| ```{r read data back in} | |
| england <- readr::read_rds("england.rds") | |
| tbl_full <- readr::read_rds("tbl_full.rds") | |
| set.seed(1444) # for sf::st_jitter() | |
| shape_breaks <- sort(unique(tbl_full[["subtype"]]))[c(1, 2, 4, 6, 5, 3)] | |
| shape_values <- rep("square filled", length(shape_breaks)) |> | |
| rlang::set_names(shape_breaks) |> | |
| purrr::assign_in("Specialist", "circle filled") |> | |
| purrr::assign_in("Teaching", "triangle filled") |> | |
| purrr::assign_in("Multi-Service", "diamond filled") | |
| size_values <- rep(4, length(shape_breaks)) |> | |
| rlang::set_names(shape_breaks) |> | |
| purrr::assign_in("Large", 6) |> | |
| purrr::assign_in("Teaching", 5) |> | |
| purrr::assign_in("Small", 2) | |
| nt <- nrow(tbl_full) | |
| tbl_full |> | |
| dplyr::arrange(match(.data[["subtype"]], shape_breaks[c(1, 2, 4:6, 3)])) |> | |
| sf::st_as_sf(crs = 27700, coords = c("eastings", "northings")) |> | |
| sf::st_jitter(6e3) |> | |
| ggplot2::ggplot() + | |
| ggplot2::geom_sf(data = england, fill = "bisque") + | |
| ggplot2::geom_sf( | |
| ggplot2::aes( | |
| fill = .data[["decile"]], | |
| shape = .data[["subtype"]], | |
| size = .data[["subtype"]] | |
| ), | |
| alpha = 0.8, | |
| stroke = 0.6, | |
| colour = "grey33" | |
| ) + | |
| ggplot2::scale_fill_viridis_d() + | |
| ggplot2::scale_shape_manual(breaks = shape_breaks, values = shape_values) + | |
| ggplot2::scale_size_manual(breaks = shape_breaks, values = size_values) + | |
| ggplot2::theme_void() + | |
| ggplot2::labs( | |
| title = "NHS England: Acute trusts by AMR league table ranking (decile)", | |
| subtitle = glue::glue( | |
| "Trusts are scored and then ranked nationally, with the lowest (best) ", | |
| "score given rank 1, down to rank {nt}. For ease of display, trusts ", | |
| "are here grouped into deciles." | |
| ) |> | |
| stringr::str_wrap(92), | |
| caption = paste0( | |
| "Source: NHS England Oversight Framework, September 2025\n", | |
| "https://www.england.nhs.uk/long-read/acute-trust-league-table/\n", | |
| "AMR = aggregated metric rankings. Locations are jittered slightly to ", | |
| "reduce symbol overlap." | |
| ) | |
| ) + | |
| ggplot2::theme( | |
| text = ggplot2::element_text("Fira Sans", colour = "grey33", size = 12), | |
| title = ggplot2::element_text(face = "bold"), | |
| plot.title = ggplot2::element_text( | |
| size = 18, | |
| margin = ggplot2::margin(0, 4, 4, 6) | |
| ), | |
| plot.subtitle = ggplot2::element_text(margin = ggplot2::margin(l = 6)), | |
| plot.title.position = "plot", | |
| plot.caption = ggplot2::element_text(face = "plain", hjust = 1), | |
| plot.caption.position = "plot", | |
| legend.title.position = "top", | |
| legend.position = "inside", | |
| plot.margin = ggplot2::margin(4, 12, 2, 12) | |
| ) + | |
| ggplot2::guides( | |
| fill = ggplot2::guide_legend( | |
| "decile (1 = highest ranked 10% of trusts)", | |
| nrow = 1, | |
| theme = ggplot2::theme( | |
| legend.direction = "horizontal", | |
| legend.text.position = "bottom", | |
| legend.spacing.x = grid::unit(0, "mm"), | |
| legend.key.spacing.x = grid::unit(0, "mm"), | |
| legend.justification.inside = c(0.05, 0.3) | |
| ), | |
| override.aes = list(size = 8, shape = "square filled", stroke = 0) | |
| ), | |
| shape = ggplot2::guide_legend( | |
| "Trust type", | |
| ncol = 2, | |
| theme = ggplot2::theme(legend.justification.inside = c(0.05, 0.45)), | |
| override.aes = list( | |
| colour = "grey33", | |
| fill = "grey66", | |
| size = size_values | |
| ) | |
| ), | |
| size = ggplot2::guide_none() | |
| ) | |
| ``` |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment