R: How to slice a window of elements in named vector

Viewed 754

Given the following named vector:

x <- c(54, 36, 67, 25, 76)
names(x) <- c('a', 'b', 'c', 'd', 'e')

How one can extract the elements between 'b' and 'd'? I can do that for data tables with the dplyr::select(dt, b:d) but for some reason, I cannot find a solution for named vectors (all the examples I find are for extracting element(s) by giving all the names not a range of names)...

4 Answers

You could do

x[which(names(x) == "b"):which(names(x) == "d")]
#>  b  c  d 
#> 36 67 25 

The problem being that there is no guarantee in a named vector that names are unique, and if there are duplicate names the entire concept becomes meaningless.

If you wanted a complete solution that allows for tidyverse-style non-standard evaluation and sensible error messages you could have

subset_named <- function(data, exp)
{
  if(missing(exp)) return(data)
  exp <- as.list(match.call())$exp
  if(is.numeric(exp)) return(data[exp])
  if(is.character(exp)) return(data[exp])
  
  tryCatch({
    ss <- suppressWarnings(eval(exp))
    return(data[ss])},
    error = function(e)
    {
      if(as.character(exp[[1]]) != ":")
        stop("`exp` must be a sequence created by ':'")
      n <- names(data)
      first <- as.character(exp[[2]])
      second <- as.character(exp[[3]])
      first_match <- which(n == first)
      second_match <- which(n == second)
      if(length(first_match) == 0)
        stop("\"", first, "\" not found in names(", 
             deparse(substitute(data)), ")")
      if(length(second_match) == 0)
        stop("\"", second, "\" not found in names(", 
             deparse(substitute(data)), ")")
      if(length(first_match) > 1) {
        warning("\"", first, 
                "\" found more than once. Using first occurence only")
        first_match <- first_match[1]
      }
      if(length(second_match) > 1) {
        warning("\"", second, 
                "\" found more than once. Using first occurence only")
        second_match <- second_match[1]
      }
      return(data[first_match:second_match])
    })
}

That allows the following behaviour:

subset_named(x, "b":"d")
#>  b  c  d 
#> 36 67 25

subset_named(x, b:d)
#>  b  c  d 
#> 36 67 25

subset_named(x, 1:3)
#>  a  b  c 
#> 54 36 67

subset_named(x, "e")
#>  e 
#> 76

subset_named(x)
#>  a  b  c  d  e 
#> 54 36 67 25 76

One option could be:

x[Reduce(`:`, which(names(x) %in% c("b", "d")))]

 b  c  d 
36 67 25 

You can use match in base R :

x[match('b', names(x)):match('d', names(x))]
# b  c  d 
#36 67 25 

Or if you want to use something like b:d convert it into dataframe as column

library(dplyr)

t(x) %>%
  as.data.frame() %>%
  select(b:d)

1) subset In base R this can be done using the select argument of subset. The only catch is that only the data.frame method of subset supports the select argument but we can convert x to a data.frame and then convert back. It also allows more complex specifications such as c(b:d, d) .

unlist(subset(data.frame(as.list(x)), select = b:d))
##  b  c  d 
## 36 67 25 

2) evalq Another base R possibility is to create a list with the values 1, 2, 3, ... and the same names as x and then evaluate b:d with respect to it giving the desired indexes which can then be indexed into x. This also allows complex specifications as in (1).

x[ evalq(b:d, setNames(as.list(seq_along(x)), names(x))) ]
##  b  c  d 
## 36 67 25 

We could turn this into a function like this:

sel <- function(x, select, envir = parent.frame()) {
  ix <- setNames(as.list(seq_along(x)), names(x))
  x[ eval(substitute(select), ix, envir) ]
}

sel(x, b:d)
sel(x, c(b:c, d))
sel(x, d:b)  # reverse order

3) logical condition Again with only base R, if the names are in sorted order, as in the question, then we can check for names between the endpoints:

x[names(x) >= "b" & names(x) <= "d"]
##  b  c  d 
## 36 67 25 

4) zoo If the names are in ascending order, as in the question, we could create a zoo series with those names as the times and then use window.zoo to pick out the subseries and finally convert back.

library(zoo)

coredata(window(zoo(x, names(x)), start = "b", end = "d"))
##  b  c  d 
## 36 67 25 
Related