Code for automatic detection using MFCC cross-correlation

warbleR_options(wav.path = "~/Dropbox/Recordings/flight_coordination_Thyroptera/converted_sound_files_90_kHz/", wl = 300, parallel = parallel::detectCores() - 4, bp = "frange", fast = F, threshold = 15, ovlp = 20)

opts_knit$set(root.dir = "..")

opts_chunk$set( fig.width = 8, fig.height = 3.5)

MFCC cross-correlation detection

# get in whole file clips
all_files <- selection_table(whole.recs = TRUE)

all_files$sound.files <- as.character(all_files$sound.files)

# get template
templ <- all_files[all_files$sound.files == "ch4MPI2020-01-19_15-03-57_0000057.wav", , drop = FALSE ]


templ$start[1] <- 96.587    
templ$end[1] <- 96.638      
# templ2$top.freq[1] <- 42.8226 
# templ2$bottom.freq[1] <- 13.3548
templ$selec <- "temp"
all_files_temp <- rbind(templ, all_files)

comp_mat <- cbind(paste(all_files_temp$sound.files[1], all_files_temp$selec[1], sep = "-"), all_files$sound.files)

# make a sequence to do 100 files at the time
sq <- c(seq(0, nrow(comp_mat), by = 100), nrow(comp_mat))

for(i in 2:length(sq))
{
# i<- 2
  xc.output <- xcorr(X = all_files_temp, output = "list",
    compare.matrix = comp_mat[(sq[i-1] + 1):sq[i], ], pb = TRUE, bp = c(12, 42), type = "mfcc", na.rm = TRUE)

  # saveRDS(xc.output, paste0("./output/detection_xcorr_", i - 1, ".RDS"))
  
  #   xc.output <- xcorr(X = all_files_temp, output = "list", 
  #   compare.matrix = comp_mat[(sq[i-1] + 1):sq[i], ], pb = TRUE, bp = c(12, 42), type = "spectrogram", na.rm = TRUE, ovlp = 50)
  # 
  # 
  # saveRDS(xc.output, paste0("./output/detection_xcorr_", i - 1, ".RDS"))
        
    # nrow(mfcc.output$scores)
    # nrow(spc.output$scores)
    # 
    # hybrid <- mfcc.output
    
    
    # score_l <- lapply(unique(hybrid$scores$dyad), function(x){
    # 
    #   Y <- hybrid$scores[hybrid$scores$dyad == x, ]
    #   Z <- spc.output$scores[spc.output$scores$dyad == x, ] 
    #   
    #   ap <- try(approx(x = seq(0, max(Y$)), 
    #       y = Z$scores, 
    #       xout = seq(from = min(dfrq$relative.time, 
    #         na.rm = TRUE), to = max(dfrq$relative.time, 
    #         na.rm = TRUE), length.out = length.out), 
    #       method = "linear"), silent = TRUE)
    #   
    #   
    # })
    # 
    # outputs <- list(mfcc.output, spc.output)

  saveRDS(xc.output, paste0("./output/detection_xcorr_", i - 1, ".RDS"))

}

Redo problematic ones

# get in whole file clips
all_files <- selection_table(whole.recs = TRUE)

all_files$sound.files <- as.character(all_files$sound.files)


xcd_l <- list.files(path = "./output/", pattern = "xcorr_", full.names = TRUE)

wavs <- list.files(path = .Options$warbleR$wav.path)

done <- unlist(lapply(xcd_l, function(i){
  
    xcd <- readRDS(i)
    
    X <- xcd$scores
    
    y <- X$sound.files[!is.na(X$score)]
    
    return(as.character(unique(y)))
    
    }
))


not_done <- c(setdiff(wavs, unique(gsub("-whole.file", "", done))), "ch4MPI2020-01-19_15-03-57_0000057.wav")

# get template
templ <- all_files[all_files$sound.files == "ch4MPI2020-01-19_15-03-57_0000057.wav", , drop = FALSE]

templ$start[1] <- 96.587    
templ$end[1] <- 96.638      
# templ2$top.freq[1] <- 42.8226 
# templ2$bottom.freq[1] <- 13.3548
templ$selec <- 'templ'


while(length(not_done) > 0) {
print(length(not_done))
miss <- all_files[all_files$sound.files %in% not_done, ]

miss_temp <- rbind(templ, miss)

comp_mat <- cbind(paste(miss_temp$sound.files[1], miss_temp$selec[1], sep = "-"), miss$sound.files)

xc.output <- xcorr(X = miss_temp, output = "list", 
    compare.matrix = comp_mat, pb = TRUE, bp = c(12, 42), type = "mfcc", na.rm = TRUE)

xc.output$scores <- xc.output$scores[!is.na(xc.output$scores$score), ]


num <- length(list.files(path = "./output/", pattern = "xcorr_", full.names = TRUE))

if (nrow(xc.output$scores) > 0)
  saveRDS(xc.output, paste0("./output/detection_xcorr_", num + 1, ".RDS")) else print("nothing worked")

xcd_l <- list.files(path = "./output/", pattern = "xcorr_", full.names = TRUE)

done <- unlist(lapply(xcd_l, function(i){
  
    xcd <- readRDS(i)
    
    X <- xcd$scores
    
    y <- X$sound.files[is.na(X$score)]
    
    return(as.character(unique(y)))
    
    }
))


not_done <- c(setdiff(wavs, gsub("-whole.file", "", done)), "ch4MPI2020-01-19_15-03-57_0000057.wav")


}

Find peaks

# find peaks
xcd_l <- list.files(path = "./output/", pattern = "xcorr_", full.names = TRUE)

for(i in xcd_l){
  
    xcd <- readRDS(i)
  
  pks <- find_peaks(xc.output = xcd, pb = TRUE, cutoff = 0.30, output = "list")
  
  saveRDS(pks, paste0("./output/peaks_", gsub("./output//detection_xcorr_", "", i)))

}

Extract peaks to selection table

pks_l <- list.files(path = "./output/", pattern = "peaks_", full.names = TRUE)

pks <- lapply(pks_l, function(x){

  pks <- readRDS(x)
  pks$selection.table$bottom.freq <-  13.3548
pks$selection.table$top.freq <-  42.8226


return(pks$selection.table)
})

peaks <- do.call(rbind, pks)

write.csv(peaks, "./data/processed/peaks_xcorr_mfcc.csv", row.names = FALSE)

Select subset for training and plot spectrograms specreator

peaks <- read.csv("./data/processed/peaks_xcorr_mfcc.csv", stringsAsFactors = FALSE)


set.seed(102)
smp <- sample(1:nrow(peaks), 3000)
smpl_pks <- peaks[smp, ]

# spectrograms(smpl_pks, sxrow = 2, flim = c(10, 50), ovlp = 90, fast.spec = TRUE, res = 100, dest.path = "./img/manual_labeling_spectrograms",  xl = 1.5, pal = viridis, col = "black")

filt_ind <- filter_sels(X = smpl_pks, path = "./img/manual_labeling_spectrograms", index = TRUE)

smpl_pks$class <- "no_call"

smpl_pks$class[filt_ind] <- "call"

catalog(X = smpl_pks[smpl_pks$class == "call", ], flim = c(10, 50), nrow = 10, ncol = 10, 
        same.time.scale = T, mar = 0.01, gr = FALSE, img.suffix = "calls", 
        labels = c("sound.files", "selec"), legend = 0, rm.axes = TRUE, 
        box = F, width = 20, height = 15, fast.spec = TRUE,  pal = viridis)

catalog(X = smpl_pks[smpl_pks$class == "no_call", ], flim = c(10, 50), nrow = 10, ncol = 10, 
        same.time.scale = T, mar = 0.01, gr = FALSE, img.suffix = "no_calls", 
        labels = c("sound.files", "selec"), legend = 0, rm.axes = TRUE, 
        box = F, width = 20, height = 15, fast.spec = TRUE,  pal = viridis)

sp <- specan(smpl_pks, bp = c(10, 50), threshold = 5)

sp$class <- smpl_pks$class

anyNA(sp)

write.csv(sp, "acoustic parameters 3000 samples.csv", row.names = FALSE)

Measure spectral parameters on all detections

sp <- read.csv("acoustic parameters 3000 samples.csv")

# RF model
rfm <- ranger(class ~ ., data = sp[, !names(sp) %in%  grep("dom$|slope$|^sound.files$|^selec$", names(sp), value = TRUE)], num.trees = 100000)


rfm

saveRDS(rfm, "random_forest_model_3000_samples.RDS")

# run on new data
peaks <- read.csv("./data/processed/peaks_xcorr_mfcc.csv", stringsAsFactors = FALSE)


set.seed(102)
smp <- sample(1:nrow(peaks), 3000)
not_smpl_pks <- peaks[-smp, ]

sp_test <- specan(not_smpl_pks, bp = c(10, 50), threshold = 5)

sp_test$xc.score <- snr$score

sp_test$start <- not_smpl_pks$start
sp_test$end <- not_smpl_pks$end
sp_test$score <- not_smpl_pks$score

write.csv(sp_test, "spectral_parameters_test_data.csv", row.names = FALSE)

Predict all detections

sp_test <- read.csv("spectral_parameters_test_data.csv")

rfm <- readRDS("random_forest_model_3000_samples.RDS")

sqs <- c(seq(0, nrow(sp_test), by = 3000), nrow(sp_test))

na_vars <- names(sp_test)[sapply(sp_test, function(x) sum(is.na(x))) > 0]

# replace NAs with mean values
for(i in na_vars) sp_test[ is.na(sp_test[, i]), i] <- mean(sp_test[, i], na.rm = TRUE)

# predict new data
preds <- pblapply(2:length(sqs), cl = detectCores() - 4, function(x){

  Y <- sp_test[(sqs[x - 1] + 1):sqs[x], ]
  
  pred <- predict(rfm, data = Y)

 res <- data.frame(sp_test[(sqs[x - 1] + 1):sqs[x], c("sound.files", "selec", "start", "end", "score")], class = pred$predictions)
  
 return(res)
 })


preds_df <- do.call(rbind, preds)

preds_df$event <- substr(preds_df$sound.files, 4, 1000)

nrow(preds_df)

anyNA(preds_df)

saveRDS(preds_df, "./output/predictions.RDS")

Find duplicates and plot spectros


preds_df <- readRDS("./output/predictions.RDS")

# check predictions
table(preds_df$class)

preds_calls <- preds_df[preds_df$class == "call", ]

# remove overlaps and keep only channel 1 no duplicates
ovlp_l_nd <- lapply(unique(preds_calls$event), function(x){
  # print(x)  
  Y  <- preds_calls[preds_calls$event == x, ]
  Y$org.sound.files <- Y$sound.files
  Y$sound.files <- Y$event
  Y$selec <- 1:nrow(Y)
  
  # max overlap 1/4 of the duration of the template
  ov <- ovlp_sels(X = Y, max.ovlp = 0.051 / 4)
  ov$sound.files <- sort(unique(Y$org.sound.files))[1]
  
  ov <- ov[order(ov$ovlp.sels, - ov$score), ]
  
  if (!all(is.na(ov$ovlp.sels)))
  ov <- ov[!duplicated(ov$ovlp.sels, incomparables = NA), ]
  
  return(ov)
})

call_df <- do.call(rbind, ovlp_l_nd)

call_df$sound.files <- call_df$org.sound.files

preds_no_calls <- preds_df[preds_df$class != "call", ]

# remove overlaps no duplicates
ovlp_l_nd_nc <- lapply(unique(preds_no_calls$event), function(x){
  # print(x)  
  Y  <- preds_no_calls[preds_no_calls$event == x, ]
  Y$org.sound.files <- Y$sound.files
  Y$sound.files <- Y$event
  Y$selec <- 1:nrow(Y)
  
  ov <- ovlp_sels(X = Y)
  ov$sound.files <- sort(unique(Y$org.sound.files))[1]
  
  ov <- ov[order(ov$ovlp.sels, - ov$score), ]
  
  if (!all(is.na(ov$ovlp.sels)))
  ov <- ov[!duplicated(ov$ovlp.sels, incomparables = NA), ]
  
  return(ov)
})

no_call_df <- do.call(rbind, ovlp_l_nd_nc)


no_call_df$sound.files <- no_call_df$org.sound.files

non_dup_preds <- rbind(call_df, no_call_df)


# remove overlaps and keep only those no - call that do not overlap with calls
ovlp_c_nc <- lapply(unique(non_dup_preds$event), function(x){
  # print(x)  
  Y  <- non_dup_preds[non_dup_preds$event == x, ]
  Y$org.sound.files <- Y$sound.files
  Y$sound.files <- Y$event
  Y$selec <- 1:nrow(Y)
  
  ov <- ovlp_sels(X = Y, parallel = 1)
  ov$sound.files <- sort(unique(Y$org.sound.files))[1]
  
  ov <- ov[order(ov$ovlp.sels, - ov$score), ]

    if (!all(is.na(ov$ovlp.sels))){
    
      ovs <- lapply(c(na.omit(unique(ov$ovlp.sels))), function(i) {
       
          W <- ov[ov$ovlp.sels == i, ]
      
          if (any(W$class == "call"))
              W <- W[W$class == "call", ] else
         
           W[!duplicated(W$ovlp.sels, incomparables = NA), ]
 })
      
      ov2 <- do.call(rbind, ovs)
  
      ov.na <- ov[is.na(ov$ovlp.sels), ]
      
      ov <- rbind(ov2, ov.na)
                
    }  
  
  
  return(ov)
})

non_dup_preds <- do.call(rbind, ovlp_c_nc)

non_dup_preds <- non_dup_preds[!is.na(non_dup_preds$sound.files), ]

non_dup_preds$sound.files <- non_dup_preds$org.sound.files


table(non_dup_preds$class)


spectrograms(X = non_dup_preds[non_dup_preds$class == "no_call", ], flim = c(10, 50), img.suffix = "pred_no_calls", fast.spec = TRUE,  pal = viridis, res = 70, dest.path = "./img/pred_test_no_calls_single_spectro", mar = 0.01, inner.mar = rep(0, 4), picsize = 0.5)

spectrograms(X = non_dup_preds[non_dup_preds$class == "call", ], flim = c(10, 50), img.suffix = "pred_no_calls", fast.spec = TRUE,  pal = viridis, res = 70, dest.path = "./img/pred_test_calls_single_spectro", mar = 0.01, inner.mar = rep(0, 4), picsize = 0.5)


## HERE SPECTROGRAMS WERE FILTERED MANUALLY ###

filt_ind_call <- filter_sels(X = non_dup_preds, path = "./img/pred_test_calls_single_spectro", index = TRUE)

filt_ind_no_call <- filter_sels(X = non_dup_preds, path = "./img/pred_test_calls_single_spectro", index = TRUE, missing = TRUE)


length(filt_ind_no_call) + length(filt_ind_call) == nrow(non_dup_preds)



non_dup_preds$class_fixed <- "no_call"

non_dup_preds$class_fixed[filt_ind_call] <- "call"

# overall performance
sum(non_dup_preds$class == non_dup_preds$class_fixed) / nrow(non_dup_preds)

# sensitivity (true positive rate)
sum(non_dup_preds$class[non_dup_preds$class == "call"] == non_dup_preds$class_fixed[non_dup_preds$class == "call"]) / sum(non_dup_preds$class == "call")

# specificity (true negative rate)
sum(non_dup_preds$class[non_dup_preds$class == "no_call"] == non_dup_preds$class_fixed[non_dup_preds$class == "no_call"]) / sum(non_dup_preds$class == "no_call")

write.csv(non_dup_preds, "./data/processed/non_duplicated_detections.csv", row.names = FALSE)



sp <- read.csv("acoustic parameters 3000 samples.csv")

names(non_dup_preds)
names(sp)

peaks <- read.csv("./data/processed/peaks_xcorr_mfcc.csv", stringsAsFactors = FALSE)


set.seed(102)
smp <- sample(1:nrow(peaks), 3000)
smpl_pks <- peaks[smp, ]

smpl_pks$class <- smpl_pks$class_fixed <- sp$class


clms <- intersect(names(smpl_pks), names(non_dup_preds))

all_detects <- rbind(smpl_pks[, clms], non_dup_preds[, clms])
all_detects$bottom.freq <- smpl_pks$bottom.freq[1]
all_detects$top.freq <- smpl_pks$top.freq[1]

all_detects$selec <- 1:nrow(all_detects)

all_detects <- sort_colms(all_detects)

cs <- check_sels(all_detects)


# sensitivity (true positive rate)
sum(all_detects$class[all_detects$class == "call"] == all_detects$class_fixed[all_detects$class == "call"]) / sum(all_detects$class == "call")

# specificity (true negative rate)
sum(all_detects$class[all_detects$class == "no_call"] == all_detects$class_fixed[all_detects$class == "no_call"]) / sum(all_detects$class == "no_call")


# catalogs
catalog(X = all_detects[all_detects$class_fixed == "call", ], flim = c(10, 50), nrow = 10, ncol = 14, 
        same.time.scale = T, mar = 0.01, gr = FALSE, img.suffix = "calls", 
        labels = c("sound.files", "selec"), legend = 0, rm.axes = TRUE, 
        box = F, width = 24 / 2, height = 15 / 2, fast.spec = TRUE,  pal = viridis, res = 60, lab.mar = 0.01)

move_imgs(from = .Options$warbleR$wav.path, to = "./img/catalog_pred_test_calls")

catalog2pdf(path = "./img/catalog_pred_test_calls") 

catalog(X = all_detects[all_detects$class_fixed == "no_call", ], flim = c(10, 50), nrow = 10, ncol = 14, 
        same.time.scale = T, mar = 0.01, gr = FALSE, img.suffix = "no_calls", 
           labels = c("sound.files", "selec"), legend = 0, rm.axes = TRUE, 
        box = F, width = 24 / 2, height = 15 / 2, fast.spec = TRUE,  pal = viridis, res = 60, lab.mar = 0.01)


move_imgs(from = .Options$warbleR$wav.path, to = "./img/catalog_pred_test_no_calls")

catalog2pdf(path = "./img/catalog_pred_test_no_calls") 

table(all_detects$class_fixed)

# save data
write.csv(all_detects, "./data/processed/all_detections_no_duplicates.csv", row.names = FALSE)


est_all <- selection_table(all_detects, mar = 0.01, extended = TRUE, confirm.extended = FALSE)


attributes(est_all)$description <- "Spix's disc-winged bat inquiry calls detected with MFCC cross-correlation and random forest. Data contains both signals and noise ('class_fixed' column). Wrong detections were manually fixed"

saveRDS(est_all, "fixed_detections_inquiry_calls.RDS")

saveRDS(est_all[est_all$class_fixed == "call", ], "CALLS_fixed_detections_inquiry_calls.RDS")

saveRDS(est_all[est_all$class_fixed == "no_call", ], "NOISE_fixed_detections_inquiry_calls.RDS")

Plot full spectrograms

# read all detections
all_d <- read.csv("./data/processed/all_detections_no_duplicates.csv")

calls <- all_d[all_d$class_fixed == "call", ]


# force all to be on channel 1
calls$org.sound.files <- calls$sound.files
calls$sound.files <- gsub("ch2|ch3|ch4", "ch1", calls$sound.files)

full_spec(X = calls, flim = c(10, 50), width = 24 / 1.5, height = 15 / 1.5, fast.spec = TRUE, pal = reverse.gray.colors.1, res = 70, sxrow = 5, rows = 15)

move_imgs(from = .Options$warbleR$wav.path, to = "./img/full_spectrograms")

Adjust manual on Raven

# read all detections
all_d <- read.csv("./data/processed/all_detections_no_duplicates.csv")

cs <- check_sels(all_d)

calls <- all_d[all_d$class_fixed == "call", ]


# force all to be on channel 1
calls$org.sound.files <- calls$sound.files
calls$sound.files <- gsub("ch2|ch3|ch4", "ch1", calls$sound.files)



# fix those that don't have a first channel recording
prblm_files <- c("ch1MPI2020-01-18_15-15-35_0000050.wav", "ch1MPI2020-01-24_12-11-07_0000130.wav", "ch1MPI2020-01-22_13-23-54_0000093.wav", "ch1MPI2020-01-23_09-42-32_0000099.wav")

fixed_files <- gsub("ch1", "ch2", prblm_files)
fixed_files <- gsub("ch2MPI2020-01-23_09-42-32_0000099.wav", "ch4MPI2020-01-23_09-42-32_0000099.wav", fixed_files)

for(i in 1:length(prblm_files))
  calls$sound.files <- gsub(prblm_files[i], fixed_files[i], calls$sound.files) 
    
cs <- check_sels(calls)
calls$sound.files[cs$check.res != "OK"]

exp_raven(X = calls, sound.file.path = .Options$warbleR$wav.path, file.name = "./data/processed/raven_selection_table_calls.txt")


############ check false negatives in raven here ####################


fixed_calls <- imp_raven(path = "./data/processed", files = "raven_selection_table_calls.txt", all.data = TRUE, warbler.format = TRUE)


str(fixed_calls)

fixed_calls$detection <- ifelse(fixed_calls$class == "", "manual", "automatic")  

fixed_calls$class <- "call"

fixed_calls$org.sound.files[fixed_calls$org.sound.files == ""] <- fixed_calls$sound.files[fixed_calls$org.sound.files == ""]


write.csv(fixed_calls, "./data/processed/manually_fixed_call_detections.csv", row.names = FALSE)

Session information

## R version 4.0.2 (2020-06-22)
## Platform: x86_64-pc-linux-gnu (64-bit)
## Running under: Ubuntu 20.04 LTS
## 
## Matrix products: default
## BLAS:   /usr/lib/x86_64-linux-gnu/blas/libblas.so.3.9.0
## LAPACK: /usr/lib/x86_64-linux-gnu/lapack/liblapack.so.3.9.0
## 
## locale:
##  [1] LC_CTYPE=pt_BR.UTF-8       LC_NUMERIC=C              
##  [3] LC_TIME=es_CR.UTF-8        LC_COLLATE=pt_BR.UTF-8    
##  [5] LC_MONETARY=es_CR.UTF-8    LC_MESSAGES=pt_BR.UTF-8   
##  [7] LC_PAPER=es_CR.UTF-8       LC_NAME=C                 
##  [9] LC_ADDRESS=C               LC_TELEPHONE=C            
## [11] LC_MEASUREMENT=es_CR.UTF-8 LC_IDENTIFICATION=C       
## 
## attached base packages:
## [1] parallel  stats     graphics  grDevices utils     datasets  methods  
## [8] base     
## 
## other attached packages:
##  [1] cowplot_1.1.0       ggplot2_3.3.2       readxl_1.3.1       
##  [4] Sim.DiffProc_4.7    ranger_0.12.1       rfigshare_0.3.7.100
##  [7] RJSONIO_1.3-1.4     viridis_0.5.1       viridisLite_0.3.0  
## [10] Rraven_1.0.10       pbapply_1.4-3       bioacoustics_0.2.4 
## [13] warbleR_1.1.25      NatureSounds_1.0.3  knitr_1.30         
## [16] seewave_2.1.6       tuneR_1.3.3         devtools_2.3.2     
## [19] usethis_1.6.3      
## 
## loaded via a namespace (and not attached):
##  [1] httr_1.4.2        pkgload_1.1.0     moments_0.14      assertthat_0.2.1 
##  [5] cellranger_1.1.0  yaml_2.2.1        remotes_2.2.0     sessioninfo_1.1.1
##  [9] pillar_1.4.6      backports_1.1.10  lattice_0.20-41   glue_1.4.2       
## [13] digest_0.6.25     promises_1.1.1    colorspace_1.4-1  htmltools_0.5.0  
## [17] httpuv_1.5.4      Matrix_1.2-18     XML_3.99-0.5      pkgconfig_2.0.3  
## [21] purrr_0.3.4       scales_1.1.1      processx_3.4.4    later_1.1.0.1    
## [25] dtw_1.22-3        tibble_3.0.3      proxy_0.4-24      generics_0.0.2   
## [29] ellipsis_0.3.1    withr_2.3.0       cli_2.0.2         magrittr_1.5     
## [33] crayon_1.3.4      memoise_1.1.0     evaluate_0.14     ps_1.3.4         
## [37] fs_1.5.0          fansi_0.4.1       MASS_7.3-51.6     pkgbuild_1.1.0   
## [41] tools_4.0.2       prettyunits_1.1.1 lifecycle_0.2.0   stringr_1.4.0    
## [45] fftw_1.0-6        munsell_0.5.0     callr_3.4.4       Deriv_4.1.0      
## [49] compiler_4.0.2    signal_0.7-6      rlang_0.4.7       grid_4.0.2       
## [53] RCurl_1.98-1.2    rjson_0.2.20      bitops_1.0-6      rmarkdown_2.4    
## [57] testthat_2.3.2    gtable_0.3.0      R6_2.4.1          gridExtra_2.3    
## [61] dplyr_1.0.2       rprojroot_1.3-2   desc_1.2.0        stringi_1.5.3    
## [65] Rcpp_1.0.5        vctrs_0.3.4       tidyselect_1.1.0  xfun_0.17