Nothing
#' ClusterCentroids Algorithm
#'
#' Under-samples the majority classes by replacing them with cluster
#' representatives found with k-means.
#'
#' @inheritParams step_cluster_centroids
#' @param df data.frame or tibble. Must have 1 factor variable and remaining
#' numeric variables.
#' @param var Character, name of variable containing factor variable.
#'
#' @return A data.frame or tibble, depending on type of `df`.
#' @export
#'
#' @template details-cluster_centroids
#'
#' @details
#' All columns used in this function must be numeric with no missing data.
#'
#' @seealso [step_cluster_centroids()] for step function of this method
#' @family Direct Implementations
#'
#' @examples
#' circle_numeric <- circle_example[, c("x", "y", "class")]
#'
#' res <- cluster_centroids(circle_numeric, var = "class")
#'
#' res <- cluster_centroids(circle_numeric, var = "class", voting = "hard")
#'
#' res <- cluster_centroids(circle_numeric, var = "class", under_ratio = 1.5)
cluster_centroids <- function(
df,
var,
under_ratio = 1,
voting = "soft"
) {
check_data_frame(df)
check_var(var, df)
check_ratio(under_ratio)
voting <- rlang::arg_match(voting, c("soft", "hard"))
predictors <- setdiff(colnames(df), var)
check_numeric(df[, predictors])
check_na(select(df, -all_of(var)))
cluster_centroids_impl(
df,
var,
ignore_vars = character(),
under_ratio = under_ratio,
voting = voting
)
}
cluster_centroids_impl <- function(
df,
var,
ignore_vars = character(),
under_ratio = 1,
voting = "soft",
call = caller_env()
) {
predictors <- setdiff(names(df), c(var, ignore_vars))
counts <- table(drop_unused_levels(df[[var]]))
# `pmax()` copies attributes from its first argument, so the target must come
# first for the class names to survive.
n_target <- pmax(floor(under_target(counts, under_ratio, call = call)), 1)
under_classes <- names(counts)[counts > n_target]
if (length(under_classes) == 0) {
return(df)
}
keep_rows <- rep(TRUE, nrow(df))
out_dfs <- list()
for (class in under_classes) {
rows <- which(df[[var]] == class)
class_data <- as.matrix(df[rows, predictors, drop = FALSE])
centers <- kmeans_centers(class_data, n_target[[class]], class, call = call)
if (voting == "hard") {
# The representative must come from the class being under-sampled, so the
# neighbor search only ever sees that class's observations.
selected <- unique(RANN::nn2(class_data, centers, k = 1)$nn.idx[, 1])
keep_rows[setdiff(rows, rows[selected])] <- FALSE
} else {
keep_rows[rows] <- FALSE
out_dfs[[class]] <- centroid_rows(centers, df, var, predictors, rows[1])
}
}
final <- rbind(df[keep_rows, ], do.call(rbind, out_dfs))
if (is.factor(df[[var]])) {
final[[var]] <- factor(final[[var]], levels = levels(df[[var]]))
}
rownames(final) <- NULL
final
}
# Turn a matrix of centroids into rows shaped like `df`. Columns that are
# neither the outcome nor a predictor have no centroid value, so they are filled
# with a typed missing value, matching how the synthesizing steps treat
# non-predictor columns.
centroid_rows <- function(centers, df, var, predictors, class_row) {
out_df <- as.data.frame(centers)
names(out_df) <- predictors
out_df[[var]] <- df[[var]][class_row]
for (col in setdiff(names(df), c(predictors, var))) {
out_df[[col]] <- df[[col]][rep(NA_integer_, nrow(out_df))]
}
out_df[names(df)]
}
kmeans_centers <- function(data, n_clusters, class, call = caller_env()) {
n_distinct_rows <- nrow(unique(data))
if (n_distinct_rows < n_clusters) {
cli::cli_abort(
c(
"Not enough distinct observations in {.val {class}} to compute \\
{n_clusters} cluster{?s}.",
i = "{n_distinct_rows} distinct observation{?s} {?was/were} found.",
i = "Try a smaller {.arg under_ratio} or remove duplicated rows."
),
call = call
)
}
kmeans_fit(data, n_clusters)$centers
}
# Hartigan-Wong reports a warning when it hits an internal iteration limit but
# still returns usable results, so those warnings are muffled rather than passed
# on to the user.
kmeans_fit <- function(data, n_clusters) {
withCallingHandlers(
stats::kmeans(data, centers = n_clusters, iter.max = 100),
warning = function(cnd) {
if (grepl("converge|Quick-TRANSfer", conditionMessage(cnd))) {
rlang::cnd_muffle(cnd)
}
}
)
}
Any scripts or data that you put into this service are public.
Add the following code to your website.
For more information on customizing the embed code, read Embedding Snippets.