Montefiore Medical Center/ Albert Einstein College of Medicine
The code in this script creates 60-minute driving catchment areas around transplant centers.
These catchment areas are later used to estimate how many people living with HIV reside within a realistic travel time of transplant programs.
This section generates travel-time–based service areas for transplant centers.
Unlike simple radius buffers (e.g., 50 miles in every direction), these service areas reflect actual road travel time, accounting for highways, traffic patterns, and geography.
What is an Isochrone?
An isochrone is a geographic area showing how far someone can travel within a specified amount of time.
In this analysis:
A 60-minute isochrone represents all locations that can be reached within 60 minutes of driving from a transplant center.
This provides a more realistic measure of geographic access than straight-line (circular) buffers.
Isochrones are generated using the mapboxapi R package, which interfaces with the Mapbox routing API:
Each object is an sf polygon representing the area reachable within 60 minutes of driving from the center.
Why Travel Time Instead of Miles?
Straight-line distance (e.g., 50 miles) does not account for:
Road networks
Traffic congestion
Geographic barriers (mountains, water)
Travel-time–based service areas better approximate real-world access to transplant care.
These 60-minute isochrones are later intersected with census tract–level HIV population data to quantify access to transplant services.
Click to show/hide R Code
#set isochrones#Convert 7 AM local time to UTC time for every time zone in the US, using the with_tz functionconverted_times<-list()for (year_loop in year_list){#Create list for each year that converts the time zones converted_times[[year_loop]] <-tibble(timezone = timezones ) %>%mutate(local_time =map2( reference_date[[year_loop]], timezone,~ymd_hms(paste(.x, "07:00:00"), tz = .y) ),utc_time =map(local_time, ~with_tz(.x, "UTC")) ) %>%unnest(c(local_time, utc_time))}#Create isochrone objects for transplant centersusing the mapboxapi package if results not already cachedfor (organ_loop in organ_list){ all_center_path<-paste0("cache/Transplant_center_all_60min_buffer_",organ_loop, ".RDS")if (file.exists(all_center_path)) { Transplant_center_all_buffer[[organ_loop]][["60min"]] <-readRDS(all_center_path) }else { Transplant_center_all_buffer[[organ_loop]][["60min"]]<-set_60_min_isochrone_at_7AM(Transplant_centers_all_sf[[organ_loop]], "2017")saveRDS(Transplant_center_all_buffer[[organ_loop]][["60min"]], all_center_path) } for (year_loop in year_list){ active_center_path<-paste0("cache/Transplant_centers_active_60min_buffer_",organ_loop, "_", year_loop,".RDS")if (file.exists(active_center_path)) { Transplant_centers_active_buffer[[organ_loop]][["60min"]][[year_loop]] <-readRDS(active_center_path) }else { Transplant_centers_active_buffer[[organ_loop]][["60min"]][[year_loop]]<-set_60_min_isochrone_at_7AM(Transplant_centers_active_SF[[organ_loop]][[year_loop]], year_loop)saveRDS(Transplant_centers_active_buffer[[organ_loop]][["60min"]][[year_loop]], active_center_path) } HIV_center_path<-paste0("cache/Transplant_centers_HIV_60min_buffer_",organ_loop, "_", year_loop,".RDS")if (file.exists(HIV_center_path)) { Transplant_centers_HIV_buffer[[organ_loop]][["60min"]][[year_loop]]<-readRDS(HIV_center_path) } else { Transplant_centers_HIV_buffer[[organ_loop]][["60min"]][[year_loop]]<-set_60_min_isochrone_at_7AM(Transplant_centers_HIV_sf[[organ_loop]][[year_loop]], year_loop)saveRDS(Transplant_centers_HIV_buffer[[organ_loop]][["60min"]][[year_loop]], HIV_center_path) } HOPE_center_path<-paste0("cache/Transplant_centers_HOPE_60min_buffer_",organ_loop, "_", year_loop,".RDS")if (file.exists(HOPE_center_path)) { Transplant_centers_HOPE_buffer[[organ_loop]][["60min"]][[year_loop]]<-readRDS(HOPE_center_path) } else { Transplant_centers_HOPE_buffer[[organ_loop]][["60min"]][[year_loop]]<-set_60_min_isochrone_at_7AM(Transplant_centers_HOPE_sf[[organ_loop]][[year_loop]], year_loop)saveRDS(Transplant_centers_HOPE_buffer[[organ_loop]][["60min"]][[year_loop]], HOPE_center_path) } }}
Other portions of the analysis
Setup: Defines global paths, data sources, cohort inclusion criteria, and analysis-wide constants.
Functions: Reusable helper functions for cohort construction, matching, costing, and modeling.
Tables: Summary tables and regression outputs generated from the final models.
Figures:Visualizations of costs, risks, and model-based estimates.
---title: "Set isochrones"format: html---The code in this script creates **60-minute driving catchment areas** around transplant centers. These catchment areas are later used to estimate how many people living with HIV reside within a realistic travel time of transplant programs.::: {.Rcode title="Source code"}The full R script is available at:- [`R/set_isochrones.R`](https://github.com/VagishHemmige/HOPE-CDC-analysis-2026/blob/master/R/set_isochrones.R)This R script file is itself reliant on the following helper files:- [`R/setup.R`](https://github.com/VagishHemmige/HOPE-CDC-analysis-2026/blob/master/R/setup.R)- [`R/functions.R`](https://github.com/VagishHemmige/HOPE-CDC-analysis-2026/blob/master/R/functions.R):::---## Creating 60-Minute Driving Catchment AreasThis section generates **travel-time–based service areas** for transplant centers.Unlike simple radius buffers (e.g., 50 miles in every direction), these service areas reflect **actual road travel time**, accounting for highways, traffic patterns, and geography.### What is an Isochrone?An **isochrone** is a geographic area showing how far someone can travel within a specified amount of time.In this analysis:- A **60-minute isochrone** represents all locations that can be reached within 60 minutes of driving from a transplant center.- This provides a more realistic measure of geographic access than straight-line (circular) buffers.Isochrones are generated using the **`mapboxapi` R package**, which interfaces with the Mapbox routing API:- https://walker-data.com/mapboxapi/- CRAN: https://cran.r-project.org/package=mapboxapi---## Standardizing Time: Why 7:00 AM?Travel time varies depending on traffic conditions. To ensure consistency:- Isochrones are calculated assuming **departure at 7:00 AM local time**.- This approximates peak commuting conditions.- Using a fixed time improves comparability across regions.Because Mapbox requires time in **UTC**, the script:1. Creates a 7:00 AM timestamp for each U.S. time zone2. Converts that time to UTC using `with_tz()`3. Stores these converted times in `converted_times`This ensures that each transplant center’s isochrone is calculated at the correct local time.---## Caching Isochrone ResultsGenerating isochrones requires external API calls and can be computationally expensive.To avoid recomputing them repeatedly:- The script checks whether a cached `.RDS` file already exists.- If it exists → it loads the file.- If not → it generates the isochrone and saves it to the `cache/` directory.This dramatically speeds up Quarto re-rendering and ensures reproducibility.---## Isochrones CreatedFor each:- Organ type - Year - Center category The script creates 60-minute isochrones for:- All transplant centers - Active transplant centers - HIV R+ transplant centers - HOPE (HIV D+/R+) transplant centers The resulting structure is:- `Transplant_center_all_buffer[[organ]][["60min"]]`- `Transplant_centers_active_buffer[[organ]][["60min"]][[year]]`- `Transplant_centers_HIV_buffer[[organ]][["60min"]][[year]]`- `Transplant_centers_HOPE_buffer[[organ]][["60min"]][[year]]`Each object is an `sf` polygon representing the area reachable within 60 minutes of driving from the center.---## Why Travel Time Instead of Miles?Straight-line distance (e.g., 50 miles) does not account for:- Road networks - Traffic congestion - Geographic barriers (mountains, water) Travel-time–based service areas better approximate real-world access to transplant care.These 60-minute isochrones are later intersected with census tract–level HIV population data to quantify access to transplant services.---```{r, eval=FALSE}#set isochrones#Convert 7 AM local time to UTC time for every time zone in the US, using the with_tz functionconverted_times<-list()for (year_loop in year_list){ #Create list for each year that converts the time zones converted_times[[year_loop]] <- tibble( timezone = timezones ) %>% mutate( local_time = map2( reference_date[[year_loop]], timezone, ~ ymd_hms(paste(.x, "07:00:00"), tz = .y) ), utc_time = map(local_time, ~ with_tz(.x, "UTC")) ) %>% unnest(c(local_time, utc_time))} #Create isochrone objects for transplant centersusing the mapboxapi package if results not already cached for (organ_loop in organ_list){ all_center_path<-paste0("cache/Transplant_center_all_60min_buffer_",organ_loop, ".RDS") if (file.exists(all_center_path)) { Transplant_center_all_buffer[[organ_loop]][["60min"]] <- readRDS(all_center_path) }else { Transplant_center_all_buffer[[organ_loop]][["60min"]]<- set_60_min_isochrone_at_7AM(Transplant_centers_all_sf[[organ_loop]], "2017") saveRDS(Transplant_center_all_buffer[[organ_loop]][["60min"]], all_center_path) } for (year_loop in year_list){ active_center_path<-paste0("cache/Transplant_centers_active_60min_buffer_",organ_loop, "_", year_loop,".RDS") if (file.exists(active_center_path)) { Transplant_centers_active_buffer[[organ_loop]][["60min"]][[year_loop]] <- readRDS(active_center_path) }else { Transplant_centers_active_buffer[[organ_loop]][["60min"]][[year_loop]]<- set_60_min_isochrone_at_7AM(Transplant_centers_active_SF[[organ_loop]][[year_loop]], year_loop) saveRDS(Transplant_centers_active_buffer[[organ_loop]][["60min"]][[year_loop]], active_center_path) } HIV_center_path<-paste0("cache/Transplant_centers_HIV_60min_buffer_",organ_loop, "_", year_loop,".RDS") if (file.exists(HIV_center_path)) { Transplant_centers_HIV_buffer[[organ_loop]][["60min"]][[year_loop]]<- readRDS(HIV_center_path) } else { Transplant_centers_HIV_buffer[[organ_loop]][["60min"]][[year_loop]]<- set_60_min_isochrone_at_7AM(Transplant_centers_HIV_sf[[organ_loop]][[year_loop]], year_loop) saveRDS(Transplant_centers_HIV_buffer[[organ_loop]][["60min"]][[year_loop]], HIV_center_path) } HOPE_center_path<-paste0("cache/Transplant_centers_HOPE_60min_buffer_",organ_loop, "_", year_loop,".RDS") if (file.exists(HOPE_center_path)) { Transplant_centers_HOPE_buffer[[organ_loop]][["60min"]][[year_loop]]<- readRDS(HOPE_center_path) } else { Transplant_centers_HOPE_buffer[[organ_loop]][["60min"]][[year_loop]]<- set_60_min_isochrone_at_7AM(Transplant_centers_HOPE_sf[[organ_loop]][[year_loop]], year_loop) saveRDS(Transplant_centers_HOPE_buffer[[organ_loop]][["60min"]][[year_loop]], HOPE_center_path) } }}```## Other portions of the analysis- [**Setup**](setup.qmd): Defines global paths, data sources, cohort inclusion criteria, and analysis-wide constants.- [**Functions**](functions.qmd): Reusable helper functions for cohort construction, matching, costing, and modeling.- [**Tables**](tables.qmd): Summary tables and regression outputs generated from the final models.- [**Figures**](figures.qmd):Visualizations of costs, risks, and model-based estimates.- [**About**](about.qmd): methods, assumptions, and disclosures