Как использовать итерацию при создании новых целей с помощью `target::tar_target()` на основе существующих целей
Я использую этот пакет в проекте анализа данных, где мне нужно получить наборы данных из удаленной базы данных Oracle.
Моя общая задача двояка:
- получать данные из разбросанных по разным представлениям и таблицам в Oracle DB.
- вычислить и обработать новый столбец данных на основе данных, которые я получил из 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
обнаруживает отношения зависимости с помощью статического анализа кода
Для тех, кто дошел до этого, может быть, у вас есть идея, как объединить итерацию и обращение к уже существующим целям?