Nothing
##################
## work horse
##################
.sumtab <- \(data, vlst, fmt = '%.1f', ...) {# useNA = c('no', 'always'),
out_num <- if (length(.num <- c(vlst$integer, vlst$numeric))) {
names(.num) <- .num
data[.num] |>
lapply(FUN = .sumstat.default, fmt = fmt, ...) |>
unlist(use.names = TRUE)
} #else NULL
out_difft <- if (length(.difft <- vlst$difftime)) {
d_difft <- data[.difft]
names(d_difft) <- paste0(.difft, ' (', vapply(data[.difft], FUN = attr, which = 'units', exact = TRUE, FUN.VALUE = ''), ')') # ?base::units.difftime
d_difft |>
lapply(FUN = .sumstat.default, fmt = fmt, ...) |>
unlist(use.names = TRUE)
} #else NULL
out_bool <- if (length(.bool <- vlst$logical)) {
d_bool <- data[.bool]
names(d_bool) <- paste0(.bool, ': n (%)')
d_bool |>
vapply(FUN = .sumstat.logical, fmt = fmt, ..., FUN.VALUE = '')
} #else NULL
out_factor <- if (length(.fact <- c(vlst$character, vlst$factor, vlst$ordered))) {
d_fact <- data[.fact]
names(d_fact) <- paste0(.fact, ': n (%)')
d_fact |>
lapply(FUN = .sumstat, fmt = fmt, ...) |>
unlist(use.names = TRUE)
} #else NULL
ret0 <- c(out_num, out_difft, out_bool, out_factor)
nm <- paste0('n=', nrow(data))
ret <- array(ret0, dim = c(length(ret0), 1L), dimnames = list(names(ret0), nm))
attr(ret, which = 'group') <- nm
attr(ret, which = 'compare') <- FALSE
class(ret) <- c('sumtab', class(ret)) |>
unique.default()
return(ret)
}
# @param robust \link[base]{logical} scalar. If `TRUE` (default), use non-parametric methods for non-normally distributed \link[base]{numeric} variables.
# @param pairwise \link[base]{integer} scalar, minimum number of groups where pairwise comparisons need to be performed. Default `3L`.
# @param compare \link[base]{logical} scalar. If `TRUE` (default), comparisons between group(s) will be made.
.sumtab_by <- \(data, data.name, vlst, f, robust = TRUE, compare = TRUE, pairwise = 3L, ...) { # SMD = FALSE,
######## parameter `f` is a 'factor' !!!
gidx <- split.default(seq_along(f), f = f) # missingness in `f` dropped
gN <- lengths(gidx, use.names = FALSE)
ret <- gidx |>
lapply(FUN = \(id) { # (id = gidx[[1L]])
.sumtab(data[id, , drop = FALSE], vlst = vlst, ...) # data.name = '',
}) |>
do.call(what = cbind)
colnames(ret) <- sprintf(fmt = '%s\nn=%d (%.1f%%)', names(gidx), gN, 1e2*gN/length(f)) # before removing NA!!!
# removing single 'group' for p-values
txt_g1 <- if (any(g1 <- (gN == 1L))) {
gidx <- gidx[!g1]
paste0('(', sum(g1), ' level(s) of\n single obs omitted)')
} # else NULL
ng <- length(gidx)
if (ng < 2L) return(ret)
if (compare) {
p_double <- c(vlst$integer, vlst$numeric, vlst$difftime) |> vapply(FUN = \(i) compare_double(demo_get(x = data[[i]], gidx = gidx), robust = robust, pairwise = pairwise, ...), FUN.VALUE = '')
p_bool <- vlst$logical |> vapply(FUN = \(i) compare_bool(demo_get(x = data[[i]], gidx = gidx), pairwise = pairwise, ...), FUN.VALUE = '')
p_factor <- c(vlst$character, vlst$factor, vlst$ordered) |> vapply(FUN = \(i) compare_factor(x = data[[i]], g = f, ...), FUN.VALUE = '')
pval <- c(p_double, p_bool, p_factor)
if (dim(ret)[1L] != length(pval)) stop('demographic table contruction wrong: pval do not match summary stats')
ret_compare <- as.matrix(pval)
colnames(ret_compare) <- paste(c('Signif', txt_g1), collapse = '\n')
} else ret_compare <- NULL
ret <- cbind(ret, ret_compare)
attr(ret, which = 'compare') <- compare
class(ret) <- c('sumtab', class(ret)) |>
unique.default()
return(ret)
}
#' @export
as_flextable.sumtab <- function(x, ...) {
x1 <- data.frame(
' ' = dimnames(x)[[1L]],
unclass(x),
row.names = NULL, check.names = FALSE, fix.empty.names = FALSE, stringsAsFactors = FALSE
)
dnm <- attr(x, which = 'data.name', exact = TRUE)
names(x1)[1L] <- dnm
ret0 <- x1 |>
flextable() |>
autofit(part = 'all') |>
border_inner(part = 'all') |>
vline(j = 1L, border = .border_hard(), part = 'all')
group <- attr(x, which = 'group', exact = TRUE)
if (!nzchar(group)) return(ret0)
nc <- ncol(x)
compare <- x |>
attr(which = 'compare', exact = TRUE)
ncolor <- if (compare) nc - 1L else nc
ret0 |>
color(j = seq_len(ncolor) + 1L, color = pal_hue()(n = ncolor), part = 'all') |>
add_header_row(values = c(dnm, group), colwidths = c(1, nc), top = TRUE) |>
color(i = 1, color = 'black', part = 'header') |>
align(i = 1L, j = NULL, align = 'center', part = 'header') |>
merge_v(part = 'header')
}
#' @export
print.sumtab <- function(x, ...) {
x |>
as_flextable.sumtab(...) |>
print() # ?flextable:::print.flextable()
}
# fastmd::label_pvalue_sym
symb <- \(p) { # vectorized
ret <- character(length = length(p))
ret[p < .05] <- '\u2605 '
return(ret)
}
pText_pairwise.htest <- \(x) {
dnm <- dimnames(pv0 <- x$p.value)
fmt <- '\u2e22%s\u2e25'
dnm1 <- sprintf(fmt = fmt, dnm[[1L]])
dnm2 <- sprintf(fmt = fmt, dnm[[2L]])
id <- lower.tri(pv0, diag = TRUE)
pv <- pv0[id]
pnm <- outer(dnm1, dnm2, FUN = paste, sep = ' vs. ')[id]
sprintf(fmt = paste0(symb(pv), '%.3f; %s'), pv, pnm)
}
demo_get <- \(x, gidx) {
# `x`: 'double', 'logical' or 'factor' responses to be compared
# `gidx`: a 'list' of group indices
xm <- is.matrix(x)
xs <- lapply(gidx, FUN = \(i) {
y <- unclass(if (xm) c(x[i, ]) else x[i])
if (all(is.na(y))) return(NULL) # remove all-NA elements
return(y)
})
return(xs[lengths(xs, use.names = FALSE) > 1L])
}
# @param pairwise \link[base]{integer} scalar, the maximum group number under which pairwise tests,
# \link[stats]{pairwise.t.test} and \link[stats]{pairwise.wilcox.test}, are preferred. Default value `3L`.
compare_double <- \(xs, CLT = TRUE, robust = TRUE, pairwise = 3L, alternative = c('two.sided', 'less', 'greater'), ...) {
ng <- length(xs)
if (ng <= 1L) return('1 arm or less')
alternative <- match.arg(alternative)
p_shapiro <- if (!robust) 1 else vapply(xs, FUN = pval_shapiro, CLT = CLT, FUN.VALUE = 0, USE.NAMES = FALSE)
if (ng == 2L) { # ?stats::t.test or ?stats::wilcox.test
# always `2L` vs. `1L` (column 1 as reference)
if (any(p_shapiro < .05)) {
test <- tryCatch(suppressWarnings(wilcox.test(x = xs[[2L]], y = xs[[1L]], exact = FALSE, conf.int = TRUE, alternative = alternative)), error = identity)
if (inherits(test, what = 'error')) return('')
est <- test$estimate
fmt <- paste0('%.3f', symb(test$p.value), '\n', 'Wilcoxon-\nMann-Whitney')
} else {
test <- t.test(x = xs[[2L]], y = xs[[1L]], alternative = alternative)
if (is.na(test$statistic)) return('') # very likely identical(x, y) in ?stats::t.test
est <- test$estimate[1L] - test$estimate[2L]
fmt <- paste0('%.3f', symb(test$p.value), '\n', 'Two-Sample', ' t')
}
#return(sprintf(fmt = fmt, est, test$conf.int[1L], test$conf.int[2L], test$p.value))
return(sprintf(fmt = fmt, test$p.value))
}
if (!is.numeric(pairwise) || length(pairwise) != 1L || anyNA(pairwise) || pairwise < 2L) stop('illegal `pairwise`')
# `is.numeric(pairwise)` not `is.integer(pairwise)` to allow Inf
x <- unlist(xs, use.names = FALSE)
g <- rep(names(xs), times = lengths(xs, use.names = FALSE))
if (any(p_shapiro < .05)) {
if (ng <= pairwise) {
suppressWarnings(tmp <- pairwise.wilcox.test(x = x, g = g, p.adjust.method = 'none', alternative = alternative))
# ?stats::pairwise.wilcox.test only provides p-value, not confidence intervals
return(paste(c(pText_pairwise.htest(tmp), 'Pairwise Wilcoxon-Mann-Whitney'), collapse = '\n'))
}
return(tryCatch(expr = {
p.value <- kruskal.test(x = x, g = g, ...)$p.value
sprintf(fmt = paste0(symb(p.value), '%.3f\nKruskal-Wallis'), p.value)
}, error = 'Kruskal-Wallis test\nnot available'))
}
if (ng <= pairwise) {
tmp <- pairwise.t.test(x = x, g = g, pool.sd = FALSE, p.adjust.method = 'none', alternative = alternative)
return(paste(c(pText_pairwise.htest(tmp), 'Pairwise Two-Sample t'), collapse = '\n'))
}
# ?stats::aov requires formula~data parameterization
return(tryCatch(expr = {
p.value <- summary(aov(x ~ g))[[1L]][1L, 'Pr(>F)']
sprintf(fmt = paste0(symb(p.value), '%.3f\nANOVA'), p.value)
}, error = 'ANOVA not available'))
}
compare_bool <- \(xs, pairwise = 3L, alternative = c('two.sided', 'less', 'greater'), ...) {
ng <- length(xs)
if (ng <= 1L) return('1 arm or less')
alternative <- match.arg(alternative)
xs0 <- lapply(xs, FUN = \(x) x[!is.na(x)])
X <- vapply(xs0, FUN = sum, FUN.VALUE = 0L, USE.NAMES = TRUE)
N <- lengths(xs0, use.names = TRUE)
fish <- tryCatch(fisher.test(cbind(X, N-X), alternative = alternative), error = function(e) {
if (grepl('consider using \'simulate.p.value=TRUE\'$', e$message)) {
fisher.test(cbind(X, N-X), alternative = alternative, simulate.p.value = TRUE)
} else stop(e$message)
})
fisher_txt <- sprintf(fmt = paste0(symb(fish$p.value), '%.3f\nFisher\'s Exact'), fish$p.value)
if (ng == 2L) {
if (any(X == 0L, X == N)) return('') # p-value means nothing
return(tryCatch(expr = {
#p.value <- binom.test(x = X, n = N, alternative = alternative)$p.value
#sprintf(fmt = paste0(symb(p.value), '%.3f\nExact Binomial'), p.value) # sometimes looks wrong..
p.value <- prop.test(x = X, n = N, alternative = alternative)$p.value
sprintf(fmt = paste0(symb(p.value), '%.3f\n\u03C7\u00B2 (chi-square)'), p.value)
}, warning = function(w) fisher_txt))
}
if (ng <= pairwise) {
tmp <- suppressWarnings(pairwise.prop.test(x = X, n = N, p.adjust.method = 'none', alternative = alternative))
return(paste(c(pText_pairwise.htest(tmp), '\u03C7\u00B2'), collapse = '\n'))
}
return(fisher_txt)
}
compare_factor <- \(x, g, ...) {
# will use ?stats::fisher.test or ?stats::chisq.test even if the factor has 2 levels (i.e. essentially binary)
if (is.matrix(x)) g <- rep(g, times = dim(x)[2L]) # as of 2022-03-08, ?base::table will not recycle shorter argument
tab <- table(x, g, useNA = 'no') # `x` can be either 'factor' or 'character'
if (anyNA(tab)) stop('should not happen')
if (any(dim(tab) == 1L)) return('') # no comparison should be given
# ?stats::fisher.test removes all-0 rows or columns
tmp <- tryCatch(fisher.test(tab), error = function(e) {
tmp <- if (grepl('simulate.p.value=TRUE', x = e$message)) {
tryCatch(fisher.test(tab, simulate.p.value = TRUE), error = identity, warning = identity)
} else tryCatch(chisq.test(tab), error = identity, warning = identity)
if (inherits(tmp, what = 'error')) return('Fisher\'s exact\nnor Chi2 test available')
if (inherits(tmp, what = 'warning')) return(suppressWarnings(chisq.test(tab)))
return(tmp)
})
if (is.character(tmp)) return(tmp)
p.value <- tmp$p.value
if (grepl('^Fisher', tmp$method)) return(sprintf(fmt = paste0(symb(p.value), '%.3f\nFisher\'s Exact'), p.value))
return(sprintf(fmt = paste0(symb(p.value), '%.3f\n\u03C7\u00B2 (chi-square)'), p.value))
}
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.