R/methods.R

Defines functions c.spacc_coverage c.spacc_beta c.spacc_hill print.summary.spacc_rare summary.spacc_rare print.summary.spacc_decay summary.spacc_decay c.spacc `[.spacc` as.data.frame.spacc_alpha as.data.frame.spacc_partition as.data.frame.spacc_endemism as.data.frame.spacc_dar as.data.frame.spacc_metrics as.data.frame.spacc_func as.data.frame.spacc_phylo as.data.frame.spacc_comp as.data.frame.spacc_decay as.data.frame.spacc_wavefront as.data.frame.spacc_coverage as.data.frame.spacc_beta as.data.frame.spacc_hill as.data.frame.spacc_fit .predict_from_summary predict.spacc as.data.frame.spacc print.summary.spacc summary.spacc print.spacc is_grouped

Documented in c.spacc c.spacc_beta c.spacc_coverage c.spacc_hill predict.spacc

# Helper to detect grouped spacc objects
is_grouped <- function(x) {
  is.list(x$curves) && !is.matrix(x$curves)
}


#' @export
print.spacc <- function(x, ...) {
  if (is_grouped(x)) {
    cat(sprintf("spacc: %d groups (%s)\n", length(x$group_names), x$method))
    for (i in seq_along(x$group_names)) {
      # Get n_sites from curves matrix (groups may have different site counts)
      group_n_sites <- ncol(x$curves[[i]])
      cat(sprintf("  %s: %d sites, %d species\n",
                  x$group_names[i],
                  group_n_sites,
                  x$n_species[[i]]))
    }
  } else {
    cat(sprintf(
      "spacc: %d sites, %d species, %d seeds (%s)\n",
      x$n_sites, x$n_species, x$n_seeds, x$method
    ))
  }
  invisible(x)
}


#' @export
summary.spacc <- function(object, saturation_threshold = 0.9, ci_level = 0.95, ...) {

  if (is_grouped(object)) {
    # Summarize each group
    summaries <- lapply(seq_along(object$group_names), function(i) {
      # Create a temporary ungrouped spacc for summary
      tmp <- object
      tmp$curves <- object$curves[[i]]
      tmp$n_species <- object$n_species[[i]]
      # Get n_sites from the curves matrix (groups may have different site counts)
      tmp$n_sites <- ncol(tmp$curves)
      class(tmp) <- "spacc"
      summary(tmp, saturation_threshold = saturation_threshold, ci_level = ci_level, ...)
    })
    names(summaries) <- object$group_names
    return(summaries)
  }

  curves <- object$curves
  n_sites <- object$n_sites

  alpha <- (1 - ci_level) / 2

  # Compute statistics per site position
  mean_curve <- colMeans(curves)
  lower <- apply(curves, 2, stats::quantile, probs = alpha)
  upper <- apply(curves, 2, stats::quantile, probs = 1 - alpha)
  sd_curve <- apply(curves, 2, stats::sd)

  # Saturation point: first site where mean reaches threshold * max
  max_species <- mean_curve[n_sites]
  threshold_value <- saturation_threshold * max_species
  saturation_point <- which(mean_curve >= threshold_value)[1]

  # CV at midpoint
  midpoint <- ceiling(n_sites / 2)
  cv_midpoint <- sd_curve[midpoint] / mean_curve[midpoint] * 100

  structure(
    list(
      sites = seq_len(n_sites),
      mean = mean_curve,
      lower = lower,
      upper = upper,
      sd = sd_curve,
      saturation_point = saturation_point,
      saturation_threshold = saturation_threshold,
      cv_midpoint = cv_midpoint,
      ci_level = ci_level,
      n_seeds = object$n_seeds,
      n_species = object$n_species,
      method = object$method
    ),
    class = "summary.spacc"
  )
}


#' @export
print.summary.spacc <- function(x, ...) {
  cat("Spatial Species Accumulation Curve\n")
  cat(strrep("-", 36), "\n")
  cat("Method:          ", x$method, "\n")
  cat("Seeds:           ", x$n_seeds, "\n")
  cat("Sites:           ", length(x$sites), "\n")
  cat("Total species:   ", x$n_species, "\n")
  cat(sprintf("Final species:   %.1f (%.0f%% CI: %.1f - %.1f)\n",
              x$mean[length(x$mean)],
              x$ci_level * 100,
              x$lower[length(x$lower)],
              x$upper[length(x$upper)]))
  cat(sprintf("Saturation (%d%%): %d sites\n",
              round(x$saturation_threshold * 100),
              x$saturation_point))
  cat(sprintf("CV at midpoint:  %.1f%%\n", x$cv_midpoint))
  invisible(x)
}


#' @export
as.data.frame.spacc <- function(x, row.names = NULL, optional = FALSE, ...) {
  if (is_grouped(x)) {
    summaries <- summary(x)
    df_list <- lapply(names(summaries), function(g) {
      s <- summaries[[g]]
      data.frame(
        sites = s$sites,
        mean = s$mean,
        lower = s$lower,
        upper = s$upper,
        sd = s$sd,
        group = g
      )
    })
    return(do.call(rbind, df_list))
  }
  summ <- summary(x)
  data.frame(
    sites = summ$sites,
    mean = summ$mean,
    lower = summ$lower,
    upper = summ$upper,
    sd = summ$sd
  )
}


#' Predict from Empirical Accumulation Curve
#'
#' Interpolate the mean empirical accumulation curve at arbitrary site counts
#' using linear interpolation. Unlike the predict method for `spacc_fit`
#' objects, this does not use a fitted model; it interpolates the observed
#' curve directly.
#'
#' @param object A `spacc` object.
#' @param n Numeric vector of site counts at which to interpolate. Defaults to
#'   25%, 50%, and 100% of total sites.
#' @param ci Logical. If `TRUE` (default), return a data frame with columns
#'   `n`, `mean`, `lower`, `upper`. If `FALSE`, return a named numeric vector.
#' @param ci_level Confidence level for the interval (default 0.95).
#' @param warn Logical. Warn when `n` values fall outside the observed range
#'   (default `TRUE`).
#' @param ... Ignored.
#'
#' @return A data frame (if `ci = TRUE`) or named numeric vector (if `ci = FALSE`).
#'   Out-of-range values return `NA`.
#'
#' @examples
#' \donttest{
#' coords <- data.frame(x = runif(50), y = runif(50))
#' species <- matrix(rbinom(50 * 30, 1, 0.3), nrow = 50)
#' sac <- spacc(species, coords, n_seeds = 10, progress = FALSE)
#' predict(sac, n = c(10, 25, 50))
#' predict(sac, n = c(10, 25), ci = FALSE)
#' }
#'
#' @export
predict.spacc <- function(object, n = NULL, ci = TRUE,
                          ci_level = 0.95, warn = TRUE, ...) {

  if (is_grouped(object)) {
    summaries <- summary(object, ci_level = ci_level)
    results <- lapply(names(summaries), function(g) {
      s <- summaries[[g]]
      n_default <- if (is.null(n)) {
        ns <- length(s$sites)
        unique(c(floor(ns / 4), floor(ns / 2), ns))
      } else {
        n
      }
      res <- .predict_from_summary(s, n_default, ci = ci, warn = warn)
      if (ci) {
        res$group <- g
      } else {
        names(res) <- paste0(g, ".", names(res))
      }
      res
    })
    if (ci) {
      return(do.call(rbind, results))
    } else {
      return(unlist(results))
    }
  }

  summ <- summary(object, ci_level = ci_level)

  if (is.null(n)) {
    ns <- length(summ$sites)
    n <- unique(c(floor(ns / 4), floor(ns / 2), ns))
  }

  .predict_from_summary(summ, n, ci = ci, warn = warn)
}


# Internal interpolation helper
.predict_from_summary <- function(summ, n, ci, warn) {
  sites <- summ$sites

  # Warn about out-of-range
  out_of_range <- n < min(sites) | n > max(sites)
  if (warn && any(out_of_range)) {
    warning(sprintf(
      "n values outside observed range [%d, %d] will return NA: %s",
      min(sites), max(sites),
      paste(n[out_of_range], collapse = ", ")
    ))
  }

  interp_mean <- stats::approx(sites, summ$mean, xout = n, rule = 1)$y

  if (!ci) {
    out <- interp_mean
    names(out) <- n
    return(out)
  }

  interp_lower <- stats::approx(sites, summ$lower, xout = n, rule = 1)$y
  interp_upper <- stats::approx(sites, summ$upper, xout = n, rule = 1)$y

  data.frame(
    n = n,
    mean = interp_mean,
    lower = interp_lower,
    upper = interp_upper
  )
}


#' @export
as.data.frame.spacc_fit <- function(x, row.names = NULL, optional = FALSE, ...) {
  # x$data has columns x (sites) and y (observed mean)
  data.frame(
    sites = x$data$x,
    observed = x$data$y,
    predicted = unname(stats::predict(x$fit, newdata = data.frame(x = x$data$x))),
    asymptote = unname(x$asymptote),
    model = x$model
  )
}


#' @export
as.data.frame.spacc_hill <- function(x, row.names = NULL, optional = FALSE, ...) {
  # curves is a list of matrices, one per q value
  df_list <- lapply(seq_along(x$q), function(i) {
    curves <- x$curves[[i]]
    data.frame(
      sites = seq_len(ncol(curves)),
      q = x$q[i],
      mean = colMeans(curves),
      lower = apply(curves, 2, stats::quantile, probs = 0.025),
      upper = apply(curves, 2, stats::quantile, probs = 0.975),
      sd = apply(curves, 2, stats::sd)
    )
  })
  do.call(rbind, df_list)
}


#' @export
as.data.frame.spacc_beta <- function(x, row.names = NULL, optional = FALSE, ...) {
  # beta_total, beta_turnover, beta_nestedness are matrices (seeds x sites)
  data.frame(
    sites = seq_len(ncol(x$beta_total)),
    beta_total = colMeans(x$beta_total),
    beta_turnover = colMeans(x$beta_turnover),
    beta_nestedness = colMeans(x$beta_nestedness),
    beta_total_sd = apply(x$beta_total, 2, stats::sd),
    beta_turnover_sd = apply(x$beta_turnover, 2, stats::sd),
    beta_nestedness_sd = apply(x$beta_nestedness, 2, stats::sd)
  )
}


#' @export
as.data.frame.spacc_coverage <- function(x, row.names = NULL, optional = FALSE, ...) {
  # richness, individuals, coverage are matrices (seeds x sites)
  data.frame(
    sites = seq_len(ncol(x$richness)),
    richness = colMeans(x$richness),
    individuals = colMeans(x$individuals),
    coverage = colMeans(x$coverage),
    richness_sd = apply(x$richness, 2, stats::sd),
    coverage_sd = apply(x$coverage, 2, stats::sd)
  )
}


#' @export
as.data.frame.spacc_wavefront <- function(x, row.names = NULL, optional = FALSE, ...) {
  # curves is a matrix (seeds x steps), radius is a vector per step
  # sites_included is a matrix (seeds x steps) - take mean per step
  curves <- x$curves
  n_steps <- ncol(curves)

  # sites_included may be a matrix or need reshaping
  if (is.matrix(x$sites_included)) {
    sites_mean <- colMeans(x$sites_included)
  } else {
    # Reshape vector to matrix if needed
    sites_mean <- colMeans(matrix(x$sites_included, nrow = nrow(curves), ncol = n_steps))
  }

  data.frame(
    step = seq_len(n_steps),
    radius = x$radius,
    sites = sites_mean,
    mean = colMeans(curves),
    lower = apply(curves, 2, stats::quantile, probs = 0.025),
    upper = apply(curves, 2, stats::quantile, probs = 0.975),
    sd = apply(curves, 2, stats::sd)
  )
}


#' @export
as.data.frame.spacc_decay <- function(x, row.names = NULL, optional = FALSE, ...) {
  curves <- x$curves
  data.frame(
    distance = x$breaks,
    mean = colMeans(curves),
    lower = apply(curves, 2, stats::quantile, probs = 0.025),
    upper = apply(curves, 2, stats::quantile, probs = 0.975),
    sd = apply(curves, 2, stats::sd)
  )
}


#' @export
as.data.frame.spacc_comp <- function(x, row.names = NULL, optional = FALSE, ...) {
  data.frame(
    comparison = paste(x$x_name, "vs", x$y_name),
    auc_x = x$auc_x,
    auc_y = x$auc_y,
    auc_diff = x$auc_diff,
    saturation_x = x$saturation_x,
    saturation_y = x$saturation_y,
    saturation_diff = x$saturation_diff,
    p_value = x$p_value,
    method = x$method
  )
}


#' @export
as.data.frame.spacc_phylo <- function(x, row.names = NULL, optional = FALSE, ...) {
  df_list <- lapply(seq_along(x$metric), function(i) {
    curves <- x$curves[[i]]
    data.frame(
      sites = seq_len(ncol(curves)),
      metric = x$metric[i],
      mean = colMeans(curves),
      lower = apply(curves, 2, stats::quantile, probs = 0.025),
      upper = apply(curves, 2, stats::quantile, probs = 0.975),
      sd = apply(curves, 2, stats::sd)
    )
  })
  do.call(rbind, df_list)
}


#' @export
as.data.frame.spacc_func <- function(x, row.names = NULL, optional = FALSE, ...) {
  df_list <- lapply(seq_along(x$metric), function(i) {
    curves <- x$curves[[i]]
    data.frame(
      sites = seq_len(ncol(curves)),
      metric = x$metric[i],
      mean = colMeans(curves),
      lower = apply(curves, 2, stats::quantile, probs = 0.025),
      upper = apply(curves, 2, stats::quantile, probs = 0.975),
      sd = apply(curves, 2, stats::sd)
    )
  })
  do.call(rbind, df_list)
}


#' @export
as.data.frame.spacc_metrics <- function(x, row.names = NULL, optional = FALSE, ...) {
  df <- x$metrics
  if (!is.null(x$coords)) {
    df$x <- x$coords$x
    df$y <- x$coords$y
  }
  df
}


#' @export
as.data.frame.spacc_dar <- function(x, row.names = NULL, optional = FALSE, ...) {
  df_list <- lapply(seq_along(x$q), function(i) {
    mat <- x$hill$curves[[i]]
    data.frame(
      sites = seq_len(x$n_sites),
      q = x$q[i],
      mean_area = colMeans(x$area),
      mean_diversity = colMeans(mat),
      lower = apply(mat, 2, stats::quantile, probs = 0.025),
      upper = apply(mat, 2, stats::quantile, probs = 0.975),
      sd = apply(mat, 2, stats::sd)
    )
  })
  do.call(rbind, df_list)
}


#' @export
as.data.frame.spacc_endemism <- function(x, row.names = NULL, optional = FALSE, ...) {
  data.frame(
    sites = seq_len(x$n_sites),
    mean_richness = colMeans(x$richness),
    mean_endemism = colMeans(x$endemism),
    endemism_lower = apply(x$endemism, 2, stats::quantile, probs = 0.025),
    endemism_upper = apply(x$endemism, 2, stats::quantile, probs = 0.975),
    endemism_sd = apply(x$endemism, 2, stats::sd)
  )
}


#' @export
as.data.frame.spacc_partition <- function(x, row.names = NULL, optional = FALSE, ...) {
  data.frame(
    q = x$q,
    alpha = x$alpha,
    beta = x$beta,
    gamma = x$gamma
  )
}


#' @export
as.data.frame.spacc_alpha <- function(x, row.names = NULL, optional = FALSE, ...) {
  df <- x$values
  df
}


#' @export
`[.spacc` <- function(x, i, ...) {
  if (is_grouped(x)) {
    x$curves <- lapply(x$curves, function(m) m[i, , drop = FALSE])
    x$n_seeds <- nrow(x$curves[[1]])
  } else {
    x$curves <- x$curves[i, , drop = FALSE]
    x$n_seeds <- nrow(x$curves)
  }
  x
}


#' Combine spacc Objects
#'
#' @param ... Named `spacc` objects to combine into a grouped `spacc`.
#' @return A grouped `spacc` object with per-group curves.
#' @export
c.spacc <- function(...) {
  objects <- list(...)

  # Get names
  nms <- names(objects)
  if (is.null(nms)) {
    nms <- paste0("group_", seq_along(objects))
  }

  # Use first object as template
  base <- objects[[1]]

  structure(
    list(
      curves = stats::setNames(lapply(objects, `[[`, "curves"), nms),
      group_names = nms,
      coords = base$coords,
      n_seeds = base$n_seeds,
      n_sites = base$n_sites,
      n_species = stats::setNames(lapply(objects, `[[`, "n_species"), nms),
      method = base$method,
      distance = base$distance,
      backend = base$backend,
      sigma = base$sigma,
      cone_width = base$cone_width,
      time = base$time,
      w_space = base$w_space,
      w_time = base$w_time,
      support = base$support,
      call = match.call()
    ),
    class = "spacc"
  )
}


#' @export
summary.spacc_decay <- function(object, ...) {
  df <- as.data.frame(object)
  structure(
    list(
      n_bins = length(object$breaks) - 1,
      n_seeds = object$n_seeds,
      distance_range = range(object$breaks),
      mean_similarity = df$mean,
      distance = df$distance
    ),
    class = "summary.spacc_decay"
  )
}


#' @export
print.summary.spacc_decay <- function(x, ...) {
  cat("Distance-Decay Summary\n")
  cat(strrep("-", 24), "\n")
  cat("Distance bins: ", x$n_bins, "\n")
  cat("Seeds:         ", x$n_seeds, "\n")
  cat(sprintf("Distance range: %.2f - %.2f\n", x$distance_range[1], x$distance_range[2]))
  cat(sprintf("Similarity range: %.3f - %.3f\n", min(x$mean_similarity), max(x$mean_similarity)))
  invisible(x)
}


#' @export
summary.spacc_rare <- function(object, ...) {
  structure(
    list(
      n_individuals = object$n,
      expected_richness = object$expected,
      se = object$sd,
      n_boot = object$n_boot
    ),
    class = "summary.spacc_rare"
  )
}


#' @export
print.summary.spacc_rare <- function(x, ...) {
  cat("Rarefaction Summary\n")
  cat(strrep("-", 20), "\n")
  n <- length(x$n_individuals)
  cat("Sample sizes: ", n, "\n")
  if (n > 0) {
    cat(sprintf("Range: %.0f - %.0f individuals\n", min(x$n_individuals), max(x$n_individuals)))
    cat(sprintf("Expected richness: %.1f - %.1f species\n",
                min(x$expected_richness), max(x$expected_richness)))
  }
  invisible(x)
}


#' Combine spacc_hill Objects
#'
#' Combine multiple `spacc_hill` objects by stacking their curve matrices.
#' All objects must have the same number of sites.
#'
#' @param ... `spacc_hill` objects to combine.
#' @return A combined `spacc_hill` object with more seeds.
#' @export
c.spacc_hill <- function(...) {
  objects <- list(...)
  base <- objects[[1]]

  # Validate compatible objects
  for (i in seq_along(objects)[-1]) {
    if (objects[[i]]$n_sites != base$n_sites) {
      stop("All objects must have the same number of sites")
    }
  }

  # Stack curves (list of matrices, one per q)
  combined_curves <- lapply(seq_along(base$curves), function(q_idx) {
    do.call(rbind, lapply(objects, function(obj) obj$curves[[q_idx]]))
  })
  names(combined_curves) <- names(base$curves)

  total_seeds <- sum(vapply(objects, function(o) o$n_seeds, numeric(1)))

  base$curves <- combined_curves
  base$n_seeds <- total_seeds
  base
}


#' Combine spacc_beta Objects
#'
#' Combine multiple `spacc_beta` objects by stacking their curve matrices.
#' All objects must have the same number of sites and index.
#'
#' @param ... `spacc_beta` objects to combine.
#' @return A combined `spacc_beta` object with more seeds.
#' @export
c.spacc_beta <- function(...) {
  objects <- list(...)
  base <- objects[[1]]

  for (i in seq_along(objects)[-1]) {
    if (objects[[i]]$n_sites != base$n_sites) {
      stop("All objects must have the same number of sites")
    }
  }

  base$beta_total <- do.call(rbind, lapply(objects, `[[`, "beta_total"))
  base$beta_turnover <- do.call(rbind, lapply(objects, `[[`, "beta_turnover"))
  base$beta_nestedness <- do.call(rbind, lapply(objects, `[[`, "beta_nestedness"))
  base$distance <- do.call(rbind, lapply(objects, `[[`, "distance"))
  base$n_seeds <- sum(vapply(objects, function(o) o$n_seeds, numeric(1)))

  base
}


#' Combine spacc_coverage Objects
#'
#' Combine multiple `spacc_coverage` objects by stacking their curve matrices.
#' All objects must have the same number of sites.
#'
#' @param ... `spacc_coverage` objects to combine.
#' @return A combined `spacc_coverage` object with more seeds.
#' @export
c.spacc_coverage <- function(...) {
  objects <- list(...)
  base <- objects[[1]]

  for (i in seq_along(objects)[-1]) {
    if (objects[[i]]$n_sites != base$n_sites) {
      stop("All objects must have the same number of sites")
    }
  }

  base$richness <- do.call(rbind, lapply(objects, `[[`, "richness"))
  base$individuals <- do.call(rbind, lapply(objects, `[[`, "individuals"))
  base$coverage <- do.call(rbind, lapply(objects, `[[`, "coverage"))
  base$n_seeds <- sum(vapply(objects, function(o) o$n_seeds, numeric(1)))

  base
}

Try the spacc package in your browser

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

spacc documentation built on June 20, 2026, 5:07 p.m.