Создание функций с помощью методов в R.

В последнее время я больше занимаюсь пространственным анализом в R с помощью пакета sf. В одном шейп-файле, с которым я работал, было несколько столбцов с ужасными именами, и, естественно, я попытался очистить их с помощью функции clean_names() из пакета janitor. Но вот, произошла вопиющая ошибка. С этой целью я официально подал жалобу как вопрос. Представленное решение состояло в том, чтобы просто создать метод для sf объектов.

Да, методы, насколько жесткими они могут быть? Судя по всему процесс совсем не сложный. Но понять процесс? Это было трудно. В этом посте объясняется, как я преобразовывал функцию clean_names() в общую (я объясню это через секунду) и создавал метод для sf и tbl_graph объектов.

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

Итак, что такое общая функция? Мое любимое определение, которое я видел до сих пор, исходит от LispWorks Ltd (их веб-сайт является исторической достопримечательностью, я рекомендую вам взглянуть на него, чтобы вспомнить, каким был Интернет раньше). Они определяют общую функцию как

функция, поведение которой зависит от классов или идентификаторов переданных ей аргументов.

Это означает, что мы должны создать функцию, которая просматривает класс объекта и выполняет операцию на основе класса объекта. Это означает, что если есть объект "numeric" или "list", они будут обрабатываться по-разному. Они называются methods. Примечание: вы можете найти класс объекта, используя функцию class() для любого объекта.

Чтобы снова украсть у LispWorks Ltd, метод

часть общей функции, которая предоставляет информацию о том, как эта общая функция должна вести себя [для] определенных классов.

Это означает, что метод является частью универсальной функции и должен быть определен отдельно. Представьте, что у нас есть общая функция f с методами для объектов list и numeric. Мы бы обозначили эти методы, поставив точку после имени функции и указав тип объекта, на котором должна использоваться функция. Они будут выглядеть как f.list и f.numeric соответственно.

Но для экономии времени вы всегда можете создать метод default, который будет выполняться (использоваться) для любого объекта, для которого не указано явно, как работать с ним (конкретным методом).

Теперь, когда интуитивно понятно, какие общие функции и методы R, мы можем приступить к их фактическому созданию. В этом руководстве будут рассмотрены шаги, которые я предпринял для изменения clean_names() из стандартной функции в универсальную функцию с методами для объектов sf и tbl_graph из пакетов sf и tidygraph соответственно.

Краткий обзор процесса:

  1. Определите общую функцию
  2. Создать метод по умолчанию
  3. Создайте дополнительные методы

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

Первым шагом, как описано выше, является создание универсальной функции. Общие функции создаются путем создания новой функции, тело которой содержит только вызов функции UseMethod(). Единственным аргументом для этого является имя вашей универсальной функции — оно должно совпадать с именем функции, которую вы создаете. Это сообщает R, что вы создаете универсальную функцию. Кроме того, вы должны добавить любые аргументы, которые будут необходимы для вашей функции. Здесь есть два аргумента: dat и case. Они указывают данные, которые необходимо очистить, и предпочтительный стиль их очистки.

Я не устанавливаю никаких значений по умолчанию для dat, чтобы сделать его обязательным, тогда как я устанавливаю case на "snake".

clean_names <- function(dat, case = "snake") {
     UseMethod("clean_names") 
}

Теперь мы создали общую функцию. Но эта функция не знает, как работать с любым заданным типом объектов. Другими словами, нет никаких связанных с ним методов. Чтобы проиллюстрировать это, попробуйте использовать функцию clean_names(), которую мы только что определили для объектов разных типов.

clean_names(1) # numeric 
clean_names("test") # character 
clean_names(TRUE) # logical
## [1] "no applicable method for 'clean_names' applied to an object of class \"c('double', 'numeric')\""
## [1] "no applicable method for 'clean_names' applied to an object of class \"character\""
## [1] "no applicable method for 'clean_names' applied to an object of class \"logical\""

Вывод этих вызовов говорит no applicable method for 'x' applied to an object of [class]. Чтобы этого не произошло, мы можем создать метод по умолчанию. Метод по умолчанию всегда будет использоваться, если у функции нет метода для предоставленного типа объекта.

Помните, что методы обозначаются записью function.method. Также важно отметить, что method должен указывать на класс объекта. Чтобы выяснить, к какому классу относится объект, вы можете использовать функцию class(). Например, class(1) говорит вам, что число 1 является «числовым».

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

В качестве фона функция clean_names() берет фрейм данных и изменяет заголовки столбцов в соответствии с заданным стилем. clean_names() в разрабатываемой версии основан на функции make_clean_names(), которая берет вектор символов и заставляет каждое значение соответствовать заданному стилю (по умолчанию — змея, и вы должны использовать только змеиный регистр, потому что все остальное неправильно *сарказм*).

Чтобы предотвратить загрузку всего пакета janitor и перезапись нашей версии функции clean_names(), мы можем импортировать функцию make_clean_names() непосредственно из GitHub, прочитав файл напрямую.

source("https://raw.githubusercontent.com/sfirke/janitor/master/R/make_clean_names.R")

Теперь посмотрим, как работает эта функция. Для этого мы будем использовать самый уродливый вектор символов, который я когда-либо видел из тестов для clean_names() (h/t @sfirke для этого).

ugly_names <- c( "sp ace", "repeated", "a**^@", "%", "*", "!", "d(!)9", "REPEATED", "can\"'t", "hi_`there`", " leading spaces", "€", "ação", "Farœ", "a b c d e f", "testCamelCase", "!leadingpunct", "average # of days", "jan2009sales", "jan 2009 sales" ) ugly_names
## [1] "sp ace" "repeated" "a**^@" 
## [4] "%" "*" "!" 
## [7] "d(!)9" "REPEATED" "can\"'t" 
## [10] "hi_`there`" " leading spaces" "€" 
## [13] "ação" "Farœ" "a b c d e f" 
## [16] "testCamelCase" "!leadingpunct" "average # of days" 
## [19] "jan2009sales" "jan 2009 sales"

Теперь посмотрим, как работает эта функция:

make_clean_names(ugly_names)
## [1] "sp_ace" "repeated" 
## [3] "a" "percent" 
## [5] "x" "x_2" ## [7] "d_9" "repeated_2" 
## [9] "cant" "hi_there" 
## [11] "leading_spaces" "x_3" 
## [13] "acao" "faroe" 
## [15] "a_b_c_d_e_f" "test_camel_case" 
## [17] "leadingpunct" "average_number_of_days" ## [19] "jan2009sales" "jan_2009_sales"

Великолепно!

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

# create a data frame with 20 columns test_df <- as_tibble(matrix(sample(100, 20), ncol = 20)) # makes the column names the `ugly_names` vector names(test_df) <- ugly_names # print the data frame. test_df
## # A tibble: 1 x 20 
## `sp ace` repeated `a**^@` `%` `*` `!` `d(!)9` REPEATED `can"'t` ## <int> <int> <int> <int> <int> <int> <int> <int> <int> 
## 1 53 32 77 61 1 65 82 50 75 
## # ... with 11 more variables: `hi_\`there\`` <int>, ` leading 
## # spaces` <int>, `€` <int>, ação <int>, Farœ <int>, `a b c d e f` <int>, 
## # testCamelCase <int>, `!leadingpunct` <int>, `average # of days` <int>, 
## # jan2009sales <int>, `jan 2009 sales` <int>

Процесс написания этой функции таков:

  • взять фрейм данных
  • возьмите старые имена столбцов и очистите их
  • переназначить имена столбцов в качестве новых чистых имен
  • вернуть объект
clean_names.default <- function(dat, case = "snake") { 
    # retrieve the old names 
    old_names <- names(dat) 
    # clean the old names 
    new_names <- make_clean_names(old_names, case = case) 
    # assign the column names as the clean names vector 
    names(dat) <- new_names 
    # return the data 
    return(dat) 
}

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

clean_names(test_df)
## # A tibble: 1 x 20 
## sp_ace repeated a percent x x_2 d_9 repeated_2 cant hi_there 
## <int> <int> <int> <int> <int> <int> <int> <int> <int> <int> 
## 1 53 32 77 61 1 65 82 50 75 37 
## # ... with 10 more variables: leading_spaces <int>, x_3 <int>, acao <int>, 
## # faroe <int>, a_b_c_d_e_f <int>, test_camel_case <int>, 
## # leadingpunct <int>, average_number_of_days <int>, jan2009sales <int>, 
## # jan_2009_sales <int>

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

# create a vector with 20 elements 
test_vect <- c(1:20) 
# name each element with the ugly_names vector 
names(test_vect) <- ugly_names 
# try cleaning! 
clean_names(test_vect)
## sp_ace repeated a 
## 1 2 3 
## percent x x_2 
## 4 5 6 
## d_9 repeated_2 cant 
## 7 8 9 
## hi_there leading_spaces x_3 
## 10 11 12 
## acao faroe a_b_c_d_e_f 
## 13 14 15 
## test_camel_case leadingpunct average_number_of_days 
## 16 17 18 
## jan2009sales jan_2009_sales 
## 19 20

Похоже, эта функция по умолчанию отлично работает с именованными объектами! Теперь мы рассмотрим проблему, с которой я начал, sf объектов.

В этом разделе будет рассмотрен процесс создания метода sf. Если вы никогда не использовали пакет sf, я предлагаю вам попробовать! Он создает объекты dataframe со связанными с ними пространственными данными. Это позволяет выполнять многие функции от tidyverse до пространственных данных.

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

library(sf)
test_sf <- test_df %>% 
  # create xy columns 
  mutate(long = -80, lat = 40) %>% 
  # convert to sf object 
  st_as_sf(coords = c("long", "lat")) 
# converting geometry column name to poor style 
names(test_sf)[21] <- "Geometry" 
# telling sf which column is now the geometry 
st_geometry(test_sf) <- "Geometry" test_sf
## Simple feature collection with 1 feature and 20 fields 
## geometry type: POINT 
## dimension: XY 
## bbox: xmin: -80 ymin: 40 xmax: -80 ymax: 40 
## epsg (SRID): NA 
## proj4string: NA 
## # A tibble: 1 x 21 
## `sp ace` repeated `a**^@` `%` `*` `!` `d(!)9` REPEATED `can"'t` ## <int> <int> <int> <int> <int> <int> <int> <int> <int> 
## 1 53 32 77 61 1 65 82 50 75 
## # ... with 12 more variables: `hi_\`there\`` <int>, ` leading 
## # spaces` <int>, `€` <int>, ação <int>, Farœ <int>, `a b c d e f` <int>, 
## # testCamelCase <int>, `!leadingpunct` <int>, `average # of days` <int>, 
## # jan2009sales <int>, `jan 2009 sales` <int>, Geometry <POINT>

Объект sf создан. Но как теперь наш метод функции clean_names() по умолчанию работает с этим объектом? Есть только один способ узнать, попробуйте.

clean_names(test_sf) 
Error in st_geometry.sf(x) : attr(obj, "sf_column") does not point to a geometry column. Did you rename it, without setting st_geometry(obj) <- "newname"?

Обратите внимание, как это не удается. sf заметил, что я изменил имя столбца геометрии, не сообщив об этом явным образом. Поскольку столбец геометрии почти всегда является последним столбцом sf-объекта, мы можем использовать функцию make_clean_names() для каждого столбца, кроме последнего! Для этого воспользуемся функцией rename_at() из dplyr. Эта функция позволяет вам переименовывать столбцы на основе их имени или положения, а также функцию, которая переименовывает их (в данном случае make_clean_names()).

Допустим, для этого примера набора данных я хотел очистить первый столбец. Как бы я это сделал? Обратите внимание, что первый столбец называется sp ace. Как это работает можно увидеть на простом примере. В приведенном ниже вызове функции мы используем функцию rename_at() (для получения дополнительной информации перейдите здесь), выбираем имя первого столбца и переименовываем его с помощью функции make_clean_names().

rename_at(test_df, .vars = vars(1), .funs = make_clean_names)
## # A tibble: 1 x 20 
## sp_ace repeated `a**^@` `%` `*` `!` `d(!)9` REPEATED `can"'t` 
## <int> <int> <int> <int> <int> <int> <int> <int> <int> 
## 1 53 32 77 61 1 65 82 50 75 
## # ... with 11 more variables: `hi_\`there\`` <int>, ` leading 
## # spaces` <int>, `€` <int>, ação <int>, Farœ <int>, `a b c d e f` <int>, 
## # testCamelCase <int>, `!leadingpunct` <int>, `average # of days` <int>, 
## # jan2009sales <int>, `jan 2009 sales` <int>

Обратите внимание, что только первая колонка была очищена. Он изменился с sp ace на sp_ace. Цель состоит в том, чтобы воспроизвести это для всех столбцов, кроме последнего.

Чтобы написать метод sf, приведенную выше строку кода можно адаптировать для выбора столбцов от 1 до количества столбцов минус 1 (поэтому геометрия не выбрана). Чтобы это работало, нам нужно определить предпоследний столбец — он будет предоставлен как конечное значение выбранных нами переменных.

clean_names.sf <- function(dat, case = "snake") { 
  # identify last column that is not geometry 
  last_col_to_clean <- ncol(dat) - 1 
  # create a new dat object 
  dat <- rename_at(dat, 
      # rename the first up until the second to last 
      .vars = vars(1:last_col_to_clean),
 
      # clean using the make_clean_names 
      .funs = make_clean_names) 
  return(dat) 
}

Вуаля! Создан наш первый нестандартный метод. Это означает, что когда объект sf передается нашей универсальной функции clean_names(), она просматривает класс объекта — class(sf_object) — замечает, что это объект sf, а затем отправляет (использует) метод clean_names.sf() вместо метода по умолчанию.

clean_names(test_sf)
## Simple feature collection with 1 feature and 20 fields 
## geometry type: POINT 
## dimension: XY 
## bbox: xmin: -80 ymin: 40 xmax: -80 ymax: 40 
## epsg (SRID): NA 
## proj4string: NA 
## # A tibble: 1 x 21 
## sp_ace repeated a percent x x_2 d_9 repeated_2 cant hi_there 
## <int> <int> <int> <int> <int> <int> <int> <int> <int> <int> 
## 1 53 32 77 61 1 65 82 50 75 37 
## # ... with 11 more variables: leading_spaces <int>, x_3 <int>, acao <int>, 
## # faroe <int>, a_b_c_d_e_f <int>, test_camel_case <int>, 
## # leadingpunct <int>, average_number_of_days <int>, jan2009sales <int>, 
## # jan_2009_sales <int>, Geometry <POINT>

Здесь мы видим, что это сработало именно так, как мы надеялись. Все столбцы, кроме последнего, были изменены. Это позволяет sf называть свои столбцы геометрии как угодно, не нарушая их.

Вскоре после того, как это дополнение было добавлено в пакет, мне стало известно о другом типе объектов, у которых были проблемы с использованием clean_names(). Это объект tbl_graph из пакета tidygraph от Томаса Лина Педерсона.

В выпуске #252 @gvdr отметил, что вызов clean_names() на tbl_graph не выполняется. К счастью, @Tazinho отметил, что вы можете легко очистить заголовки столбцов, используя функцию rename_all() из dplyr.

Здесь решение было еще проще, чем выше. Напоминаем, что для создания метода tbl_graph нам нужно указать имя дженерика, за которым следует класс объекта.

clean_names.tbl_graph <- function(dat, case = "snake") { 
  # rename all columns 
  dat <- rename_all(dat, make_clean_names) 
  return(dat) 
}

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

library(tidygraph) 
# create test graph to test clean_names 
test_graph <- play_erdos_renyi(0, 0.5) %>%
   # attach test_df as columns 
   bind_nodes(test_df) test_graph
## # A tbl_graph: 1 nodes and 0 edges 
## # 
## # A rooted tree
## # 
## # Node Data: 1 x 20 (active) 
## `sp ace` repeated `a**^@` `%` `*` `!` `d(!)9` REPEATED `can"'t` ## <int> <int> <int> <int> <int> <int> <int> <int> <int> 
## 1 53 32 77 61 1 65 82 50 75 
## # ... with 11 more variables: `hi_\`there\`` <int>, ` leading 
## # spaces` <int>, `€` <int>, ação <int>, Farœ <int>, `a b c d e f` <int>, 
## # testCamelCase <int>, `!leadingpunct` <int>, `average # of days` <int>,
## # jan2009sales <int>, `jan 2009 sales` <int> 
## # 
## # Edge Data: 0 x 2 
## # ... with 2 variables: from <int>, to <int>

Здесь мы видим, что есть граф только с 1 узлом и 0 ребрами (отношениями) с плохими заголовками столбцов (подробнее см. на странице GitHub). Теперь мы можем проверить и это.

clean_names(test_graph)
## # A tbl_graph: 1 nodes and 0 edges 
## # 
## # A rooted tree 
## # 
## # Node Data: 1 x 20 (active) 
## sp_ace repeated a percent x x_2 d_9 repeated_2 cant hi_there 
## <int> <int> <int> <int> <int> <int> <int> <int> <int> <int> 
## 1 53 32 77 61 1 65 82 50 75 37 
## # ... with 10 more variables: leading_spaces <int>, x_3 <int>, acao <int>, 
## # faroe <int>, a_b_c_d_e_f <int>, test_camel_case <int>,
## # leadingpunct <int>, average_number_of_days <int>, jan2009sales <int>, 
## # jan_2009_sales <int> 
## # 
## # Edge Data: 0 x 2 
## # ... with 2 variables: from <int>, to <int>

Это сработало, как и ожидалось!

В предыдущих разделах мы узнали, что такое универсальные функции и методы. Как создать общую функцию, метод по умолчанию и методы для объектов разных классов.

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

Процесс создания функции с методом заключается в следующем:

  1. Создайте общую функцию с помощью: f_x <- function() { UseMethod("f_x") }
  2. Определите метод по умолчанию с помощью:f_x.default <- function() { do something }
  3. Определите методы, специфичные для класса объекта, с помощью: f_x.class <- function() { do something else}

Если вы еще не сталкивались с пакетом janitor, он очень поможет вам в различных процессах очистки данных. Очевидно, что clean_names() — моя любимая функция, поскольку она помогает мне применять мой предпочтительный стиль (и единственный). Если вы не знакомы с правильным стилем R, я предлагаю вам прочитать руководство по стилю в Advanced R.

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

Я хотел бы поблагодарить @sfirke за исключительную помощь в руководстве моим вкладом в пакет janitor.

Первоначально опубликовано на http://josiahparry.com/post/function-methods/ 28 ноября 2018 г.