Home > OS >  In R, how to modify/ re-assign a list element using index?
In R, how to modify/ re-assign a list element using index?

Time:10-20

Question:

How do I modify an element within an established list and re-assign it back to the list in the same index/position?

Setup and Example

First, here's a dataframe that I am breaking up by groups into a list:

library(tidyverse) # Not absolutely required, but I'm working this way.
df <- tibble(A = rep(paste("Group", c(1:3)),3), 
             B = seq(1, 18, 2),
             C = (1:9))
lst <- df %>% 
  group_by(A) %>% 
  group_split()

Dataframe and resulting list should be as below:

> df
# A tibble: 9 × 3
  A           B     C
  <chr>   <dbl> <int>
1 Group 1     1     1
2 Group 2     3     2
3 Group 3     5     3
4 Group 1     7     4
5 Group 2     9     5
6 Group 3    11     6
7 Group 1    13     7
8 Group 2    15     8
9 Group 3    17     9

> lst
<list_of<
  tbl_df<
    A: character
    B: double
    C: integer
  >
>[3]>
[[1]]
# A tibble: 3 × 3
  A           B     C
  <chr>   <dbl> <int>
1 Group 1     1     1
2 Group 1     7     4
3 Group 1    13     7

[[2]]
# A tibble: 3 × 3
  A           B     C
  <chr>   <dbl> <int>
1 Group 2     3     2
2 Group 2     9     5
3 Group 2    15     8

[[3]]
# A tibble: 3 × 3
  A           B     C
  <chr>   <dbl> <int>
1 Group 3     5     3
2 Group 3    11     6
3 Group 3    17     9

Here's the problem...

For reasons irrelevant here, I need to give each sub-dataframe in the list a different treatment based on the Group. I thought I could apply the modifications in a loop like below, leaving the list structure in place.

for (j in 1:3){
  lst[[j]] <- lst[[j]] %>% 
    mutate(D = B * C)
}

...but that throws this error:

Error in `[[<-`:
! Can't convert from `value` <tbl_df<
  A: character
  B: double
  C: integer
  D: double
>> to <tbl_df<
  A: character
  B: double
  C: integer
>> due to loss of precision.

I know that the assignment back to the list is the problem, because I can successfully do this:

df2 <- NULL
df_final <- NULL

for (j in 1:3){
  
  df2 <- lst[[j]] %>% 
    mutate(D = B * C)
  
  df_final <- rbind(df_final, df2)
}
df_final

...which returns a dataframe that I could break back up as in the beginning.

> df_final
# A tibble: 9 × 4
  A           B     C     D
  <chr>   <dbl> <int> <dbl>
1 Group 1     1     1     1
2 Group 1     7     4    28
3 Group 1    13     7    91
4 Group 2     3     2     6
5 Group 2     9     5    45
6 Group 2    15     8   120
7 Group 3     5     3    15
8 Group 3    11     6    66
9 Group 3    17     9   153

...but I feel like I'm missing some nuance in how the list could be assigned "in place" as above, and I don't understand the error message. What am I missing about assigning to lists that makes lst[[j]] <- lst[[j]] %>% <ANY MODIFICATION> fail?

CodePudding user response:

The result of group_split() is not a simple list, but has some track to the tables inside it that prevents modification of only one item. You can avoid this with lst <- as.list(lst).


library(dplyr) # Not absolutely required, but I'm working this way.

df <- tibble(A = rep(paste("Group", c(1:3)),3), 
             B = seq(1, 18, 2),
             C = (1:9))
lst <- df %>% 
  group_by(A) %>% 
  group_split()

for (j in 1:3){
  lst[[j]] <- lst[[j]] %>% 
    mutate(D = B * C)
}
#> Error in `[[<-`:
#> ! Can't convert from `value` <tbl_df<
#>   A: character
#>   B: double
#>   C: integer
#>   D: double
#> >> to <tbl_df<
#>   A: character
#>   B: double
#>   C: integer
#> >> due to loss of precision.

lst <- as.list(lst)

for (j in 1:3){
  lst[[j]] <- lst[[j]] %>% 
    mutate(D = B * C)
} # OK

df_final <- bind_rows(lst)

df_final
#> # A tibble: 9 × 4
#>   A           B     C     D
#>   <chr>   <dbl> <int> <dbl>
#> 1 Group 1     1     1     1
#> 2 Group 1     7     4    28
#> 3 Group 1    13     7    91
#> 4 Group 2     3     2     6
#> 5 Group 2     9     5    45
#> 6 Group 2    15     8   120
#> 7 Group 3     5     3    15
#> 8 Group 3    11     6    66
#> 9 Group 3    17     9   153

OR

you can use map to map the function to every item of the list.


lst <- map(lst, ~ mutate(., D=B*C))
df_final <- bind_rows(lst)
df_final

CodePudding user response:

Here's a potential solution using purrr:

library(tidyverse) # Not absolutely required, but I'm working this way.
df <- tibble(A = rep(paste("Group", c(1:3)),3), 
             B = seq(1, 18, 2),
             C = (1:9))
df %>% 
  group_by(A) %>% 
  group_split() %>% 
  map_df(~.x %>% mutate(D = B * C))
#> # A tibble: 9 × 4
#>   A           B     C     D
#>   <chr>   <dbl> <int> <dbl>
#> 1 Group 1     1     1     1
#> 2 Group 1     7     4    28
#> 3 Group 1    13     7    91
#> 4 Group 2     3     2     6
#> 5 Group 2     9     5    45
#> 6 Group 2    15     8   120
#> 7 Group 3     5     3    15
#> 8 Group 3    11     6    66
#> 9 Group 3    17     9   153

# conditionally mutate based on group
# could also just do this with a dataframe though
df %>% 
  split(.$A) %>% 
  map_df(function(x) {
    x %>% 
      mutate(D = case_when(A == "Group 1" ~ B   C,
                           A == "Group 2" ~ B   1000,
                           A == "Group 3" ~ B * C))
  })
#> # A tibble: 9 × 4
#>   A           B     C     D
#>   <chr>   <dbl> <int> <dbl>
#> 1 Group 1     1     1     2
#> 2 Group 1     7     4    11
#> 3 Group 1    13     7    20
#> 4 Group 2     3     2  1003
#> 5 Group 2     9     5  1009
#> 6 Group 2    15     8  1015
#> 7 Group 3     5     3    15
#> 8 Group 3    11     6    66
#> 9 Group 3    17     9   153

Created on 2022-10-19 by the reprex package (v2.0.1)

CodePudding user response:

Using R base functions also works

tmp <- lapply(lst, function(x){
  do.call(cbind, list(x, D=(x$B*x$C)))
})

do.call(rbind, tmp)

Result:

       A  B C   D
1 Group 1  1 1   1
2 Group 1  7 4  28
3 Group 1 13 7  91
4 Group 2  3 2   6
5 Group 2  9 5  45
6 Group 2 15 8 120
7 Group 3  5 3  15
8 Group 3 11 6  66
9 Group 3 17 9 153
  • Related