Nothing
globalVariables(c(
'_tablet_level', 'n','N', '.data','_tablet_value',
'_tablet_N','_tablet_n', '_tablet_x',
'_tablet_devalued_N','_tablet_devalued_n',
'_tablet_widget', '_tablet_name', '_tablet_groupless',
'_tablet_stat',
'_tablet_groupfull','where',
'_tablet_class'
))
#' Identify Categories
#'
#' Identifies categories. Generic, with method \code{\link{categoricals.data.frame}}.
#' @param x object
#' @param ... passed
#' @export
#' @return see methods
#' @keywords internal
#' @examples
#' example(classifiers)
#' categoricals(x)
#'
categoricals <- function(x, ...)UseMethod('categoricals')
#' Identify Numerics
#'
#' Identifies numerics. Generic, with method \code{\link{numerics.data.frame}}.
#' @param x object
#' @param ... passed
#' @export
#' @return see methods
#' @keywords internal
#' @examples
#' example(classifiers)
#' numerics(x)
numerics <- function(x, ...)UseMethod('numerics')
#' Identify Observations
#'
#' Identifies observations. Generic, with method \code{\link{observations.data.frame}}.
#' @param x object
#' @param ... passed
#' @export
#' @return see methods
#' @keywords internal
#' @examples
#' example(classifiers)
#' observations(x)
observations <- function(x, ...)UseMethod('observations')
#' Identify Classifiers
#'
#' Identifies classifiers. Generic, with method \code{\link{classifiers.data.frame}}.
#' @param x object
#' @param ... passed
#' @export
#' @return see methods
#' @keywords internal
#' @examples
#' library(dplyr)
#' x <- data.frame(grp = 1:2, a = factor(1:2), b = 1:2, c = factor(1:2, levels = 2:1))
#' x <- group_by(x, grp)
#' classifiers(x)
classifiers <- function(x, ...)UseMethod('classifiers')
#' Identify Classifiers for Data Frame
#'
#' Identifies classifiers: literally, groups. Supplies tablet_n and tablet_N: groupwise
#' and ungrouped counts.
#' @param x data.frame
#' @param ... ignored
#' @importFrom dplyr groups ungroup tally add_count select group_by
#' @export
#' @keywords internal
#' @return data.frame, grouped if x was (see \code{\link[dplyr]{group_by}})
classifiers.data.frame <- function(x, ...){
for(i in c('_tablet_N','_tablet_n'))if(i %in% names(x))stop('x cannot include ', i)
grp <- groups(x)
x <- ungroup(x)
x <- add_count(x, name = '_tablet_N') #, wt = n())
suppressMessages(
x <- select(x, `_tablet_N`, everything())
)
for(col in grp){
#nm <- paste0('n_', col)
x <- group_by(x, .data[[col]], .add = TRUE)
#x <- add_tally(x, name = nm, wt = n())
}
x <- add_tally(x, name = '_tablet_n') # , wt = n())
x
}
#' Identify Categoricals for Data Frame
#'
#' Identifies categorical columns: literally, factors. Stacks factor levels and supplies value = 1.
#' @param x data.frame; names must not start with '_tablet_'
#' @param ... passed to \code{\link{classifiers}}
#' @param na.rm_fac whether to drop NA observations; passed to \code{\link[tidyr]{gather}} as na.rm
#' @param exclude_fac which factor levels to exclude; see \code{\link{factor}} (exclude)
#' @param all_levels whether to supply records for unobserved levels
#' @importFrom dplyr groups ungroup add_tally add_count select group_by mutate
#' @importFrom tidyr gather replace_na
#' @importFrom dplyr all_of across everything full_join anti_join
#' @importFrom magrittr %>% %<>%
#' @export
#' @method categoricals data.frame
#' @keywords internal
#' @return same class as x
#' @examples
#' example(classifiers)
#' categoricals(x)
#' levels(categoricals(x)$`_tablet_level`)
categoricals.data.frame <- function(
x,
...,
na.rm_fac = FALSE,
exclude_fac = NULL,
all_levels = FALSE
){
y <- x
for(i in c('_tablet_name','_tablet_level','_tablet_value'))if(i %in% names(x))stop('names x cannot include ',i)
if(any(duplicated(names(x))))stop('names cannot be duplicated')
x <- select(x, !!!groups(x),where(is.factor))
var <- setdiff(names(x), sapply(groups(x), rlang::as_string))
for(i in var){
# prepend each factor level with column name for uniqueness
levels(x[[i]]) <- paste(sep = '_tablet_', i, levels(x[[i]]))
}
lev <- unlist(lapply(select(ungroup(x), all_of(var)), levels))
# if('tablet_numeric' %in% lev)stop('levels of factors in x cannot include \'tablet_numeric\'')
x <- classifiers(x,...)
x <- mutate(x, across(where(is.factor), as.character))
x <- suppressWarnings(
# attributes not identical across groups, they will be dropped
# this is where _tablet_name is created
gather(
x,
factor_key = TRUE,
key = '_tablet_name',
value = '_tablet_level',
var,
na.rm = na.rm_fac
)
)
# possibly no var
if(length(var) == 0){
x <- slice(x, 0)
x <- mutate(
x,
# _tablet_name created here too
`_tablet_name` = factor(), # was character() before 0.4.3
`_tablet_level` = factor()
)
}
x <- mutate(x, `_tablet_level` = factor(`_tablet_level`, levels = lev, exclude = exclude_fac))
x <- mutate(x, `_tablet_value` = 1L) # bogus value
# missing observation of some category is moot if na.rm = TRUE
# if exclude is NULL, NA is a distinct category and value should be available for tally
# but if na not removed and not categorized, it should be passed to summary functions as na
# ???
# if(!na.rm){
# if(!is.null(exclude)){
# x <- mutate(x, value = ifelse(is.na(level), NA_integer_, value))
# }
# }
if(!all_levels) return(x)
if(length(var) == 0) return(x) # at 0.5.7
# if(!length(groups(x))) return(x) # strike at 0.5.7?
# what groups must be populated?
template <- function(x, ...)UseMethod('template')
template.factor <- function(x,...){
# create vector of all levels
levs <- levels(x)
x <- x[0]
for(i in seq_along(levs))x[i] <- levs[i]
x
}
groups <- x %>% select(c(!!!groups(x),`_tablet_level`)) #, `_tablet_N`, `_tablet_n`)) %>% unique
groups <- groups[0,,drop = FALSE]
groups <- lapply(groups, template.factor)
# names(groups) <- groups(x)
for(g in names(groups)){
groups[[g]] <- data.frame(groups[[g]])
names(groups[[g]]) <- g
}
while (length(groups) > 1){
groups[[1]] <- full_join(by = character(), groups[[1]], groups[[2]])
groups[[2]] <- NULL
}
groups <- groups[[1]]
groups$`_tablet_N` <- rep(max(0, unique(x$`_tablet_N`)), nrow(groups))
# groups %<>% left_join(x %>% select(c(!!!groups(x)), `_tablet_n`, `_tablet_level`))
zz <- x %>% select(c(!!!groups(x)), `_tablet_n`) %>% unique # at 0.5.7
common <- intersect(names(groups), names(zz))
groups %<>% left_join(zz, by = common)
groups$`_tablet_n` %<>% replace_na(0L)
# what factors are in play?
factors <- y %>% select(where(is.factor)) %>% names
factors %<>% setdiff(groups(y))
`_tablet_name` <- character(0)
`_tablet_level` <- character(0)
for(i in factors){
levs <- levels(y[[i]])
for(lev in levs){
`_tablet_name` %<>% append(i)
`_tablet_level` %<>% append(paste0(i, '_tablet_', lev))
}
}
stopifnot(length(`_tablet_name`) == length(`_tablet_level`))
factors <- data.frame(`_tablet_name`,`_tablet_level`, check.names = F)
factors$`_tablet_name` %<>% factor(levels = levels(x$`_tablet_name`))
factors$`_tablet_level` %<>% factor(levels = levels(x$`_tablet_level`))
suppressMessages(
# groups %<>% full_join(factors, by = character())
groups %<>% full_join(factors) # at 0.5.7
)
suppressMessages(
groups %<>% anti_join(x %>% select(-c(`_tablet_N`, `_tablet_n`))) # exclude combos already observed
)
groups$`_tablet_value` <- rep(0L, nrow(groups))
stopifnot(setequal(names(groups), names(x)))
x %<>% bind_rows(groups)
x %<>% arrange(!!!groups(x), `_tablet_name`, `_tablet_level`)
x
}
#' Identify Numerics for Data Frame
#'
#' Identifies numeric columns. Stacks values and supplies factor level 'numeric'.
#' @param x data.frame; names must not include 'name' or 'level'
#' @param ... passed to \code{\link{classifiers}}
#' @param na.rm_num whether to drop NA observations; passed to \code{\link[tidyr]{gather}} as na.rm
#' @param all_levels whether to supply records for unobserved levels
#' @importFrom dplyr groups ungroup tally add_count select group_by mutate slice
#' @importFrom tidyr gather
#' @export
#' @method numerics data.frame
#' @keywords internal
#' @return same class as x
numerics.data.frame <- function(x, ..., na.rm_num = FALSE, all_levels = FALSE){
for(i in c('_tablet_level', '_tablet_name','_tablet_value'))if(i %in% names(x))stop('names x cannot include ',i)
y <- x
x <- select(x, !!!groups(x),where(is.numeric))
var <- setdiff(names(x), sapply(groups(x),rlang::as_string))
x <- classifiers(x,...)
x <- mutate(x, `_tablet_level` = factor('numeric')) # bogus level
x <- suppressWarnings(
# attributes not identical across groups, they will be dropped
# this is also where _tablet_name is created
gather(
x,
factor_key = TRUE,
key = '_tablet_name',
value = '_tablet_value',
var,
na.rm = na.rm_num
)
)
if(!length(var)){
x <- slice(x, 0)
x$`_tablet_name`<- factor()
x$`_tablet_value` <- numeric(0)
}
if(!all_levels)return(x)
if(!length(groups(x)))return(x) # ? at 0.5.7
# what groups must be populated?
template <- function(x, ...)UseMethod('template')
template.factor <- function(x,...){
# create vector of all levels
levs <- levels(x)
x <- x[0]
for(i in seq_along(levs))x[i] <- levs[i]
x
}
groups <- x %>% select(c(!!!groups(x))) #, `_tablet_N`, `_tablet_n`)) %>% unique
groups <- groups[0,]
groups <- lapply(groups, template.factor)
names(groups) <- groups(x)
for(g in names(groups)){
groups[[g]] <- data.frame(groups[[g]])
names(groups[[g]]) <- g
}
while (length(groups) > 1){
groups[[1]] <- full_join(by = character(), groups[[1]], groups[[2]])
groups[[2]] <- NULL
}
groups <- groups[[1]]
groups$`_tablet_N` <- max(0, unique(x$`_tablet_N`))
suppressMessages(
groups %<>% left_join(x %>% select(c(!!!groups(x)), `_tablet_n`))
)
groups$`_tablet_n` %<>% replace_na(0L)
# what numerics are in play?
numerics <- y %>%
ungroup %>%
select(-where(is.factor)) %>%
names
numerics %<>% setdiff(groups(y))
`_tablet_name` <- character(0)
`_tablet_level` <- character(0)
for(i in numerics){
`_tablet_name` %<>% append(i)
`_tablet_level` %<>% append('numeric')
}
stopifnot(length(`_tablet_name`) == length(`_tablet_level`))
numerics <- data.frame(`_tablet_name`,`_tablet_level`, check.names = F)
numerics$`_tablet_name` %<>% factor(levels = levels(x$`_tablet_name`))
numerics$`_tablet_level` %<>% factor(levels = levels(x$`_tablet_level`))
suppressMessages(
groups %<>% full_join(numerics, by = character())
)
suppressMessages(
groups %<>% anti_join(x %>% select(-c(`_tablet_N`, `_tablet_n`))) # exclude combos already observed
)
# groups$`_tablet_value` <- rep(0L, nrow(groups))
groups$`_tablet_value` <- rep(NA_real_, nrow(groups))
stopifnot(setequal(names(groups), names(x)))
x %<>% bind_rows(groups)
x %<>% arrange(!!!groups(x), `_tablet_name`, `_tablet_level`)
x
}
#' Identify Observations for Data Frame
#'
#' Identifies observations. Stacks values by level, supplying defaults as necessary.
#' @param x data.frame
#' @param ... passed to categoricals(), numerics()
#' @param exclude_name whether to drop NA levels of name; passed to \code{\link[tidyr]{gather}}
#' @importFrom dplyr groups ungroup tally add_count select group_by mutate arrange bind_rows
#' @importFrom tidyr gather
#' @export
#' @keywords internal
#' @return class 'observations' arranged by groups (presented first):
#' \item{_tablet_N}{number of records}
#' \item{_tablet_n}{number of records in group}
#' \item{_tablet_name}{observation identifier}
#' \item{_tablet_level}{factor level (or special value 'numeric' for numerics)}
#' \item{_tablet_value}{numeric value (or special value 1 for factors)}
observations.data.frame <- function(x, ..., exclude_name = NULL){
x <- select(x, !!!groups(x), everything()) # groups first for consistency
nms <- names(x)
y <- categoricals(x, ...)
z <- numerics(x, ...)
x <- bind_rows(y, z)
# x <- mutate(x, name = factor(exclude = exclude_name, name, levels = intersect(nms, levels(name))))
# try to make output order match input across fac, num
# in a mutate call,
# next step operates once per group
# groups may not each have all values
# resulting level steps are combined in group order
# yielding unpredictable results
# we avoid mutate
levs <- intersect(nms, unique(as.character(x$`_tablet_name`)))
x$`_tablet_name` <- factor(x$`_tablet_name`, exclude = exclude_name, levels = levs)
# x <- mutate(x,`_tablet_name` = factor(exclude = exclude_name,`_tablet_name`,levels = intersect( nms, unique(as.character(`_tablet_name`)))))
x <- group_by(x, `_tablet_name`, .add = TRUE)
x <- group_by(x, `_tablet_level`, .add = TRUE)
x <- arrange(x, .by_group = TRUE)
class(x) <- union('observations', class(x))
x
}
# Reduce a List to its Formula Members
#
# Reduces a List to its members which are formulas with LHS.
# Captures LHS as string and returns a thus-named list.
# @importFrom rlang is_formula f_lhs as_string f_rhs
# @examples
# .named_formulas(
# list(
# 3,
# 'x',
# ~ 3,
# x ~ 3,
# y ~ NULL
# )
# )
#
.named_formulas <- function(x, ...){
stopifnot(is.list(x))
is.formula <- sapply(x, function(f)rlang::is_formula(f, scoped = TRUE, lhs = TRUE))
if(!length(is.formula)){
is.formula <- logical(0)
}
x <- x[is.formula]
LHS <- sapply(x, rlang::f_lhs)
LHS <- sapply(LHS, rlang::as_string)
names(x) <- LHS
x
}
# Identify NULL Formulas in a List
#
# Identifies by name the named members of a list of formulas
# whose RHS are NULL
# @examples
# .null_formulas(
# .named_formulas(
# list(
# 3,
# 'x',
# ~ 3,
# x ~ 3,
# y ~ NULL
# )
# )
# )
#
.null_formulas <- function(x, ...){
stopifnot(is.list(x))
x <- .named_formulas(x)
isNull <- sapply(x, function(f)rlang::is_null(rlang::f_rhs(f)))
if(!length(isNull)){
isNull <- logical(0)
}
x <- x[isNull]
names(x)
}
#' Aggregate Values
#'
#' Aggregated values. Generic, with method \code{\link{devalued.observations}}.
#' @param x object
#' @param ... passed
#' @export
#' @return see methods
#' @keywords internal
#' @examples
#' example(classifiers)
#' devalued(observations(x))
devalued <- function(x, ...)UseMethod('devalued')
#' Aggregate Values for Observations
#' Aggregates values for observations.
#' Accepts a list of formulas with result name on left and
#' aggregating expression on the right.
#' Behavior is undefined if any expression does not aggregate! (I.e., length != 1).
#' Expressions are evaluated in an environment where values are available as 'x',
#' grouped count is available as 'n' and ungouped count is available as 'N'.
#'
#'
#' @param x observations
#' @param ... passed formulas with matching LHS will replace corresponding element of fun.
#' @param fun default aggregate functions expressed as formulas
#' @param silent whether to suppress warnings from evaluations of 'fun'
#' @importFrom rlang f_lhs f_rhs eval_tidy :=
#' @importFrom dplyr mutate distinct rename
#' @export
#' @keywords internal
#' @return class 'devalued', presumably one record per group:
#' \item{_tablet_N}{number of records}
#' \item{_tablet_n}{number of records in group}
#' \item{_tablet_name}{observation identifier}
#' \item{_tablet_level}{factor level (or special value 'numeric' for numerics)}
#' \item{(other)}{additional column for each statistic in 'fun'}
devalued.observations <- function(
x,
...,
fun = list(
sum ~ sum(x, na.rm = TRUE),
pct ~ signif(digits = 3, sum / n * 100 ),
ave ~ signif(digits = 3, mean(x, na.rm = TRUE)),
std ~ signif(digits = 3, sd(x, na.rm = TRUE)),
med ~ signif(digits = 3, median(x, na.rm = TRUE)),
min ~ signif(digits = 3, min(x, na.rm = TRUE)),
max ~ signif(digits = 3, max(x, na.rm = TRUE))
),
silent = TRUE
){
nms <- names(x)
# remove from fun all x ~ NULL in list(...)
ext <- .named_formulas(list(...))
fun <- .named_formulas(fun)
nul <- .null_formulas(ext)
fun <- fun[!(names(fun) %in% nul)]
ext <- ext[!(names(ext) %in% nul)]
# form <- sapply(extra, function(f)inherits(f, 'formula'))
# if(length(form))extra <- extra[form]
# LHS <- sapply(extra, rlang::f_lhs)
# LHS <- sapply(LHS, rlang::as_string)
# guard
if('x' %in% names(x)){
x <- rename(x, `_tablet_x` = x)
}
if('N' %in% names(x)){
x <- rename(x, `_tablet_devalued_N` = N)
}
if('n' %in% names(x)){
x <- rename(x, `_tablet_devalued_n` = n)
}
# supply
x <- rename(x, x = `_tablet_value`)
x <- rename(x, N = `_tablet_N`)
x <- rename(x, n = `_tablet_n`)
# evaluate
for(lhs in names(fun)){
this <- NULL
if(lhs %in% names(ext)){
this <- ext[[lhs]]
} else {
this <- fun[[lhs]]
}
if(is.null(this))stop('formula cannot be NULL')
# lhs <- rlang::as_string(rlang::f_lhs(this))
# if(lhs %in% names(ext)) this <- ext[[match(lhs, LHS)]]
rhs <- rlang::f_rhs(this)
if(silent){
x <- suppressWarnings(mutate(x, !!lhs := rlang::eval_tidy(rhs)))
} else {
x <- mutate(x, !!lhs := rlang::eval_tidy(rhs))
}
}
# for(i in seq_along(ns)){ # cleanup
# that <- paste0('n_', i)
# x[[that]] <- NULL
# }
x <- select(x, -x)
# restore
x <- rename(x, `_tablet_N` = N)
x <- rename(x, `_tablet_n` = n)
if('_tablet_x' %in% names(x)){
x <- rename(x, x = `_tablet_x`)
}
if('_tablet_devalued_N' %in% names(x)){
x <- rename(x, N = `_tablet_devalued_N`)
}
if('_tablet_devalued_n' %in% names(x)){
x <- rename(x, n = `_tablet_devalued_n`)
}
x <- distinct(x)
class(x) <- union('devalued', class(x))
x
}
#' Calculate Widgets
#'
#' Calculates widgets. Generic, with method \code{\link{widgets.devalued}}.
#' @param x object
#' @param ... passed
#' @export
#' @return see methods
#' @keywords internal
#' @examples
#' example(classifiers)
#' widgets(devalued(observations(x)))
widgets <- function(x, ...)UseMethod('widgets')
# https://adv-r.hadley.nz/quasiquotation.html
#' Calculate Widgets for Devalued
#'
#' Calculates widgets for class 'devalued'.
#' In this context, a widget is a text fragment
#' that formats one or more aggregate statistics,
#' all of which must have been present as LHS of
#' some element of 'fun' in the call to devalued().
#' Furthermore, LHS must be unique across 'fac' and
#' 'num' for expected behavior.
#'
#' In the context of this call, "+" is redefined
#' to allow concatenation of text.
#' Evaluation proceeds left to right as usual.
#' @param x devalued
#' @param fac a list of formulas to generate widgets for factors
#' @param num a list of formulas to generate widgets for numerics
#' @param ... formulas with matching LHS replace defaults; x ~ NULL removes x
#' @importFrom rlang f_lhs f_rhs eval_tidy
#' @importFrom tidyr gather
#' @importFrom dplyr filter
#' @export
#' @keywords internal
#' @return class 'widgets', arranged by groups:
#' \item{_tablet_name}{observation identifier}
#' \item{_tablet_level}{factor level (or special value 'numeric' for numerics)}
#' \item{_tablet_N}{number of records}
#' \item{_tablet_n}{number of records in group}
#' \item{_tablet_stat}{the LHS of formulas in 'fac' and 'num'}
#' \item{_tablet_widget}{the RHS of formulas in 'fac' and 'num' (evaluated)}
widgets.devalued <- function(
x,
fac = list(
` ` ~ sum + ' (' + pct + '%' + ')'
),
num = list(
`Mean (SD)` ~ ave + ' (' + std + ')',
`Median (range)` ~ med + ' (' + min + ', ' + max + ')'
),
...
){
`+` <- function(e1, e2){
if(is.numeric(e1) && is.numeric(e2)){
res <- .Primitive("+")(e1, e2)
return(res)
}
e1 <- as.character(e1)
e2 <- as.character(e2)
res <- paste0(e1, e2)
return(res)
}
for(i in c('_tablet_stat', '_tablet_widget'))if(i %in% names(x))stop('names x cannot include ',i)
ext <- .named_formulas(list(...))
fac <- .named_formulas(fac)
num <- .named_formulas(num)
nul <- .null_formulas(ext)
fac <- fac[!(names(fac) %in% nul)]
num <- num[!(names(num) %in% nul)]
ext <- ext[!(names(ext) %in% nul)]
# form <- sapply(extra, function(f)inherits(f, 'formula'))
# if(length(form))extra <- extra[form]
# LHS <- sapply(extra, rlang::f_lhs)
# LHS <- sapply(LHS, rlang::as_string)
keep <- unlist(sapply(c(fac, num), rlang::f_lhs))
for(lhs in names(fac)){ # evaluate
this <- NULL
if(lhs %in% names(ext)){
this <- ext[[lhs]]
} else {
this <- fac[[lhs]]
}
if(is.null(this))stop('formula cannot be NULL')
# lhs <- rlang::as_string(rlang::f_lhs(this))
# if(lhs %in% LHS)this <- extra[[match(lhs, LHS)]]
rhs <- rlang::f_rhs(this)
x <- mutate(x, !!lhs := ifelse(`_tablet_level` == 'numeric', NA, rlang::eval_tidy(rhs)))
}
for(lhs in names(num)){ # evaluate
this <- NULL
if(lhs %in% names(ext)){
this <- ext[[lhs]]
} else {
this <- num[[lhs]]
}
if(is.null(this))stop('formula cannot be NULL')
# lhs <- rlang::as_string(rlang::f_lhs(this))
# if(lhs %in% LHS)this <- extra[[match(lhs, LHS)]]
rhs <- rlang::f_rhs(this)
x <- mutate(x, !!lhs := ifelse(`_tablet_level` != 'numeric', NA, rlang::eval_tidy(rhs)))
}
x <- gather(x, key = '_tablet_stat', value = '_tablet_widget', !!!keep, factor_key = TRUE)
x <- filter(x, !is.na(`_tablet_widget`))
x <- arrange(x, .by_group = TRUE)
x <- select(x,!!!groups(x), `_tablet_N`:`_tablet_level`, `_tablet_stat`, `_tablet_widget`)
x <- mutate(x, `_tablet_widget` = as.character(`_tablet_widget`)) # in case logi
class(x) <- union('widgets', class(x))
x
}
#' Calculate Without Groups
#'
#' Calculates without groups. Generic, with method \code{\link{groupless.data.frame}}.
#' It is meaningless to use this toolchain function on classes
#' 'observations', 'devalued', or 'widgets'.
#' @param x object
#' @param ... passed
#' @export
#' @return see methods
#' @keywords internal
groupless <- function(x, ...)UseMethod('groupless')
#' Calculate Widgets Without Groups
#'
#' Calculates widgets without groups.
#' Removes grouping variables and groups, then executes:
#' data.frame -> observations -> devalued -> widgets.
#' @param x data.frame
#' @param ... passed to observations(), devalued(), widgets()
#' @importFrom dplyr groups ungroup select
#' @export
#' @keywords internal
#' @return class 'groupless', with output like \code{\link{widgets.devalued}}
groupless.data.frame <- function(x,...){
# remove grouping variables and remove groups
# then execute
grp <- groups(x)
x <- ungroup(x)
#x <- select(x, setdiff(names(x), grp))
x <- select(x, setdiff(names(x), sapply(grp, rlang::as_string))) # very important when grp name contains spaces!
x <- observations(x, ...)
x <- devalued(x, ...)
x <- widgets(x, ...)
if(any(x$`_tablet_n` != x$`_tablet_N`))warning('unexpected difference between N and n')
x$`_tablet_n` <- NULL
class(x) <- union('groupless', class(x))
x
}
#' Calculate With Groups
#'
#' Calculates with groups. Generic, with method \code{\link{groupfull.data.frame}}.
#' It is meaningless to use this toolchain function on classes
#' 'observations', 'devalued', or 'widgets'.
#' @param x object
#' @param ... passed
#' @export
#' @return see methods
#' @keywords internal
groupfull <- function(x, ...)UseMethod('groupfull')
#' Calculate Widgets With Groups
#'
#' Calculates widgets with groups.
#' Executes: data.frame -> observations -> devalued -> widgets.
#' @param x data.frame
#' @param ... passed to observations(), devalued(), widgets()
#' @export
#' @keywords internal
#' @return class 'groupfull', with output like \code{\link{widgets.devalued}}
groupfull.data.frame <- function(x,...){
# execute data.frame -> observations -> devalued -> widgets
x <- observations(x, ...)
x <- devalued(x, ...)
x <- widgets(x, ...)
class(x) <- union('groupfull', class(x))
x
}
#' Calculate With and Without Groups
#'
#' Calculates with and without groups. Generic, with method \code{\link{groupwise.data.frame}}.
#' It is meaningless to use this toolchain function on classes
#' 'observations', 'devalued', 'widgets', 'groupless', or 'groupfull'.
#' @param x object
#' @param ... passed
#' @importFrom dplyr left_join
#' @export
#' @return see methods
#' @keywords internal
#' @examples
#' example(classifiers)
#' groupwise(x)
groupwise <- function(x, ...)UseMethod('groupwise')
#' Calculate Widgets With and Without Groups
#'
#' Calculates widgets with and without groups.
#' Supplies 'groupfull' and 'groupless' (prefixed) columns instead of 'widgets'.
# Column attributes 'label' and 'title' (highest priority) are substituted for column name, if present.
#' @param x data.frame
#' @param fun passed to groupfull() and groupless()
#' @param fac passed to groupfull() and groupless()
#' @param num passed to groupfull() and groupless()
#' @param ... passed to groupfull() and groupless()
#' @export
#' @keywords internal
#' @return class 'groupwise', arranged by groups:
#' \item{_tablet_name}{observation identifier}
#' \item{_tablet_level}{factor level or LHS of formulas in 'num'}
#' \item{_tablet_N}{number of records}
#' \item{_tablet_n}{number of records in group}
# \item{_tablet_stat}{the LHS of formulas in 'fac' and 'num'}
#' \item{_tablet_groupfull}{the LHS of formulas in 'fac' and 'num'}
#' \item{_tablet_groupless}{the LHS of formulas in 'fac' and 'num'}
groupwise.data.frame <- function(
x,
fun = list(
sum ~ sum(x, na.rm = TRUE),
pct ~ signif(digits = 3, sum / n * 100 ),
ave ~ signif(digits = 3, mean(x, na.rm = TRUE)),
std ~ signif(digits = 3, sd(x, na.rm = TRUE)),
med ~ signif(digits = 3, median(x, na.rm = TRUE)),
min ~ signif(digits = 3, min(x, na.rm = TRUE)),
max ~ signif(digits = 3, max(x, na.rm = TRUE))
),
fac = list(
` ` ~ sum + ' (' + pct + '%' + ')'
),
num = list(
`Mean (SD)` ~ ave + ' (' + std + ')',
`Median (range)` ~ med + ' (' + min + ', ' + max + ')'
),
...
){
# # promote labels and titles where present
# for(i in seq_len(ncol(x))){
# lab <- attr(x[[i]],'label')
# ttl <- attr(x[[i]], 'title')
# if(length(lab)) names(x)[[i]] <- lab
# if(length(ttl)) names(x)[[i]] <- ttl
# }
# execute data.frame -> observations -> devalued -> widgets
y <- groupfull(x, fun = fun, fac = fac, num = num, ...)
y <- rename(y, `_tablet_groupfull` = `_tablet_widget`)
z <- groupless(x, fun = fun, fac = fac, num = num, ...)
z <- rename(z, `_tablet_groupless` = `_tablet_widget`)
n <- nrow(y)
y <- suppressMessages(left_join(y, z))
if(nrow(y) != n)warning('unexpected change in row count')
discard <- c('groupless','groupfull','widgets','observations','devalued')
myclass <- setdiff(class(x), discard)
myclass <- union('groupwise', myclass)
class(y) <- myclass
y
}
#' Generate a Tablet
#'
#' Generates a tablet. Generic. See \code{\link{tablet.data.frame}} and \code{\link{as_kable.tablet}}.
#' @param x object
#' @param ... passed arguments
#' @export
#' @return see methods
#' @keywords internal
#' @examples
#' example(tablet.data.frame)
tablet <- function(x, ...)UseMethod('tablet')
#' Generate a Group-wise Tablette
#'
#' Generates a group-wise tablette. Calculates statistics
#' for all factors and numerics, with and without grouping
#' variables. Column names represent finest level of
#' grouping, distinguished by attribute 'nest' (the values of
#' higher groups). Column names include 'all' column
#' for same statistics without groups. Result columns
#' have corresponding attribute 'n'. 'lab' supplies a label attribute
#' for each column where the RHS succeeds: by default appending 'n' to result column names.
# Column attributes 'label' and 'title' (highest priority) are substituted for column name, if present.
#' @param x groupwise
#' @param ... formulas of the form lab ~ NULL will remove elements of 'lab'
#' @param all a column name for ungrouped statistics; can have length zero to suppress ungrouped column
#' @param lab a list of formulas to generate column labels; \\n is translated as <br> in html context; each formula in succession is applied to each column
#' @importFrom dplyr groups ungroup select
#' @importFrom tidyr spread
#' @importFrom rlang as_string
#' @export
#' @keywords internal
#' @return 'tablette', with columns for each combination of groups, and:
#' \item{_tablet_name}{observation identifier}
#' \item{_tablet_level}{factor level (or special value 'numeric' for numerics)}
# \item{_tablet_sort}{sorting column}
#' \item{_tablet_stat}{the LHS of formulas in 'fac' and 'num'}
#' \item{All (or value of 'all' argument)}{ungrouped results}
tablette.groupwise <- function(
x,
...,
all = 'All',
lab = list(
lab ~ name + '\n(N = ' + n + ')'
)
){
# convert certain columns to attributes
myclass <- class(x)
stopifnot(
(length(all) == 0 ) | # can be NULL or character(0)
(is.character(all) & length(all) == 1) # otherwise atomic char
)
N <- unique(x$`_tablet_N`)
if(!length(N))N <- 0L
x$`_tablet_N` <- NULL
grp <- groups(x)
grp <- lapply(grp, rlang::as_string)
strata <- setdiff(grp, c('_tablet_name','_tablet_level'))
strata <- rev(strata)
base <- character(0)
if(length(strata)){
base <- strata[[1]]
}
if(length(base)){
# x[[base]] <- factor(x[[base]])
# levs <- levels(x[[base]])
# quan <- x$`_tablet_n`[match(levs, x[[base]])]
# levels(x[[base]]) <- paste(levs, quan, sep = '_tablet_nlev_')
# nested elements of base may be repeated,
# with differing n,
# and therefore must not collapse to mere factor
x[[base]] <- as.character(x[[base]])
x[[base]] <- paste(x[[base]], x$`_tablet_n`, sep = '_tablet_nlev_')
}
x$`_tablet_n` <- NULL
tot <- select(
ungroup(x),
`_tablet_name`,
`_tablet_level`,
`_tablet_stat`,
`_tablet_groupless`
)
tot <- unique(tot)
if(length(all)){
names(tot)[names(tot)=='_tablet_groupless'] <- all
} else {
tot$`_tablet_groupless` <- NULL
}
x <- ungroup(x)
# distribute columns
if(length(strata)){
x$`_tablet_class` <- do.call(
paste,
c(
select(x, !!!strata),
list(sep = '_tablet_strata_')
)
)
x$`_tablet_class` <- factor(x$`_tablet_class`, levels = unique(x$`_tablet_class`))
# strsplit is used later.
# if last class is empty string, it will be discarded.
# guard empty string with one more instance of separator.
levels(x$`_tablet_class`) <- paste0(levels(x$`_tablet_class`),'_tablet_strata_')
x <- select(
x,
`_tablet_class`,
`_tablet_name`,
`_tablet_level`,
`_tablet_stat`,
`_tablet_groupfull`
)
x <- spread(x, `_tablet_class`, `_tablet_groupfull`)
} else {
x <- tot
}
# bind ungrouped stats
suppressMessages(x <- left_join(x,tot))
if(length(all))attr(x[[all]], 'n') <- N
for(i in seq_len(ncol(x))){
cl <- names(x)[[i]]
if(grepl('_tablet_strata_', cl)){
splits <- strsplit(cl, '_tablet_strata_')[[1]]
names(x)[[i]] <- splits[[1]]
attr(x[[i]], 'nest') <- splits[-1]
}
}
for(i in seq_len(ncol(x))){
cl <- names(x)[[i]]
if(grepl('_tablet_nlev_', cl)){
splits <- strsplit(cl, '_tablet_nlev_')[[1]]
names(x)[[i]] <- splits[[1]]
attr(x[[i]], 'n') <- splits[[2]]
}
}
# markup result columns
`+` <- function(e1, e2){
if(is.numeric(e1) && is.numeric(e2)){
res <- .Primitive("+")(e1, e2)
return(res)
}
e1 <- as.character(e1)
e2 <- as.character(e2)
res <- paste0(e1, e2)
return(res)
}
ext <- .named_formulas(list(...))
lab <- .named_formulas(lab)
nul <- .null_formulas(ext)
lab <- lab[!(names(lab) %in% nul)]
ext <- ext[!(names(ext) %in% nul)]
for(lhs in names(lab)){ # evaluate
this <- NULL
if(lhs %in% names(ext)){
this <- ext[[lhs]]
} else {
this <- lab[[lhs]]
}
if(is.null(this))stop('formula cannot be NULL')
# lhs <- rlang::as_string(rlang::f_lhs(this))
# if(lhs %in% LHS)this <- extra[[match(lhs, LHS)]]
rhs <- rlang::f_rhs(this)
for(j in seq_len(ncol(x))){
nm <- names(x)[[j]]
dat <- attributes(x[[j]])
dat <- c(dat, list(name = nm))
out <- try(silent = TRUE, rlang::eval_tidy(rhs, data = dat))
if(!inherits(out, 'try-error')){
attr(x[[j]], 'label') <- out
}
}
}
# restore levels
# for vignette transpose example:
# Error in .subset2(chunks, self$get_current_group()) : attempt to select less than one element in integerOneIndex
# x <- mutate(x, `_tablet_sort` = as.numeric(`_tablet_level`))
# x <- mutate(x, `_tablet_level` = as.character(`_tablet_level`))
# x <- mutate(x, `_tablet_level` = sub('.*_tablet_', '', `_tablet_level`))
#x$`_tablet_sort` <- as.numeric(x$`_tablet_level`)
x$`_tablet_level` <- as.character(x$`_tablet_level`)
x$`_tablet_level` <- sub('.*_tablet_', '', x$`_tablet_level`)
constitutive <- c(
'_tablet_name',
'_tablet_level',
# '_tablet_sort',
'_tablet_stat'
)
x <- cbind(
x[, constitutive, drop = FALSE],
x[,!(names(x) %in% constitutive), drop = FALSE]
)
# this is the only constructor for 'tablet'
x$`_tablet_name` <- as.character(x$`_tablet_name`)
x$`_tablet_level` <- as.character(x$`_tablet_level`)
x$`_tablet_stat` <- as.character(x$`_tablet_stat`)
x$`_tablet_level` <- ifelse(
x$`_tablet_level` == 'numeric',
x$`_tablet_stat`,
x$`_tablet_level`
)
x$`_tablet_stat` <- NULL
class(x) <- union('tablette', setdiff(class(x), 'groupwise'))
#x <- tablet(x, ...) # tablet.tablette
x
}
#' Create Header List.
#'
#' Creates header list. Generic, with method \code{\link{headerlist.tablette}}.
#' @param x object
#' @param ... passed
#' @export
#' @return see methods
#' @keywords internal
#' @examples
#' example(classifiers)
#' headerlist(tablette(groupwise(x)))
headerlist <- function(x,...)UseMethod('headerlist')
#' Create Header List for Tablet
#'
#' Creates header list for tablet.
#' @param x tablet
#' @param ... ignored
#' @export
#' @keywords internal
#' @return list of named integer where each element describes an additional header row with names as indicated, repeated integer times
headerlist.tablette <- function(x, ...){
numheaders <- 0
out <- list()
for(i in seq_len(ncol(x))){
h <- attr(x[[i]],'nest')
numheaders <- max(numheaders, length(h))
}
for(e in seq_len(numheaders)){ # for each header level
this <- numeric(0)
for(col in seq_len(ncol(x))){ # for each column
h <- attr(x[[col]], 'nest')
if(length(h) >= e){
head <- h[[e]] # use the right level if you have it
} else {
head <- ' ' # otherwise use one space
}
if(length(this) == 0){
this <- 1L
names(this) <- head
} else{
if(rev(names(this))[[1]] == head){
this[[length(this)]] <- this[[length(this)]] + 1
} else {
this[[length(this) + 1]] <- 1
names(this)[[length(this)]] <- head
}
}
}
out[[e]] <- this
}
return(out)
}
#' Create Index
#'
#' Creates index. Generic, with method \code{\link{index.tablette}}.
#' @param x object
#' @param ... passed
#' @export
#' @return see methods
#' @keywords internal
#' @examples
#' example(classifiers)
#' index(tablette(groupwise(x)))
index <- function(x,...)UseMethod('index')
#' Create Index for Tablet
#'
#' Creates index for tablet.
#' @param x tablet
#' @param ... ignored
#' @export
#' @keywords internal
#' @return named integer giving row groupings with names as indicated, repeated integer times
index.tablette <- function(x, ...) {
out <- numeric(0)
for (row in seq_len(nrow(x))) {
r <- as.character(x$`_tablet_name`)[[row]]
if (length(out) == 0) {
out <- 1L
names(out) <- r
} else {
if (names(out)[[length(out)]] == r) {
out[[length(out)]] <- out[[length(out)]] + 1
} else {
out[[length(out) + 1]] <- 1
names(out)[[length(out)]] <- r
}
}
}
return(out)
}
#' Coerce to Kable
#'
#' Coerces to kable output class. Generic,
#' with method \code{\link{as_kable.tablet}}.
#'
#' @param x object
#' @param ... passed arguments
#' @export
#' @return see methods
#' @keywords internal
#' @examples
#' example(classifiers)
#' as_kable(tablet(tablette(groupwise(x))))
as_kable <- function(x, ...)UseMethod('as_kable')
# https://colinfay.me/writing-r-extensions/generic-functions-and-methods.html
# https://stackoverflow.com/questions/13984470/possible-to-create-latex-multicolumns-in-xtable
#' Coerce Tablet to Kable
#'
#' Renders a tablet. Calls \code{\link[kableExtra]{kbl}} and implements
#' special features like grouped columns.
#'
#' See also \code{\link{tablet.data.frame}}.
#' Column \code{_tablet_name} must inherit 'character' and
#' by default (in a latex render context) its values
#' will eventually be processed by \code{escape_latex}.
#' Thus, if \code{_tablet_name} is of class 'latex'
#' it will be handled by method \code{\link{escape_latex.latex}}
#' (which tries not to re-escape metacharacters).
#'
#'
#' @param x \code{\link{tablet}}
#' @param ... passed to \code{\link[kableExtra]{kbl}}
#' @param booktabs passed to \code{\link[kableExtra]{kbl}}
#' @param escape passed to \code{\link[kableExtra]{kbl}}; defaults FALSE to allow header linebreaks
#' @param escape_latex a function to pre-process column names and content if 'escape' is FALSE (e.g., manual escaping, latex only); default \code{\link{escape_latex}}
#' @param escape_html a function to pre-process column names and content if 'escape' is FALSE (e.g., manual escaping, html only)
#' @param variable a column name for the variables
#' @param col.names passed to \code{\link[kableExtra]{kbl}} after any linebreaking
#' @param linebreak whether to invoke \code{\link[kableExtra]{linebreak}} for column names
#' @param align passed to \code{\link[kableExtra]{linebreak}} for column names
#' @param double_escape passed to \code{\link[kableExtra]{linebreak}} for column names
#' @param linebreaker passed to \code{\link[kableExtra]{linebreak}} for column names in latex; for html, linebreaker is replaced with <br/>
#' @param pack_rows named list passed to \code{\link[kableExtra]{pack_rows}} for finer control of variable names
#' @param secondary passed to escape_latex
#' @importFrom kableExtra kbl pack_rows add_header_above linebreak
#' @importFrom dplyr rename group_vars
#' @export
#' @return like \code{\link[kableExtra]{kbl}}
#' @examples
#' library(boot)
#' library(dplyr)
#' library(magrittr)
#' library(haven)
#' library(yamlet)
#' library(spork)
#' melanoma %>%
#' select(-time, -year) %>%
#' mutate(sex = factor(sex), ulcer = factor(ulcer)) %>%
#' group_by(status) %>%
#' tablet %>%
#' as_kable
#'
#' x <- system.file(package = 'tablet', 'shiny-examples/mesa/data/adsl.sas7bdat')
#' x %<>% read_sas %>% data.frame
#' decorations(x)
# note weight in pounds
# x %<>% mutate(weight = signif(digits = 3, weight * 2.2))
#'
#' # calculate BMI by assuming all males are 1.75 m, all females 1.63 cm
#' x %<>% mutate(height = ifelse(sex == 'F', 1.63, 1.75))
#' x %<>% mutate(bmi = signif(digits = 3, weight / (height^2)))
#' x %<>% filter(saffl == 'Y')
#' x %<>% select(trt01a, age, sex, weight, bmi)
#' x %<>% redecorate('
#' trt01a: [ Treatment, [ Placebo, TRT 10 mg, TRT 20 mg ]]
#' age: [ Age, year ]
#' sex: [ Sex, [ Female: F, Male: M ]]
#' weight: [ Body Weight, kg ]
#' bmi: [ Index_body mass, kg/m^2 ]
#' ')
#' x %<>% resolve
#' x %<>% group_by(trt01a)
#'
#' x %>% tablet %>% as_kable
#'
#' # supply default and unit-conditional latex titles
#' x %<>% modify(title = concatenate(as_latex(as_spork(c(.data$label)))))
#' x %<>% modify(
#' age, weight, bmi,
#' title = concatenate(
#' sep = '', # default ok in pdf
#' as_latex(
#' as_spork(
#' c(.data$label, ' (', .data$units, ')')
#' )
#' )
#' )
#' )
#' x %>% tablet %>% as_kable
as_kable.tablet <- function(
x,
...,
booktabs = TRUE,
escape = FALSE,
escape_latex = tablet::escape_latex,
escape_html = function(x, ...)x,
variable = ' ',
col.names = NA,
linebreak = TRUE,
align = 'c',
double_escape = FALSE,
linebreaker = '\n',
pack_rows = list(escape = escape),
secondary = FALSE
){
x <- tablette(x, ...)
# if(is.na(escape)){
# if (knitr::is_latex_output()){
# escape <- FALSE
# } else {
# escape <- FALSE # for <br>
# }
# }
stopifnot(is.logical(escape), length(escape) == 1)
# x$`_tablet_sort` <- NULL
index <- index(x)
# draws on _tablet_name, which should be character or c('latex', 'character')
nmsi <- names(index) # isolate to assign class
stopifnot(is.character(x$`_tablet_name`))
class(nmsi) <- class(x$`_tablet_name`) # class propagation
x$`_tablet_name` <- NULL # done
if(!escape){
if (knitr::is_latex_output()) {
# invokes class-specific method,
#possibly escaping or ignoring latex metacharacters
# revisit if kableExtra changes
nmsi <- escape_latex(nmsi, secondary = TRUE, primary = TRUE)
if (linebreak){
nmsi <- linebreak(
nmsi,
align = 'l',
double_escape = TRUE,
linebreaker = linebreaker
)
}
} else {
nmsi <- escape_html(nmsi)
}
}
# nmsi now as informed as possible ... assign back
names(index) <- nmsi
#x$`_tablet_level` <- as.character(x$`_tablet_level`)
# x$`_tablet_stat` <- as.character(x$`_tablet_stat`)
# x$`_tablet_level` <- ifelse(
# x$`_tablet_level` == 'numeric',
# x$`_tablet_stat`,
# x$`_tablet_level`
# )
# x$`_tablet_stat` <- NULL
# names(x)[names(x) == 'level'] <- ''
headerlist <- headerlist(x)
for(i in seq_len(ncol(x))){
lab <- attr(x[[i]], 'label')
if(length(lab)){
names(x)[[i]] <- lab
}
}
#x <- rename(x, !!variable := `_tablet_level`)
stopifnot(is.character(variable), length(variable) == 1)
names(x)[names(x) == '_tablet_level'] <- variable
# escape is false by default to allow internal discretion
# here we handle the escaping of column names
if(!escape){
if (knitr::is_latex_output()) {
# @ 0.6.10: apparently secondary should be FALSE, now default.
x[] <- lapply(x, escape_latex, secondary = secondary, ...)
these <- names(x)
if('latex' %in% attr(x,'name_class')){
class(these) <- c('latex','character')
}
these <- escape_latex(these, secondary = FALSE, ...)
names(x) <- these
} else {
x[] <- lapply(x, escape_html, ...)
names(x) <- escape_html(names(x), ...)
}
}
# names of each element in headerlist derive from the content
# of grouping variables, and are expected to have
# exactly the same escaping needs as names(x)
if(!escape){
for(i in seq_along(headerlist)){
if (knitr::is_latex_output()) {
these <- names(headerlist[[i]])
if('latex' %in% attr(x,'name_class')){
class(these) <- c('latex','character')
}
these <- escape_latex(these, secondary = FALSE, ...)
names(headerlist[[i]]) <- these
} else {
names(headerlist[[i]]) <- escape_html(names(headerlist[[i]]), ...)
}
}
}
if(is.na(col.names))col.names <- names(x)
if (linebreak){
if(knitr::is_latex_output()) {
col.names <- linebreak(
col.names,
align = align,
double_escape = double_escape,
linebreaker = linebreaker
)
} else {
col.names <- gsub(linebreaker,'<br/>', col.names)
}
}
y <- kableExtra::kbl(
x,
booktabs = booktabs,
escape = escape,
col.names = col.names,
...
)
for(i in seq_along(headerlist)){
this <- headerlist[[i]]
if(linebreak){
if(knitr::is_latex_output()){
names(this) <- linebreak(
names(this),
align = align,
double_escape = double_escape,
linebreaker = linebreaker
)
} else {
names(this) <- gsub(linebreaker, '<br/>', names(this))
}
}
# kableExtra:::pdfTable_add_header_above()
# uses str_replace(string, pattern, replacement)
# which silently deletes orphan backslashes,
# i.e. backslashes not followed by an integer
# therefore, names(this) must have all backslash doubled
# @ 0.6.7 2024-03-22
# next action only for latex?
if(knitr::is_latex_output()){
names(this) <- gsub('\\','\\\\', names(this), fixed = TRUE)
}
y <- add_header_above(y, this, escape = escape)
}
# at 0.5.7, skip this if length(index) == 0
# attempt to prevent error in mesa() when
# tabulating virtual category agegr1 in isolation:
# Error in `$<-.data.frame`(`*tmp*`, "start", value = 1) : replacement has 1 row, data has 0
#if(length(index) > 0){
y <- do.call(
kableExtra::pack_rows,
c(
list(y, index = index), # @0.4.9 removing ', escape = escape
pack_rows
)
)
# }
y
}
#' Generate a Tablet for Data Frame
#'
#' Generates a 'tablet': a summary table of
#' formatted statistics for factors (is.factor()) and
#' numerics (is.numeric()) in x, with and without grouping
#' variables (if present, see \code{\link[dplyr]{group_by}}).
#' Column names represent finest level of
#' grouping, distinguished by attribute 'nest' (the values of
#' higher other groups, if any) along with the 'all' column
#' for ungrouped statistics. Column attribute 'n' indicates
#' relevant corresponding observation count.
#' Input should not have column names beginning with '_tablet'.
#'
#' Arguments 'fun', 'fac', 'num', and 'lab' are lists
#' of two-sided formulas that are evaluated in
#' an environment where '+' expresses concatenation
#' (for character elements).
#' The values of LHS should be unique across all four lists.
#' 'fun' is a list of aggregate statistics that have access to
#' \code{N} (number of original records),
#' \code{n} (number of group members), and
#' \code{x} (the numeric observations, or 1 for each factor value).
#' Aggregate statistics generated by 'fun' are available
#' for use in 'fac' and 'num'
#' which create visualizations thereof ('widgets'). Column-specific
#' attributes are available to elements of 'lab', including
#' the special attribute \code{name} (the current column name).
#' For 'lab' only, if the RHS succeeds, it becomes the label
#' attribute of the corresponding output column. 'lab' is used
#' here principally to support annotation of *output*
#' columns; if *input* columns have attributes 'label' or 'title'
#' (highest priority) those will have been already substituted
#' for default column names at the appropriate positions in the
#' output.
#'
#' Missingness of observations (and to a lesser extent, levels of
#' grouping variables) merits special consideration.
#' Be aware that na.rm_fac and na.rm_num take their defaults
#' from na.rm. Furthermore, na.rm_fac may interact with
#' exclude_fac, which is passed to \code{\link{factor}} as \code{exclude}.
#' The goal is to support all possible ways of expressing or ignoring
#' missingness. That said, if aggregate functions are removing
#' NA, the values of arguments beginning with 'na.rm' or 'exclude'
#' may not matter.
#'
#' Column 1 of output is character.
#' Its values are typically the names of the original columns
#' that were factor or numeric but not in groups(x). If any
#' of these had a label attribute or (priority) a title attribute
#' with class 'latex', then column 1 is assigned the
#' class 'latex' as well. It makes sense therefore to be consistent
#' across input columns regarding the presence or not of a 'latex'
#' label or title. By default, \code{\link{as_kable.tablet}} dispatches
#' class-specific methods for \code{\link{escape_latex}}.
#'
#' Similarly, row 1 of output is typically character. As of version 0.6.6,
#' if any of the grouping variables inherits 'latex', then the return value
#' of tablet.data.frame() has an attribute 'name_class' with value 'latex'.
#'
#' @usage
#' \method{tablet}{data.frame}(
#' x,
#' ...,
#' na.rm = FALSE,
#' all = 'All',
#' fun = list(
#' sum ~ sum(x, na.rm = TRUE),
#' pct ~ signif(digits = 3, sum / n * 100 ),
#' ave ~ signif(digits = 3, mean(x, na.rm = TRUE)),
#' std ~ signif(digits = 3, sd(x, na.rm = TRUE)),
#' med ~ signif(digits = 3, median(x, na.rm = TRUE)),
#' min ~ signif(digits = 3, min(x, na.rm = TRUE)),
#' max ~ signif(digits = 3, max(x, na.rm = TRUE))
#' ),
#' fac = list(
#' ` ` ~ sum + ' (' + pct + '\%' + ')'
#' ),
#' num = list(
#' `Mean (SD)` ~ ave + ' (' + std + ')',
#' `Median (range)` ~ med + ' (' + min + ', ' + max + ')'
#' ),
#' lab = list(
#' lab ~ name + '\\n(N = ' + n + ')'
#' ),
#' na.rm_fac = na.rm,
#' na.rm_num = na.rm,
#' exclude_fac = NULL,
#' exclude_name = NULL,
#' all_levels = FALSE
#' )
#' @param x data.frame (possibly grouped)
#' @param ... substitute formulas for elements of fun, fac, num, lab; if RHS is NULL, element is removed
#' @param na.rm whether to remove NA in general
#' @param all a column name for ungrouped statistics; can have length zero to suppress ungrouped column
#' @param fun default aggregate functions expressed as formulas
#' @param fac a list of formulas to generate widgets for factors
#' @param num a list of formulas to generate widgets for numerics
#' @param lab a list of formulas to generate label attributes for columns (see details)
#' @param na.rm_fac whether to drop NA 'factor' observations; passed to \code{\link[tidyr]{gather}} as na.rm, interacts with exclude_fac
#' @param na.rm_num whether to drop NA numeric observations; passed to \code{\link[tidyr]{gather}} as na.rm
#' @param exclude_fac which factor levels to exclude; see \code{\link{factor}} (exclude)
#' @param exclude_name whether to drop NA values of column name (for completeness); passed to \code{\link[tidyr]{gather}}
#' @param all_levels whether to supply records for unobserved levels
#' @importFrom dplyr all_of across everything full_join anti_join
#' @importFrom magrittr %>% %<>%
#' @importFrom yamlet as_dvec
#' @export
#' @return 'tablet'
#' A tablet is a special case of data.frame with grouped rows and columns.
#' \item{*}{There is always one level of row groups.}
#' \item{*}{There can be any number of column groups, including zero.}
#' \item{*}{All columns are character (as tested by \code{is.character()}).}
#' \item{*}{The first column has empty strings that represent
#' the last non-empty value. It can be class 'latex' or 'character'.}
#' \item{*}{Leading element(s) of first column are deliberately blank (one space character)
#' and correspond to header rows. See \code{\link{header_rows}}.}
#' \item{*}{The second column represents group-specific property
#' names. It is populated always and only where column 1 is not.}
#' \item{*}{All other columns represent group-specific property values;
#' elements before the first non-empty group value represent nested headers.}
#' \item{*}{Header values may be repeated.}
#' \item{*}{Header values may be empty strings, representing the last non-empty value
#' to the left, or single spaces, which are deliberately blank.}
#' \item{*}{Internally, character NA is equivalent to an empty string.}
#'
#' @seealso \code{\link{as_kable.tablet}}
#' @examples
#' library(boot)
#' library(dplyr)
#' library(magrittr)
#' melanoma %>%
#' select(-time, -year) %>%
#' mutate(sex = factor(sex), ulcer = factor(ulcer)) %>%
#' group_by(status) %>%
#' tablet
tablet.data.frame <- function(
x,
...,
na.rm = FALSE,
all = 'All',
fun = list(
sum ~ sum(x, na.rm = TRUE),
pct ~ signif(digits = 3, sum / n * 100 ),
ave ~ signif(digits = 3, mean(x, na.rm = TRUE)),
std ~ signif(digits = 3, sd(x, na.rm = TRUE)),
med ~ signif(digits = 3, median(x, na.rm = TRUE)),
min ~ signif(digits = 3, min(x, na.rm = TRUE)),
max ~ signif(digits = 3, max(x, na.rm = TRUE))
),
fac = list(
` ` ~ sum + ' (' + pct + '%' + ')'
),
num = list(
`Mean (SD)` ~ ave + ' (' + std + ')',
`Median (range)` ~ med + ' (' + min + ', ' + max + ')'
),
lab = list(
lab ~ name + '\n(N = ' + n + ')'
),
na.rm_fac = na.rm,
na.rm_num = na.rm,
exclude_fac = NULL,
exclude_name = NULL,
all_levels = FALSE
){
# @ 0.6.12 fun, fac, num, lab must be list
if(!length(fun)) fun <- list()
if(!length(fac)) fac <- list()
if(!length(num)) num <- list()
if(!length(lab)) lab <- list()
y <- groupwise( # groupwise.data.frame, returns tablette
x,
...,
na.rm = na.rm,
# all = all,
fun = fun,
fac = fac,
num = num,
# lab = lab,
na.rm_fac = na.rm_fac,
na.rm_num = na.rm_num,
exclude_fac = exclude_fac,
exclude_name = exclude_name,
all_levels = all_levels
)
y <- tablette(y, ..., all = all, lab = lab ) # tablette.groupwise
# y$`_tablet_level` may have latex elements from factors @ 0.6.10
if(any(sapply(x, function(col)inherits(col,'latex')))){
class(y$`_tablet_level`) <- union('latex', class(y$`_tablet_level`))
}
y$`_tablet_name` <- as.character(y$`_tablet_name`)
codes <- unique(y$`_tablet_name`)
decod <- codes
for(i in seq_along(codes)){
lbl <- attr(x[[ codes[[i]] ]],'label')
ttl <- attr(x[[ codes[[i]] ]], 'title')
if(length(lbl)) decod[[i]] <- lbl
if(length(ttl)) decod[[i]] <- ttl
}
y$`_tablet_name` %<>% factor(levels = codes, labels = decod) %>% as.character
codelist <- codes
names(codelist) <- decod
codelist %<>% as.list
attr(y$`_tablet_name`, 'codelist') <- codelist
# check for prime target inheriting 'latex' and coerce _tablet_name accordingly
fac <- unlist(sapply(x, is.factor)) # unlist necessary because 0 columns returns list() instead of logical vector
num <- unlist(sapply(x, is.numeric))
col <- names(x)[fac | num]
# don't need to consider grouping vars!
# they will not contribute to _tablet_name
col <- setdiff(col, dplyr::group_vars(x))
# if(length(col)){
for(this in col){
prime <- this
targets <- intersect(c('title','label'), names(attributes(x[[prime]])))
if(length(targets)){
target <- targets[[1]]
if(inherits(attr(x[[prime]], target), 'latex'))
class(y$`_tablet_name`) <- union('latex', class(y$`_tablet_name`))
}
}
# y$`_tablet_level` <- as.character(y$`_tablet_level`)
# y$`_tablet_stat` <- as.character(y$`_tablet_stat`)
# y$`_tablet_level` <- ifelse(
# y$`_tablet_level` == 'numeric',
# y$`_tablet_stat`,
# y$`_tablet_level`
# )
# y$`_tablet_stat` <- NULL
# x$`_tablet_sort` <- NULL
y <- tablet(y, ...)
# propagate name class to protect pre-formatted latex elements
for(i in group_vars(x)){
if (inherits(x[[i]], 'latex')){
attr(y, 'name_class') <- 'latex'
}
}
y
}
#' Splice Some Things Together
#'
#' Spices some things together. Generic, with tablet-oriented method \code{\link{splice.data.frame}}.
#'
#' @param x object
#' @param ... passed arguments
#' @export
#' @return see methods
#' @keywords internal
#' @examples
#' example(splice.data.frame)
splice <- function(x, ...)UseMethod('splice')
#' Splice A Data Frame
#'
#' Splices a data.frame. If the data.frame has groups, tablet() is
#' called for each group in succession, only the last of which requests
#' 'all'. The results are column-bound, and duplicate columns
#' are removed.
#' @param x data.frame
#' @param all a column name for ungrouped statistics; can have length zero to suppress ungrouped column
#' @param ... passed to \code{\link{tablet}}
#' @return tablet
#' @export
#' @keywords internal
#' @examples
#' library(boot)
#' library(dplyr)
#' library(magrittr)
#' melanoma %>%
#' select(-time, -year) %>%
#' mutate(sex = factor(sex), ulcer = factor(ulcer)) %>%
#' mutate(status2 = ifelse(status == 2, 2, 4)) %>%
#' group_by(status, status2) %>%
#' splice
splice.data.frame <- function(x, all = 'All', ...){
grp <- groups(x)
if(!length(grp))return(tablet(x, ...))
x <- ungroup(x)
part <- list()
for(g in seq_along(grp)){
group <- grp[[g]]
this <- group_by(x, !!group)
for(i in setdiff(grp, list(group))) this[i] <- NULL
tot <- all
if(g < length(grp)) tot <- character(0) # 'all' only for last table
that <- tablette(tablet(this, all = tot, ...))
part[[group]] <- that
}
names(part) <- NULL
out <- do.call(cbind, part)
# remove dup columns
dupcol <- rep(FALSE, ncol(out))
if(ncol(out))
for(i in seq_along(out)[-1]){
for( j in 1:(i-1)){
if(identical(out[[i]], out[[j]])){
dupcol[[i]] <- TRUE
}
}
}
out <- out[,!dupcol, drop = FALSE]
class(out) <- class(part[[1]])
out <- tablet(out)
out
}
#' Escape Special Characters for Latex
#'
#' Escapes special characters in Latex context. Generic, with method \code{\link{escape_latex.default}}.
#'
#' @export
#' @keywords internal
#' @return see methods
#' @param x typically inherits character
#' @param ... passed arguments
#' @family escape
#' @examples
#' example(escape_latex.default)
escape_latex <- function(x, ...)UseMethod('escape_latex')
#' Escape Special Characters for Latex by Default
#'
#' Escapes text characters that have special meaning in latex.
#' Adapted with gratitude from KableExtra internals.
#' I.e. inactivates material that otherwise looks like latex.
#'
#' This function is used in \code{\link{as_kable.tablet}} in
#' preparation for a call to \code{\link[kableExtra]{kbl}}.
#' At kableExtra 1.3.4, sim_double_escape() only doubles
#' primary (leading) backslashes; the \code{secondary} argument
#' by default pre-doubles later backslashes to prevent
#' certain display errors. Behavior may change if kableExtra changes.
#'
#' @export
#' @keywords internal
#' @return latex
#' @param x typically inherits character
#' @param secondary logical: whether secondary backslashes should be pre-doubled
#' @param ... ignored
#' @family escape
#' @examples
#' escape_latex('([#$%&_{}])')
escape_latex.default <- function(x, secondary = TRUE, ...){
x = gsub("\\\\", "\\\\textbackslash", x)
x = gsub("([#$%&_{}])", "\\\\\\1", x)
x = gsub("\\\\textbackslash", "\\\\textbackslash{}", x)
x = gsub("~", "\\\\textasciitilde{}", x)
x = gsub("\\^", "\\\\textasciicircum{}", x)
if(secondary){
# hotfix for kableExtra::sim_double_escape @1.3.4
# https://github.com/haozhu233/kableExtra/issues/622
x = gsub('\\', '\\\\', x, fixed = TRUE)
x = sub( '\\\\','\\', x, fixed = TRUE) # first will be doubled later by as_kable etc.
}
#x <- as_latex(x)
# @ 0.6.5, apparently this formerly set class to latex.
# as_latex is defined by spork.
# @ spork 0.2.7, there is no default method.
# experimentally, we hard code the class 'latex'
class(x) <- union('latex', class(x))
x
}
#' Escape Latex for Class 'latex'
#'
#' Returns argument typically unmodified.
#' Prevents accidental double-escaping
#' of the same text.
#'
#' This function is used in \code{\link{as_kable.tablet}} in
#' preparation for a call to \code{\link[kableExtra]{kbl}}.
#' At kableExtra 1.3.4, sim_double_escape() only doubles
#' primary (leading) backslashes; the \code{secondary} argument
#' by default pre-doubles later backslashes to prevent
#' certain display errors. Behavior may change if kableExtra changes.
#' @export
#' @keywords internal
#' @return latex
#' @param x latex
#' @param secondary logical: whether secondary backslashes should be pre-doubled
#' @param secondary logical: whether first backslashes should be pre-doubled
#' @param ... ignored
#' @family escape
#' @examples
#' identical(
#' escape_latex('([#$%&_{}])'),
#' escape_latex(escape_latex('([#$%&_{}])'))
#' )
#'
escape_latex.latex <- function(x, secondary = TRUE, primary = FALSE, ...){
if(secondary){
# hotfix for kableExtra::sim_double_escape @1.3.4
# https://github.com/haozhu233/kableExtra/issues/622
x = gsub('\\', '\\\\', x, fixed = TRUE)
# first will be doubled later by as_kable etc.
x = sub( '\\\\','\\', x, fixed = TRUE)
}
if(primary){
x = sub('\\', '\\\\', x, fixed = TRUE)
}
# x <- as_latex(x)
class(x) <- union('latex', class(x))
x
}
#' Relativize a Filepath
#'
#' Relativizes a filepath. Somewhat the opposite of \code{\link{normalizePath}}.
#'
#' x and dir are first normalized, then x is expressed relative to dir.
#' If x and dir are on different drives (i.e. C:/ D:/)
#' x is returned as an absolute path.
#'
#' @export
#' @keywords internal
#' @param x length one character: a file path
#' @param dir a reference directory
#' @param sep path separator
#' @param ... ignored arguments
#'
relativizePath <- function (x, dir = getwd(), sep = "/", ...)
{
stopifnot(length(x) == 1)
stopifnot(file.info(dir)$isdir)
y <- normalizePath(x, winslash = "/")
z <- normalizePath(dir, winslash = "/")
if (!identical(substr(y, 1, 1), substr(z, 1, 1))) {
return(y)
}
y <- strsplit(y, sep)[[1]]
z <- strsplit(z, sep)[[1]]
count <- 0
while (length(y) && length(z) && y[[1]] == z[[1]]) {
y <- y[-1]
z <- z[-1]
}
z <- rep("..", length(z))
y <- c(z, y)
y <- do.call(file.path, as.list(y))
y
}
#' Absolutize a Filepath
#'
#' Absolutizes a filepath. Somewhat the opposite of \code{\link{relativizePath}}.
#'
#' x and dir are first normalized, then x is expressed relative to dir.
#' If x and dir are on different drives (i.e. C:/ D:/)
#' x is returned as an absolute path.
#'
#' @export
#' @keywords internal
#' @importFrom fs is_absolute_path
#' @param x length one character: a file path
#' @param dir a reference directory
#' @param winslash path separator on windows, passed to \code{\link{normalizePath}}
#' @param ... ignored arguments
#'
absolutizePath <- function (x, dir = getwd(), winslash = "/", ...){
stopifnot(length(x) == 1)
stopifnot(is.character(x))
stopifnot(length(dir) == 1)
stopifnot(is.character(dir))
if(is_absolute_path(x))return(x)
out <- normalizePath(
winslash = '/',
file.path(dir,x)
)
return(out)
}
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.