# install.packages("renv")
renv::restore()Land Cover Analysis
Using renv
To reproduce this analysis, make sure to restore the renv environment.
Loading sites data (before 2025)
We load the possible sites (quiet = TRUE is for not displaying verbose loading information).
# NOTE: this is commented out as it is now outdated (pre 2025)
# sites_possible <- sf::st_read(
# "data/sites/GRTS_PossibleCaARU_sample_draw_base.shp",
# quiet = TRUE) |>
# dplyr::mutate(source = "GRTS_PossibleCaARU_sample_draw_base") |>
# dplyr::mutate(fullID = paste(SampleID, ID, source, sep = "_"))
#
# additional_sites <- readr::read_csv("data/sites/Selected_Peat_Sites.csv") |>
# sf::st_as_sf(coords=c("lon_WGS84", "lat_WGS84"), crs = 4326) |>
# sf::st_transform(sf::st_crs(sites_possible)) |>
# dplyr::mutate(source = "Selected_Peat_Sites") |>
# dplyr::mutate(fullID = paste(SampleID, ID, source, sep = "_"))
#
# all_sites <- sites_possible |>
# dplyr::bind_rows(additional_sites)Loading the ecodistict and land cover data
We load the ecodistrict polygons. We use its crs to reproject the new data as well. We also load the two halves of the far north land cover dataset, along with the the attribute table of land cover classes. Finally we load the ecodistrict data and select for the relevant lowlands disctrict, coded as 1028.
ecodistrict <- sf::st_read(
"data/ecodistrict_shp/Ecodistricts/ecodistricts.shp",
quiet = TRUE) |>
dplyr::filter(ECODISTRIC == 1028)
lu_16 <- raster::raster("data/land_use/FarNorth_LandCover_Class_UTM16.tif")
lu_17 <- raster::raster("data/land_use/FarNorth_LandCover_Class_UTM17.tif")
lu_dat <- readr::read_csv("data/land_use/attr_table_northen_ont_lc.txt") |>
dplyr::mutate(cats = as.factor(code))Loading the sites data (2025)
We load the possible sites. Using the new 2025 sites:
all_new_sites <- readr::read_csv("data/sites/All Posssible Peat Sites 2025 2.0.csv") |>
sf::st_as_sf(coords=c("x", "y"), crs = 4326) |>
sf::st_transform(crs = sf::st_crs(ecodistrict)) |>
dplyr::mutate(source = "2025_peat_sites") |>
dplyr::mutate(fullID = paste(OBJECTID, SampleID, source, sep = "_"))
# Check that all points are in Ecodistrict 1028
stopifnot(all(all_new_sites$Ecodistric == 1028))Plotting spatial data
It is always a good idea to try and plot spatial data before any processing.
ggplot() +
geom_sf(data = ecodistrict) +
geom_sf(data = sf::st_transform(all_new_sites,
sf::st_crs(ecodistrict))) +
theme_bw()
Plotting the land cover data is difficult because it is provided is two different UTMs.
Extracting Land Cover data
The following functions will take care of land cover extraction for sites.
extract_from_points <- function(scale_m, sites, lu) {
sites_buffer <- sites |>
sf::st_transform(sf::st_crs(lu)) |>
sf::st_buffer(dist = scale_m) |>
dplyr::select(fullID)
extr <- exactextractr::exact_extract(lu, sites_buffer,
progress = FALSE,
include_cols = "fullID")
extr <- mapply(extr, 1:length(extr),
FUN = \(x, y) dplyr::mutate(x, id = y),
SIMPLIFY = F)
extr_df <- do.call(rbind, extr) |>
dplyr::filter(!is.na(value)) |>
dplyr::relocate(id)
return(extr_df)
}
compute_land_cover <- function(scale_m, sites,
lu_16, lu_17, lu_dat,
summarise_all = TRUE) {
extr_16_df <- extract_from_points(scale_m, sites, lu_16)
extr_17_df <- extract_from_points(scale_m, sites, lu_17)
stopifnot(all(!(extr_16_df$siteID %in% extr_17_df$siteID)))
extr <- rbind(extr_16_df, extr_17_df) |>
dplyr::arrange(id, value)
if (summarise_all) {
extr_table <- extr |>
dplyr::group_by(value) |>
dplyr::summarise(coverage_fraction_sum = sum(coverage_fraction)) |>
dplyr::mutate(prop =
coverage_fraction_sum/sum(coverage_fraction_sum)) |>
dplyr::ungroup() |>
dplyr::mutate(value = as.factor(value)) |>
dplyr::left_join(lu_dat, by = c("value" = "cats")) |>
dplyr::select(category_code, prop, label)
} else {
extr_table <- extr |>
dplyr::group_by(fullID, value) |>
dplyr::summarise(coverage_fraction_sum = sum(coverage_fraction)) |>
dplyr::mutate(prop =
coverage_fraction_sum/sum(coverage_fraction_sum)) |>
dplyr::ungroup() |>
dplyr::mutate(value = as.factor(value)) |>
dplyr::left_join(lu_dat, by = c("value" = "cats")) |>
dplyr::select(fullID, category_code, prop, label)
}
extr_table[is.na(extr_table)] <- 0
return(extr_table)
}We extract at different scales (buffer radius around points): 1 m, 50 m, 100 m and 1 km.
res_points <- mapply(FUN = compute_land_cover,
c(`1 m` = 1, `50 m` = 50,
`100 m` = 100, `1 km` = 1000),
MoreArgs = list(
sites = all_new_sites,
lu_16 = lu_16, lu_17 = lu_17, lu_dat = lu_dat),
SIMPLIFY = F) |>
dplyr::bind_rows(.id = 'scale') |>
dplyr::mutate(scale = forcats::fct_relevel(scale, "1 m", "50 m",
"100 m", "1 km"),
label = forcats::fct_reorder(label, prop)) |>
dplyr::arrange(scale, dplyr::desc(prop))
knitr::kable(res_points)| scale | category_code | prop | label |
|---|---|---|---|
| 1 m | TrBOG | 0.28 | Treed Bog |
| 1 m | OBOG | 0.24 | Open Bog |
| 1 m | TrFEN | 0.19 | Treed Fen |
| 1 m | ConSWA | 0.09 | Coniferous Swamp |
| 1 m | OFEN | 0.08 | Open Fen |
| 1 m | ConTRE | 0.05 | Coniferous Treed |
| 1 m | ThSWA | 0.03 | Thicket Swamp |
| 1 m | MixTRE | 0.02 | Mixed Treed |
| 1 m | SpTRE | 0.01 | Sparse Treed |
| 1 m | DecTRE | 0.01 | Deciduous Treed |
| 1 m | WAT | 0.00 | Clear Open Water |
| 50 m | TrBOG | 0.29 | Treed Bog |
| 50 m | OBOG | 0.22 | Open Bog |
| 50 m | TrFEN | 0.19 | Treed Fen |
| 50 m | ConSWA | 0.11 | Coniferous Swamp |
| 50 m | OFEN | 0.09 | Open Fen |
| 50 m | ConTRE | 0.05 | Coniferous Treed |
| 50 m | WAT | 0.02 | Clear Open Water |
| 50 m | ThSWA | 0.02 | Thicket Swamp |
| 50 m | SpTRE | 0.01 | Sparse Treed |
| 50 m | MixTRE | 0.01 | Mixed Treed |
| 50 m | DecTRE | 0.00 | Deciduous Treed |
| 100 m | TrBOG | 0.27 | Treed Bog |
| 100 m | OBOG | 0.23 | Open Bog |
| 100 m | TrFEN | 0.18 | Treed Fen |
| 100 m | ConSWA | 0.12 | Coniferous Swamp |
| 100 m | OFEN | 0.10 | Open Fen |
| 100 m | ConTRE | 0.05 | Coniferous Treed |
| 100 m | WAT | 0.03 | Clear Open Water |
| 100 m | ThSWA | 0.01 | Thicket Swamp |
| 100 m | SpTRE | 0.01 | Sparse Treed |
| 100 m | MixTRE | 0.01 | Mixed Treed |
| 100 m | DecTRE | 0.00 | Deciduous Treed |
| 1 km | TrBOG | 0.23 | Treed Bog |
| 1 km | TrFEN | 0.20 | Treed Fen |
| 1 km | OBOG | 0.20 | Open Bog |
| 1 km | ConSWA | 0.16 | Coniferous Swamp |
| 1 km | OFEN | 0.09 | Open Fen |
| 1 km | WAT | 0.05 | Clear Open Water |
| 1 km | ConTRE | 0.05 | Coniferous Treed |
| 1 km | MixTRE | 0.01 | Mixed Treed |
| 1 km | ThSWA | 0.01 | Thicket Swamp |
| 1 km | SpTRE | 0.00 | Sparse Treed |
| 1 km | DecTRE | 0.00 | Deciduous Treed |
| 1 km | NSWood | 0.00 | Disturbance - Non and Sparse Woody |
| 1 km | TrOrSHr | 0.00 | Disturbance - Treed and/or Shrub |
| 1 km | XWAT | 0.00 | Turbid Water |
| 1 km | BED | 0.00 | Bedrock |
We also want to do the same operation for the ecodistrict to allow for comparison. We don’t need to use exact extraction, insteadt the crop and mask each raster. This operation is costly so we write out the rasters and load them again (see unrendered code).
# NOTE: commented out as it is lengthy and intermediate outputs are already present
# ecodistrict_16 <- sf::st_transform(ecodistrict, sf::st_crs(lu_16))
# ecodistrict_17 <- sf::st_transform(ecodistrict, sf::st_crs(lu_17))
#
# lu_16_crop <- raster::crop(lu_16, ecodistrict_16)
# lu_16_crop_mask <- raster::mask(lu_16_crop, ecodistrict_16)
#
# lu_17_crop <- raster::crop(lu_17, ecodistrict_17)
# lu_17_crop_mask <- raster::mask(lu_17_crop, ecodistrict_17)We can then get the frequencies of values. This operation is also costly so we write out the objects and load them again (see unrendered code).
# NOTE: commented out as it is lengthy and intermediate outputs are already present
# lu_16_freq <- raster::freq(lu_16_crop_mask)
# lu_17_freq <- raster::freq(lu_17_crop_mask)We combine the results of both UTMs.
res_ecodistrict <- rbind(lu_16_freq, lu_17_freq) |>
as.data.frame() |>
dplyr::group_by(value) |>
dplyr::summarise(count = sum(count)) |>
dplyr::ungroup() |>
dplyr::filter(!is.na(value)) |>
dplyr::mutate(prop = count/sum(count)) |>
dplyr::mutate(value = as.factor(value)) |>
dplyr::left_join(lu_dat, by = c("value" = "cats")) |>
dplyr::filter(!is.na(label)) |>
dplyr::select(category_code, prop, label) |>
dplyr::mutate(scale = "Ecodistrict") |>
dplyr::relocate(scale) |>
dplyr::arrange(scale, dplyr::desc(prop))
knitr::kable(res_ecodistrict)| scale | category_code | prop | label |
|---|---|---|---|
| Ecodistrict | TrFEN | 0.25 | Treed Fen |
| Ecodistrict | OBOG | 0.20 | Open Bog |
| Ecodistrict | TrBOG | 0.19 | Treed Bog |
| Ecodistrict | ConSWA | 0.12 | Coniferous Swamp |
| Ecodistrict | OFEN | 0.09 | Open Fen |
| Ecodistrict | WAT | 0.07 | Clear Open Water |
| Ecodistrict | ConTRE | 0.04 | Coniferous Treed |
| Ecodistrict | NSWood | 0.01 | Disturbance - Non and Sparse Woody |
| Ecodistrict | TrOrSHr | 0.01 | Disturbance - Treed and/or Shrub |
| Ecodistrict | MixTRE | 0.01 | Mixed Treed |
| Ecodistrict | ThSWA | 0.01 | Thicket Swamp |
| Ecodistrict | SpTRE | 0.00 | Sparse Treed |
| Ecodistrict | XWAT | 0.00 | Turbid Water |
| Ecodistrict | DecTRE | 0.00 | Deciduous Treed |
| Ecodistrict | MIN | 0.00 | Sand/Gravel/Mine Tailings |
| Ecodistrict | FrMAR | 0.00 | Freshwater Marsh |
| Ecodistrict | BED | 0.00 | Bedrock |
| Ecodistrict | URB | 0.00 | Community/Infrastructure |
| Ecodistrict | DecSWA | 0.00 | Deciduous Swamp |
| Ecodistrict | InMAR | 0.00 | Intertidal Marsh |
And then combine the results between scales and utm.
res <- rbind(res_points, res_ecodistrict) |>
tidyr::complete(scale, label) |>
tidyr::replace_na(list(prop = 0)) |>
dplyr::mutate(label = forcats::fct_reorder(label, prop))For individual site identity, at different scales:
res_points_by_site <-
mapply(FUN = compute_land_cover,
c(`1 m` = 1, `50 m` = 50,
`100 m` = 100, `1 km` = 1000),
MoreArgs = list(
sites = all_new_sites,
lu_16 = lu_16, lu_17 = lu_17, lu_dat = lu_dat),
summarise_all = FALSE,
SIMPLIFY = F) |>
dplyr::bind_rows(.id = 'scale') |>
dplyr::mutate(scale = forcats::fct_relevel(scale, "1 m", "50 m",
"100 m", "1 km"),
label = forcats::fct_reorder(label, prop)) |>
dplyr::group_by(scale, fullID) |>
dplyr::arrange(dplyr::desc(prop)) |>
dplyr::rename(primary_category_code = category_code,
primary_prop = prop,
primary_label = label) |>
dplyr::mutate(secondary_category_code = primary_category_code[2],
secondary_prop = primary_prop[2],
secondary_label = primary_label[2],
prop_sum = primary_prop + secondary_prop) |>
dplyr::slice(1) |>
dplyr::ungroup() |>
dplyr::arrange(fullID)`summarise()` has grouped output by 'fullID'. You can override using the
`.groups` argument.
`summarise()` has grouped output by 'fullID'. You can override using the
`.groups` argument.
`summarise()` has grouped output by 'fullID'. You can override using the
`.groups` argument.
`summarise()` has grouped output by 'fullID'. You can override using the
`.groups` argument.
DT::datatable(res_points_by_site)We save this table.
readr::write_csv(res_points_by_site, "outputs/res_points_by_site_2025.csv")Results
We can plot the results with “dodged” ggplot2 barplots.
my_pal <- c('#c7e9b4','#7fcdbb','#41b6c4','#1d91c0','#225ea8','#0c2c84')
ggplot(res) +
geom_bar(aes(x = label, y = prop, fill = scale, colour = scale),
alpha = 0.8,
stat = "identity",
position = "dodge") +
theme_bw() +
theme(axis.text.x = element_text(angle = 20, vjust = 1, hjust = 1)) +
labs(x = "Land Use Class", y = "Proportion",
fill = "Scale", colour = "Scale") +
scale_fill_manual(values = my_pal) +
scale_color_manual(values = my_pal)
Removing the land use classes than are not present around sites, we get a slightly easier graph to read.
only_at_sites <- res |>
dplyr::filter(prop > 0) |>
dplyr::filter(scale != "Ecodistrict") |>
dplyr::pull(label)
res_filt <- res |>
dplyr::filter(label %in% only_at_sites)
ggplot(res_filt) +
geom_bar(aes(x = label, y = prop, fill = scale, colour = scale),
alpha = 0.8,
stat = "identity",
position = "dodge") +
theme_bw() +
theme(axis.text.x = element_text(angle = 20, vjust = 1, hjust = 1)) +
labs(x = "Land Use Class", y = "Proportion",
fill = "Scale", colour = "Scale") +
scale_fill_manual(values = my_pal) +
scale_color_manual(values = my_pal)