R/ct_independence.R

Defines functions ct_independence

Documented in ct_independence

#' Evaluate independent detections
#'
#' Filters camera trap data to ensure temporal independence between detections,
#' removing consecutive entry of the same species at the same location within a
#' specified time window.
#'
#' @param data A `data.frame`, `tbl_df`, or `tbl` containing the event data. This should include
#'   a column with datetime values. If `NULL`, the function will use the `datetime` argument
#'   instead of the `data` argument.
#' @param species_column An optional column name specifying the species grouping.
#'   If provided, independence will be assessed separately within each species group.
#' @param site_column An optional column name specifying the site/camera grouping.
#'   If provided, independence will be assessed separately within each site group.
#' @param datetime A `character` string specifying the name of the column in `data` that contains
#'   the datetime values. This argument is required if `data` is provided.
#' @param format A `character` string defining the format used to parse the datetime values in
#'   the `datetime` column.
#' @param threshold A `numeric` value representing the time difference threshold (in seconds) to
#'   determine whether events are independent. Events are considered independent if the time
#'   difference between them is greater than or equal to this threshold. The default is 30 minutes
#'   (1800 seconds).
#'
#' @return
#' - If `data` is provided and `only` is `TRUE`, a tibble of events identified as independent.
#' - If `data` is provided and `only` is `FALSE`, a tibble of the original data with additional columns
#'   indicating the `independent` status and `deltatime` differences (in second).
#' - If `data` is not provided, a tibble of the `deltatime` values with `independent` status.
#'
#' @details
#' Following Ridout & Linkie (2009), consecutive photos
#' of the same species at the same location within 30 minutes are considered non-independent and removed.
#'
#' The approach mirrors the methodology applied by Linkie & Ridout (2011)
#' for Sumatran tiger-prey interactions study and Ahmad et al. (2024) to calculate
#' activity levels where such filtering is essential for:
#' - Avoiding autocorrelation in activity pattern data
#' - Ensuring each record represents an independent observation
#' - Creating a random sample from the underlying activity distribution
#'
#' The filtered data can then be used to estimate probability density functions of daily activity patterns,
#' assuming animals are equally detectable during their active periods.
#'
#' @references
#' Ridout, M.S., & Linkie, M. (2009). Estimating overlap of daily activity patterns
#' from camera trap data. Journal of Agricultural, Biological, and Environmental
#' Statistics, 14(3), 322-337. \doi{10.1198/jabes.2009.08038}
#'
#' Linkie, M., & Ridout, M.S. (2011). Assessing tiger-prey interactions in Sumatran
#' rainforests. Journal of Zoology, 284(3), 224-229.\doi{10.1111/j.1469-7998.2011.00801.x}
#'
#' Ahmad, F., Mori, T., Rehan, M., Bosso, L., & Kabir, M. (2024). Applying a Random
#' Encounter Model to Estimate the Asiatic Black Bear (Ursus thibetanus) Density from
#' Camera Traps in the Hindu Raj Mountains, Pakistan. Biology, 13(5), 341.
#' \doi{10.3390/biology13050341}
#'
#' @examples
#'
#' library(dplyr)
#' data(penessoulou)
#'
#' # Load example dataset
#' cam_data <- penessoulou %>%
#'   dplyr::filter(project == "Last")
#'
#' # Independence without considering species
#' indep1 <- cam_data %>%
#'   ct_independence(data = ., datetime = datetimes, format = "%Y-%m-%d %H:%M:%S")
#'
#' sprintf("Independent observations: %s", nrow(indep1))
#'
#' # Independence considering species
#' indep2 <- cam_data %>%
#'   ct_independence(data = ., datetime = datetimes,
#'                   format = "%Y-%m-%d %H:%M:%S",
#'                   species_column = "species")
#'
#' sprintf("Independent observations: %s", nrow(indep2))
#'
#' # Use a standalone vector of datetime values
#' dtime <- cam_data$datetimes
#' ct_independence(datetime = dtime, format = "%Y-%m-%d %H:%M:%S")
#'
#' @export
ct_independence <- function(data = NULL,
                            species_column,
                            site_column,
                            datetime,
                            format,
                            threshold = 30*60) {

  # Prevent all possible error
  if (!is.null(data)) {
    if (!any(class(data) %in% c("data.frame", "tbl_df", "tbl"))){
      rlang::abort("Wrong data provided")
    }

    dt_str_ <- ifelse(methods::hasArg(datetime), as.character(dplyr::ensym(datetime)), "datetime")

    if (!any(dt_str_ %in% colnames(data))) {
      rlang::abort(sprintf("%s not found in data", dt_str_))
    }
  }

  if (!hasArg(datetime)) {
    rlang::abort("'datetime' must be provided")
  }

  if (!hasArg(format)) {
    rlang::abort("'format' cannot be missed")
  }

  ## Get datetime and build new data
  if (hasArg(data)) {

    dt_str_ <- as.character(dplyr::ensym(datetime))
    data$original_datetime <- data[[dt_str_]] # original_datetime to handle warning
    data[[dt_str_]] <- strptime(data[[dplyr::ensym(datetime)]], format = format)
    data <- data %>%
      dplyr::arrange(!!dplyr::sym(dt_str_)) %>%
      dplyr::rename(datetime = !!dplyr::sym(dt_str_)) %>%
      dplyr::as_tibble()

  }else{
    original_datetime <- datetime
    data <- dplyr::tibble(original_datetime = datetime,
                          datetime = strptime(datetime, format = format)) %>%
      dplyr::arrange(datetime)
  }

  # Error for incorrect format
  if (all(is.na(data$datetime))) {
    rlang::abort(sprintf("%s is ambiguous format", format))
  }

  # warning for ambiguous datetime
  if (!all(is.na(data$datetime))) {
    if (any(is.na(data$datetime))) {
      na_date <- data$original_datetime[is.na(data$datetime)]
      is_are <- ifelse(length(na_date) >= 2, "are", "is")
      rlang::warn(sprintf("The following datetime %s ambiguous: %s", is_are, paste0(na_date, collapse = ", ")))
    }
  }


  # Apply grouping by site and species if provided
  site_column <- tryCatch(as.character(dplyr::ensym(site_column)), error = function(e)NULL)
  species_column <- tryCatch(as.character(dplyr::ensym(species_column)), error = function(e)NULL)
  grouped_by <- c(site_column, species_column)
  grouped_by <- grouped_by[grouped_by != ""]

  ##
  .indep <- function(data) {
    selected_rows <- c(1)
    last_time <- data$datetime[1]
    for (r in 2:nrow(data)) {
      diff_time <- difftime(time1 = data$datetime[r],
                            time2 = last_time, units = "secs")
      if (!is.na(diff_time) && abs(diff_time) >= threshold) {
        selected_rows <- c(selected_rows, r)
        last_time <- data$datetime[r]
      }
    }
    return(data[selected_rows, ])
  }

  if (length(grouped_by) > 0) {
    ## Confirm column presence
    missed_col_error(data = data, grouped_by)

    data <- data %>%
      dplyr::group_by(!!!rlang::syms(grouped_by)) %>%
      dplyr::arrange(datetime, .by_group = TRUE) %>%
      .indep() %>%
      dplyr::ungroup() %>%
      dplyr::select(-original_datetime)
  } else {
    data <- data %>%
      dplyr::arrange(datetime) %>%
      .indep() %>%
      dplyr::select(-original_datetime)
  }

  return(data)
}

Try the ct package in your browser

Any scripts or data that you put into this service are public.

ct documentation built on Oct. 22, 2026, 1:07 a.m.