Skip to content

Commit 4f5ac7a

Browse files
committed
add na.rm capabilites
1 parent 8f0eed2 commit 4f5ac7a

7 files changed

Lines changed: 345 additions & 101 deletions

File tree

NEWS.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,8 @@
5050

5151
14. `first()` and `last()` with `n>1` are now GForce optimized (e.g. `DT[, first(x, n=3), by=grp]`), [#4239](https://github.com/Rdatatable/data.table/issues/4239). Also adds a new internal `gforce_dynamic` mechanism to track any GForce result which returns other than exactly 1 row per group so that results are correctly replicated. Thanks to @nbenn for the report and @ben-schwen and @mattdowle for the implementation.
5252

53+
15. `first()` and `last()` gain an `na.rm` argument to skip missing values (e.g. `DT[, first(x, na.rm=TRUE), by=grp]`), [#4239](https://github.com/Rdatatable/data.table/issues/4239) and [#4446](https://github.com/Rdatatable/data.table/issues/4446). A group with no non-missing values returns `NA` for `n=1` (matching `median()`/`var()`), or zero rows for `n>1`. GForce optimized. Thanks to @nbenn and @MichaelChirico for the reports and @ben-schwen and @mattdowle for the implementation.
54+
5355
### BUG FIXES
5456

5557
1. `fread()` with `skip=0` and `(header=TRUE|FALSE)` no longer skips the first row when it has fewer fields than subsequent rows, [#7463](https://github.com/Rdatatable/data.table/issues/7463). Thanks @emayerhofer for the report and @ben-schwen for the fix.

R/data.table.R

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3337,8 +3337,8 @@ gfuns = c(gdtfuns,
33373337
`g[` = `g[[` = function(x, n) .Call(Cgnthvalue, x, as.integer(n)) # n is of length=1 here.
33383338
ghead = function(x, n) .Call(Cghead, x, as.integer(n))
33393339
gtail = function(x, n) .Call(Cgtail, x, as.integer(n))
3340-
gfirst = function(x, n=1L) .Call(Cgfirst, x, as.integer(n))
3341-
glast = function(x, n=1L) .Call(Cglast, x, as.integer(n))
3340+
gfirst = function(x, n=1L, na.rm=FALSE) .Call(Cgfirst, x, as.integer(n), na.rm)
3341+
glast = function(x, n=1L, na.rm=FALSE) .Call(Cglast, x, as.integer(n), na.rm)
33423342
gsum = function(x, na.rm=FALSE) .Call(Cgsum, x, na.rm)
33433343
gmean = function(x, na.rm=FALSE) .Call(Cgmean, x, na.rm)
33443344
gweighted.mean = function(x, w, ..., na.rm=FALSE) {
@@ -3390,11 +3390,14 @@ is_constantish = function(q, check_singleton=FALSE) {
33903390
length(q) == 3L &&
33913391
is_constantish(q[[3L]], check_singleton = TRUE)
33923392
}
3393-
# first(x, n) / last(x, n) with n>1, #4446 #4239.
3393+
# first(x, n=, na.rm=) / last(x, n=, na.rm=), #4446 #4239. Called only when length(q)>=3
3394+
# (the length(q)==2L case, i.e. first(x)/last(x), is already handled earlier in .gforce_ok).
33943395
.gfirstlast_ok = function(q, envir) {
3395-
length(q) == 3L &&
3396-
is_constantish(q[[3L]], check_singleton = TRUE) &&
3397-
is.numeric(n <- eval(q[[3L]], envir)) && length(n)==1L && !is.na(n) && n>=1
3396+
q = match.call(first, q) # first's signature is the same as last's: first(x, n=1L, na.rm=FALSE, ...)
3397+
(is.null(q[["n"]]) || (is_constantish(q[["n"]], check_singleton=TRUE) &&
3398+
is.numeric(n <- eval(q[["n"]], envir)) && length(n)==1L && !is.na(n) && n>=1)) &&
3399+
(is.null(q[["na.rm"]]) || (is_constantish(q[["na.rm"]], check_singleton=TRUE) &&
3400+
isTRUEorFALSE(eval(q[["na.rm"]], envir))))
33983401
}
33993402
`.g[_ok` = function(q, x, envir=parent.frame(3L)) {
34003403
length(q) == 3L &&
@@ -3449,7 +3452,7 @@ is_constantish = function(q, check_singleton=FALSE) {
34493452
if (!is.null(q1)) {
34503453
q2 = .unwrap_conversions(q[[2L]])
34513454
if (!is.symbol(q2) || (!q2 %chin% names(x) && q2 != ".I")) return(FALSE)
3452-
if (length(q)==2L || (.arg_is_narm(q) && is_constantish(q[[3L]]) &&
3455+
if (length(q)==2L || (length(q)==3L && .arg_is_narm(q) && is_constantish(q[[3L]]) &&
34533456
!(is.symbol(q[[3L]]) && q[[3L]] %chin% names(x)))) return(TRUE)
34543457
return(switch(as.character(q1),
34553458
"shift" = .gshift_ok(q),

R/last.R

Lines changed: 28 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,20 @@
22
# for xts class objects it will dispatch to xts::last
33
# reworked to avoid loading xts namespace (#3857) then again to fix dispatching of xts class (#4053)
44
# nocov start. Tests 19.* in other.Rraw, not in the main suite.
5-
last = function(x, n=1L, ...) {
5+
last = function(x, n=1L, na.rm=FALSE, ...) {
66
verbose = isTRUE(getOption("datatable.verbose", FALSE))
77
if (!inherits(x, "xts")) {
88
if (nargs()>1L) {
99
if ("package:xts" %chin% search()) {
1010
if (verbose)
1111
catf("%s: using %s: %s\n", "last", "xts::last", "!is.xts(x) & nargs>1 & 'package:xts'%in%search()")
12-
xts::last(x, n=n, ...)
12+
xts::last(x, n=n, na.rm=na.rm, ...)
13+
} else if (is.null(dim(x)) && !is.data.frame(x)) {
14+
if (verbose)
15+
catf("%s: using %s: %s\n", "last", "'.firstlast'", "!is.xts(x) & nargs>1 & is.null(dim(x)) & !is.data.frame(x)")
16+
.firstlast(x, n=n, first=FALSE, na.rm=na.rm)
1317
} else {
18+
if (!isFALSE(na.rm)) stopf("na.rm=TRUE is not currently supported for '%s'", class(x)[1L])
1419
# nocov start
1520
if (verbose)
1621
catf("%s: using %s: %s\n", "last", "utils::tail", "!is.xts(x) & nargs>1 & !'package:xts'%in%search()")
@@ -39,19 +44,24 @@ last = function(x, n=1L, ...) {
3944
stopf("'xts' class passed to %s function but 'xts' is not available, you should have 'xts' installed already", "data.table::last") # nocov
4045
if (verbose)
4146
catf("%s: using %s: %s\n", "last", "xts::last", "is.xts(x)")
42-
xts::last(x, n=n, ...)
47+
xts::last(x, n=n, na.rm=na.rm, ...)
4348
}
4449
}
4550

46-
first = function(x, n=1L, ...) {
51+
first = function(x, n=1L, na.rm=FALSE, ...) {
4752
verbose = isTRUE(getOption("datatable.verbose", FALSE))
4853
if (!inherits(x, "xts")) {
4954
if (nargs()>1L) {
5055
if ("package:xts" %chin% search()) {
5156
if (verbose)
5257
catf("%s: using %s: %s\n", "first", "xts::first", "!is.xts(x) & nargs>1 & 'package:xts'%in%search()")
53-
xts::first(x, n=n, ...)
58+
xts::first(x, n=n, na.rm=na.rm, ...)
59+
} else if (is.null(dim(x)) && !is.data.frame(x)) {
60+
if (verbose)
61+
catf("%s: using %s: %s\n", "first", "'.firstlast'", "!is.xts(x) & nargs>1 & is.null(dim(x)) & !is.data.frame(x)")
62+
.firstlast(x, n=n, first=TRUE, na.rm=na.rm)
5463
} else {
64+
if (!isFALSE(na.rm)) stopf("na.rm=TRUE is not currently supported for '%s'", class(x)[1L])
5565
# nocov start
5666
if (verbose)
5767
catf("%s: using %s: %s\n", "first", "utils::head", "!is.xts(x) & nargs>1 & !'package:xts'%in%search()")
@@ -80,7 +90,19 @@ first = function(x, n=1L, ...) {
8090
stopf("'xts' class passed to %s function but 'xts' is not available, you should have 'xts' installed already", "data.table::first") # nocov
8191
if (verbose)
8292
catf("%s: using %s: %s\n", "first", "xts::first", "is.xts(x)")
83-
xts::first(x, n=n, ...)
93+
xts::first(x, n=n, na.rm=na.rm, ...)
8494
}
8595
}
8696
# nocov end
97+
98+
.firstlast = function(x, n, first, na.rm) {
99+
if (!isTRUEorFALSE(na.rm)) stopf("'%s' must be TRUE or FALSE", "na.rm")
100+
if (!na.rm) return(if (first) utils::head(x, n=n) else utils::tail(x, n=n))
101+
if (!length(x)) return(x)
102+
# matches 'missing' used for GForce first()/last()
103+
# for a list, an element is missing when it is NULL or a length-1 logical NA, not (only) when is.na()
104+
isna = if (is.list(x)) vapply(x, function(el) is.null(el) || (is.logical(el) && length(el)==1L && is.na(el)), FALSE) else is.na(x)
105+
nna = which(!isna)
106+
if (!length(nna)) return(if (n==1L) x[NA_integer_] else x[0L])
107+
x[if (first) utils::head(nna, n) else utils::tail(nna, n)]
108+
}

inst/tests/optimize.Rraw

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -522,4 +522,55 @@ test(2285.01, optimize=opt, DT5[, .(shift(x), mean(x)), by=g, verbose=TRUE],
522522
test(2285.02, copy(DT5)[, c("s","m") := .(shift(x), mean(x)), by=g],
523523
data.table(g=c(1,1,1,2,2), x=1:5, y=1:5, s=c(NA,1L,2L,NA,4L), m=c(2,2,2,4.5,4.5)))
524524
test(2285.03, optimize=opt, DT5[, .(head(x,2), head(y,3)), by=g], error="Supplied 2 items for column 1 of group 1 which has 3 rows")
525-
# head/tail with := and mismatched n: previously errored (Supplied N items...), now aligns/pads instead, see tests 2233.28/2233.29 in tests.Rraw
525+
526+
# first()/last() gain na.rm=, #4239 #4446. GForce optimized (gfirst()/glast()) for both the simple
527+
# (n=1, always exactly 1 row per group) and n>1 (data-dependent rows per group, using the same
528+
# gforce_dynamic mechanism as n>1 without na.rm) cases.
529+
opt = 0:2
530+
DT = data.table(g=c(1,1,1,2,2), x=c(1,NA,3,NA,5), y=11:15)
531+
out = c("GForce FALSE", "GForce FALSE", "GForce optimized j")
532+
test(2286.01, optimize=opt, DT[, first(x, na.rm=TRUE), by=g, verbose=TRUE],
533+
data.table(g=c(1,2), V1=c(1,5)), output=out)
534+
test(2286.02, optimize=opt, DT[, .(first(x, na.rm=TRUE), mean(y)), by=g, verbose=TRUE],
535+
data.table(g=c(1,2), V1=c(1,5), V2=c(12,14.5)), output=out)
536+
test(2286.03, optimize=opt, DT[, last(x, na.rm=TRUE), by=g, verbose=TRUE],
537+
data.table(g=c(1,2), V1=c(3,5)), output=out)
538+
test(2286.04, optimize=opt, DT[, first(x, n=2, na.rm=TRUE), by=g, verbose=TRUE],
539+
data.table(g=c(1,1,2), V1=c(1,3,5)), output=out)
540+
test(2286.05, optimize=opt, DT[, last(x, n=2, na.rm=TRUE), by=g],
541+
data.table(g=c(1,1,2), V1=c(1,3,5)))
542+
test(2286.06, optimize=opt, DT[, .(first(x, n=2, na.rm=TRUE), mean(y)), by=g],
543+
data.table(g=c(1,1,2), V1=c(1,3,5), V2=c(12,12,14.5)))
544+
test(2286.07, optimize=opt, copy(DT)[, v := first(x, na.rm=TRUE), by=g, verbose=TRUE],
545+
data.table(g=c(1,1,1,2,2), x=c(1,NA,3,NA,5), y=11:15, v=c(1,1,1,5,5)), output=out)
546+
test(2286.08, DT[, v := first(x, n=2, na.rm=TRUE), by=g],
547+
error="Supplied 3 items to be assigned to 5 items of column 'v'.")
548+
549+
# all-NA group returns NA, like gmedian/gvar do for insufficient data, not an empty/dropped group
550+
DT = data.table(g=c(1,1,2,2), x=c(NA,NA,3,4))
551+
test(2286.11, optimize=opt, DT[, first(x, na.rm=TRUE), by=g],
552+
data.table(g=c(1,2), V1=c(NA,3)))
553+
test(2286.12, optimize=opt, DT[, first(x, n=2, na.rm=TRUE), by=g],
554+
data.table(g=2, V1=c(3,4)))
555+
DT = data.table(g=c(1,1,1,1), x=c(1,2,3,4), y=c(NA,6,7,8))
556+
test(2286.13, optimize=opt, DT[, .(first(x, n=2), first(y, n=3, na.rm=TRUE)), by=g],
557+
error="Supplied 2 items for column 1 of group 1 which has 3 rows")
558+
DT = data.table(g=c(1,1,1,1), x=c(1,NA,3,4), y=c(NA,NA,NA,9))
559+
test(2286.14, optimize=opt, DT[, .(first(x, n=2, na.rm=TRUE), first(y, n=2, na.rm=TRUE)), by=g],
560+
data.table(g=c(1,1), V1=c(1,3), V2=c(9,9)))
561+
DT = data.table(g=c(1,1,1,2,2), s=c("a",NA,"c",NA,"e"), l=list(1,NA,3,NA,5),
562+
lg=c(TRUE,NA,FALSE,NA,TRUE), cx=c(1+1i,NA,3+3i,NA,5+5i))
563+
test(2286.21, DT[, .(first(s,na.rm=TRUE)), by=g], data.table(g=c(1,2), V1=c("a","e")))
564+
test(2286.22, DT[, .(first(l,na.rm=TRUE)), by=g], data.table(g=c(1,2), V1=list(1,5)))
565+
test(2286.23, DT[, .(first(lg,na.rm=TRUE)), by=g], data.table(g=c(1,2), V1=c(TRUE,TRUE)))
566+
test(2286.24, DT[, .(first(cx,na.rm=TRUE)), by=g], data.table(g=c(1,2), V1=c(1+1i,5+5i)))
567+
if (test_bit64) {
568+
DT[, i64 := as.integer64(c(1,NA,3,NA,5))]
569+
test(2286.25, DT[, .(first(i64,na.rm=TRUE)), by=g], data.table(g=c(1,2), V1=bit64::as.integer64(c(1,5))))
570+
}
571+
test(2286.31, first(c(1,NA,3,NA,5), na.rm=TRUE), 1)
572+
test(2286.32, first(c(1,NA,3,NA,5), n=2, na.rm=TRUE), c(1,3))
573+
test(2286.33, last(c(1,NA,3,NA,5), n=2, na.rm=TRUE), c(3,5))
574+
test(2286.34, first(c(NA_real_,NA_real_), na.rm=TRUE), NA_real_)
575+
test(2286.35, first(list(1,NULL,3,NA,5), n=3, na.rm=TRUE), list(1,3,5))
576+
test(2286.36, first(x <- data.frame(a=1:2), na.rm=TRUE), error="na.rm=TRUE is not currently supported for 'data.frame'")

man/last.Rd

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,18 @@ or data.table. The main difference to head/tail is that the default for \code{n}
88
rather than 6.
99
}
1010
\usage{
11-
first(x, n=1L, \dots)
12-
last(x, n=1L, \dots)
11+
first(x, n=1L, na.rm=FALSE, \dots)
12+
last(x, n=1L, na.rm=FALSE, \dots)
1313
}
1414
\arguments{
1515
\item{x}{ A vector, list, data.frame or data.table. Otherwise the S3 method
1616
of \code{xts::first} is deployed. }
1717
\item{n}{ A numeric vector length 1. How many items to select. }
18+
\item{na.rm}{ \code{TRUE} or \code{FALSE} (default). When \code{TRUE}, missing values in \code{x}
19+
(\code{NA}, or \code{NULL} list elements) are skipped, so that the first/last \code{n}
20+
\emph{non-missing} items are returned; fewer than \code{n} are returned if fewer than \code{n}
21+
non-missing items are found. Only supported when \code{x} is a plain vector or list (not
22+
\code{data.frame}, \code{data.table}, matrix/array, or \code{xts}). }
1823
\item{\dots}{ Not applicable for \code{data.table} first/last. Any arguments here
1924
are passed through to \code{xts}'s first/last. }
2025
}
@@ -37,5 +42,12 @@ first(x) # same as head(x, 1)
3742
last(1:5) # [1] 5
3843
x = data.table(x=1:5, y=6:10)
3944
last(x) # same as tail(x, 1)
45+
46+
first(c(NA, 2, NA, 4), na.rm=TRUE) # [1] 2
47+
last(c(1, NA, 3, NA), n=2, na.rm=TRUE) # [1] 1 3
48+
49+
DT = data.table(g=c(1,1,1,2,2), x=c(1,NA,3,NA,5))
50+
DT[, first(x, na.rm=TRUE), by=g] # first non-missing x per group
51+
DT[, first(x, n=2, na.rm=TRUE), by=g] # up to 2 non-missing x per group; fewer rows if fewer are non-missing
4052
}
4153
\keyword{ data }

src/data.table.h

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -453,8 +453,8 @@ SEXP rleid(SEXP, SEXP);
453453
SEXP gmedian(SEXP, SEXP);
454454
SEXP gtail(SEXP, SEXP);
455455
SEXP ghead(SEXP, SEXP);
456-
SEXP glast(SEXP, SEXP);
457-
SEXP gfirst(SEXP, SEXP);
456+
SEXP glast(SEXP, SEXP, SEXP);
457+
SEXP gfirst(SEXP, SEXP, SEXP);
458458
SEXP gnthvalue(SEXP, SEXP);
459459
SEXP dim(SEXP);
460460
SEXP warn_matrix_column_r(SEXP);

0 commit comments

Comments
 (0)