diff --git a/scripts/create_loss_layer.R b/scripts/create_loss_layer.R new file mode 100644 index 0000000..bbb3126 --- /dev/null +++ b/scripts/create_loss_layer.R @@ -0,0 +1,149 @@ +library(terra) +library(dplyr) +library(tidyr) + + +######################### data setup ######################### +lt_data_dir <- file.path("rasters/lt_nbr_ftv_1985_2021_combo6") +lt_name <- "lt_nbr_1985_2021_combo6" +ftv_file_name <- "lt_nbr_ftv_1985_2021_combo6" +vertex_file_name <- "lt_nbr_is_vertex_1985_2021_combo6" +dir.create(paste0(lt_data_dir, "/ftv_tiles")) +dir.create(paste0(lt_data_dir, "/vertex_tiles")) +dir.create(paste0(lt_data_dir, "/masked_ftv")) +dir.create(paste0(lt_data_dir, "/losses")) + +######################### merge tiles function ######################### +#ftv stacks from LT are often very large, and as such are split into several tiles. This function will stitch those tiles back together +#if you have a single ftv file, you can skip this step +#requires gdal installation +merge_tiles <- function(source_dir, dest_file, pattern = ".*tif") { + vrt_file <- gsub("(tiff|tif)", "vrt", dest_file) + + files <- list.files(source_dir, full.names = TRUE, pattern = pattern) + index_file <- file.path(source_dir, "files.txt") + writeLines(files, index_file) + system( + glue::glue("gdalbuildvrt {vrt_file} -input_file_list {index_file}") + ) + system( + glue::glue( + paste( + "gdal_translate -co COMPRESS=DEFLATE -co PREDICTOR=3 -co TILED=YES", + "-co BIGTIFF=YES {vrt_file} {dest_file}" + ) + ) + ) + unlink(index_file) + unlink(vrt_file) + return(dest_file) +} + +merge_tiles(lt_data_dir, paste0(lt_data_dir, "/", ftv_file_name, "_all.tif"), pattern = "lt_nbr_ftv.*tif" ) + + +######################### retile ftv and vertex layers ######################### +# load merged ftv and vertex layers +ftv <- terra::rast(paste0(lt_data_dir, "/", ftv_file_name, "_all.tif")) +names(ftv) <- 1985:2022 +is_vertex <- terra::rast(paste0(lt_data_dir, "/", vertex_file_name, ".tif")) +names(is_vertex) <- 1985:2022 + + +#analysis requires much smaller tiles, can adjust size of tiles based on processing ability of your computer +tile_grid <- terra::rast( + extent = terra::ext(ftv), + ncols = 30, + nrows = 30 +) + +terra::makeTiles( + ftv, + tile_grid, + path.expand(paste0(lt_data_dir, '/ftv_tiles/ftv_', ".tif")), + overwrite = TRUE +) + +terra::makeTiles( + is_vertex, + tile_grid, + path.expand(paste0(lt_data_dir, '/vertex_tiles/is_vertex_', ".tif")), + overwrite = TRUE +) + +######################### mask smoothed ftv traj with vertices ######################### +#removing pixels that are not breakpoints in spectral trajectory +vertex_files <- list.files(paste0(lt_data_dir,"/vertex_tiles/"), pattern = "is_vertex_.*.tif", full.names = TRUE) +ftv_files <- list.files(paste0(lt_data_dir,"/ftv_tiles/"), pattern = "ftv_.*.tif", full.names = TRUE) + +lapply(1:length(ftv_files), \(i) { + message(paste0("Working on iteration ", i)) + vertex_file <- vertex_files[i] + ftv_file <- ftv_files[i] + index <- stringr::str_split(gsub(pattern = "\\.tif", "", basename(vertex_file)), "_")[[1]] |> tail(1) + masked_ftv <- file.path(paste0(lt_data_dir,"/masked_ftv/masked_ftv_", index, ".tif")) + + if (!file.exists(masked_ftv)){ + terra::mask(terra::rast(ftv_file), terra::rast(vertex_file), maskvalues = 0, filename = masked_ftv) + } +}) + +######################### create all loss layer ######################### +#using masked ftv to identify decreasing segments +#value of previous vertex greater than value of current vertex + +detect_loss <- function(model, data, ...){ + # Collect these for later + years <- names(data) + + # Build a place holder data.frame + results_container <- data.frame( + matrix(nrow = nrow(data), ncol = length(years)) + ) + names(results_container) <- years + + # Compute magnitudes for loss vertices, with > 1 segment + data <- data |> + mutate(id = 1:nrow(data)) |> # keep track of pixel order + pivot_longer(-id, names_to = "year") |> + mutate(year = as.numeric(year) + 1) |> # LT defaults adds a year to the vertex date to assign year of disturbance + group_by(id) |> + filter(!is.na(value)) |> + filter(n() > 2) |> + mutate(change = value - lead(value)) |> + filter(change > 0) |> + select(-value) |> + ungroup() |> + pivot_wider(names_from = "year", values_from = "change") + + # Pad the results, filling in the years that don't have disturbances + fill_years <- setdiff(years, names(data)) + fill_cols <- lapply(fill_years, \(fy) { + return(rep(NA, nrow(data))) + }) + names(fill_cols) <- fill_years + data <- bind_cols(data, fill_cols) |> + dplyr::relocate(all_of(years)) + + # Insert results in the correct location + results_container[data$id, ] <- data |> select(-id) + return(results_container) +} + +#use loss funtction to create loss tiles +masked_ftvs <- list.files(paste0(lt_data_dir, "/masked_ftv/"), full.names = TRUE) +lapply(masked_ftvs, \(masked_file) { + index <- stringr::str_split(gsub(pattern = "\\.tif", "", basename(masked_file)), "_")[[1]] |> tail(1) + losses <- file.path(paste0(lt_data_dir,"/losses/losses", index , ".tif")) + + if (!file.exists(losses)){ + terra::predict(terra::rast(masked_file), list(), detect_loss, filename = losses) + } +}) + +#merge loss tiles into new loss.tif +merge_tiles(paste0(lt_data_dir, "/losses"), paste0(lt_data_dir,"/losses/", lt_name,"_all_losses.tif"), pattern = "*.tif" ) +all_losses <- rast("rasters/lt_nbr_rt1_1985-2022/losses/rt1_1985_2022_all_losses.tif") +names(all_losses) <- 1985:2022 +all_losses <- subset(all_losses, 6:38) #remove pre 1990 years, optional step +writeRaster(all_losses, paste0(lt_data_dir,"/losses/", lt_name,"_all_losses.tif"), overwrite = TRUE) diff --git a/scripts/harvest_brick.R b/scripts/harvest_brick.R new file mode 100644 index 0000000..52145fc --- /dev/null +++ b/scripts/harvest_brick.R @@ -0,0 +1,59 @@ +library(terra) +######################## set up######################## +years <- 1990:2021 #study years +harvest_directory <- file.path("../GIS_Files/harvest_rasters") +dir.create(paste0(harvest_directory, "/harv_layers")) +harv_records <- vect("../GIS_Files/albers_combined_harv_records.gpkg") +#need study area bounds +harv_boundaries <- vect("../GIS_Files/combined_tract_boundaries.gpkg") + +crs <- labrador.client::get_cafri_crs() + + +######################## rasterize harvests ######################## +harv_records$value <- 1 #dummy value for creating harvest raster + +#use your LT output as reference raster here, want harvest layer to have same extent +ref_raster <- rast("rasters/combo12_all_losses2.tif")[[1]] +values(ref_raster) <- NA + +lapply(1:length(years), \(i) { + message(paste0("Working on iteration ", i)) + + harv_subset <- terra::subset(harv_records, + (years[i] >= harv_records$start_date & years[i] <= harv_records$end_date) + | as.integer(harv_records$start_date) + 1 == years[i] + | as.integer(harv_records$end_date) + 1== years[i] + | as.integer(harv_records$start_date) - 1 == years [i] + | as.integer(harv_records$end_date) - 1 == years[i] ) + + harv_file <- file.path(paste0(harvest_directory, "/harv_layers/harv_layer_", years[i], ".tif")) + harv_exists <- file.exists(harv_file) + + if (!harv_exists & nrow(harv_subset) != 0){ + terra::rasterize(harv_subset, ref_raster, field = "value", filename = harv_file) + } else if (!harv_exists){ + writeRaster(ref_raster, filename = harv_file) + } +}) + +######################## combine havrvest rasters into single harvest brick layer ######################## +rasters <- list.files(paste0(harvest_directory, "/harv_layers/"), full.names = TRUE) +harv_raster <- rast(rasters) +names(harv_raster) <- years +harv_raster[is.na(harv_raster)] <- 0 +writeRaster(harv_raster, paste0(harvest_directory,"/harvest_brick.tif"), overwrite = TRUE) +harv_raster <- rast(paste0(harvest_directory,"/harvest_brick.tif")) + +######################## mask harvest brick with study area boundaries ######################## +#terra throws a fit if the crs info doesn't match exactly, just making the labels match here, its the same crs +crs(harvest_rast) <- crs + +#dummy value for making raster +harv_boundaries$rast_value <- 1 +harv_boundaries <- rasterize(harv_boundaries, harv_raster, field = "rast_value") +#areas outside harvests set to NA +harv_raster <- mask(harv_raster, harv_boundaries) + +writeRaster(harv_raster, paste0(harvest_directory,"/harvest_brick.tif"), overwrite = TRUE) +rast \ No newline at end of file diff --git a/scripts/multi_disturbance_accuracy_assesment.R b/scripts/multi_disturbance_accuracy_assesment.R new file mode 100644 index 0000000..e5e1e4b --- /dev/null +++ b/scripts/multi_disturbance_accuracy_assesment.R @@ -0,0 +1,222 @@ +library(terra) +library(exactextractr) +library(dplyr) +library(future) +library(future.apply) +library(progressr) + + +############################################# set up ############################################# +#this accuracy assessment process is designed to match a an annual disturbance raster stack to polygon reference data +#replace reference data with your own polygon data for best results +#required data: LT output, target event polygons, raster of event polygons, polygons of study area bounds (tracts) + +#adjusts allowable ram usaage +terra::terraOptions(memfrac=0.85) + +study_begin <- 1990 +study_end <- 2021 +crs <- labrador.client::get_cafri_crs() + +#load data +crs <- labrador.client::get_cafri_crs() +tracts <- vect("../GIS_Files/combined_tract_boundaries.gpkg") +harvests <- vect("../GIS_Files/albers_combined_harv_records.gpkg") |> terra::as.data.frame(geom = "WKT") +harvest_rast <- rast("../GIS_Files/harvest_rasters/harvest_brick.tif") +losses <- rast("rasters/combo12_all_losses2.tif") +#terra throws a fit if the crs info doesn't match exactly, just making the labels match here, its the same crs +crs(losses) <- crs + +#data names +lt_output_name <- "combo_12" + +############################################# functions ########################################### + +#harvest accuracy assessment function +#this function assesses the area within harvests +#assesses accuracy for each harvest event + +polygon_acc_assesment <- function(harvest, loss_layer){ + + # Assumes loss_layer is passed as a filename - important for parallelization with terra + loss_layer <- terra::rast(loss_layer) |> terra::crop(harvest) + start_year <- harvest$start_date + end_year <- harvest$end_date + + #assigning buffer years, each harvest gets +- 1 year buffer window, unless they align with the end of the study period + if (end_year == 2021) { + harv_years <- c((as.numeric(start_year)-1):(as.numeric(end_year))) + } else if (start_year == 1990) { + harv_years <- c((as.numeric(start_year)):(as.numeric(end_year) + 1)) + } else { + harv_years <- c((as.numeric(start_year)-1):(as.numeric(end_year) +1)) + } + + #subset loss layer so that only the layers for the harvest years remain + harv_year_losses <- subset(loss_layer, as.character(harv_years)) + # exact extract wont count NA values, so this makes counting TPs easy + harv_year_losses[harv_year_losses < 0] <- NA + + TP <- exact_extract(harv_year_losses, harvest, 'count') + #add up TP for all years of the harvest + TP <- sum(TP) + #FN = total pixels in the harvest - TPs + #the number of pixels in the harvest is constant so loss_layer[[1]] calls the first layer of the loss object to count them + FN <- exact_extract(loss_layer[[1]], harvest, 'count') + FN <- FN - TP + + results <- as.data.frame(TP) |> cbind(FN) + + #this method essentially takes an overhead view of the harvest + #instead of treating each year individually, we look at the harvest as a whole + #to get the right number of pixels for the multi year stack, multiply by the number of years in the harvest + results <- results * as.numeric(length(harv_years)) + return(results) +} + +# tract accuracy assessment function +#this function looks at areas outside your harvests but within your study area boundaries +#assess accuracy on an annual basis + +tract_acc_assesment <- function(tract_losses_file_path, i, tract){ + #going one year at a time + # Assumes loss file is passed as a filename - important for parallelization with terra + year_layer <- terra::rast(tract_losses_file_path, lyrs=i) |> + terra::crop(tract) + + #FP = total number of disturbances detected, since outside reference data area any detection are automatic FP + #TN = total number of pixels - FPS + #however to count this easily first need total number of pixels + total_pixels <- exact_extract(year_layer, tract, 'count') + + #set all the 0 pixels to NA now + #couldn't have done this before now without making it difficult to calculate the total number of pixels inside the tracts + year_layer[year_layer <= 0] <- NA + + #count FP + FP <- exact_extract(year_layer, tract, 'count') + + #count TN + TN <- total_pixels - FP + + results <- as.data.frame(sum(TN)) |> cbind(sum(FP)) + return(results) +} + + +####################################### reference data formatting ####################################### + +#match reference data dates to LT output dates, check validity of harvest dates +harvests <- harvests %>% filter(!(is.na(start_date)| is.na(end_date))) %>% + filter(!(end_date < start_date)) %>% + filter(!(start_date < study_begin | end_date > study_end | start_date > study_end)) +harvests <- vect(harvests, geom = "geometry") +#terra also looses the crs information when moving from a vector -> dataframe -> vector so need to reassign it here +crs(harvests) <- crs + + +####################################### harvest polygon accuracy assessment ####################################### +# tallying TP and FN + +#create new loss layer that only has values for areas within harvests +#duplicate loss layer so don't overwrite original +polygon_losses <- losses +#set all NA to -1, use -1 not zero in case there were any very small disturbance magnitudes +polygon_losses[is.na(polygon_losses)] <- -1 +#mask so all areas outside harvest polygons are NA, areas inside harvest are now either disturbance magnitude or -1 +polygon_losses <- mask(polygon_losses, harvests) +#need to write new losses file for polygon acc assessment function +writeRaster(polygon_losses, paste0(lt_output_name, "_polygon_assesment_loss_layer.tif"), overwrite = TRUE) +#read file path back +loss_layer <- file.path(paste0(lt_output_name, "_polygon_assesment_loss_layer.tif")) + +#harvests have to be sf object to work with parallelization +harvests <- sf::st_as_sf(harvests) + +#make empty data frame for results +polygon_results <- data.frame(TP=0, FN = 0) +names(polygon_results) <- c("TP", "FN") + +#run polygon assessment function +future::plan('multisession') + +progressr::with_progress({ + p <- progressr::progressor(along = 1:nrow(harvests)) + polygon_results <- future.apply::future_lapply(seq_len(nrow(harvests)), \(i) { + p(message = paste0("Processing harvest: ", (i))) + harvest <- harvests[i,] + polygon_acc_assesment(harvest, loss_layer) + }) +}) + +polygon_results <- dplyr::bind_rows(polygon_results) + +#write.csv(polygon_results, paste0(lt_output_name, "_multidisturbance_polygon_accuracy.csv"), row.names = FALSE) + + +####################################### asses area outside harvest polygons ####################################### +#tallies FP and TN + + +##### make tract brick ####### +#can use any raster with same extent as template +tract_rast <- fasterize::fasterize(sf::st_as_sf(tracts), raster::raster(losses)) +#convert to spatRaster +tract_rast <- rast(tract_rast) + +###### recrop loss layer ##### +tract_losses <- crop(losses, tract_rast) +#focused on losses outside harvest areas now +#set areas outside of the tracts to -1, -1 just dummy value +tract_losses <- mask(tract_losses, tract_rast, updatevalue = -1) +#set any NA pixels inside tracts to 0 +tract_losses[is.na(tract_losses)] <- 0 +#set outside values back to NA, now areas outside tracts are NA and areas inside are either the loss magnitude or 0 +tract_losses[tract_losses == -1] <- NA +#remove harvested areas from loss layer +#the combination of mask_values = 0 and inverse = true means we are keeping the areas outside the harvests +tract_losses <- mask(tract_losses, harvest_rast, maskvalues = 0, inverse = TRUE) +crs(harvest_rast) <- crs +#write tract loss layer for tract accuracy assessment function +writeRaster(swiss_cheese, paste0(lt_output_name, "_tract_loss_layer.tif"), overwrite = TRUE) + +#tract layer has to be sf object for exact extract functions +tracts <- sf::st_as_sf(tracts) + +#create blank df for results +tract_results <- data.frame(TN=0, FP = 0) +names(tract_results) <- c("TN", "FP") + +#load tract losses file path, has to be file path for function +tract_losses_file_path <- file.path(paste0(lt_output_name, "_tract_loss_layer.tif")) + +years <- names(rast(tract_losses)) + + +#run tract accuracy assesment function +future::plan('multisession') + +progressr::with_progress({ + p <- progressr::progressor(along = 1:length(names)) + tract_results <- future.apply::future_lapply(seq_len(length(names)), \(i) { + p(message = paste0("Processing layer: ", (i))) + tract_acc_assesment(tract_losses, i, tracts) + }) +}) + +tract_results <- dplyr::bind_rows(tract_results) +#write.csv(tract_results, paste0(lt_output_name, "_multidisturbance_tract_accuracy.csv"), row.names = FALSE) + + +####################### combine results ########################## + +full_results <- cbind(sum(polygon_results[['TP']]), sum(polygon_results[['FP']]), sum(tract_results[['sum(TN)']]), sum(tract_results[['sum(FP)']])) +full_results <- as.data.frame(full_results) +names(full_results) <- c("TP", "FN", "TN", "FP") + +#write.csv(full_results, paste0(lt_output_name, "_multidisturbance__accuracy.csv"), row.names = FALSE) + +precision <- full_results$TP / (full_results$TP + full_results$FP) +recall <- full_results$TP / (full_results$TP + full_results$FN) +f1 <- 2 * (recall * precision)/(recall + precision) +accuracy <- (full_results$TP + full_results$TN)/(full_results$TN + full_results$FN + full_results$TP + full_results$FP) diff --git a/scripts/single_disturbance_accuracy_assesment.R b/scripts/single_disturbance_accuracy_assesment.R new file mode 100644 index 0000000..2993758 --- /dev/null +++ b/scripts/single_disturbance_accuracy_assesment.R @@ -0,0 +1,101 @@ +library(terra) +library(dplyr) + +#this accuracy assessment process is designed to match a disturbance raster to polygon reference data, replace reference data with your own polygon data for best results +#required data: LT output, target event polygons, polygons of study area bounds (tracts) + +################################## set beginning and end of study window ################################## +#our reference data has different time ranges so we need to handle them separately for whole script, if you have just one area could simplify +uhw_study_begin <- 2010 +uhw_study_end <- 2019 +hwf_study_begin <- 1990 +hwf_study_end <- 2019 + +################################## load reference data ################################## +harvests <- vect("../GIS_Files/combined_harvest_records.gpkg") #%>% project(labrador.client::get_cafri_crs()) #match reference data to disturbance raster crs +#remove harvests that start or end after the end of lt output +harvests <- harvests[harvests$start_date <= uhw_study_end & harvests$end_date < uhw_study_end] +harvests$start_date <- as.numeric(harvests$start_date) +harvests$end_date <- as.numeric(harvests$end_date) + +#out reference data has different ranges of harvest dates, have to be handled separately +HWF_harvests <- harvests[harvests$Property == "HWF"] +UHW_harvests <- harvests[harvests$Property == "UHW"] + +#tract data is outer bounds of study area, for us here it is the property boundaries for our harvest record data +tracts <- vect("../GIS_Files/combined_tract_boundaries.gpkg") +tracts <- project(tracts, "EPSG:26918") +uhw_tracts <- tracts[tracts$Ownership == "UHW" ] +hwf_tracts <- tracts[tracts$Ownership == "HWF" ] + +################################## load and adjust disturbance raster ################################## +disturbances <- rast("rasters/tuning_combos/lt_gee_nbr_greatest_tuning_recoverythreshold_75.tif") + +#remove disturbances that occur after end of reference data, +1 is becuase we use +- 1 year buffer window for accuracy assesment below +disturbances[disturbances > (uhw_study_end +1)] <- 0 +#clip/mask rasters to extent of harvest polygons +uhw_disturbances <- crop(disturbances, uhw_tracts) +uhw_disturbances <- mask(uhw_disturbances, uhw_tracts) +hwf_disturbances <- crop(disturbances, hwf_tracts) +hwf_disturbances <- mask(hwf_disturbances, hwf_tracts) +#adjust start dates of disturbance rasters to match reference data +uhw_disturbances[uhw_disturbances < uhw_study_begin] <- 0 +hwf_disturbances[hwf_disturbances < hwf_study_begin] <- 0 + +#create harvest start and end date rasters +uhw_start_date <- rasterize(UHW_harvests, uhw_disturbances, field = 'start_date', background = NA) +uhw_end_date <- rasterize(UHW_harvests, uhw_disturbances, field = 'end_date', background = NA) + +hwf_start_date <- rasterize(HWF_harvests, hwf_disturbances, field = 'start_date', background = NA) +hwf_end_date <- rasterize(HWF_harvests, hwf_disturbances, field = 'end_date', background = NA) + +#set no data values to zero +uhw_disturbances[is.na(uhw_disturbances)] <- 0 +uhw_start_date[is.na(uhw_start_date)] <- 0 +uhw_end_date[is.na(uhw_end_date)] <- 0 + +hwf_disturbances[is.na(hwf_disturbances)] <- 0 +hwf_start_date[is.na(hwf_start_date)] <- 0 +hwf_end_date[is.na(hwf_end_date)] <- 0 + +################################## accuracy assesments ################################## + +#UHW accuracy assessment calculations, note +/- 1 year buffer window +#create blank raster with same extent to map accuracy assesment values onto +uhw_accuracy_assessment <- uhw_disturbances +uhw_accuracy_assessment[!is.na(uhw_accuracy_assessment)] <- NA +#accuracy assessment +uhw_accuracy_assessment[uhw_disturbances == 0 & uhw_start_date == 0] <- 1 #no start date, no harvest, TN +uhw_accuracy_assessment[uhw_disturbances == 0 & uhw_start_date > 1] <- 2 #FN +uhw_accuracy_assessment[uhw_disturbances != 0 & (uhw_disturbances >= (uhw_start_date - 1)) & (uhw_disturbances <= (uhw_end_date + 1))] <- 3 #TP +uhw_accuracy_assessment[uhw_disturbances > 0 & (uhw_disturbances <= (uhw_start_date - 1) | uhw_disturbances >= (uhw_end_date + 1))] <- 4 #FP + +uhw_accuracy_assessment <- crop(uhw_accuracy_assessment, uhw_tracts) +uhw_accuracy_assessment <- mask(uhw_accuracy_assessment, uhw_tracts) + +#HWF accuracy assessment, note +/- 1 year buffer window +#create blank raster with same extent to map accuracy assesment values onto +hwf_accuracy_assessment <- hwf_disturbances +hwf_accuracy_assessment[!is.na(hwf_accuracy_assessment)] <- NA +#accuracy assessment +hwf_accuracy_assessment[hwf_disturbances == 0 & hwf_start_date == 0] <- 1 +hwf_accuracy_assessment[hwf_disturbances == 0 & hwf_start_date > 1] <- 2 +hwf_accuracy_assessment[hwf_disturbances != 0 & (hwf_disturbances >= (hwf_start_date - 1)) & (hwf_disturbances <= (hwf_end_date + 1))] <- 3 +hwf_accuracy_assessment[hwf_disturbances > 0 & (hwf_disturbances <= (hwf_start_date - 1) | hwf_disturbances >= (hwf_end_date + 1))] <- 4 + +hwf_accuracy_assessment <- crop(hwf_accuracy_assessment, hwf_tracts) +hwf_accuracy_assessment <- mask(hwf_accuracy_assessment, hwf_tracts) + +################################## calculate accuracy metrics ################################## +#accuracy metrics +freq_hwf <- freq(hwf_accuracy_assessment) +freq_uhw <- freq(uhw_accuracy_assessment) + +TN <- freq_hwf[1,3] + freq_uhw[1,3] +FN <- freq_hwf[2,3] + freq_uhw[2,3] +TP <- freq_hwf[3,3] + freq_uhw[3,3] +FP <- freq_hwf[4,3] + freq_uhw[4,3] +precision <- TP/(TP + FP) +recall <- TP/(TP + FN) +F1 <- 2 * (recall * precision)/(recall + precision) +accuracy <- (TP + TN)/(TN + FN + TP + FP)