Is there a fast way to modify consistent components of a nested list in R?

Viewed 616

I have a vector of values that I wish to use to modify a consistent component of a nested list. Currently I'm using a for loop to do this which is proving quite slow and I'm wondering if there's a faster way to do it.

By way of reproducible example:

#Create Data and List
set.seed(100)
Students <- c("Amy", "Ben", "Caz")
Subject <- c("Maths", "English", "Science")

ExamResults <- lapply(Students, function (r) {
  Scores <- lapply(Subject, function(x) round(runif(4, 0, 100)))
  names(Scores) <- Subject
  Scores
})
names(ExamResults) <- Students

Which creates a list that looks like this:

$Amy
$Amy$Maths
[1] 31 26 55  6

$Amy$English
[1] 47 48 81 37

$Amy$Science
[1] 55 17 62 88

$Ben
$Ben$Maths
[1] 28 40 76 67

$Ben$English
[1] 20 36 36 69

$Ben$Science
[1] 54 71 54 75

$Caz
$Caz$Maths
[1] 42 17 77 88

$Caz$English
[1] 55 28 49 93

$Caz$Science
[1] 35 95 70 89

Now let's say the second maths paper for all students is remarked:

#New Data
MathsRemark <- c(24, 50, 45) #assume in correct (alphabetical) order

And we wish to modify their existing second maths score for these new values. My current solution for doing this is:

#Current Slow Solution
for (i in 1:length(Students)) {
  ExamResults[[i]][["Maths"]][[2]] <- MathsRemark[i]
}

On larger datasets this is proving very slow. Is anyone aware of a faster way to do this? I've been taught to avoid using for loops where vectorising is possible but I can't figure out how to vectorise this problem. Any ideas would be much appreciated.

3 Answers

One option is Map().

Map(function(x, y) { x$Maths[2] <- y; x }, ExamResults, MathsRemark)

A second option is to unlist, replace, and relist.

u <- unlist(ExamResults)
relist(replace(u, endsWith(names(u), "Maths2"), MathsRemark), ExamResults)

I want to point out that the next release of purrr is going to have assign_in, which will allow to very elegantly express your solution as:

library( purrr )
map2( ExamResults, MathsRemark, assign_in, where=list("Maths",2) )

However, it's not nearly as fast as @Rich Scriven's solution:

microbenchmark::microbenchmark(
    f0 = for (i in 1:length(Students))
         {ExamResults[[i]][["Maths"]][[2]] <- MathsRemark[i]},
    f1 = map2( ExamResults, MathsRemark, assign_in, where=list("Maths",2) ),
    f2 = Map(function(x, y) { x$Maths[2] <- y; x }, ExamResults, MathsRemark)
)
# Unit: microseconds
# expr      min        lq       mean    median       uq      max neval
#   f0 2148.158 2258.7700 2580.87901 2333.9455 2741.233 4550.022   100
#   f1  180.081  206.7990  247.42790  235.8160  254.058 1310.912   100
#   f2    9.410   14.7515   33.05673   18.8505   19.943 1558.125   100

You might be better off turning your original list into a dataframe and then modifying. map from the purrr package could help with this.

library(tidyverse)

extract_subject_df<- function(specific_subject){
# Outputs a dataframe for a specific subject
    ExamResults %>% 
        map(specific_subject) %>%
        data.frame() %>%
        mutate(specific_subject=specific_subject)
}

new_df <- map_df(Subject, extract_subject_df)

new_df[new_df$specific_subject == "Maths"][2, 1:3] <- c(24, 50, 45)
Related