Skip to content

Commit 8974064

Browse files
authored
Merge pull request #842 from fastverse/development
Development
2 parents 4ce0a24 + 54e69b7 commit 8974064

28 files changed

Lines changed: 660 additions & 77 deletions

.Rbuildignore

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@
66
^CONTRIBUTING\.md$
77
^README\.md$
88
^README\.Rmd$
9+
^CLAUDE\.md$
10+
^\.positai$
11+
^\.claude$
912
^cran-comments\.md$
1013
^experimental$
1114
^testing$
@@ -27,13 +30,9 @@ man/figures
2730
_cache$
2831
_snaps
2932
^CITATION\.cff$
30-
^\.DS_Store$
3133
^revdep$
3234
\.orig$
33-
\.DS_store$
34-
.DS_store
35+
(^|/)\.DS_Store$
3536
testthat-problems.rds
3637
vignettes/figure
3738
vignettes/cache
38-
39-

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,3 +15,4 @@ Meta
1515
testing
1616
.Rproj.user
1717
*.db
18+
.positai

DESCRIPTION

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
Package: collapse
22
Title: Advanced and Fast Data Transformation
3-
Version: 2.1.6
4-
Date: 2025-12-21
3+
Version: 2.1.7
4+
Date: 2026-05-17
55
Authors@R: c(
66
person("Sebastian", "Krantz", role = c("aut", "cre"),
77
email = "sebastian.krantz@graduateinstitute.ch",
@@ -18,7 +18,7 @@ Authors@R: c(
1818
person("Martyn", "Plummer", role = "cph"),
1919
person("1999-2016 The R Core Team", role = "cph")
2020
)
21-
Description: A large C/C++-based package for advanced data transformation and
21+
Description: A C/C++-based package for advanced data transformation and
2222
statistical computing in R that is extremely fast, class-agnostic, robust, and
2323
programmer friendly. Core functionality includes a rich set of S3 generic grouped
2424
and weighted statistical functions for vectors, matrices and data frames, which
@@ -31,7 +31,8 @@ Description: A large C/C++-based package for advanced data transformation and
3131
fast data object conversions, functions for memory efficient R programming, and
3232
helpers to effectively deal with variable labels, attributes, and missing data. It
3333
seamlessly supports base R objects/classes as well as 'units', 'integer64', 'xts'/
34-
'zoo', 'tibble', 'grouped_df', 'data.table', 'sf', and 'pseries'/'pdata.frame'.
34+
'zoo', 'tibble', 'grouped_df', 'data.table', 'sf', and 'pseries'/'pdata.frame'.
35+
See Krantz (2026) <doi:10.18637/jss.v116.i01>.
3536
URL: https://fastverse.org/collapse/, https://github.com/fastverse/collapse
3637
BugReports: https://github.com/fastverse/collapse/issues
3738
License: GPL (>= 2) | file LICENSE

NEWS.md

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,26 @@
1+
# collapse 2.1.7
2+
3+
* The *collapse* article is now published in the Journal of Statistical Software: https://doi.org/10.18637/jss.v116.i01. This article is now the primary citation for academic use of *collapse*. It is also a great reference to quickly and thoroughly understand the package. `citation("collapse")` was also updated in this regard. The APA-style citation is:
4+
5+
Krantz, S. (2026). **collapse**: Advanced and fast statistical computing and data transformation in R. *Journal of Statistical Software, 116*(1), 1–38. [https://doi.org/10.18637/jss.v116.i01](https://doi.org/10.18637/jss.v116.i01)
6+
7+
8+
* Performance improvements to `fsum()` and `fmean()` in the non-grouped case through multiple-accumulator SIMD optimizations, particularly benefiting systems without OpenMP support. `fsum()` sees ~2x speedup and `fmean()` ~7x speedup on such systems, with smaller but notable gains on systems with OpenMP. Thanks @TylerSagendorf for the implementation and benchmarking (#824, #828, #832, #833).
9+
10+
* `GRP.default()` gains a `drop = TRUE` argument. Setting `drop = FALSE` (and providing at least one factor among the grouping columns) retains all combinations of factor levels with the observed unique values of non-factor grouping columns---the full Cartesian product---similar to `dplyr::group_by(.drop = FALSE)`. Unobserved combinations get `group.sizes` of `0` and `group.starts` of `0L`. Correspondingly, `fgroup_by()`/`gby()` gain a `.drop` argument, and `fcount()`/`fcountv()`/`collap()`/`collapv()` gain a `drop` argument, enabling counts and aggregations that retain empty groups. Thanks @egoipse for the feature request (#820, #839).
11+
12+
* Fixed a bug in `descr()` that caused R to crash with a segmentation fault when called on zero-row data frames. Thanks @hatschibratschi for reporting (#831).
13+
14+
* Consistency with internal updates to *data.table* regarding growable vectors. Thanks @aitap (#809).
15+
16+
* Force C locale sorting in internal operations for consistency. Thanks @MichaelChirico (#815).
17+
18+
* Added contributor Ivan Krylov (@aitap) to package authors.
19+
120
# collapse 2.1.6
221

322
* The repo has moved to `fastverse/collapse` and the website to [fastverse.org/collapse](https://fastverse.org/collapse/)---for better visibility and maintenance. Appropriate redirects from the old repo/site have been implemented.
4-
Selected people now have access to the repo through the organization account and may respond to issues or submit fixes.
23+
Selected people now have access to the repo through the organization account and may respond to issues or submit fixes.
524

625
* Added new AI-generated interactive/chattable [DeepWiki documentation](https://deepwiki.com/fastverse/collapse).
726

R/GRP.R

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -95,21 +95,21 @@ GRP.GRP <- function(X, ...) X
9595

9696
GRP.default <- function(X, by = NULL, sort = .op[["sort"]], decreasing = FALSE, na.last = TRUE,
9797
return.groups = TRUE, return.order = sort, method = "auto",
98-
call = TRUE, ...) {
98+
drop = TRUE, call = TRUE, ...) {
9999

100100
use.group <- switch(method, auto = !sort, hash = TRUE, radix = FALSE, stop("method needs to be 'auto', 'hash' or 'radix'."))
101101

102102
if(is.na(na.last)) stop("here na.last needs to be TRUE or FALSE, otherwise the GRP object does not match the data dimensions.")
103103

104104
if(is.list(X)) {
105105
if(inherits(X, "GRP")) return(X)
106-
if(is.null(by)) {
106+
by_null <- is.null(by)
107+
if(by_null) {
107108
by <- seq_along(unclass(X))
108109
# # This is so that fgroup_by(iris, Species = GRP(Species)) is possible.
109110
# if(length(by) == 1L && is.list(.subset2(X, 1L)) && inherits(.subset2(X, 1L), "GRP")) return(.subset2(X, 1L))
110111
namby <- attr(X, "names")
111112
if(is.null(namby)) attr(X, "names") <- namby <- paste0("Group.", by)
112-
o <- switchGRP(X, na.last, decreasing, return.groups || !use.group, TRUE, sort, use.group)
113113
} else {
114114
if(is.call(by)) {
115115
namby <- all.vars(by, unique = FALSE)
@@ -126,8 +126,26 @@ GRP.default <- function(X, by = NULL, sort = .op[["sort"]], decreasing = FALSE,
126126
attr(X, "names") <- paste0("Group.", seq_along(unclass(X))) # best ?
127127
}
128128
}
129-
o <- switchGRP(.subset(X, by), na.last, decreasing, return.groups || !use.group, TRUE, sort, use.group)
130129
}
130+
# drop = FALSE: full Cartesian product of (factor) levels, computed in C.
131+
# Falls through to the existing path when no grouping column is a factor (matches dplyr).
132+
if(!drop) {
133+
cols <- if(by_null) unclass(X) else .subset(unclass(X), by)
134+
if(any(.Call(C_vtypes, cols, 2L))) {
135+
res <- .Call(C_GRP_default_drop, X, cols, namby, return.groups)
136+
return(`oldClass<-`(list(N.groups = res[[1L]],
137+
group.id = res[[2L]],
138+
group.sizes = res[[3L]],
139+
groups = if(return.groups) res[[5L]] else NULL,
140+
group.vars = namby,
141+
ordered = c(ordered = NA, sorted = NA),
142+
order = NULL,
143+
group.starts = res[[4L]],
144+
call = if(call) match.call() else NULL), "GRP"))
145+
}
146+
}
147+
o <- switchGRP(if(by_null) X else .subset(X, by),
148+
na.last, decreasing, return.groups || !use.group, TRUE, sort, use.group)
131149
} else {
132150
if(length(by)) stop("by can only be used to subset list / data.frame columns")
133151
namby <- l1orlst(as.character(substitute(X))) # paste(all.vars(call), collapse = ".") # good in all circumstances ?
@@ -382,7 +400,7 @@ GRP.pseries <- function(X, effect = 1L, ..., group.sizes = TRUE, return.groups =
382400
GRP.pdata.frame <- function(X, effect = 1L, ..., group.sizes = TRUE, return.groups = TRUE, call = TRUE)
383401
GRP.pseries(X, effect, ..., group.sizes = group.sizes, return.groups = return.groups, call = call)
384402

385-
fgroup_by <- function(.X, ..., sort = .op[["sort"]], decreasing = FALSE, na.last = TRUE, return.groups = TRUE, return.order = sort, method = "auto") { # e <- substitute(list(...)) # faster but does not preserve attributes of unique groups !
403+
fgroup_by <- function(.X, ..., sort = .op[["sort"]], decreasing = FALSE, na.last = TRUE, return.groups = TRUE, return.order = sort, method = "auto", .drop = TRUE) { # e <- substitute(list(...)) # faster but does not preserve attributes of unique groups !
386404
clx <- oldClass(.X)
387405
oldClass(.X) <- NULL
388406
m <- match(c("GRP_df", "grouped_df", "data.frame"), clx, nomatch = 0L)
@@ -416,7 +434,7 @@ fgroup_by <- function(.X, ..., sort = .op[["sort"]], decreasing = FALSE, na.last
416434
} else names(e) <- vars
417435
}
418436
}
419-
attr(.X, "groups") <- GRP.default(e, NULL, sort, decreasing, na.last, return.groups, return.order, method, FALSE)
437+
attr(.X, "groups") <- GRP.default(e, NULL, sort, decreasing, na.last, return.groups, return.order, method, .drop, FALSE)
420438
# if(any(clx == "sf")) oldClass(.X) <- clx[clx != "sf"]
421439
# attr(.X, "groups") <- GRP.default(fselect(if(m[2L]) fungroup(.X) else .X, ...), NULL, sort, decreasing, na.last, TRUE, return.order, method, FALSE)
422440
# Needed: wlddev %>% fgroup_by(country) gives error if dplyr is loaded. Also sf objects etc..

R/collap.R

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ rbindlist_factor <- function(l, idcol = "Function") {
9595
collap <- function(X, by, FUN = fmean, catFUN = fmode, cols = NULL, w = NULL, wFUN = fsum, custom = NULL,
9696
...,
9797
keep.by = TRUE, keep.w = TRUE, keep.col.order = TRUE, sort = .op[["sort"]], decreasing = FALSE,
98-
na.last = TRUE, return.order = sort, method = "auto", parallel = FALSE, mc.cores = 2L,
98+
na.last = TRUE, return.order = sort, method = "auto", drop = TRUE, parallel = FALSE, mc.cores = 2L,
9999
return = c("wide","list","long","long_dupl"), give.names = "auto") {
100100

101101
return <- switch(return[1L], wide = 1L, list = 2L, long = 3L, long_dupl = 4L, stop("Unknown return output option"))
@@ -127,14 +127,14 @@ collap <- function(X, by, FUN = fmean, catFUN = fmode, cols = NULL, w = NULL, wF
127127
numby <- ckmatch(all.vars(by), nam)
128128
if(ncustoml) v <- if(is.null(cols)) seq_along(X)[-numby] else cols2int(cols, X, nam)
129129
}
130-
by <- GRP.default(X, numby, sort, decreasing, na.last, keep.by, return.order, method, call = FALSE)
130+
by <- GRP.default(X, numby, sort, decreasing, na.last, keep.by, return.order, method, drop = drop, call = FALSE)
131131
} else if(is.atomic(by)) {
132132
numby <- 0L
133133
if(ncustoml) if(is.null(cols)) vl <- FALSE else v <- cols2int(cols, X, nam)
134-
by <- GRP.default(`names<-`(list(by), l1orlst(as.character(substitute(by)))), NULL, sort, decreasing, na.last, keep.by, return.order, method, call = FALSE)
134+
by <- GRP.default(`names<-`(list(by), l1orlst(as.character(substitute(by)))), NULL, sort, decreasing, na.last, keep.by, return.order, method, drop = drop, call = FALSE)
135135
} else {
136136
if(ncustoml) if(is.null(cols)) vl <- FALSE else v <- cols2int(cols, X, nam)
137-
if(!is_GRP(by)) by <- GRP.default(by, NULL, sort, decreasing, na.last, keep.by, return.order, method, call = FALSE)
137+
if(!is_GRP(by)) by <- GRP.default(by, NULL, sort, decreasing, na.last, keep.by, return.order, method, drop = drop, call = FALSE)
138138
numby <- rep(0L, length(by[[5L]]))
139139
if(keep.by && !vl && any(m <- nam %in% by[[5L]])) {
140140
v <- whichv(m, FALSE)
@@ -322,7 +322,7 @@ collap <- function(X, by, FUN = fmean, catFUN = fmode, cols = NULL, w = NULL, wF
322322
# collapv: allows vector input to by and w
323323
collapv <- function(X, by, FUN = fmean, catFUN = fmode, cols = NULL, w = NULL, wFUN = fsum, custom = NULL, ...,
324324
keep.by = TRUE, keep.w = TRUE, keep.col.order = TRUE, sort = .op[["sort"]], decreasing = FALSE,
325-
na.last = TRUE, return.order = sort, method = "auto", parallel = FALSE, mc.cores = 2L,
325+
na.last = TRUE, return.order = sort, method = "auto", drop = TRUE, parallel = FALSE, mc.cores = 2L,
326326
return = c("wide","list","long","long_dupl"), give.names = "auto") {
327327

328328
return <- switch(return[1L], wide = 1L, list = 2L, long = 3L, long_dupl = 4L, stop("Unknown return output option"))
@@ -343,7 +343,7 @@ collapv <- function(X, by, FUN = fmean, catFUN = fmode, cols = NULL, w = NULL, w
343343

344344
# identifying by
345345
numby <- cols2int(by, X, nam)
346-
by <- GRP.default(X, numby, sort, decreasing, na.last, keep.by, return.order, method, call = FALSE)
346+
by <- GRP.default(X, numby, sort, decreasing, na.last, keep.by, return.order, method, drop = drop, call = FALSE)
347347
if(ncustoml) v <- if(is.null(cols)) seq_along(X)[-numby] else cols2int(cols, X, nam)
348348

349349

R/fcount.R

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -34,18 +34,20 @@ fcount_core <- function(x, g, w = NULL, name = "N", add = FALSE) {
3434
condalc(copyMostAttributes(c(res, `names<-`(list(g$group.sizes), name[1L])), res), inherits(x, "data.table"))
3535
}
3636

37-
fcount <- function(x, ..., w = NULL, name = "N", add = FALSE, sort = FALSE, decreasing = FALSE) {
37+
fcount <- function(x, ..., w = NULL, name = "N", add = FALSE, sort = FALSE, decreasing = FALSE, drop = TRUE) {
3838
if(is.list(x)) w <- eval(substitute(w), x, parent.frame())
3939
else x <- qDF(x)
4040
if(is.character(add)) add <- switch(add, gv =, group_vars = 2L, stop("add must be TRUE, FALSE or group_vars (gv)")) # add = "g", "groups" or "group_vars"
4141
# Note: this code duplication with GRP() is needed for GRP() to capture x (using substitute) if x is atomic.
4242
# if(is.atomic(x)) `names<-`(list(x), l1orlst(as.character(substitute(x)))) else
43-
g <- if(missing(...)) GRP(x, sort = sort, decreasing = decreasing, return.groups = !add, return.order = FALSE, call = FALSE) else
44-
GRP.default(fselect(x, ...), sort = sort, decreasing = decreasing, return.groups = !add, return.order = FALSE, call = FALSE)
43+
g <- if(missing(...)) {
44+
if(inherits(x, "grouped_df")) GRP(x, sort = sort, decreasing = decreasing, return.groups = !add, return.order = FALSE, call = FALSE)
45+
else GRP.default(x, sort = sort, decreasing = decreasing, return.groups = !add, return.order = FALSE, drop = drop, call = FALSE)
46+
} else GRP.default(fselect(x, ...), sort = sort, decreasing = decreasing, return.groups = !add, return.order = FALSE, drop = drop, call = FALSE)
4547
fcount_core(x, g, w, name, add)
4648
}
4749

48-
fcountv <- function(x, cols = NULL, w = NULL, name = "N", add = FALSE, sort = FALSE, ...) {
50+
fcountv <- function(x, cols = NULL, w = NULL, name = "N", add = FALSE, sort = FALSE, drop = TRUE, ...) {
4951
# Safe enough ? or only allow character ? what about collapv() ?, extra option ?
5052
# if(length(w) == 1L && is.list(x) && length(unclass(x)) > 1L && (is.character(w) || is.integer(w) || (is.numeric(w) && w %% 1 < 1e-6)))
5153
if(is.atomic(x)) x <- qDF(x)
@@ -54,7 +56,9 @@ fcountv <- function(x, cols = NULL, w = NULL, name = "N", add = FALSE, sort = FA
5456
if(is.null(w)) stop("Unknown column: ", w)
5557
}
5658
if(is.character(add)) add <- switch(add, gv =, group_vars = 2L, stop("add must be TRUE, FALSE or group_vars (gv)")) # add = "g", "groups" or "group_vars"
57-
g <- if(is.null(cols)) GRP(x, sort = sort, return.groups = !add, return.order = FALSE, call = FALSE, ...) else
58-
GRP.default(colsubset(x, cols), sort = sort, return.groups = !add, return.order = FALSE, call = FALSE, ...)
59+
g <- if(is.null(cols)) {
60+
if(inherits(x, "grouped_df")) GRP(x, sort = sort, return.groups = !add, return.order = FALSE, call = FALSE, ...)
61+
else GRP.default(x, sort = sort, return.groups = !add, return.order = FALSE, drop = drop, call = FALSE, ...)
62+
} else GRP.default(colsubset(x, cols), sort = sort, return.groups = !add, return.order = FALSE, drop = drop, call = FALSE, ...)
5963
fcount_core(x, g, w, name, add)
6064
}

README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
[![minimal R version](https://img.shields.io/badge/R%3E%3D-3.5.0-6666ff.svg)](https://cran.r-project.org/)
1414
[![dependencies](https://tinyverse.netlify.app/badge/collapse)](https://CRAN.R-project.org/package=collapse)
1515
[![DOI](https://zenodo.org/badge/172910283.svg)](https://zenodo.org/badge/latestdoi/172910283)
16-
[![arXiv](https://img.shields.io/badge/arXiv-2403.05038-0969DA.svg)](https://arxiv.org/abs/2403.05038)
16+
[![JSS](https://img.shields.io/badge/JSS-10.18637%2Fjss.v116.i01-blue)](https://doi.org/10.18637/jss.v116.i01)
1717
[![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/fastverse/collapse)
1818
<!-- badges: end -->
1919

@@ -79,9 +79,9 @@ In addition there are several [vignettes](<https://fastverse.org/collapse/articl
7979
<a href="https://raw.githubusercontent.com/fastverse/collapse/master/misc/collapse%20cheat%20sheet/collapse_cheat_sheet.pdf"><img src="https://raw.githubusercontent.com/fastverse/collapse/master/misc/collapse%20cheat%20sheet/preview/page1.png" width="330"/></a> <!-- height="227" 294 -->
8080
<a href="https://raw.githubusercontent.com/fastverse/collapse/master/misc/collapse%20cheat%20sheet/collapse_cheat_sheet.pdf"><img src="https://raw.githubusercontent.com/fastverse/collapse/master/misc/collapse%20cheat%20sheet/preview/page2.png" width="330"/></a>
8181

82-
### Article on arXiv
82+
### JSS Article
8383

84-
An [**article**](https://arxiv.org/abs/2403.05038) on *collapse* is forthcoming at [Journal of Statistical Software](https://www.jstatsoft.org/).
84+
The [**collapse** article](https://doi.org/10.18637/jss.v116.i01) is published in the [Journal of Statistical Software](https://www.jstatsoft.org/) (volume 116, issue 1). It is the primary reference for academic use of the package and a concise introduction to its design and capabilities.
8585

8686
### Presentation at [useR 2022](https://user2022.r-project.org)
8787

inst/CITATION

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,18 @@
11
citHeader("To cite collapse in publications, please use:")
22

33
bibentry(bibtype = "misc",
4-
key = "krantz2024collapse",
5-
title = "collapse: Advanced and Fast Statistical Computing and Data Transformation in R",
6-
author = person("Sebastian", "Krantz"),
7-
year = "2024",
8-
eprint="2403.05038",
9-
archivePrefix="arXiv",
10-
primaryClass="stat.CO",
11-
url = "https://arxiv.org/abs/2403.05038",
12-
textVersion = "Krantz, S. (2024). collapse: Advanced and Fast Statistical Computing and Data Transformation in R [Preprint]. arXiv. https://arxiv.org/abs/2403.05038")
4+
key = "krantz2026collapse",
5+
title = "{collapse}: Advanced and Fast Statistical Computing and Data Transformation in {R}",
6+
author = person(given = "Sebastian",
7+
family = "Krantz",
8+
email = "sebastian.krantz@kielinstitut.de"),
9+
journal = "Journal of Statistical Software",
10+
year = "2026",
11+
volume = "116",
12+
number = "1",
13+
pages = "1--38",
14+
doi = "10.18637/jss.v116.i01",
15+
textVersion = "Krantz, S. (2026). collapse: Advanced and fast statistical computing and data transformation in R. Journal of Statistical Software, 116(1), 1–38. https://doi.org/10.18637/jss.v116.i01")
1316

1417
year <- sub("-.*", "", meta$Date)
1518
note <- sprintf("R package version %s", meta$Version)

0 commit comments

Comments
 (0)