Skip to content

Commit b2f4416

Browse files
strengejackeetiennebacherCopilot
authored
Allow expressions in data_summary() that return > 1 column summaries (#673)
* Allow `data_summary()` to return expressions with > 1 rows * typo * fixes * add tests * fix for grouped data frames * update comment * add examples in docs * fix, add tests * address comments * Update R/data_summary.R Co-authored-by: Etienne Bacher <52219252+etiennebacher@users.noreply.github.com> * whitespace * comments * rename `strict` * use back ticks * revise * add missing snapshot * ... * docs * add test * automatic suffixes * add comments * clarify * Update NEWS.md Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update R/data_summary.R Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update R/data_summary.R Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * update RD * allow named list in suffix * wording * minor * update * Update R/data_summary.R Co-authored-by: Etienne Bacher <52219252+etiennebacher@users.noreply.github.com> * update RD * remove trailing whitespace --------- Co-authored-by: Etienne Bacher <52219252+etiennebacher@users.noreply.github.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
1 parent 0ea3962 commit b2f4416

6 files changed

Lines changed: 543 additions & 48 deletions

File tree

DESCRIPTION

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
Type: Package
22
Package: datawizard
33
Title: Easy Data Wrangling and Statistical Transformations
4-
Version: 1.3.0
4+
Version: 1.3.0.1
55
Authors@R: c(
66
person("Indrajeet", "Patil", , "patilindrajeet.science@gmail.com", role = "aut",
77
comment = c(ORCID = "0000-0003-1995-6531")),

NEWS.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,11 @@
22

33
CHANGES
44

5+
* `data_summary()` now allows expressions to return more than one summary
6+
value. For each value, a new column is created. Additionally, the optional
7+
`suffix` argument controls the naming of these columns; if `suffix = NULL`,
8+
column names are auto-generated (e.g., with numeric suffixes).
9+
510
* `standardize()` now works on `fixest` estimations (#665).
611

712
# datawizard 1.3.0

R/data_summary.R

Lines changed: 202 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,20 @@
1111
#' @param remove_na Logical. If `TRUE`, missing values are omitted from the
1212
#' grouping variable. If `FALSE` (default), missing values are included as a
1313
#' level in the grouping variable.
14+
#' @param suffix Optional, suffixes to be added to the new variable names,
15+
#' especially useful when a function returns several values (e.g. `quantile()`).
16+
#' Can be:
17+
#' * a character vector: all expressions in `...` must return the same number
18+
#' of values as elements in `suffix`.
19+
#' * a list of named character vectors: the names of elements in `suffix` must
20+
#' match the names of the expressions. It is also allowed to specify suffixes
21+
#' for selected expressions only.
22+
#'
23+
#' The new column names are a combination of the left-hand side (i.e.,
24+
#' the name) of the expression and the related suffixes. If `suffix = NULL` (the
25+
#' default), and a summary expression returns multiple values, either the names
26+
#' of the returned values (if any) or automatically numbered suffixes such as
27+
#' `_1`, `_2`, etc. are used. See 'Examples'.
1428
#' @param ... One or more named expressions that define the new variable name
1529
#' and the function to compute the summary statistic. Example:
1630
#' `mean_sepal_width = mean(Sepal.Width)`. The expression can also be provided
@@ -50,15 +64,72 @@
5064
#' last = mpg[length(mpg)],
5165
#' by = c("am", "gear")
5266
#' )
67+
#'
68+
#' # allow more than one-column-summaries for expressions
69+
#' d <- data.frame(
70+
#' x = rnorm(100, 1, 1),
71+
#' y = rnorm(100, 2, 2),
72+
#' groups = rep(1:4, each = 25)
73+
#' )
74+
#'
75+
#' # since we have multiple columns for one expression, the names of the
76+
#' # returned summary results are used as suffix by default
77+
#' data_summary(
78+
#' d,
79+
#' quant_x = quantile(x, c(0.25, 0.75)),
80+
#' mean_x = mean(x),
81+
#' quant_y = quantile(y, c(0.25, 0.5, 0.75))
82+
#' )
83+
#'
84+
#' # if a summary function, like `fivenum()`, returns no named vector, suffixes
85+
#' # are automatically numbered
86+
#' data_summary(
87+
#' d,
88+
#' quant_x = quantile(x, c(0.25, 0.75)),
89+
#' mean_x = mean(x),
90+
#' fivenum_y = fivenum(y)
91+
#' )
92+
#'
93+
#' # specify column suffix for expressions, matching by names
94+
#' data_summary(
95+
#' d,
96+
#' quant_x = quantile(x, c(0.25, 0.75)),
97+
#' mean_x = mean(x),
98+
#' quant_y = quantile(y, c(0.25, 0.5, 0.75)),
99+
#' suffix = list(quant_y = c("_Q1", "_Q2", "_Q3"))
100+
#' )
101+
#'
102+
#' # name multiple expression suffixes, grouped by variable
103+
#' data_summary(
104+
#' d,
105+
#' quant_x = quantile(x, c(0.25, 0.75)),
106+
#' mean_x = mean(x),
107+
#' quant_y = quantile(y, c(0.25, 0.5, 0.75)),
108+
#' suffix = list(quant_x = c("Q1", "Q3"), quant_y = c("_Q1", "_Q2", "_Q3")),
109+
#' by = "groups"
110+
#' )
111+
#'
53112
#' @export
54113
data_summary <- function(x, ...) {
55114
UseMethod("data_summary")
56115
}
57116

58117

59118
#' @export
60-
data_summary.matrix <- function(x, ..., by = NULL, remove_na = FALSE) {
61-
data_summary(as.data.frame(x), ..., by = by, remove_na = remove_na)
119+
data_summary.matrix <- function(
120+
x,
121+
...,
122+
by = NULL,
123+
remove_na = FALSE,
124+
suffix = NULL
125+
) {
126+
data_summary(
127+
as.data.frame(x),
128+
...,
129+
by = by,
130+
remove_na = remove_na,
131+
suffix = suffix
132+
)
62133
}
63134

64135

@@ -72,7 +143,13 @@ data_summary.default <- function(x, ...) {
72143

73144
#' @rdname data_summary
74145
#' @export
75-
data_summary.data.frame <- function(x, ..., by = NULL, remove_na = FALSE) {
146+
data_summary.data.frame <- function(
147+
x,
148+
...,
149+
by = NULL,
150+
remove_na = FALSE,
151+
suffix = NULL
152+
) {
76153
dots <- eval(substitute(alist(...)))
77154

78155
# do we have any expression at all?
@@ -84,9 +161,10 @@ data_summary.data.frame <- function(x, ..., by = NULL, remove_na = FALSE) {
84161

85162
if (is.null(by)) {
86163
# when we have no grouping, just compute a one-row summary
87-
summarise <- .process_datasummary_dots(dots, x)
88-
out <- data.frame(summarise)
89-
colnames(out) <- vapply(summarise, names, character(1))
164+
summarise <- .process_datasummary_dots(dots, x, suffix)
165+
# coerce to data frame
166+
out <- as.data.frame(t(summarise))
167+
colnames(out) <- names(summarise)
90168
} else {
91169
# sanity check - is "by" a character string?
92170
if (!is.character(by)) {
@@ -122,15 +200,23 @@ data_summary.data.frame <- function(x, ..., by = NULL, remove_na = FALSE) {
122200
return(NULL)
123201
}
124202
# summarize data
125-
summarise <- .process_datasummary_dots(dots, s)
203+
summarise <- .process_datasummary_dots(dots, s, suffix)
126204
# coerce to data frame
127-
summarised_data <- data.frame(summarise)
205+
summarised_data <- as.data.frame(t(summarise))
128206
# bind grouping-variables and values
129207
summarised_data <- cbind(s[1, by], summarised_data)
130208
# make sure we have proper column names
131-
colnames(summarised_data) <- c(by, unlist(lapply(summarise, names)))
209+
colnames(summarised_data) <- c(by, names(summarise))
132210
summarised_data
133211
})
212+
# check for correct number of columns. If one expression returns different
213+
# number of values (which now means, we have different number of columns
214+
# to bind) for each group, tell user
215+
if (!all(lengths(out) == lengths(out)[1])) {
216+
insight::format_error(
217+
"Each expression must return the same number of values for each group. Some of the expressions seem to return varying numbers of values."
218+
)
219+
}
134220
out <- do.call(rbind, out)
135221
}
136222
# sort data
@@ -143,7 +229,13 @@ data_summary.data.frame <- function(x, ..., by = NULL, remove_na = FALSE) {
143229

144230

145231
#' @export
146-
data_summary.grouped_df <- function(x, ..., by = NULL, remove_na = FALSE) {
232+
data_summary.grouped_df <- function(
233+
x,
234+
...,
235+
by = NULL,
236+
remove_na = FALSE,
237+
suffix = NULL
238+
) {
147239
# extract group variables
148240
grps <- attr(x, "groups", exact = TRUE)
149241
group_variables <- data_remove(grps, ".rows")
@@ -154,13 +246,13 @@ data_summary.grouped_df <- function(x, ..., by = NULL, remove_na = FALSE) {
154246
# remove information specific to grouped df's
155247
attr(x, "groups") <- NULL
156248
class(x) <- "data.frame"
157-
data_summary(x, ..., by = by, remove_na = remove_na)
249+
data_summary(x, ..., by = by, remove_na = remove_na, suffix = suffix)
158250
}
159251

160252

161253
# helper -----------------------------------------------------------------------
162254

163-
.process_datasummary_dots <- function(dots, data) {
255+
.process_datasummary_dots <- function(dots, data, suffix = NULL) {
164256
out <- NULL
165257
if (length(dots)) {
166258
# we check for character vector of expressions, in which case
@@ -199,39 +291,113 @@ data_summary.grouped_df <- function(x, ..., by = NULL, remove_na = FALSE) {
199291
}
200292
}
201293

294+
# sanity check: check the input for the `suffix` argument
295+
# `suffix` can be NULL, or must be a (named) list
296+
if (!is.null(suffix)) {
297+
# if `suffix` is a character vector, we transform it into a list,
298+
# matching the names of the expressions
299+
if (is.character(suffix)) {
300+
suffix <- rep(list(suffix), length(dots))
301+
names(suffix) <- names(dots)
302+
}
303+
# no list? error
304+
if (!is.list(suffix)) {
305+
insight::format_error(
306+
"Argument `suffix` must be a list of (named) character vectors, where the names match the names of the expressions, e.g.:",
307+
paste0(
308+
"`suffix = list(",
309+
names(dots)[1],
310+
" = c(\"_suffix1\", \"_suffix2\")`."
311+
)
312+
)
313+
}
314+
# not all elements named? error
315+
if (!length(which(nzchar(names(suffix), keepNA = TRUE)))) {
316+
insight::format_error("All elements of `suffix` must have names.")
317+
}
318+
# names of suffix do not match names of expressions? error
319+
if (!all(names(suffix) %in% names(dots))) {
320+
wrong_name <- which(!names(suffix) %in% names(dots))[1]
321+
insight::format_error(
322+
paste0(
323+
"Names of `suffix` must match the names of the expressions. Suffix `",
324+
names(suffix)[wrong_name],
325+
"` has no corresponding expression."
326+
)
327+
)
328+
}
329+
# identical suffixes for one expression? error
330+
identical_suffix <- vapply(
331+
suffix,
332+
function(i) insight::n_unique(i) != length(i),
333+
logical(1)
334+
)
335+
if (any(identical_suffix)) {
336+
insight::format_error(
337+
paste0(
338+
"All suffixes for a single expression must be unique. Suffix for element `",
339+
names(identical_suffix)[which(identical_suffix)][1],
340+
"` has duplicate values."
341+
)
342+
)
343+
}
344+
}
345+
202346
out <- lapply(seq_along(dots), function(i) {
203347
new_variable <- .get_new_dots_variable(dots, i, data)
348+
# check special case here - we want bayestestR::ci to work with
349+
# data summary, to easily create CIs for, say, posterior draws
204350
if (inherits(new_variable, c("bayestestR_ci", "bayestestR_eti"))) {
205351
stats::setNames(new_variable, c("CI", "CI_low", "CI_high"))
206352
} else {
207-
stats::setNames(new_variable, names(dots)[i])
353+
# init
354+
current_suffix <- NULL
355+
# find matches and set use suffix if found
356+
matching_names <- which(names(suffix) == names(dots)[i])
357+
# either use suffixes based on matching names, or try to extract
358+
# names from the returned summary expression (saved in "new_variable"),
359+
# if the summary function returned a named vector
360+
if (length(matching_names) > 0) {
361+
current_suffix <- suffix[[matching_names]]
362+
} else if (
363+
length(new_variable) > 1 &&
364+
all(nzchar(names(new_variable), keepNA = TRUE))
365+
) {
366+
current_suffix <- names(new_variable)
367+
}
368+
# if we don't have suffixes for multiple columns, but expression
369+
# returns multiple columns, we get NA column names - we use
370+
# automatically numbered suffixes in this case
371+
if (is.null(current_suffix) && length(new_variable) > 1) {
372+
current_suffix <- paste0("_", seq_along(new_variable))
373+
}
374+
375+
# if number of suffixes does not match the number of returned values
376+
# by the expression, error
377+
if (
378+
!is.null(current_suffix) &&
379+
length(current_suffix) != length(new_variable)
380+
) {
381+
insight::format_error(
382+
paste0(
383+
"Argument `suffix` must have the same length as the result of the corresponding summary expression. `suffix` has ",
384+
length(current_suffix),
385+
" elements (",
386+
text_concatenate(current_suffix, enclose = "`"),
387+
") for the expression `",
388+
insight::safe_deparse(dots[[i]]),
389+
"`, which returned ",
390+
length(new_variable),
391+
" values."
392+
)
393+
)
394+
}
395+
stats::setNames(new_variable, paste0(names(dots)[i], current_suffix))
208396
}
209397
})
210398
}
211399

212-
# check for correct length of output - must be a single value!
213-
# Exception: bayestestR::ci()
214-
wrong_length <- !sapply(
215-
out,
216-
inherits,
217-
what = c("bayestestR_ci", "bayestestR_eti")
218-
) &
219-
lengths(out) != 1 # nolint
220-
if (any(wrong_length)) {
221-
insight::format_error(
222-
paste0(
223-
"Each expression must return a single value. Following expression",
224-
ifelse(sum(wrong_length) > 1, "s", " "),
225-
" returned more than one value: ",
226-
text_concatenate(
227-
vapply(dots[wrong_length], insight::safe_deparse, character(1)),
228-
enclose = "\""
229-
)
230-
)
231-
)
232-
}
233-
234-
out
400+
unlist(out)
235401
}
236402

237403

0 commit comments

Comments
 (0)