How to add a new column in dataframe based on a single character in dataframe names?

Viewed 183

I am trying to add new columns in a dataframe based on the dataframe names.

So I have this list of dataframes called df_list, in this list are dataframes called

(1.3.A), (2.3.A), (1.5.A), (2.5.A), (1.3.B), (2.3.B), (1.5.B)

Each of these letter and numbers are important in the data. For example, all dataframes with a name that starts with "1" are from trial 1 of the experiment and those that end in A are from group A.

Can someone suggest how I can add a new column in all of these data frames with the value that is equal to a character in the dataframe names?

The expected output is something like

dataframe (1.3.A)

Person       Height        Weight       Trial      Day      Group
Alex           175           75           1         3          A    
Gerard         180           85           1         3          A
Clyde          179           79           1         3          A

dataframe (2.2.A)
Person       Height        Weight       Trial      Day       Group
Missy           175           75           2         2          A    
Britany         180           85           2         2          A
Sussie          179           79           2         2          A
 

dataframe (1.1.B)
Person       Height        Weight       Trial      Day       Group
Luke           175           75           1         1          B    
Alex           180           85           1         1          B
Haley          179           79           1         1          B

currently, all dataframes have the column 'Person' 'Height' and 'Weight' and I want to add the columns 'Trial' 'Day' and 'Group'.

I hope someone could help me with this. Sorry, I'm just a beginner in R. I was thinking of maybe using ifelse() and names() then map() it to the entire dataframe list but could not figure out how. thanks

3 Answers

You can use bind_rows() from dplyr and separate from tidyr:

library(dplyr)
library(tidyr)
df_list %>% 
  bind_rows(.id = "code") %>% 
  mutate(code = str_remove_all(code, "[(|)]")) %>% 
  separate(code,
           into = c("Trial", "Day","Group"),
           remove = FALSE
  )

# A tibble: 9 x 7
  code  Trial Day   Group Person  Height Weight
  <chr> <chr> <chr> <chr> <chr>    <int>  <int>
1 1.3.A 1     3     A     Alex       175     75
2 1.3.A 1     3     A     Gerard     180     85
3 1.3.A 1     3     A     Clyde      179     79
4 2.2.A 2     2     A     Missy      175     75
5 2.2.A 2     2     A     Britany    180     85
6 2.2.A 2     2     A     Sussie     179     79
7 1.1.B 1     1     B     Luke       175     75
8 1.1.B 1     1     B     Alex       180     85
9 1.1.B 1     1     B     Haley      179     79

Just in case you are not familiar with tidy syntax, read %>% as "an then". So the above call can be interpreted as: take df_list AND THEN bind_rows() AND THEN separate(). More precisely,

  • bind_rows() binds data frames together by rows, such as appending one frame after the other. You could bind two frames such as bind_rows(df1, df2), but here we have a list of frames df_list, so it will bind all data frames from this list into one data frame.

  • The argument .id creates a new column based on the names of each dataframe in the list, and the new column is named code.

  • the parenthesis around the newly created column code is removed in the mutate() step.

  • The code column is short lived, though, because code is split into three columns with separate(code, into = c("Trial", "Day","Group")). Since the names of each data frame in the df_list seems well-behaved, you don't need to specify the splitting regex, which is sep = "[^[:alnum:]]+" by default. In case this regex does not work for all frames, you might have to adapt accordingly. If you don't need the code column, just set the argument remove to TRUE, or remove the argument altogether, since remove = TRUE is the default option.

Alternative solution

This would bind all data frames together. If you wish to keep them separated you can use purrr's imap():

purrr::imap(df_list,
            ~mutate(.x, code = .y) %>% 
              separate(code, into = c("Trial", "Day", "Group"))) 
#> $`1.3.A`
#> # A tibble: 3 x 6
#>   Person Height Weight Trial Day   Group
#>   <chr>   <int>  <int> <chr> <chr> <chr>
#> 1 Alex      175     75 1     3     A    
#> 2 Gerard    180     85 1     3     A    
#> 3 Clyde     179     79 1     3     A    
#> 
#> $`2.2.A`
#> # A tibble: 3 x 6
#>   Person  Height Weight Trial Day   Group
#>   <chr>    <int>  <int> <chr> <chr> <chr>
#> 1 Missy      175     75 2     2     A    
#> 2 Britany    180     85 2     2     A    
#> 3 Sussie     179     79 2     2     A    
#> 
#> $`1.1.B`
#> # A tibble: 3 x 6
#>   Person Height Weight Trial Day   Group
#>   <chr>   <int>  <int> <chr> <chr> <chr>
#> 1 Luke      175     75 1     1     B    
#> 2 Alex      180     85 1     1     B    
#> 3 Haley     179     79 1     1     B

data

edit: added parenthesis on names.

df_list <- list(
  `(1.3.A)` = 
    tibble::tribble(
      ~Person, ~Height, ~Weight,
      "Alex",    175L,     75L,
      "Gerard",    180L,     85L,
      "Clyde",    179L,     79L
    ),
  `(2.2.A)` = 
    tibble::tribble(
      ~Person, ~Height, ~Weight,
      "Missy",    175L,     75L,
      "Britany",    180L,     85L,
      "Sussie",    179L,     79L
    ), 
  `(1.1.B)` = 
    tibble::tribble(
      ~Person, ~Height, ~Weight,
      "Luke",    175L,     75L,
      "Alex",    180L,     85L,
      "Haley",    179L,     79L
    )
)

I think you could use the following solution too in base R:

  • base::Map function applies a function to corresponding elements of given vectors, here we first define 2 arguments x and y in our anonymous function and after our function was defined we put as many vectors as the number of arguments we defined in the function. Here I defined first your df_list a list with 3 arguments and names(df_list) a character vector with 3 elements
  • The anonymous function is applied on our two elements in parallel for example the first element of your list (a data frame in fact) and its name are taken into our anonymous function together
  • In our anonymous function I used base::strsplit to split the names of our data frames from the dot . however I need to escape it first with double backslash like \\. so that it matches a literal dot. As a result we have a character vector of length 3 like c("1", "1", "A")
  • In order to create 3 of each elements and put each set of three under their corresponding new vectors such as Trial I needed to create a matrix with 3 columns and row numbers corresponding to each data frame's number of rows and fill it with our elements row-wise. But in Marcelo's tidyverse solution it's not required. You can just remove the matrix and see how our elements are recycled without it
  • One more thing to consider is the output of base::strsplit is a list of length equal to the length of output so I used double brackets to extract our character vectors with three elements
Map(function(x, y) { 
  x[, c("Trial", "Day", "Group")] <- matrix(strsplit(y, "\\.")[[1]], 
                                            ncol = 3, nrow = nrow(x), byrow = TRUE)
  x
  }, df_list, names(df_list))
$`1.3.A`
  Person Height Weight Trial Day Group
1   Alex    175     75     1   3     A
2 Gerard    180     85     1   3     A
3  Clyde    179     79     1   3     A

$`2.2.A`
   Person Height Weight Trial Day Group
1   Missy    175     75     2   2     A
2 Britany    180     85     2   2     A
3  Sussie    179     79     2   2     A

$`1.1.B`
  Person Height Weight Trial Day Group
1   Luke    175     75     1   1     B
2   Alex    180     85     1   1     B
3  Haley    179     79     1   1     B

Considering name restriction in R, I remove brackets from dataframe name, so I think the reproducible example should be something like this :

df_list <- list()
Height <- c(175,180,179)
Weight <- c(75,85,79)
Person <- c('Alex','Gerard','Clyde')
df_list$'1.3.A' <- data.frame(Person,Height,Weight)
Person <- c('Missy','Britany','Sussie')
df_list$'2.2.A' <- data.frame(Person,Height,Weight)
Person <- c('Luke','Alex','Haley')
df_list$'1.1.B' <- data.frame(Person,Height,Weight)
$`1.3.A`
  Person Height Weight
1   Alex    175     75
2 Gerard    180     85
3  Clyde    179     79

$`2.2.A`
   Person Height Weight
1   Missy    175     75
2 Britany    180     85
3  Sussie    179     79

$`1.1.B`
  Person Height Weight
1   Luke    175     75
2   Alex    180     85
3  Haley    179     79

Without using any libraries, similar to Anoushiravan R, I use strsplit and Map to extract Trial, Day and Group value from dataframe name :

ColAdd <- \(DF,Names){
  DF[,c('Trial','Day','Group')] <- 
    (Names |> strsplit(split = '.', fixed = T))[[1]] |> 
    rep(nrow(DF)) |> 
    matrix(ncol=3,byrow = T)
  return(DF)
}

df_list <- Map(ColAdd,df_list,names(df_list))
$`1.3.A`
  Person Height Weight Trial Day Group
1   Alex    175     75     1   3     A
2 Gerard    180     85     1   3     A
3  Clyde    179     79     1   3     A

$`2.2.A`
   Person Height Weight Trial Day Group
1   Missy    175     75     2   2     A
2 Britany    180     85     2   2     A
3  Sussie    179     79     2   2     A

$`1.1.B`
  Person Height Weight Trial Day Group
1   Luke    175     75     1   1     B
2   Alex    180     85     1   1     B
3  Haley    179     79     1   1     B
Related