2
votes

I often want to map over a vector of column names in a data frame, and keep track of the output using the .id argument. But to write the column names related to each map iteration into that .id column seems to require doubling up their name in the input vector - in other words, by naming each column name with its own name. If I don't name the column with its own name, then .id just stores the index of the iteration.

This is expected behavior, per the purrr::map docs:

.id
Either a string or NULL. If a string, the output will contain a variable with that name, storing either the name (if .x is named) or the index (if .x is unnamed) of the input.

But my approach feels a little clunky, so I imagine I'm missing something. Is there a better way to get a list of the columns I'm iterating over, that doesn't require writing each column name twice in the input vector? Any suggestions would be much appreciated!

Here's an example to work with:

library(rlang)
library(tidyverse)

tb <- tibble(foo = rnorm(10), bar = rnorm(10))

cols_once <- c("foo", "bar")
cols_once %>% map_dfr(~ tb %>% summarise(avg = mean(!!sym(.x))), .id="var")
# A tibble: 2 x 2
  var       avg   <-- var stores only the iteration index
  <chr>   <dbl>
1 1     -0.0519
2 2      0.204 

cols_twice <- c("foo" = "foo", "bar" = "bar")
cols_twice %>% map_dfr(~ tb %>% summarise(avg = mean(!!sym(.x))), .id="var")
# A tibble: 2 x 2
  var       avg   <-- var stores the column names
  <chr>   <dbl>
1 foo   -0.0519
2 bar    0.204 
2
I would caution against using t as a variable name; I hope you don't intend on transposing anything...Artem Sokolov
good point - this was just for demo purposes, and i wasn't thinking about t(). will update!andrew_reece
I edited my answer to match the new name :Pprosoitos

2 Answers

3
votes

Here's an alternative solution for your specific scenario using summarize_at and gather:

tb %>% summarize_at( cols_once, mean ) %>% gather( var, avg )
# # A tibble: 2 x 2
#   var      avg
#   <chr>  <dbl>
# 1 foo   0.374 
# 2 bar   0.0397

In a more general scenario, I don't think there's a way around naming your cols_once when working with map_dfr, because of the expected behavior you pointed out in your question. However, you can use the "snake case" wrapper for setNames() to do it more elegantly:

cols_once %>% set_names %>% 
  map_dfr(~ tb %>% summarise(avg = mean(!!sym(.x))), .id="var")
# # A tibble: 2 x 2
#   var      avg
#   <chr>  <dbl>
# 1 foo   0.374 
# 2 bar   0.0397
1
votes

You could create your input vector easily with:

setNames(names(tb), names(tb))

So your code would be:

setNames(names(tb), names(tb)) %>%
  map_dfr(~ tb %>% summarise(avg = mean(!!sym(.x))), .id="var")

Edit following your comment:

Still not the solution you are hoping for, but when you don't use all the column names, you could still use setNames() and subset the ones you want (or subset out the ones you don't).

tb <- tibble(foo = rnorm(10), bar = rnorm(10), taz = rnorm(10))

setNames(names(tb), names(tb))[-3]