Написав функцию для анализа персонажей Звездных войн, изучите мощные возможности абстракции R

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

Это всегда было правдой, и если вы все еще кодируете повторяющиеся задачи снова и снова, просто меняя одну или две переменной - например, если вы просто копируете / вставляете код - тогда вам нужно остановиться прямо сейчас и научиться писать функции.

Но последние события означают, что появляется все больше и больше стимулов всегда учитывать, какие части вашего кода можно абстрагировать. Разработки в пакетах R для решения нестандартных задач оценки и повышения мощности абстракции с помощью предложений и связанных выражений означают, что нам доступны удивительные возможности.

Функции в R

Начнем с простого. Функцию полезно создать, если вы выполняете идентичный анализ, но просто меняете значения переменных. Давайте поработаем с набором данных starwars в dplyr. Если нам нужен список всех человеческих персонажей, мы могли бы использовать это:

starwars_humans <- starwars %>% 
  dplyr::filter(species == "Human") %>% 
  dplyr::select(name)

Это вернет имена из 35 символов. Теперь, если нам нужен тот же список, но для нескольких других видов, мы можем просто скопировать, вставить и изменить значение для species. Или мы могли бы написать эту функцию для будущего использования:

species_search <- function(x) {
  starwars %>% 
    dplyr::filter(species == x) %>% 
    dplyr::select(name)
}

Теперь, если мы запустим species_search("Droid"), мы получим список из четырех персонажей и будем уверены, что увидим там нашего приятеля R2-D2.

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

Дальнейшее абстрагирование поиска с использованием функций rlang

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

Что, если бы мы захотели переопределить эту функцию, чтобы она возвращала список на основе любого установленного нами произвольного условия. Здесь мы теперь можем установить два аргумента функции, один для представления столбца, по которому нужно фильтровать, а другой - значение, по которому выполняется фильтрация. Мы можем использовать функцию enquo в rlang для захвата имени столбца для использования в dplyr::filter(). Нравится:

starwars_search <- function(filter, value) {
  
  filter_val <- rlang::enquo(filter)
  
  starwars %>% 
    dplyr::filter_at(vars(!!filter_val), all_vars(. == value)) %>% 
    dplyr::select(name)
}

Теперь, если мы оценим starwars_search(skin_color, "gold"), нас успокоит возвращение нашего встревоженного, но милого друга C-3PO.

Еще больше, чтобы разрешить произвольные условия фильтрации, используя purrr

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

Представим, что у нас есть набор фильтров в виде списка. Мы можем использовать функцию map2 в purrr, чтобы взять этот список и разбить его на серию выражений запроса, которые можно передать в виде отдельных операторов в dplyr::filter, используя новую функцию, которая действует на фрейм данных:

my_filter <- function(df, filt_list){     
  cols = as.list(names(filt_list))
  conds = filt_list
  fp <- purrr::map2(cols, conds, 
                    function(x, y) rlang::quo((!!(as.name(x))) %in% !!y))
  dplyr::filter(df, !!!fp)
}

Теперь это позволяет нам дополнительно абстрагироваться от нашей starwars_search функции для получения произвольного набора условий фильтрации в списке, и эти условия могут быть установлены так, чтобы соответствовать одному значению набора значений, выраженных в векторе:

starwars_search <- function(filter_list) {
  starwars %>% 
    my_filter(filter_list) %>% 
    dplyr::select(name)
}

Теперь мы можем, например, найти всех персонажей с голубыми или карими глазами, людей, родившихся с Татуина или Альдераана, используя starwars_search(list(eye_color = c("blue", “brown"), species = “Human", homeworld = c("Tatooine", “Alderaan"))), который вернет следующее:

# A tibble: 10 x 1
   name               
   <chr>              
 1 Luke Skywalker     
 2 Leia Organa        
 3 Owen Lars          
 4 Beru Whitesun lars 
 5 Biggs Darklighter  
 6 Anakin Skywalker   
 7 Shmi Skywalker     
 8 Cliegg Lars        
 9 Bail Prestor Organa
10 Raymus Antilles

Теперь вы готовы раскрыть всю мощь мощи, разработав функции, которые абстрагируют несколько элементов вашего dplyr кода. Например, вот функция, которая позволяет вам найти любые сгруппированные средние значения для определенных персонажей Звездных войн:

starwars_average <- function(mean_col, grp, filter_list) {
  calc_var <- rlang::enquo(mean_col)
  grp_var <- rlang::enquo(grp)
  
  starwars %>% 
    my_filter(filter_list) %>% 
    dplyr::group_by(!!grp_var) %>% 
    summarise(mean = mean(!!calc_var, na.rm = TRUE))
}

Итак, если вы хотите найти средний рост всех людей в соответствии с их домашними мирами, это можно сделать с помощью starwars_average(height, homeworld, list(species = "Human")), который вернет эту таблицу:

# A tibble: 16 x 2
   homeworld     mean
   <chr>        <dbl>
 1 Alderaan      176.
 2 Bespin        175 
 3 Bestine IV    180 
 4 Chandrila     150 
 5 Concord Dawn  183 
 6 Corellia      175 
 7 Coruscant     168.
 8 Eriadu        180 
 9 Haruun Kal    188 
10 Kamino        183 
11 Naboo         168.
12 Serenno       193 
13 Socorro       177 
14 Stewjon       182 
15 Tatooine      179.
16 <NA>          193

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

Изначально я был чистым математиком, затем стал психометриком и специалистом по анализу данных. Я увлечен применением всех этих дисциплин к сложным вопросам людей. Я также помешан на программировании и большой поклонник японских ролевых игр. Найдите меня в LinkedIn или Twitter.

Большое спасибо Саю Иму из моей команды за то, что он вдохновил некоторые идеи здесь своим волшебством функционального программирования.