Skip to content

Commit e7c4591

Browse files
authored
Merge pull request #124 from tidyomics/new-slices
New slices
2 parents 9009409 + 4ea5b1c commit e7c4591

4 files changed

Lines changed: 525 additions & 16 deletions

File tree

NAMESPACE

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,21 @@ S3method(setdiff_ranges_directed,GenomicRanges)
174174
S3method(slice,GroupedGenomicRanges)
175175
S3method(slice,GroupedIntegerRanges)
176176
S3method(slice,Ranges)
177+
S3method(slice_head,GroupedGenomicRanges)
178+
S3method(slice_head,GroupedIntegerRanges)
179+
S3method(slice_head,Ranges)
180+
S3method(slice_max,GroupedGenomicRanges)
181+
S3method(slice_max,GroupedIntegerRanges)
182+
S3method(slice_max,Ranges)
183+
S3method(slice_min,GroupedGenomicRanges)
184+
S3method(slice_min,GroupedIntegerRanges)
185+
S3method(slice_min,Ranges)
186+
S3method(slice_sample,GroupedGenomicRanges)
187+
S3method(slice_sample,GroupedIntegerRanges)
188+
S3method(slice_sample,Ranges)
189+
S3method(slice_tail,GroupedGenomicRanges)
190+
S3method(slice_tail,GroupedIntegerRanges)
191+
S3method(slice_tail,Ranges)
177192
S3method(slide_ranges,Ranges)
178193
S3method(stretch,AnchoredGenomicRanges)
179194
S3method(stretch,AnchoredIntegerRanges)
@@ -434,6 +449,11 @@ importFrom(dplyr,pull)
434449
importFrom(dplyr,sample_n)
435450
importFrom(dplyr,select)
436451
importFrom(dplyr,slice)
452+
importFrom(dplyr,slice_head)
453+
importFrom(dplyr,slice_max)
454+
importFrom(dplyr,slice_min)
455+
importFrom(dplyr,slice_sample)
456+
importFrom(dplyr,slice_tail)
437457
importFrom(dplyr,summarise)
438458
importFrom(dplyr,summarize)
439459
importFrom(dplyr,tbl_vars)

R/dplyr-slice.R

Lines changed: 324 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,44 @@ generate_slice <- function(.data, dots) {
99
inx
1010
}
1111

12-
#' Choose rows by their position
12+
#' Choose ranges by their position
13+
#'
14+
#' @description
15+
#' The `slice` family of functions let you select ranges by position. They
16+
#' follow the same conventions as their dplyr counterparts for data frames:
17+
#'
18+
#' * `slice()` selects ranges by explicit integer position.
19+
#' * `slice_head()` / `slice_tail()` select the first or last ranges.
20+
#' * `slice_sample()` randomly selects ranges.
21+
#' * `slice_min()` / `slice_max()` select ranges with the smallest or largest
22+
#' values of a metadata column.
23+
#'
24+
#' All functions preserve grouping: when `.data` has been grouped with
25+
#' [group_by()], selection is performed within each group.
26+
#'
27+
#' @param .data a `Ranges` object.
28+
#' @param ... <[`data-masking`][rlang::args_data_masking]> Integer row values
29+
#' indicating positions to keep. Only used by `slice()`; ignored elsewhere.
30+
#' @param n Number of ranges to select. Mutually exclusive with `prop`.
31+
#' For `slice_min()` and `slice_max()`, negative values are not supported.
32+
#' @param prop Fraction of ranges to select, in `(0, 1]`. Mutually exclusive
33+
#' with `n`.
34+
#' @param order_by <[`data-masking`][rlang::args_data_masking]> Unquoted name
35+
#' of a numeric metadata column to order by. Used by `slice_min()` and
36+
#' `slice_max()`.
37+
#' @param with_ties If `TRUE` (the default), all ranges tied at the boundary
38+
#' rank are kept, which may return more than `n` ranges. Set to `FALSE` to
39+
#' return exactly `n`. Used by `slice_min()` and `slice_max()`.
40+
#' @param weight_by <[`data-masking`][rlang::args_data_masking]> Unquoted name
41+
#' of a non-negative numeric metadata column used as sampling weights. Used
42+
#' by `slice_sample()`.
43+
#' @param replace Should sampling be with replacement? Default `FALSE`. Used
44+
#' by `slice_sample()`.
45+
#' @param .preserve Ignored; retained for compatibility with the dplyr generic.
46+
#'
47+
#' @return A `GRanges` (or `IRanges`) object of the same class as `.data`, with
48+
#' grouping structure re-applied when `.data` was grouped.
1349
#'
14-
#' @param .data a `Ranges` object
15-
#' @param ... Integer row values indicating rows to keep. If `.data` has
16-
#' been grouped via [group_by.GenomicRanges()], then the positions are selected within each group.
17-
#' @param .preserve when FALSE (the default) the grouping structure is
18-
#' recomputed, otherwise it is kept as is. Currently ignored.
19-
#' @return a GRanges object
2050
#' @method slice Ranges
2151
#' @importFrom dplyr slice
2252
#' @rdname slice-ranges
@@ -97,5 +127,292 @@ check_length <- function(length, n, replace = FALSE) {
97127
)
98128
}
99129

130+
# the following generated by Claude Sonnet 4.6 as part of adding new slice functionality
131+
# 2026-03-18
132+
133+
# ── slice_head() methods ───────────────────────────────────────────────────────
134+
135+
#' @rdname slice-ranges
136+
#' @importFrom dplyr slice_head
137+
#' @method slice_head Ranges
138+
#' @export
139+
slice_head.Ranges <- function(.data, ..., n = NULL, prop = NULL,
140+
.preserve = FALSE) {
141+
inx <- head_tail_index(length(.data), n = n, prop = prop, from_head = TRUE)
142+
.data[inx, ]
143+
}
144+
145+
#' @rdname slice-ranges
146+
#' @method slice_head GroupedGenomicRanges
147+
#' @export
148+
slice_head.GroupedGenomicRanges <- function(.data, ..., n = NULL, prop = NULL,
149+
.preserve = FALSE) {
150+
apply_grouped_slice(
151+
.data,
152+
function(grp_size) head_tail_index(grp_size, n = n, prop = prop,
153+
from_head = TRUE)
154+
)
155+
}
156+
157+
#' @rdname slice-ranges
158+
#' @method slice_head GroupedIntegerRanges
159+
#' @export
160+
slice_head.GroupedIntegerRanges <- slice_head.GroupedGenomicRanges
161+
162+
163+
# ── slice_tail() methods ───────────────────────────────────────────────────────
164+
165+
#' @rdname slice-ranges
166+
#' @importFrom dplyr slice_tail
167+
#' @method slice_tail Ranges
168+
#' @export
169+
slice_tail.Ranges <- function(.data, ..., n = NULL, prop = NULL,
170+
.preserve = FALSE) {
171+
inx <- head_tail_index(length(.data), n = n, prop = prop, from_head = FALSE)
172+
.data[inx, ]
173+
}
174+
175+
#' @rdname slice-ranges
176+
#' @method slice_tail GroupedGenomicRanges
177+
#' @export
178+
slice_tail.GroupedGenomicRanges <- function(.data, ..., n = NULL, prop = NULL,
179+
.preserve = FALSE) {
180+
apply_grouped_slice(
181+
.data,
182+
function(grp_size) head_tail_index(grp_size, n = n, prop = prop,
183+
from_head = FALSE)
184+
)
185+
}
186+
187+
#' @rdname slice-ranges
188+
#' @method slice_tail GroupedIntegerRanges
189+
#' @export
190+
slice_tail.GroupedIntegerRanges <- slice_tail.GroupedGenomicRanges
191+
192+
193+
# ── slice_sample() methods ─────────────────────────────────────────────────────
194+
195+
#' @rdname slice-ranges
196+
#' @importFrom dplyr slice_sample
197+
#' @method slice_sample Ranges
198+
#' @export
199+
slice_sample.Ranges <- function(.data, ..., n = NULL, prop = NULL,
200+
weight_by = NULL, replace = FALSE,
201+
.preserve = FALSE) {
202+
total <- length(.data)
203+
size <- resolve_n_prop(total, n = n, prop = prop, replace = replace)
204+
weights <- eval_weight_by(.data, rlang::enquo(weight_by))
205+
inx <- sample.int(total, size, replace = replace, prob = weights)
206+
.data[inx, ]
207+
}
208+
209+
#' @rdname slice-ranges
210+
#' @method slice_sample GroupedGenomicRanges
211+
#' @export
212+
slice_sample.GroupedGenomicRanges <- function(.data, ..., n = NULL,
213+
prop = NULL, weight_by = NULL,
214+
replace = FALSE,
215+
.preserve = FALSE) {
216+
weight_quo <- rlang::enquo(weight_by)
217+
rng <- .data@delegate
218+
grp_rows <- .group_rows(.data)
100219

220+
selected <- lapply(grp_rows, function(i) {
221+
sub <- rng[i]
222+
size <- resolve_n_prop(length(i), n = n, prop = prop, replace = replace)
223+
w <- eval_weight_by(sub, weight_quo)
224+
local_inx <- sample.int(length(i), size, replace = replace, prob = w)
225+
i[local_inx]
226+
})
101227

228+
rng_out <- rng[sort(unlist(selected))]
229+
group_by(rng_out, !!!groups(.data))
230+
}
231+
232+
#' @rdname slice-ranges
233+
#' @method slice_sample GroupedIntegerRanges
234+
#' @export
235+
slice_sample.GroupedIntegerRanges <- slice_sample.GroupedGenomicRanges
236+
237+
238+
# ── slice_min() methods ────────────────────────────────────────────────────────
239+
240+
#' @rdname slice-ranges
241+
#' @importFrom dplyr slice_min
242+
#' @method slice_min Ranges
243+
#' @export
244+
slice_min.Ranges <- function(.data, order_by, n = NULL, prop = NULL,
245+
with_ties = TRUE, .preserve = FALSE) {
246+
order_quo <- rlang::enquo(order_by)
247+
inx <- minmax_index(.data, order_quo, n = n, prop = prop,
248+
with_ties = with_ties, decreasing = FALSE)
249+
.data[inx, ]
250+
}
251+
252+
#' @rdname slice-ranges
253+
#' @method slice_min GroupedGenomicRanges
254+
#' @export
255+
slice_min.GroupedGenomicRanges <- function(.data, order_by, n = NULL,
256+
prop = NULL, with_ties = TRUE,
257+
.preserve = FALSE) {
258+
order_quo <- rlang::enquo(order_by)
259+
apply_grouped_minmax(.data, order_quo, n = n, prop = prop,
260+
with_ties = with_ties, decreasing = FALSE)
261+
}
262+
263+
#' @rdname slice-ranges
264+
#' @method slice_min GroupedIntegerRanges
265+
#' @export
266+
slice_min.GroupedIntegerRanges <- slice_min.GroupedGenomicRanges
267+
268+
269+
# ── slice_max() methods ────────────────────────────────────────────────────────
270+
271+
#' @rdname slice-ranges
272+
#' @importFrom dplyr slice_max
273+
#' @method slice_max Ranges
274+
#' @export
275+
slice_max.Ranges <- function(.data, order_by, n = NULL, prop = NULL,
276+
with_ties = TRUE, .preserve = FALSE) {
277+
order_quo <- rlang::enquo(order_by)
278+
inx <- minmax_index(.data, order_quo, n = n, prop = prop,
279+
with_ties = with_ties, decreasing = TRUE)
280+
.data[inx, ]
281+
}
282+
283+
#' @rdname slice-ranges
284+
#' @method slice_max GroupedGenomicRanges
285+
#' @export
286+
slice_max.GroupedGenomicRanges <- function(.data, order_by, n = NULL,
287+
prop = NULL, with_ties = TRUE,
288+
.preserve = FALSE) {
289+
order_quo <- rlang::enquo(order_by)
290+
apply_grouped_minmax(.data, order_quo, n = n, prop = prop,
291+
with_ties = with_ties, decreasing = TRUE)
292+
}
293+
294+
#' @rdname slice-ranges
295+
#' @method slice_max GroupedIntegerRanges
296+
#' @export
297+
slice_max.GroupedIntegerRanges <- slice_max.GroupedGenomicRanges
298+
299+
300+
# ── Internal helpers ───────────────────────────────────────────────────────────
301+
302+
generate_slice <- function(.data, dots) {
303+
os <- overscope_ranges(.data)
304+
inx <- overscope_eval_update(os, dots, bind_envir = FALSE)
305+
check_num <- vapply(inx, is.numeric, logical(1))
306+
if (any(!check_num)) {
307+
stop("slice condition does not evaluate to an integer or numeric vector")
308+
}
309+
inx
310+
}
311+
312+
# Resolve n / prop to a concrete integer size.
313+
resolve_n_prop <- function(total, n = NULL, prop = NULL, replace = FALSE) {
314+
if (!is.null(n) && !is.null(prop)) {
315+
stop("Exactly one of `n` or `prop` must be supplied, not both.")
316+
}
317+
if (!is.null(prop)) {
318+
if (prop <= 0 || prop > 1) stop("`prop` must be in (0, 1].")
319+
n <- max(1L, floor(prop * total))
320+
}
321+
if (is.null(n)) n <- 1L # default to n = 1
322+
n <- as.integer(n)
323+
if (!replace && n > total) {
324+
stop(paste("`n` (", n, ") must be <= length of .data (", total, ").",
325+
"Set `replace = TRUE` to use sampling with replacement."))
326+
}
327+
n
328+
}
329+
330+
# Build a positional index for head / tail selection.
331+
head_tail_index <- function(total, n = NULL, prop = NULL, from_head) {
332+
size <- resolve_n_prop(total, n = n, prop = prop)
333+
if (from_head) seq_len(size) else seq.int(total - size + 1L, total)
334+
}
335+
336+
# Evaluate an optional weight_by quosure against .data's mcols.
337+
eval_weight_by <- function(.data, weight_quo) {
338+
if (rlang::quo_is_null(weight_quo)) return(NULL)
339+
col_name <- rlang::as_name(weight_quo)
340+
w <- mcols(.data)[[col_name]]
341+
if (is.null(w)) stop(paste0("Column '", col_name, "' not found in .data."))
342+
if (any(w < 0, na.rm = TRUE)) stop("`weight_by` values must be non-negative.")
343+
w
344+
}
345+
346+
# Shared guard used by slice_min / slice_max.
347+
check_column_numeric <- function(x, col) {
348+
if (!is.numeric(x)) {
349+
stop(paste0("`", col, "` must be a numeric column for slice_min / slice_max"))
350+
}
351+
}
352+
353+
# Build an ordering index for slice_min / slice_max.
354+
minmax_index <- function(.data, order_quo, n = NULL, prop = NULL, with_ties,
355+
decreasing) {
356+
col_name <- rlang::as_name(order_quo)
357+
vals <- mcols(.data)[[col_name]]
358+
if (is.null(vals)) stop(paste0("Column '", col_name, "' not found in .data."))
359+
check_column_numeric(vals, col_name)
360+
361+
total <- length(.data)
362+
size <- resolve_n_prop(total, n = n, prop = prop)
363+
ord <- order(vals, decreasing = decreasing, na.last = NA)
364+
365+
if (with_ties) {
366+
threshold <- vals[ord[min(size, length(ord))]]
367+
keep <- if (decreasing) which(vals >= threshold) else which(vals <= threshold)
368+
sort(keep)
369+
} else {
370+
sort(ord[seq_len(min(size, length(ord)))])
371+
}
372+
}
373+
374+
# Apply a grouped slice using a per-group index function.
375+
# `index_fn` receives the group size and returns *local* indices.
376+
apply_grouped_slice <- function(.data, index_fn) {
377+
rng <- .data@delegate
378+
grp_rows <- .group_rows(.data)
379+
380+
selected <- lapply(grp_rows, function(i) {
381+
local_inx <- index_fn(length(i))
382+
local_inx <- local_inx[local_inx >= 1L & local_inx <= length(i)]
383+
i[local_inx]
384+
})
385+
386+
rng_out <- rng[sort(unlist(selected))]
387+
group_by(rng_out, !!!groups(.data))
388+
}
389+
390+
# Grouped min / max helper.
391+
apply_grouped_minmax <- function(.data, order_quo, n = NULL, prop = NULL,
392+
with_ties, decreasing) {
393+
col_name <- rlang::as_name(order_quo)
394+
rng <- .data@delegate
395+
grp_rows <- .group_rows(.data)
396+
397+
selected <- lapply(grp_rows, function(i) {
398+
sub <- rng[i]
399+
vals <- mcols(sub)[[col_name]]
400+
if (is.null(vals)) stop(paste0("Column '", col_name, "' not found."))
401+
check_column_numeric(vals, col_name)
402+
403+
size <- resolve_n_prop(length(i), n = n, prop = prop)
404+
ord <- order(vals, decreasing = decreasing, na.last = NA)
405+
406+
local_inx <- if (with_ties) {
407+
threshold <- vals[ord[min(size, length(ord))]]
408+
if (decreasing) which(vals >= threshold) else which(vals <= threshold)
409+
} else {
410+
ord[seq_len(min(size, length(ord)))]
411+
}
412+
413+
i[sort(local_inx)]
414+
})
415+
416+
rng_out <- rng[sort(unlist(selected))]
417+
group_by(rng_out, !!!groups(.data))
418+
}

0 commit comments

Comments
 (0)