Analyzing Data on The Urban Heat Island Effect in Denver with R

R
leaflet
mapping
geospatial
climate
Author
Published

August 23, 2024

Introduction

I’ve been working recently on analyzing access to cooling centers in the context of more common heat waves due to climate change. I was interested in finding some data on the urban heat island effect and seeing how that varies across the city. In this post, I’ll show how I visualized some data on the urban heat island effect in Denver using R. Specifically, I make a choropleth map (Figure 1) of the heat island effect, and a bar chart (Figure 2) showing the population affected by different temperature ranges.

Data

Load libraries

Heat Island Data

Climate Central provides data on the urban heat island (UHI) effect for census tracts in large US cities. According to their analysis description, the UHI effect is an estimate of the additional heat that local land use factors contribute to urban areas.

I downloaded the data in excel format from Climate Central. The data provide the UHI effect by city and census tract number. I used the {janitor} package (Firke 2023) to clean up the variable names and filtered to data for Denver.

Load UHI data
uhi <- readxl::read_xlsx('./data/Climate_Central_Urban_heat_islands_city_rankings___UHI_by_census_tract.xlsx', sheet = "UHI effect by census tract", col_types = c("text", "text", "numeric","numeric")) |>
  filter(city == "Denver") |> # filter to Denver only
  janitor::clean_names() |>
  rename(uhi_effect_degF = urban_heat_island_effect_temperature_in_degrees_f,
         uhi_effect_degC = urban_heat_island_effect_temperature_in_degrees_c)

DT::datatable(uhi, options = list(pageLength = 5), rownames = FALSE)
Table 1: UHI effect data for Denver from Climate Central.

Denver census tracts

I obtained geometries and populations for the census tracts in Denver with the {tidycensus} package (Walker and Herman 2024). I could use {tigris} (Walker 2024) if I wanted to just get the geometries, but I used {tidycensus} so I could get associated census data as well for further analysis (here I get the total population of each tract).

Load census tracts and population
# load Denver census tracts
den_tracts <- get_decennial(
  geography = "tract", 
  variables = "P1_001N", # total population
  year = 2020,
  state = "CO",
  county = "Denver", 
  geometry = TRUE
) |> rename(pop = value) |> # rename population column
  st_transform(4326) # CRS to plot in leaflet

DT::datatable(den_tracts, options = list(pageLength = 5), rownames = FALSE)
Table 2: 2020 Census data for tracts in Denver county.

Joining the data

Before we can analyze and map the data, we need to join the two data sets. I used the census_tract_number from the UHI data and the GEOID from the census tract data to join the two data sets.

The Analyzing US Census Data: Methods, Maps, and Models in R book by Kyle Walker (creator of the {tidycensus} and {tigris} packages) gives a nice explanation of GEOIDs, which can be used to uniquely identify geographic census units.

An example GEOID from this data set is 08031000102.

  • 08 is the FIPS code for Colorado
  • 031 is the code for Denver county in Colorado.
  • The next 6 digits 000102 represent the census tract (right-padded with zeros)
  • Put all together, the GEOID 08031000102 represents Census Tract 1.02, Denver County, Colorado.

In the UHI data the corresponding census_tract_number is 8031000102, which is the GEOID from the census data without the leading zero.

The UHI data contains some data from other counties surrounding Denver, while the census tracts are only for Denver (county code 08031), so I filter the UHI data to just the census tracts starting with 8031.

Join UHI and Census Data
# limit to Denver county (data includes some surrounding counties)
uhi_den <- uhi |> filter(stringr::str_detect(uhi$census_tract_number, pattern = "^8031"))

# add a leading 0 to match the GEOID column
uhi_den$GEOID <- paste0("0", uhi_den$census_tract_number)

# now we can join the data
dat_joined <- left_join(den_tracts, uhi_den, by = "GEOID")

#head(dat_joined)
dat_joined |> select(GEOID, NAME, pop, uhi_effect_degF) |>
DT::datatable(options = list(pageLength = 5), rownames = FALSE)
Table 3: Table of joined UHI and census data. UHI data from Climate Central.

Mapping

Now we can make a choropleth map visualizing the heat island effect. I’ll use the {leaflet} package (Cheng et al. 2024) to make a nice interactive map (Figure 1). You can zoom in/out and hovering or clicking on a census tract will display information about it.

Make map of UHI effect
pal <- colorNumeric("YlOrRd", domain = dat_joined$uhi_effect_degF)

leaflet() |>
  addTiles() |>
  addPolygons(data = dat_joined,
              label = ~NAME,
              popup = paste(dat_joined$NAME, "<br>", 
                            "UHI Effect:", dat_joined$uhi_effect_degF, " deg F"),
              color = "black",
              weight = 1,
              fillColor = ~pal(uhi_effect_degF), 
              fillOpacity = 0.5) |>
  addLegend(data = dat_joined,
            pal = pal, 
            values = ~uhi_effect_degF, 
            title = "Deg F") 
Figure 1: Map showing the urban heat island effect for Denver census tracts. Data from Climate Central.

Population Subject to Heat Island Effect

I was also interested in how much of the population in Denver is subject to different degrees of the heat island effect. To visualize this I grouped the data (each census tract) into 1 deg F bins of UHI effect (for example 4-5 deg, 5-6 deg), using the cut_width() function from the {ggplot2} package (Wickham 2016). Then I grouped and summed the population by these bins and made a bar chart (Figure 2).

Population bar chart
# bin the UHI effect values in 1 deg bins
x <- dat_joined |> mutate(bin = ggplot2::cut_width(uhi_effect_degF, width = 1,center = 0.5))
x |> 
  group_by(bin) |>
  summarise(tot_pop = sum(pop)) |>
  ggplot(aes(bin, tot_pop)) +
  geom_col(fill = "orange", color = "black") +
  scale_y_continuous(labels = scales::comma) +
  labs(x = "Urban Heat Island Effect [deg F]",
       y = "Population",
       caption = "Data from Climate Central",
       title = "Urban Heat Island Effect by Population") +
  theme_minimal(base_size = 14)
Figure 2: Bar chart showing the total population in each 1 deg (F) bin of UHI effect.

Summary and Future Work

In this post I:

  • Obtained data from Climate Central on the Urban heat island (UHI) effect in Denver.
  • Made a choropleth map (Figure 1) of the data using {leaflet}.
  • Visualized (Figure 2) how much of the population is subject to different amounts of the UHI effect.

In the future I would like to see how the spatial distribution of the UHI effect in Denver compares to the distribution of cooling centers, as well as how it correlates with other demographic data such as income and race.

SessionInfo

R version 4.4.1 (2024-06-14)
Platform: x86_64-apple-darwin20
Running under: macOS Sonoma 14.6.1

Matrix products: default
BLAS:   /Library/Frameworks/R.framework/Versions/4.4-x86_64/Resources/lib/libRblas.0.dylib 
LAPACK: /Library/Frameworks/R.framework/Versions/4.4-x86_64/Resources/lib/libRlapack.dylib;  LAPACK version 3.12.0

locale:
[1] en_US.UTF-8/en_US.UTF-8/en_US.UTF-8/C/en_US.UTF-8/en_US.UTF-8

time zone: America/Denver
tzcode source: internal

attached base packages:
[1] stats     graphics  grDevices datasets  utils     methods   base     

other attached packages:
 [1] leaflet_2.2.2    tidycensus_1.6.5 lubridate_1.9.3  forcats_1.0.0   
 [5] stringr_1.5.1    dplyr_1.1.4      purrr_1.0.2      readr_2.1.5     
 [9] tidyr_1.3.1      tibble_3.2.1     ggplot2_3.5.1    tidyverse_2.0.0 
[13] sf_1.0-17        janitor_2.2.0   

loaded via a namespace (and not attached):
 [1] gtable_0.3.5       bslib_0.8.0        xfun_0.47          htmlwidgets_1.6.4 
 [5] tigris_2.1         tzdb_0.4.0         crosstalk_1.2.1    vctrs_0.6.5       
 [9] tools_4.4.1        generics_0.1.3     curl_5.2.1         proxy_0.4-27      
[13] fansi_1.0.6        pkgconfig_2.0.3    KernSmooth_2.23-24 RColorBrewer_1.1-3
[17] readxl_1.4.3       uuid_1.2-0         lifecycle_1.0.4    farver_2.1.1      
[21] compiler_4.4.1     munsell_0.5.1      snakecase_0.11.1   sass_0.4.9        
[25] htmltools_0.5.8.1  class_7.3-22       yaml_2.3.10        jquerylib_0.1.4   
[29] pillar_1.9.0       crayon_1.5.3       DT_0.33            classInt_0.4-10   
[33] cachem_1.1.0       wk_0.9.1           tidyselect_1.2.1   rvest_1.0.4       
[37] digest_0.6.37      stringi_1.8.4      labeling_0.4.3     fastmap_1.2.0     
[41] grid_4.4.1         colorspace_2.1-0   cli_3.6.3          magrittr_2.0.3    
[45] utf8_1.2.4         e1071_1.7-14       withr_3.0.1        scales_1.3.0      
[49] rappdirs_0.3.3     timechange_0.3.0   rmarkdown_2.28     httr_1.4.7        
[53] cellranger_1.1.0   hms_1.1.3          evaluate_0.24.0    knitr_1.48        
[57] s2_1.1.6           rlang_1.1.4        Rcpp_1.0.13        glue_1.7.0        
[61] DBI_1.2.2          xml2_1.3.6         renv_1.0.4         rstudioapi_0.16.0 
[65] jsonlite_1.8.8     R6_2.5.1           units_0.8-5       

References

Cheng, Joe, Barret Schloerke, Bhaskar Karambelkar, and Yihui Xie. 2024. “Leaflet: Create Interactive Web Maps with the JavaScript ’Leaflet’ Library.” https://rstudio.github.io/leaflet/.
Firke, Sam. 2023. “Janitor: Simple Tools for Examining and Cleaning Dirty Data.” https://CRAN.R-project.org/package=janitor.
Walker, Kyle. 2024. “Tigris: Load Census TIGER/Line Shapefiles.” https://CRAN.R-project.org/package=tigris.
Walker, Kyle, and Matt Herman. 2024. “Tidycensus: Load US Census Boundary and Attribute Data as ’Tidyverse’ and ’Sf’-Ready Data Frames.” https://CRAN.R-project.org/package=tidycensus.
Wickham, Hadley. 2016. “Ggplot2: Elegant Graphics for Data Analysis.” https://ggplot2.tidyverse.org.