Skip to content

Commit fa708b6

Browse files
committed
feat(#30488): support automatic and mixed correlations
1 parent a2ecb12 commit fa708b6

2 files changed

Lines changed: 83 additions & 18 deletions

File tree

R/stats_wrapper.R

Lines changed: 61 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ do_cor <- function(df, ..., skv = NULL, fun.aggregate=mean, fill=0){
4545
#' @param dimension A column you want to use as a dimension to calculate the correlations.
4646
#' @param value A column for the values you want to use to calculate the correlations.
4747
#' @param use Operation type for dealing with missing values. This can be one of "everything", "all.obs", "complete.obs", "na.or.complete", or "pairwise.complete.obs"
48-
#' @param method Method of calculation. This can be one of "pearson", "kendall", "spearman", or "polychoric".
48+
#' @param method Method of calculation. This can be one of "auto", "pearson", "kendall", "spearman", "polychoric", or "mixed".
4949
#' @param fun.aggregate Set an aggregate function when there are multiple entries for the key column per each category.
5050
#' @param time_unit Unit of time to aggregate key_col if key_col is Date or POSIXct. NULL doesn't aggregate.
5151
#' @return correlations between pairs of groups
@@ -175,7 +175,7 @@ do_cor.kv_ <- function(df,
175175
#' @param df data frame in tidy format
176176
#' @param ... Arguments to select columns to calculate correlation.
177177
#' @param use Operation type for dealing with missing values. This can be one of "everything", "all.obs", "complete.obs", "na.or.complete", or "pairwise.complete.obs"
178-
#' @param method Method of calculation. This can be one of "pearson", "kendall", "spearman", or "polychoric".
178+
#' @param method Method of calculation. This can be one of "auto", "pearson", "kendall", "spearman", "polychoric", or "mixed".
179179
#' @return correlations between pairs of columns
180180
#' @export
181181
do_cor.cols <- function(df, ..., use = "pairwise.complete.obs", method = "pearson",
@@ -205,10 +205,9 @@ do_cor.cols <- function(df, ..., use = "pairwise.complete.obs", method = "pearso
205205
stop("More than 1 row are required to calculate correlations.")
206206
}
207207
}
208-
mat <- dplyr::select(df, !!!select_dots) %>%
209-
# Convert logical to numeric explicitly, since implicit conversion by as.matrix does not happen if all the columns are logical.
210-
dplyr::mutate(across(where(is.logical), as.numeric)) %>%
211-
as.matrix()
208+
# Keep the source column types here. Auto and mixed correlation need to distinguish
209+
# continuous numeric variables from ordinal (factor/logical) variables.
210+
mat <- dplyr::select(df, !!!select_dots) %>% as.data.frame()
212211

213212
ret <- do_cor_internal(mat, use, method, diag, output_cols, na.rm=TRUE)
214213

@@ -253,30 +252,73 @@ do_cor.cols <- function(df, ..., use = "pairwise.complete.obs", method = "pearso
253252
}
254253

255254

255+
resolve_correlation_method <- function(mat, method) {
256+
if (!identical(method, "auto")) {
257+
return(method)
258+
}
259+
260+
is_ordinal <- vapply(mat, function(x) is.factor(x) || is.logical(x), logical(1))
261+
is_continuous <- vapply(mat, is.numeric, logical(1))
262+
263+
if (all(is_continuous)) {
264+
return("pearson")
265+
}
266+
if (all(is_ordinal)) {
267+
return("polychoric")
268+
}
269+
if (all(is_continuous | is_ordinal)) {
270+
return("mixed")
271+
}
272+
273+
warning("Could not determine all variable types for automatic correlation; using Pearson.")
274+
"pearson"
275+
}
276+
277+
as_numeric_correlation_matrix <- function(df) {
278+
as.matrix(as.data.frame(lapply(df, function(x) {
279+
if (is.logical(x)) {
280+
as.numeric(x)
281+
} else if (is.factor(x)) {
282+
as.numeric(x)
283+
} else {
284+
x
285+
}
286+
})))
287+
}
288+
289+
as_hetcor_data_frame <- function(df, force_ordinal = FALSE) {
290+
ret <- as.data.frame(lapply(df, function(x) {
291+
if (force_ordinal || is.factor(x) || is.logical(x)) {
292+
ordered(x)
293+
} else {
294+
x
295+
}
296+
}))
297+
colnames(ret) <- colnames(df)
298+
ret
299+
}
300+
256301
do_cor_internal <- function(mat, use, method, diag, output_cols, na.rm) {
257302
# Sort the column name.
258303
# Now that we sort the variables based on correlation result or input order, this might not be as meaningful,
259304
# but I'm hoping this might still help align the result between Mac and Windows if there are ties in the mean correlations.
260305
# We use stringr::str_sort() as opposed to base sort() so that the result is consistent on Windows too.
261306
sorted_colnames <- stringr::str_sort(colnames(mat))
262-
mat <- mat[,sorted_colnames]
307+
mat <- as.data.frame(mat[, sorted_colnames, drop = FALSE])
308+
method <- resolve_correlation_method(mat, method)
263309

264310
# Create a matrix of P-values for Analytics View case.
265311
dim <- length(sorted_colnames)
266312

267-
if (identical(method, "polychoric")) {
313+
if (method %in% c("polychoric", "mixed")) {
268314
# Polychoric correlation is for ordinal variables (e.g. survey scales 1-5).
269315
# stats::cor()/cor.test() do not support it, so we use polycor::hetcor(), which
270316
# returns the full correlation matrix and the matrix of standard errors in one call.
271-
# We coerce every column to an ordered factor so that polychoric (not Pearson or
272-
# polyserial) is used for every pair, and derive a two-sided z-test P value from
273-
# rho / SE. Non-estimable pairs (constant or near-perfect columns) come back as NA,
274-
# which the UI treats as not-significant.
317+
# Polychoric coerces every column to ordered so every pair uses the ordinal model.
318+
# Mixed preserves continuous numerics and converts factor/logical columns to ordered,
319+
# allowing hetcor() to select Pearson, polyserial, or polychoric per pair.
275320
loadNamespace("polycor")
276-
ordered_df <- as.data.frame(lapply(seq_len(dim), function(k) {
277-
ordered(mat[, k])
278-
}))
279-
colnames(ordered_df) <- sorted_colnames
321+
ordered_df <- as_hetcor_data_frame(mat, force_ordinal = identical(method, "polychoric"))
280322
# hetcor only supports "complete.obs" and "pairwise.complete.obs", while the other
281323
# methods (via cor()/cor.test()) also accept "everything", "all.obs", and
282324
# "na.or.complete". Map those to pairwise (the do_cor default) so polychoric does not
@@ -290,13 +332,14 @@ do_cor_internal <- function(mat, use, method, diag, output_cols, na.rm) {
290332
diag(tvalue_mat) <- Inf # For i=j case, statistic should be Inf and P value 0.
291333
diag(pvalue_mat) <- 0
292334
} else {
293-
cor_mat <- cor(mat, use = use, method = method)
335+
numeric_mat <- as_numeric_correlation_matrix(mat)
336+
cor_mat <- cor(numeric_mat, use = use, method = method)
294337
pvalue_mat <- matrix(NA, dim, dim)
295338
tvalue_mat <- matrix(NA, dim, dim)
296339
for (i in 2:dim) {
297340
for (j in 1:(i-1)) {
298341
tryCatch({
299-
cor_test_res <- cor.test(mat[,i], mat[,j], method = method)
342+
cor_test_res <- cor.test(numeric_mat[,i], numeric_mat[,j], method = method)
300343
pvalue_mat[i, j] <- cor_test_res$p.value
301344
tvalue_mat[i, j] <- cor_test_res$statistic
302345
}, error = function(e) {

tests/testthat/test_stats_wrapper.R

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,28 @@ test_that("do_cor with polychoric method", {
143143
expect_equal(diag_row$p_value, 0) # Diagonal P value is 0.
144144
})
145145

146+
test_that("do_cor automatic method selects the specified correlation family", {
147+
ordinal <- ordered(rep(1:5, each = 20))
148+
expect_identical(resolve_correlation_method(data.frame(x = 1:100, y = 101:200), "auto"), "pearson")
149+
expect_identical(resolve_correlation_method(data.frame(x = ordinal, y = rev(ordinal)), "auto"), "polychoric")
150+
expect_identical(resolve_correlation_method(data.frame(x = 1:100, y = ordinal), "auto"), "mixed")
151+
})
152+
153+
test_that("do_cor supports automatic and mixed correlations with factor inputs", {
154+
set.seed(456)
155+
n <- 100
156+
ordinal <- ordered(cut(rnorm(n), breaks = c(-Inf, -0.84, -0.25, 0.25, 0.84, Inf)))
157+
df <- data.frame(continuous = rnorm(n), ordinal = ordinal)
158+
159+
auto <- df %>% do_cor(continuous, ordinal, method = "auto", distinct = FALSE, diag = TRUE, return_type = "model") %>% tidy_rowwise(model, type = "cor")
160+
mixed <- df %>% do_cor(continuous, ordinal, method = "mixed", distinct = FALSE, diag = TRUE, return_type = "model") %>% tidy_rowwise(model, type = "cor")
161+
162+
expect_equal(nrow(auto), 4)
163+
expect_equal(nrow(mixed), 4)
164+
expect_true(is.finite(auto$correlation[auto$pair.name.x != auto$pair.name.y][1]))
165+
expect_true(is.finite(mixed$correlation[mixed$pair.name.x != mixed$pair.name.y][1]))
166+
})
167+
146168
test_that("do_cor with polychoric method handles a constant column without error", {
147169
set.seed(123)
148170
n <- 100

0 commit comments

Comments
 (0)