# install.packages("renv")
::restore() renv
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.
<- sf::st_read(
ecodistrict "data/ecodistrict_shp/Ecodistricts/ecodistricts.shp",
quiet = TRUE) |>
::filter(ECODISTRIC == 1028)
dplyr<- raster::raster("data/land_use/FarNorth_LandCover_Class_UTM16.tif")
lu_16 <- raster::raster("data/land_use/FarNorth_LandCover_Class_UTM17.tif")
lu_17 <- readr::read_csv("data/land_use/attr_table_northen_ont_lc.txt") |>
lu_dat ::mutate(cats = as.factor(code)) dplyr
Loading the sites data (2025)
We load the possible sites. Using the new 2025 sites:
<- readr::read_csv("data/sites/All Posssible Peat Sites 2025 2.0.csv") |>
all_new_sites ::st_as_sf(coords=c("x", "y"), crs = 4326) |>
sf::st_transform(crs = sf::st_crs(ecodistrict)) |>
sf::mutate(source = "2025_peat_sites") |>
dplyr::mutate(fullID = paste(OBJECTID, SampleID, source, sep = "_"))
dplyr
# 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,
::st_crs(ecodistrict))) +
sftheme_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.
<- function(scale_m, sites, lu) {
extract_from_points
<- sites |>
sites_buffer ::st_transform(sf::st_crs(lu)) |>
sf::st_buffer(dist = scale_m) |>
sf::select(fullID)
dplyr
<- exactextractr::exact_extract(lu, sites_buffer,
extr progress = FALSE,
include_cols = "fullID")
<- mapply(extr, 1:length(extr),
extr FUN = \(x, y) dplyr::mutate(x, id = y),
SIMPLIFY = F)
<- do.call(rbind, extr) |>
extr_df ::filter(!is.na(value)) |>
dplyr::relocate(id)
dplyr
return(extr_df)
}
<- function(scale_m, sites,
compute_land_cover
lu_16, lu_17, lu_dat,summarise_all = TRUE) {
<- extract_from_points(scale_m, sites, lu_16)
extr_16_df <- extract_from_points(scale_m, sites, lu_17)
extr_17_df
stopifnot(all(!(extr_16_df$siteID %in% extr_17_df$siteID)))
<- rbind(extr_16_df, extr_17_df) |>
extr ::arrange(id, value)
dplyr
if (summarise_all) {
<- extr |>
extr_table ::group_by(value) |>
dplyr::summarise(coverage_fraction_sum = sum(coverage_fraction)) |>
dplyr::mutate(prop =
dplyr/sum(coverage_fraction_sum)) |>
coverage_fraction_sum::ungroup() |>
dplyr::mutate(value = as.factor(value)) |>
dplyr::left_join(lu_dat, by = c("value" = "cats")) |>
dplyr::select(category_code, prop, label)
dplyr
else {
}
<- extr |>
extr_table ::group_by(fullID, value) |>
dplyr::summarise(coverage_fraction_sum = sum(coverage_fraction)) |>
dplyr::mutate(prop =
dplyr/sum(coverage_fraction_sum)) |>
coverage_fraction_sum::ungroup() |>
dplyr::mutate(value = as.factor(value)) |>
dplyr::left_join(lu_dat, by = c("value" = "cats")) |>
dplyr::select(fullID, category_code, prop, label)
dplyr
}
is.na(extr_table)] <- 0
extr_table[
return(extr_table)
}
We extract at different scales (buffer radius around points): 1 m, 50 m, 100 m and 1 km.
<- mapply(FUN = compute_land_cover,
res_points 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) |>
::bind_rows(.id = 'scale') |>
dplyr::mutate(scale = forcats::fct_relevel(scale, "1 m", "50 m",
dplyr"100 m", "1 km"),
label = forcats::fct_reorder(label, prop)) |>
::arrange(scale, dplyr::desc(prop))
dplyr
::kable(res_points) knitr
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.
<- rbind(lu_16_freq, lu_17_freq) |>
res_ecodistrict as.data.frame() |>
::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))
dplyr
::kable(res_ecodistrict) knitr
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.
<- rbind(res_points, res_ecodistrict) |>
res ::complete(scale, label) |>
tidyr::replace_na(list(prop = 0)) |>
tidyr::mutate(label = forcats::fct_reorder(label, prop)) dplyr
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) |>
::bind_rows(.id = 'scale') |>
dplyr::mutate(scale = forcats::fct_relevel(scale, "1 m", "50 m",
dplyr"100 m", "1 km"),
label = forcats::fct_reorder(label, prop)) |>
::group_by(scale, fullID) |>
dplyr::arrange(dplyr::desc(prop)) |>
dplyr::rename(primary_category_code = category_code,
dplyrprimary_prop = prop,
primary_label = label) |>
::mutate(secondary_category_code = primary_category_code[2],
dplyrsecondary_prop = primary_prop[2],
secondary_label = primary_label[2],
prop_sum = primary_prop + secondary_prop) |>
::slice(1) |>
dplyr::ungroup() |>
dplyr::arrange(fullID) dplyr
`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.
::datatable(res_points_by_site) DT
We save this table.
::write_csv(res_points_by_site, "outputs/res_points_by_site_2025.csv") readr
Results
We can plot the results with “dodged” ggplot2 barplots.
<- c('#c7e9b4','#7fcdbb','#41b6c4','#1d91c0','#225ea8','#0c2c84')
my_pal
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.
<- res |>
only_at_sites ::filter(prop > 0) |>
dplyr::filter(scale != "Ecodistrict") |>
dplyr::pull(label)
dplyr
<- res |>
res_filt ::filter(label %in% only_at_sites)
dplyr
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)