Как использовать итерацию при создании новых целей с помощью `target::tar_target()` на основе существующих целей

Я использую этот пакет в проекте анализа данных, где мне нужно получить наборы данных из удаленной базы данных Oracle.

Моя общая задача двояка:

  1. получать данные из разбросанных по разным представлениям и таблицам в Oracle DB.
  2. вычислить и обработать новый столбец данных на основе данных, которые я получил из Oracle.
  • эти вычисляемые столбцы, хотя и происходят из отдельных таблиц базы данных, имеют достаточно общего, поэтому я могу объединить их по некоторым связанным столбцам индекса.

Каждый новый столбец, который я вычисляю, представляет собой «особую снежинку», поэтому я создал специальную функцию обработки для каждой из них и функцию более высокого порядка для вызова каждой подфункции обработки снежинок.

Моя проблема связана с итерацией по объектам данных, которые были созданы с помощью targets::tar_target().


Воспроизводимый пример

Чтобы точно передать мою проблему, мне, к сожалению, нужно выделить приличный объем кода в этом примере. Первая часть предназначена только для генерации демонстрационных данных и имитации базы данных Oracle. Вы можете просто запустить код и перейти к следующей части.

1. Моделируйте данные: Создайте базу данных с 4 таблицами,
просто запустите этот код; не критично для понимания проблемы

      library(dplyr, warn.conflicts = FALSE)
library(babynames)
library(DBI)
library(RSQLite)

set.seed(2021)

simulate_df_from_colnames <- function(vec_of_colnames, desired_nrows, vec_of_ids) {
  
  stopifnot(desired_nrows == length(vec_of_ids))
  
  ncols        <- length(vec_of_colnames)
  n_values     <- ncols * desired_nrows
  vec          <- runif(n = n_values, min = 1, max = 100)
  vec[sample(1:length(vec), 0.2 * length(vec))] <- NA # sprinkle NA randomly in 20% of values
  mat          <- matrix(vec, ncol = ncols)
  df           <- as.data.frame(mat)
  colnames(df) <- vec_of_colnames
  df$id        <- vec_of_ids
  df           <- df[,c(ncol(df),1:(ncol(df)-1))] # so the id column move from last to first position

  return(df)
}

work_related <- c("acceptance", "accountability", "achievement", "adaptability", "adventure", "authenticity", "authority", "autonomy", "balance", "boldness", "bravery", "candor", "challenge", "clarity", "collaboration", "compassion", "communication", "community", "contribution", "creativity", "curiosity", "dependability", "determination", "diversity", "empathy", "enthusiasm", "equality", "family", "fairness", "flexibility", "friendship", "growth", "happiness", "hard_work", "honesty", "humility", "humor", "impact", "improvement", "ingenuity", "innovation", "kindness", "knowledge", "leadership", "learning", "loyalty", "meaningful_work", "optimism", "ownership", "participation", "patience", "peace", "persistence", "popularity", "power", "quality", "recognition", "relationships", "reliability", "reputation", "respect", "responsibility", "results", "security", "self_improvement", "simplicity", "spirituality", "stability", "success", "sustainability", "teamwork", "tenacity", "time_management", "transparency", "trustworthiness", "wealth", "wisdom", "work_ethic", "work_life_balance")
blood_tests  <- c("white_blood_cell_count", "red_blood_cell_count", "hemoglobin", "hematocrit", "mean_corpuscular_volume", "platelet_count", "sodium", "potassium", "chloride", "carbon_dioxide", "blood_urea_nitrogen", "creatinine", "glucose", "calcium", "total_protein", "albumin", "bilirubin", "alkaline_phosphatase", "ast", "alt", "vitamin_b_12", "methylmalonic_acid", "ferritin")
physical     <- c("systolic_blood_pressure", "diastolic_blood_pressure", "pulse_rate_beats_minute", "height", "weight", "bmi", "waist_circumference", "hip_circumference")
psych_traits <- c("accessible", "active", "adaptable", "admirable", "adventurous", "agreeable", "alert", "allocentric", "amiable", "anticipative", "appreciative", "articulate", "aspiring", "athletic", "attractive", "balanced", "benevolent", "brilliant", "calm", "capable", "captivating", "caring", "challenging", "charismatic", "charming", "cheerful", "clean", "clear_headed", "clever", "colorful", "companionly", "compassionate", "conciliatory", "confident", "conscientious", "abrasive", "abrupt", "agonizing", "aimless", "airy", "aloof", "amoral", "angry", "anxious", "apathetic", "arbitrary", "argumentative", "arrogantt", "artificial", "asocial", "assertive", "astigmatic", "barbaric", "bewildered", "bizarre", "bland", "blunt", "biosterous", "brittle", "brutal", "calculating", "callous", "cantakerous", "careless", "cautious", "charmless", "childish", "clumsy", "coarse", "cold")

my_names <- 
  babynames::babynames %>%
  pull(name) %>%
  unique() %>%
  sample(1000)

df_work_related <- simulate_df_from_colnames(work_related, 1000, vec_of_ids = my_names)
df_blood_tests  <- simulate_df_from_colnames(blood_tests , 1000, vec_of_ids = my_names)
df_physical     <- simulate_df_from_colnames(physical    , 1000, vec_of_ids = my_names)
df_psych_traits <- simulate_df_from_colnames(psych_traits, 1000, vec_of_ids = my_names)

con <- DBI::dbConnect(RSQLite::SQLite(), ":memory:")

copy_to(con, df_work_related, name = "DJLNGJN3445_NFKS")
copy_to(con, df_blood_tests , name = "DKFMDGNSQWRE_320586")
copy_to(con, df_physical    , name = "KLDJNSDOIJFW_295868FJDI")
copy_to(con, df_psych_traits, name = "AQNF_223_daqVV")

Создано 2021-10-23 пакетом REPEX (v2.0.1)


2. Моя проблема начинается здесь; У меня есть база данных, полная данных, которые я хочу проанализировать.
После выполнения приведенного выше кода моделирования у нас теперь есть объект, представляющий удаленную БД. Мы можем изучить, какие таблицы находятся внутри con:

      DBI::dbListObjects(con)
#>                                  table is_prefix
#> 1          <Id> table = AQNF_223_daqVV     FALSE
#> 2        <Id> table = DJLNGJN3445_NFKS     FALSE
#> 3     <Id> table = DKFMDGNSQWRE_320586     FALSE
#> 4 <Id> table = KLDJNSDOIJFW_295868FJDI     FALSE
#> 5            <Id> table = sqlite_stat1     FALSE
#> 6            <Id> table = sqlite_stat4     FALSE

Тот, кто предоставил доступ к базе данных, также сообщил нам, что в ней хранятся данные о 1000 человек, разбросанных по 4 различным таблицам.

3. Пошаговое руководство с одной новой переменной в качестве примера
Допустим, я хочу вычислить новую переменную, которая показывает, интересно ли с человеком общаться. В таблице выше я вижу это имя таблицы "AQNF_223_daqVV"содержит психологические показатели, так что я понимаю, что это релевантная таблица. Изучая эти данные, я решаю, что моя новая переменная "fun_to_be_with" будет средним значением существующих переменных accessible, active, а также adaptable.

      library(dplyr)

compute_fun_to_be_with <- function(.dat) {
  .dat %>%
    select(id, accessible, active, adaptable) %>%
    mutate(fun_to_be_with = rowMeans(across(c(accessible, active, adaptable))), .keep = "unused")
}
      tbl(con, "AQNF_223_daqVV") %>%
  collect() %>%
  compute_fun_to_be_with()
#> # A tibble: 1,000 x 2
#>    id        fun_to_be_with
#>    <chr>              <dbl>
#>  1 Miari               NA  
#>  2 Demariana           NA  
#>  3 Halah               NA  
#>  4 Abdalah             NA  
#>  5 Infiniti            NA  
#>  6 Sydel               63.0
#>  7 Montelle            62.8
#>  8 Rhys                NA  
#>  9 Mijah               73.0
#> 10 Lamontre            NA  
#> # ... with 990 more rows

Когда я начал исследовать более интересные переменные, которые я мог вычислить, я пришел к выводу, что помимо начального вычисления (например, взятия среднего значения), есть еще несколько шагов, которые применимы для одних переменных, но не для других. Например, иногда я хочу округлить значения переменной в большую или меньшую сторону, или взять логарифм, или как-то иначе. Таким образом, каждая новая переменная, которую я вычисляю, представляет собой «особую снежинку», и у меня есть функция-оболочка, которая управляет всеми этими предпочтениями.

      compute_snowflake <- function(.dat, snowflake_name) {
  
  switch(snowflake_name,
         "fun_to_be_with" = compute_fun_to_be_with(.dat))
}


wrangle_snowflake <- function(snowflake_name, 
                              raw_data_from_db, 
                              replace_na_with_zero, 
                              take_logarithm, 
                              round = c("up", "down"), 
                              standardize_as_zscore) {

  
  raw_data_from_db %>%
    compute_snowflake(snowflake_name) %>%
    {if (replace_na_with_zero) mutate(., across({{ snowflake_name }}, tidyr::replace_na, 0)) else .} %>%
    {if (take_logarithm) mutate(., across(fun_to_be_with, log)) else .} %>%
    {if (round == "up") mutate(., across(fun_to_be_with, ceiling)) else .} %>%
    {if (round == "down") mutate(., across(fun_to_be_with, floor)) else .} %>%
    {if (standardize_as_zscore)  mutate(., across(fun_to_be_with, scale)) else .}
} 

3a) Необходимость итерации.
Для обработки только одной новой переменной, wrangle_snowflake () можно использовать как есть:

      wrangle_snowflake(snowflake_name = "fun_to_be_with", 
                  raw_data_from_db = tbl(con, "AQNF_223_daqVV") %>% collect(),
                  replace_na_with_zero = FALSE,
                  take_logarithm = TRUE,
                  round = "down",
                  standardize_as_zscore = FALSE)

Но проблема в масштабе. Мой проект требует обработки около 100 новых переменных. И я не хочу повторяться wrangle_snowflake()так 100 раз. Если мы просто сохраним таблицу БД локально как объект в среде, мы можем использовать purrr::pmap() очень хорошо для итерации:

      raw_tbl_psych <- 
  tbl(con, "AQNF_223_daqVV") %>% 
  collect()

tbl_parameters <- 
  tibble::tribble(~snowflake_name, ~raw_data_from_db, ~replace_na_with_zero, ~take_logarithm, ~round, ~standardize_as_zscore,
                "fun_to_be_with", raw_tbl_psych, FALSE, TRUE, "down", FALSE)
tbl_parameters
#> # A tibble: 1 x 6
#>   snowflake_name raw_data_from_db      replace_na_with_zero take_logarithm round
#>   <chr>          <list>                <lgl>                <lgl>          <chr>
#> 1 fun_to_be_with <tibble [1,000 x 71]> FALSE                TRUE           down 
#> # ... with 1 more variable: standardize_as_zscore <lgl>

tbl_parameters %>%
  purrr::pmap(.f = wrangle_snowflake)
#> [[1]]
#> # A tibble: 1,000 x 2
#>    id        fun_to_be_with
#>    <chr>              <dbl>
#>  1 Miari                 NA
#>  2 Demariana             NA
#>  3 Halah                 NA
#>  4 Abdalah               NA
#>  5 Infiniti              NA
#>  6 Sydel                  4
#>  7 Montelle               4
#>  8 Rhys                  NA
#>  9 Mijah                  4
#> 10 Lamontre              NA
#> # ... with 990 more rows

очень мощный, потому что я могу расширить tbl_parameters и добавить еще много снежинок, но звонок tbl_parameters %>% purrr::pmap(.f = wrangle_snowflake)останется прежним.

3b) необходимость в
одной важной проблеме не отражена в этом примере: данные, которые мне нужно получить из удаленной БД, огромны. Каждая таблица (например, AQNF_223_daqVV) может составлять от 1 до 10 миллионов строк. В таком случае я не хочу загружать все данные в среду как объект R. Скорее, пакет позволяет мне создать « цель » для каждой гигантской таблицы, которая хранится как .rdsфайл в каталоге. Таким образом, я могу использовать гигантскую таблицу косвенно, не загружая ее.

наконец: моя проблема

не работает с моим методом. Поскольку я не хочу переносить все гигантские таблицы данных в среду R, я бы предпочел просто ссылаться на них по имени. Таким образом, my будет выглядеть примерно так:

      tbl_parameters_2 <-
  tibble::tribble(
      ~snowflake_name,                  ~db_name, ~replace_na_with_zero, ~take_logarithm, ~round, ~standardize_as_zscore,
     "fun_to_be_with",          "AQNF_223_daqVV",                 FALSE,            TRUE, "down",                  FALSE,
        "work_ethics",        "DJLNGJN3445_NFKS",                  TRUE,            TRUE,   "up",                  FALSE,
                "bmi", "KLDJNSDOIJFW_295868FJDI",                 FALSE,           FALSE,   "up",                   TRUE,
  "risk_for_diabetes",     "DKFMDGNSQWRE_320586",                 FALSE,           FALSE, "down",                  FALSE
  )

Но! {targets} .

Итак, если я использую для создания одной цели для каждой таблицы БД:

      library(targets)

tar_target(raw_tbl_psych,  tbl(con, "AQNF_223_daqVV") %>% collect())
tar_target(raw_tbl_work,   tbl(con, "DJLNGJN3445_NFKS") %>% collect())
tar_target(raw_tbl_physical, tbl(con, "KLDJNSDOIJFW_295868FJDI") %>% collect())
tar_target(raw_tbl_blood,  tbl(con, "DKFMDGNSQWRE_320586") %>% collect())

А потом хочу иметь pmap() перебирать tbl_parameters_2 и для каждой строки db_name замените его на соответствующую цель, ну, это не сработает.

      swap_table_ugly_name_for_nice_target_name <- function(ugly_name) {
  
  switch(ugly_name,
         # ugly_name               # targets name
         "AQNF_223_daqVV"          = "raw_tbl_psych",
         "DJLNGJN3445_NFKS"        = "raw_tbl_work",
         "KLDJNSDOIJFW_295868FJDI" = "raw_tbl_physical",
         "DKFMDGNSQWRE_320586"     = "raw_tbl_blood"
         )
}
      tar_target(list_of_wrangled_snowflakes,
           wrangle_snowflake(snowflake_name        = tbl_parameters_2$snowflake_name,
                             db_name               = swap_table_ugly_name_for_nice_target_name(tbl_parameters_2$db_name),
                             replace_na_with_zero  = tbl_parameters_2$replace_na_with_zero,
                             take_logarithm        = tbl_parameters_2$take_logarithm,
                             round                 = tbl_parameters_2$round,
                             standardize_as_zscore = tbl_parameters_2$standardize_as_zscore)
)

Ну, это просто не работает. не позволяет ссылаться на существующую цель по строкеСогласно @landau, это потому, что:

targets обнаруживает отношения зависимости с помощью статического анализа кода


Для тех, кто дошел до этого, может быть, у вас есть идея, как объединить итерацию и обращение к уже существующим целям?

0 ответов

Другие вопросы по тегам