Динамическое перекодирование и добавление полей в dataframe / tibble с использованием yaml

#r #dplyr #tidyverse #purrr

#r #dplyr #tidyverse #purrr

Вопрос:

УЧИТЫВАЯ, что я хочу преобразовать ряд переменных, используя информацию, содержащуюся в файле yaml
И у меня есть содержимое файла yaml, загруженного в память в виде списка
КОГДА я собираюсь применить повторное сопоставление, используя $responses элемент каждой записи в файле yaml
ЗАТЕМ я получаю новое data.frame/tibble поле со столбцами, добавленными с использованием имен и перекодировок в записи карты каждого поля

Ниже приведена единственная запись в файле yaml. Запись health соответствует имени столбца в оригинале data.frame . text Поле не имеет отношения к этому вопросу. responses Поле содержит рецепт повторного сопоставления.

 health:
    study_name: global_health
    text: Would you say your health in general is excellent, very good, good, fair, or poor
    responses: 
        1: Excellent
        2: Very good
        3: Good
        4: Fair
        5: Poor
 

Вот пример исходных данных — просто сосредоточьтесь на одном поле в качестве примера:

   health
1      1
2      3
3      1
4      2
5      2
6      4
 

В этом случае (хотя и со многими другими переменными) я хотел бы следующее:

   health global_health
1      1     Excellent
2      3          Good
3      1     Excellent
4      2     Very good
5      2     Very good
6      4          Fair
 

Что я получил до сих пор, так это следующее:

 data_map <- yaml::read_yaml(map_filepath)
study_cols <- names(data_map)
 

И я смог использовать эту структуру для игнорирования записей, у которых нет заполненного responses поля в файле yaml, используя это как средство отслеживания того, какие столбцы нужно перекодировать, а какие нет.

 library(tidyverse)

# create simple local function to identify which have "responses" that need recoding 
pluck_func <- function(x) !is.null(x$responses)

# create mask using the function
recode_cols_mask <- data_map %>%
    map(~pluck(., pluck_func)) %>% 
    unlist()

# Identify variables that need recoding
recode_names <- study_cols[recode_cols_mask]
 

Теперь, что я хотел бы сделать, это взять переменные, которые существуют, recode_names и применить карту переименования только для этих переменных. Я предполагаю, что есть какое-то умное решение с purrr map семейством функций. Я просто не смог найти правильную комбинацию map() и mutate() . Для полноты картины ниже приведена запись, которая не будет перекодирована и возвращена. Опять же, способ, которым это определяется, эффективно is.null(data_map[['ANALWT_C']][['responses']]) будет оцениваться TRUE .

 ANALWT_C: 
    study_name: weights
    text: Case-level study weight based on Census estimates
 

Solutions do not need to be in tidyverse for what it is worth. Happy to use base R . I just tend to find tidyverse processing a little more readable.

UPDATE
Here is my current solution which I would love to simplify if I can. But if not I can live with this for the moment.

 recode_func <- function(df, data_map, recode_names, study_names) {
    for(v in recode_names) {
        df[data_map[[v]][['study_name']]] <- factor(
            df[[v]], 
            levels = seq_along(data_map[[v]][['responses']]), 
            labels = data_map[[v]][['responses']] %>% unlist()
        )
    }
    
    for(v in setdiff(names(data_map), recode_names)) {
        df[data_map[[v]][['study_name']]] <- df[[v]]
    }
    
    study_df <- df[study_names]
    
    return(list(base_df = df, 
                study_df = study_df))
}
 

с study_names определением как:

 get_study_names <- function(x) x[['study_name']]
study_names <- data_map %>%
    map(~pluck(., get_study_names)) %>% 
    unlist()
 

РЕШАЕМАЯ
Моя текущая реализация основана на принятом ответе ниже:

 #' Recodes dataframe using info pulled from the yaml file in the main function code{create_study_datasets}
#' 
#' @param df target data.frame
#' 
#' @param data_map list read in from yaml file containing data renames and recoding recipes
#' 
#' @param recode_names a character vector of names derived from the data_map in pre-processing steps that take place in 
#' the parent function. These are the fields that need to be recoded in some fashion. 
#' 
#' @param study_names a character vectory of names derived from the data_map in pre-processing steps that take place in
#' the parent function. These are all of the fields that should be returned - whether they are recoded and renamed or 
#' just renamed according to the data_map. 
#' 
#' @seealso create_study_datasets  
helper_recode_func <- function(df, data_map, recode_names, study_names) {

        # Apply the recodes as appropriate from the data map
    df <- df %>% 
        imap_dfc(~ if (hasName(data_map, .y) amp;amp; hasName(data_map[[.y]], "responses"))
            recode(.x, !!! data_map[[.y]][["responses"]])) %>% 
        setNames(map_chr(data_map, "study_name")[names(.)]) %>% 
        bind_cols(df, .)
    
    # Apply passthrough/name-change for variables that do not need recoding
    df <- df %>% 
        select_if((names(.) %in% setdiff(names(data_map), recode_names))) %>% 
        setNames(map_chr(data_map, "study_name")[names(.)]) %>% 
        bind_cols(df, .)

    study_df <- df[study_names]
    
    return(list(base_df = df, 
                study_df = study_df))
}
 

Который при выполнении возвращает ожидаемые выходные данные как для переменных, требующих перекодирования, так и для тех, которые этого не делают:

    ANALWT_C   weights health global_health
1 1274.9937 1274.9937      1     Excellent
2  550.3692  550.3692      3          Good
3 6043.4082 6043.4082      1     Excellent
4  262.2439  262.2439      2     Very good
5 8354.2522 8354.2522      2     Very good
 

Ответ №1:

 f <- function(x) recode(x, !!! data_map[[cur_column()]][["responses"]])

df %>% 
  mutate(across(any_of(names(keep(data_map, hasName, "responses"))),
                f,
                .names = "{data_map[[.col]][["study_name"]]}"))

#>   health global_health
#> 1      1     Excellent
#> 2      3          Good
#> 3      1     Excellent
#> 4      2     Very good
#> 5      2     Very good
#> 6      4          Fair
 

Вариант:

 df %>% 
  imap_dfc(~ if (hasName(data_map[[.y]], "responses"))
    recode(.x, !!! data_map[[.y]][["responses"]])) %>% 
  setNames(map_chr(data_map, "study_name")[names(.)]) %>% 
  bind_cols(df, .)
 

Комментарии:

1. Спасибо за это. Обновил мой исходный пост примером того, как я интегрировал ваше предложение в свое окончательное решение.