Author
Affiliation

Vagish Hemmige

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.

Source code

The full R script is available at:

This R script file is itself reliant on the following helper files:


Creating 60-Minute Driving Catchment Areas

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:

  • 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 zone
  2. 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 Results

Generating 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 Created

For 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.


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 function

converted_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: 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.
  • About: methods, assumptions, and disclosures