Skip to content

Commit 25c9dde

Browse files
Support reverse ranking in frank() (#7874)
* updated changes * .. * . * .. * .. * corected typo * finalize PR --------- Co-authored-by: Benjamin Schwendinger <benjaminschwe@gmail.com>
1 parent fe232c8 commit 25c9dde

4 files changed

Lines changed: 57 additions & 12 deletions

File tree

NEWS.md

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

4545
11. `setorderv()` now accepts a named vector for the `order` argument. When provided, the names are used to identify the columns, allowing the `cols` argument to be omitted, [#6932](https://github.com/Rdatatable/data.table/issues/6932). Thanks to @MichaelChirico for the suggestion and @venom1204 for the implementation.
4646

47+
12. `frank()` gains an `order` argument (matching `frankv()`) and now intercepts the unary minus symbol (e.g., `frank(-dates)`) to support reverse ranking even for types where unary `-` is not defined in R, such as `Date` or `character` vectors, [#5489](https://github.com/Rdatatable/data.table/issues/5489). Thanks @hope-data-science for the request and @venom1204 for the implementation.
48+
4749
### BUG FIXES
4850

4951
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/frank.R

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -73,27 +73,41 @@ frankv = function(x, cols=seq_along(x), order=1L, na.last=TRUE, ties.method=c("a
7373
ans
7474
}
7575

76-
frank = function(x, ..., na.last=TRUE, ties.method=c("average", "first", "last", "random", "max", "min", "dense")) {
76+
frank = function(x, ..., order=1L, na.last=TRUE, ties.method=c("average", "first", "last", "random", "max", "min", "dense")) {
77+
order_missing = missing(order)
78+
has_prefix = FALSE
79+
q_x = substitute(x)
80+
if (is.call(q_x) && length(q_x) == 2L && (q_x[[1L]] == quote(`-`) || q_x[[1L]] == quote(`+`))) {
81+
has_prefix = TRUE
82+
if (order_missing) order = if (q_x[[1L]] == quote(`-`)) -1L else 1L
83+
x = eval(q_x[[2L]], parent.frame())
84+
}
85+
7786
cols = substitute(list(...))[-1L]
7887
if (identical(as.character(cols), "NULL")) {
7988
cols = NULL
80-
order = 1L
8189
} else if (length(cols)) {
8290
cols=as.list(cols)
83-
order=rep(1L, length(cols))
91+
if (length(order) == 1L) order = rep(as.integer(order), length(cols))
92+
8493
for (i in seq_along(cols)) {
8594
v=as.list(cols[[i]])
86-
if (length(v) > 1L && v[[1L]] == "+") v=v[[-1L]]
87-
else if (length(v) > 1L && v[[1L]] == "-") {
95+
if (length(v) > 1L && (v[[1L]] == "+" || v[[1L]] == "-")) {
96+
has_prefix = TRUE
97+
if (order_missing && v[[1L]] == "-") order[i] = -1L
8898
v=v[[-1L]]
89-
order[i] = -1L
9099
}
91100
cols[[i]]=as.character(v)
92101
}
93102
cols=unlist(cols, use.names=FALSE)
94103
} else {
95104
cols=colnames(x)
96-
order=if (is.null(cols)) 1L else rep(1L, length(cols))
105+
if (!is.null(cols) && length(order) == 1L) {
106+
order = rep(as.integer(order), length(cols))
107+
}
108+
}
109+
if (has_prefix && !order_missing) {
110+
warningf("Both prefix sign and 'order' argument are provided; 'order' will take precedence.")
97111
}
98112
frankv(x, cols=cols, order=order, na.last=na.last, ties.method=ties.method)
99113

inst/tests/tests.Rraw

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21952,3 +21952,25 @@ test(2385.04, setorderv(DT, order=c(x=1L, x=-1L)), error="order argument has nam
2195221952
test(2385.05, setorderv(DT, order=c(x=2L)), error="Must be +1 or -1")
2195321953
DT = data.table(a=c(2,1,2), b=3:1)
2195421954
test(2385.06, setorderv(copy(DT), order=c(a=1L)), setorderv(copy(DT), cols="a", order=1L))
21955+
21956+
# #5489 frank could support reverse ranking
21957+
dates = as.Date(c("1992-02-27", "1992-02-27", "1992-01-14", "1992-02-28", "1992-02-01"))
21958+
test(2386.01, frank(-dates, ties.method="min"), frankv(dates, order=-1L, ties.method="min"))
21959+
ct = as.POSIXct(c("2020-01-03 10:00:00", "2020-01-03 10:00:00", "2020-01-01 08:00:00", "2020-01-05 12:00:00", "2020-01-02 09:00:00"))
21960+
test(2386.02, frank(-ct, ties.method="min"), frankv(ct, order=-1L, ties.method="min"))
21961+
idates = as.IDate(c("2020-01-03", "2020-01-03", "2020-01-01", "2020-01-05", "2020-01-02"))
21962+
test(2386.03, frank(-idates), frankv(idates, order=-1L))
21963+
it = as.ITime(c("10:00:00", "10:00:00", "08:00:00", "12:00:00", "09:00:00"))
21964+
test(2386.04, frank(-it), frankv(it, order=-1L))
21965+
DT = data.table(x=c(2,1,2,1,3), y=c(5,4,3,2,1))
21966+
test(2386.05, frank(DT, -x, y), frankv(DT, cols=c("x", "y"), order=c(-1L, 1L)))
21967+
DT_global = data.table(a=c(1,2,1), b=c(1,1,2))
21968+
test(2386.06, frank(DT_global, order=-1L), frankv(DT_global, order=-1L))
21969+
warn = "Both prefix sign and 'order' argument are provided; 'order' will take precedence."
21970+
test(2386.07, frank(-dates, order=1L), frankv(dates, order=1L), warning=warn)
21971+
test(2386.08, frank(DT, -x, y, order=c(1L, -1L)), frankv(DT, cols=c("x", "y"), order=c(1L, -1L)), warning=warn)
21972+
test(2386.09, frank(DT, -x, y, order=-1L), frankv(DT, cols=c("x", "y"), order=-1L), warning=warn)
21973+
test(2386.10, frank(-dates, order=-1L), frankv(dates, order=-1L), warning=warn)
21974+
test(2386.11, frank(+dates), frankv(dates, order=1L))
21975+
chars = c("b", "a", "c", "a")
21976+
test(2386.12, frank(-chars), frankv(chars, order=-1L))

man/frank.Rd

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
}
1313

1414
\usage{
15-
frank(x, \dots, na.last=TRUE, ties.method=c("average",
15+
frank(x, \dots, order=1L, na.last=TRUE, ties.method=c("average",
1616
"first", "last", "random", "max", "min", "dense"))
1717

1818
frankv(x, cols=seq_along(x), order=1L, na.last=TRUE,
@@ -21,17 +21,19 @@ frankv(x, cols=seq_along(x), order=1L, na.last=TRUE,
2121

2222
}
2323
\arguments{
24-
\item{x}{ A vector, or list with all its elements identical in length or \code{data.frame} or \code{data.table}. }
25-
\item{\dots}{ Only for \code{list}s, \code{data.frame}s and \code{data.table}s. The columns to calculate ranks based on. Do not quote column names. If \code{\dots} is missing, all columns are considered by default. To sort by a column in descending order prefix \code{"-"}, e.g., \code{frank(x, a, -b, c)}. \code{-b} works when \code{b} is of type \code{character} as well.}
24+
\item{x}{ A vector, or list with all its elements identical in length or \code{data.frame} or \code{data.table}. For vector input, unary \code{+} and \code{-} request ascending and descending order, respectively, including for types such as \code{character} and \code{Date} where unary \code{-} is not defined. }
25+
\item{\dots}{ Only for \code{list}s, \code{data.frame}s and \code{data.table}s. The columns to calculate ranks based on. Do not quote column names. If \code{\dots} is missing, all columns are considered by default. Prefix a column with \code{"+"} or \code{"-"} for ascending or descending order, e.g., \code{frank(x, a, -b, c)}. Prefixes work when \code{b} is of type \code{character} or \code{Date} as well.}
2626
\item{cols}{ A \code{character} vector of column names (or numbers) of \code{x}, for which to obtain ranks. }
27-
\item{order}{ An \code{integer} vector with only possible values of 1 and -1, corresponding to ascending and descending order. The length of \code{order} must be either 1 or equal to that of \code{cols}. If \code{length(order) == 1}, it is recycled to \code{length(cols)}. }
27+
\item{order}{ An \code{integer} vector with only possible values of 1 and -1, corresponding to ascending and descending order. The length of \code{order} must be either 1 or equal to that of \code{cols}. If \code{length(order) == 1}, it is recycled to \code{length(cols)}. In \code{frank}, an explicitly supplied \code{order} takes precedence over any unary or column prefix and produces a warning. }
2828
\item{na.last}{ Control treatment of \code{NA}s. If \code{TRUE}, missing values in the data are put last; if \code{FALSE}, they are put first; if \code{NA}, they are removed; if \code{"keep"} they are kept with rank \code{NA}. }
2929
\item{ties.method}{ A character string specifying how ties are treated, see \code{Details}. }
3030
}
3131
\details{
3232
To be consistent with other \code{data.table} operations, \code{NA}s are considered identical to other \code{NA}s (and \code{NaN}s to other \code{NaN}s), unlike \code{base::rank}. Therefore, for \code{na.last=TRUE} and \code{na.last=FALSE}, \code{NA}s (and \code{NaN}s) are given identical ranks, unlike \code{\link[base]{rank}}.
3333

34-
\code{frank} is not limited to vectors. It accepts \code{data.table}s (and \code{list}s and \code{data.frame}s) as well. It accepts unquoted column names (with names preceded with a \code{-} sign for descending order, even on character vectors), for e.g., \code{frank(DT, a, -b, c, ties.method="first")} where \code{a,b,c} are columns in \code{DT}. The equivalent in \code{frankv} is the \code{order} argument.
34+
\code{frank} is not limited to vectors. It accepts \code{data.table}s (and \code{list}s and \code{data.frame}s) as well. It accepts unquoted column names prefixed with \code{+} or \code{-} for ascending or descending order, for e.g., \code{frank(DT, a, -b, c, ties.method="first")} where \code{a,b,c} are columns in \code{DT}.
35+
36+
For vectors, \code{frank(+x)} and \code{frank(-x)} request ascending and descending order without applying the unary operator to \code{x}. This supports types where unary \code{-} is not defined, such as \code{Date} and \code{character}. When \code{order} is omitted, these vector signs and column prefixes determine direction.
3537

3638
In addition to the \code{ties.method} values possible using base's \code{\link[base]{rank}}, it also provides another additional argument \code{"dense"} which returns the ranks without any gaps in the ranking. See examples.
3739
}
@@ -66,6 +68,11 @@ frank(DT, ties.method="first", na.last="keep") # equivalent of above using frank
6668
frank(DT, x, -y, ties.method="first")
6769
# equivalent of above using frankv
6870
frankv(DT, order=c(1L, -1L), ties.method="first")
71+
72+
dates = as.Date("2022-10-19") + c(1, 3, 2, 4)
73+
frank(dates) # ascending
74+
frank(-dates) # descending (intercepts '-' to avoid error)
75+
frank(dates, order=-1L) # also descending
6976
}
7077
\seealso{
7178
\code{\link{data.table}}, \code{\link{setkey}}, \code{\link{setorder}}

0 commit comments

Comments
 (0)