4  Функции

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

4.1 Функции

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

functionName = function(parameter1, parameter2, ...){
  ...
  return(result)
  ...
  last_value = ...
}

Функция создается c помощью ключевого слова function, за которым в круглых скобках заключается произвольное количество параметров (столько, сколько вам нужно: от нуля и более). С помощью этих параметров вы сможете передавать внутрь функции значения переменных. Созданной функции необходимо дать имя, используя оператор присвоения <- или =.

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

  • Если не указано иное, то будет возвращен результат вычисления последнего выражения, выполненного внутри функции (last_value в примере выше)

  • Результат можно вернуть принудительно в любом месте функции, передав его в выражение return().

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

Как правило, функции оказываются полезны, если:

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

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

x = rnorm(4)
y = rnorm(4)
d12 = sqrt((x[1] - x[2]) ^ 2 + (y[1] - y[2]) ^ 2)
d23 = sqrt((x[2] - x[3]) ^ 2 + (y[2] - y[3]) ^ 2)
d31 = sqrt((x[3] - x[4]) ^ 2 + (y[3] - y[4]) ^ 2)
cat(d12, d23, d31)
1.78599 1.780432 2.739223

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

distance = function(x1, y1, x2, y2) {
  sqrt((x1 - x2) ^ 2 + (y1 - y2) ^ 2)
}

d12 = distance(x[1], y[1], x[2], y[2])
d23 = distance(x[2], y[2], x[3], y[3])
d31 = distance(x[3], y[3], x[4], y[4])
cat(d12, d23, d31)
1.78599 1.780432 2.739223

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

distances = function(x, y) {
  n = length(x)
  distance(x[1:(n-1)], y[1:(n-1)], x[2:n], y[2:n])
}

distances(x, y)
[1] 1.785990 1.780432 2.739223

Можно пойти еще дальше, и сделать функцию, выполняющую вычисление длины линии, заданной координатами:

line_length = function(x, y) {
  sum(distances(x, y))
}

line_length(x, y)
[1] 6.305645

Обратите внимание на то, как мы используем одну ранее написанную функцию при создании другой функции! Это весьма распространенная практика: одна и та же функция может быть как самостоятельно полезной (вызываться непосредственно в программе полтьзователя), так и применяться для решения задач внутри других функций. При этом иногда даже относительно простые фрагменты кода имеет смысл оформлять в виде функций, так как это может улучшить читаемость программы и пояснить смысл выполняемых операций. Так, например, line_length(x, y) в более явном виде обозначает операцию вычисления длины по координатам, нежели sum(distances(x, y)). В то же время, здесь важно не переусердствовать и оформлять короткие фрагменты кода в виде функций только если они будут применяться вами неоднократно.

Если вам нужно вернуть из функции несколько объектов, имеющих разный тип или смысл, заключите их в список и дайте каждому элементу списка “говорящее” имя. Например, помимо периметра, мы можем вернуть также извилистость линии (отношение длины линии к длине отрезка, соединяющего ее первую и последнюю точку):

line_params = function(x, y) {
  n = length(x)
  
  l = line_length(x, y)
  s = l / distance(x[1], y[1], x[n], y[n]) 
  
  list(length = l, sinuosity = s)
}

result = line_params(x, y)
result$length
[1] 6.305645
result$sinuosity
[1] 4.907202

4.2 Функционалы

4.2.1 Базовые функционалы

Данные (в том числе географические) практически всегда носят множественный характер и организованы в определенные структуры (см. главу 2). Эта особенность данных выдвигает логичное желание иметь процедуры, которые можно применять к полному набору данных, а не к его отдельным компонентам. Это и есть процедуры векторизованных вычислений.

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

Функция Назначение
apply() применить функцию ко всем строкам или столбцам матрицы
lapply() применить функцию к каждому компоненту вектора или списка и получить результат также в виде списка (l — list)
sapply() применить функцию к каждому компоненту вектора или списка и получить результат в виде вектора (s — simplify)
vapply() аналогична sapply(), но требует явного задания типа данных возвращаемого вектора, за счет чего работает быстрее (v — velocity)
mapply() применить функцию к каждому компоненту нескольких векторов или списков и вернуть результат в виде списка (m — multivariate)
rapply() применить функцию рекурсивно ко всем элементам переданного списка и вернуть результат в аналогичной структур (r — recursive)
tapply() применить функцию ко всем компонентам вектора или списка, сгруппировав их по значению переданного фактора

Функции семейства apply, принимающие на вход списки, могут работать и с фреймами данных. В этом случае фрейм внутри функции будет преобразован с помощью функции as.list() в список, элементами которого являются столбцы (переменные) входного фрейма данных. Данные при этом не потеряются, их типы тоже не изменятся.

Базовая функция apply() имеет следующие аргументы:

  • X — массив любой размерности (включая вектор)
  • MARGIN — измерения по которым необходимо вести вычисления. Для матрицы 1 означает строку, 2 означает столбец, c(1, 2) будет означать, что вычисления производятся по всем комбинациям строк и столбцов
  • FUN — функция, которая будет применяться к каждому элементу указанных измерений
  • ... — список аргументов, которые надо передать в функцию FUN (в этом случае массив должен передаваться обязательно в первый аргумент)

Другие функции семейства apply в приложении к фреймам данных будут работать со столбцами (переменными), интерпретируя их как элементы списка. Наиболее часто из них используются lapply(), sapply() и vapply(). В отличие от apply(), они уже не принимаеют номера измерений и работают только с элементами переданного списка.

Рассмотрим применение функций данного семейства на примере анализа основных социально-экономических характеристик столиц субъектов Северо-Западного округа за 2015 год:

library(readxl)

(df = read_excel("data/sevzap.xlsx", col_types = c('text', rep('numeric', 17))))
## # A tibble: 10 × 18
##    city     pop birth death  labor salary livspace doctors  hosp assets business
##    <chr>  <dbl> <dbl> <dbl>  <dbl>  <dbl>    <dbl>   <dbl> <dbl>  <dbl>    <dbl>
##  1 Петр…  277.   13    12.3   72.3 36268.     24.4    75      17 2.47e5    15856
##  2 Сыкт…  259.   14.5  10.4   84.1 39790      22.8    79.5    17 2.17e5    10068
##  3 Арха…  358.   11.8  11.6   97.9 40303.     22.7    82.3    19 2.79e5    13016
##  4 Нарь…   24.5  18.1   7.6   12.7 69884.     23.6    65.2     2 1.05e5      704
##  5 Воло…  313.   16.2  11.4   91   31483      24.7    60.9    18 8.27e5    21931
##  6 Кали…  460.   13.3  13.4  126.  34142      27.7    69.8    25 3.27e5    38013
##  7 Мурм…  302.   12.4  11.9   96.9 53240.     23.8    68.8    15 7.24e5    16041
##  8 Вели…  222.   13.8  13.8   74.1 32377.     24.1    78.9    14 1.59e5     9583
##  9 Псков  208.   13.2  13.7   59.6 27405.     25      59      13 1.31e5     8752
## 10 Санк… 5226.   13.6  11.9 2055.  44187      23.6    73.8   112 4.10e6   374999
## # ℹ 7 more variables: minerals <dbl>, manufact <dbl>, engaswat <dbl>,
## #   construct <dbl>, apart <dbl>, retail <dbl>, invest <dbl>

В данной таблице каждый столбец представляет независимую переменную со своими единицами измерения, поэтому ее необходимо оставить в “широкой” форме, не преобразуя в длинную. Используя apply, можно быстро получить максимальные значения каждой переменной:

apply(df[-1], 2, max)
      pop     birth     death     labor    salary  livspace   doctors      hosp 
   5225.7      18.1      13.8    2055.3   69883.5      27.7      82.3     112.0 
   assets  business  minerals  manufact  engaswat construct     apart    retail 
4102243.8  374999.0        NA 1978634.0  173292.0  397229.0    3031.0 1144607.0 
   invest 
 521293.0 

Что равносильно вызову sapply:

sapply(df[-1], max)
      pop     birth     death     labor    salary  livspace   doctors      hosp 
   5225.7      18.1      13.8    2055.3   69883.5      27.7      82.3     112.0 
   assets  business  minerals  manufact  engaswat construct     apart    retail 
4102243.8  374999.0        NA 1978634.0  173292.0  397229.0    3031.0 1144607.0 
   invest 
 521293.0 

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

(normalized = sapply(df[-1], function(X) { round(X / mean(X, na.rm = TRUE), 2) }))
       pop birth death labor salary livspace doctors hosp assets business
 [1,] 0.36  0.93  1.04  0.26   0.89     1.01    1.05 0.67   0.35     0.31
 [2,] 0.34  1.04  0.88  0.30   0.97     0.94    1.11 0.67   0.30     0.20
 [3,] 0.47  0.84  0.98  0.35   0.99     0.94    1.15 0.75   0.39     0.26
 [4,] 0.03  1.29  0.64  0.05   1.71     0.97    0.91 0.08   0.15     0.01
 [5,] 0.41  1.16  0.97  0.33   0.77     1.02    0.85 0.71   1.16     0.43
 [6,] 0.60  0.95  1.14  0.46   0.83     1.14    0.98 0.99   0.46     0.75
 [7,] 0.39  0.89  1.01  0.35   1.30     0.98    0.96 0.60   1.02     0.32
 [8,] 0.29  0.99  1.17  0.27   0.79     0.99    1.11 0.56   0.22     0.19
 [9,] 0.27  0.94  1.16  0.22   0.67     1.03    0.83 0.52   0.18     0.17
[10,] 6.83  0.97  1.01  7.42   1.08     0.97    1.03 4.44   5.76     7.37
      minerals manufact engaswat construct apart retail invest
 [1,]     0.06     0.05     0.52      0.13  0.45   0.23   0.11
 [2,]     0.00     0.28     0.43      0.09  0.31   0.21   0.14
 [3,]     0.00     0.06     0.49      0.09  0.17   0.17   0.15
 [4,]     4.12     0.05     0.03      0.08  0.05   0.02   0.52
 [5,]       NA     0.15     0.63      0.09  0.52   0.23   0.13
 [6,]     0.82     0.83     0.68      0.28  1.24   0.38   0.53
 [7,]       NA     0.23     0.42      0.07  0.02   0.29   0.75
 [8,]       NA     0.42     0.45      0.08  0.32   0.17   0.30
 [9,]       NA     0.08     0.22      0.04  0.27   0.17   0.10
[10,]     1.00     7.87     6.13      9.05  6.65   8.13   7.28

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

В приведенном выше коде мы сознательно исключили первый столбец, поскольку он является текстовым. Можно сделать более мощную и универсальную функцию, которая будет нормировать все числовые столбцы таблицы, а текстовые оставлять в оригинале. Для этого проверку типа данных надо внести внутрь функции. Поскольку код функции при этом вырастает, целесообразно определить ее заранее. Поскольку в этом случае часть векторов будет символьной, а не числовой, необходимо применять функцию lapply(), которая вернет список из векторов, а не матрицу и таким образом сохранит типы каждого столбца:

library(dplyr)
normalize = function(X) {
  if (is.numeric(X)) 
    round(X / mean(X, na.rm = TRUE), 2) 
  else X
}

(normalized_df = df |> lapply(normalize) |> as_tibble())
# A tibble: 10 × 18
   city      pop birth death labor salary livspace doctors  hosp assets business
   <chr>   <dbl> <dbl> <dbl> <dbl>  <dbl>    <dbl>   <dbl> <dbl>  <dbl>    <dbl>
 1 Петроз…  0.36  0.93  1.04  0.26   0.89     1.01    1.05  0.67   0.35     0.31
 2 Сыктыв…  0.34  1.04  0.88  0.3    0.97     0.94    1.11  0.67   0.3      0.2 
 3 Арханг…  0.47  0.84  0.98  0.35   0.99     0.94    1.15  0.75   0.39     0.26
 4 Нарьян…  0.03  1.29  0.64  0.05   1.71     0.97    0.91  0.08   0.15     0.01
 5 Вологда  0.41  1.16  0.97  0.33   0.77     1.02    0.85  0.71   1.16     0.43
 6 Калини…  0.6   0.95  1.14  0.46   0.83     1.14    0.98  0.99   0.46     0.75
 7 Мурман…  0.39  0.89  1.01  0.35   1.3      0.98    0.96  0.6    1.02     0.32
 8 Велики…  0.29  0.99  1.17  0.27   0.79     0.99    1.11  0.56   0.22     0.19
 9 Псков    0.27  0.94  1.16  0.22   0.67     1.03    0.83  0.52   0.18     0.17
10 Санкт-…  6.83  0.97  1.01  7.42   1.08     0.97    1.03  4.44   5.76     7.37
# ℹ 7 more variables: minerals <dbl>, manufact <dbl>, engaswat <dbl>,
#   construct <dbl>, apart <dbl>, retail <dbl>, invest <dbl>

4.2.2 Функционалы purrr

В качестве альтернативы функциям apply можно также воспользоваться вычислениями посредством функций семейства map из пакета purrr (еще один пакет из tidyverse). Эти функции работают аналогично, но их разнообразие довольно велико. Например, часто используюбся следующие:

Функция Тип возвращаемого значения
map() список
map_lgl() вектор logical
map_int() вектор integer
map_dbl() вектор double
map_chr() вектор character

Основные отличия от базовых функций следующие:

  • явное указание типа возвращаемого значения (apply могут быть непредсказуемы);

  • данные всегда идут первым аргументом (это условие не выполняется для mapply).

  • поддерживается большое разнообразие сочетаний входных и выходных параметров, в том числе итерации по нескольким векторам одновременно.

Вышеприведенные операции можно осуществить средствами purrr вот так:

library(purrr)
map_dbl(df[-1], max)
      pop     birth     death     labor    salary  livspace   doctors      hosp 
   5225.7      18.1      13.8    2055.3   69883.5      27.7      82.3     112.0 
   assets  business  minerals  manufact  engaswat construct     apart    retail 
4102243.8  374999.0        NA 1978634.0  173292.0  397229.0    3031.0 1144607.0 
   invest 
 521293.0 
df |> map(normalize) |> as_tibble()
# A tibble: 10 × 18
   city      pop birth death labor salary livspace doctors  hosp assets business
   <chr>   <dbl> <dbl> <dbl> <dbl>  <dbl>    <dbl>   <dbl> <dbl>  <dbl>    <dbl>
 1 Петроз…  0.36  0.93  1.04  0.26   0.89     1.01    1.05  0.67   0.35     0.31
 2 Сыктыв…  0.34  1.04  0.88  0.3    0.97     0.94    1.11  0.67   0.3      0.2 
 3 Арханг…  0.47  0.84  0.98  0.35   0.99     0.94    1.15  0.75   0.39     0.26
 4 Нарьян…  0.03  1.29  0.64  0.05   1.71     0.97    0.91  0.08   0.15     0.01
 5 Вологда  0.41  1.16  0.97  0.33   0.77     1.02    0.85  0.71   1.16     0.43
 6 Калини…  0.6   0.95  1.14  0.46   0.83     1.14    0.98  0.99   0.46     0.75
 7 Мурман…  0.39  0.89  1.01  0.35   1.3      0.98    0.96  0.6    1.02     0.32
 8 Велики…  0.29  0.99  1.17  0.27   0.79     0.99    1.11  0.56   0.22     0.19
 9 Псков    0.27  0.94  1.16  0.22   0.67     1.03    0.83  0.52   0.18     0.17
10 Санкт-…  6.83  0.97  1.01  7.42   1.08     0.97    1.03  4.44   5.76     7.37
# ℹ 7 more variables: minerals <dbl>, manufact <dbl>, engaswat <dbl>,
#   construct <dbl>, apart <dbl>, retail <dbl>, invest <dbl>

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

df |> map_if(is.numeric, \(X) round(X / mean(X, na.rm = TRUE), 2)) |> as_tibble()
# A tibble: 10 × 18
   city      pop birth death labor salary livspace doctors  hosp assets business
   <chr>   <dbl> <dbl> <dbl> <dbl>  <dbl>    <dbl>   <dbl> <dbl>  <dbl>    <dbl>
 1 Петроз…  0.36  0.93  1.04  0.26   0.89     1.01    1.05  0.67   0.35     0.31
 2 Сыктыв…  0.34  1.04  0.88  0.3    0.97     0.94    1.11  0.67   0.3      0.2 
 3 Арханг…  0.47  0.84  0.98  0.35   0.99     0.94    1.15  0.75   0.39     0.26
 4 Нарьян…  0.03  1.29  0.64  0.05   1.71     0.97    0.91  0.08   0.15     0.01
 5 Вологда  0.41  1.16  0.97  0.33   0.77     1.02    0.85  0.71   1.16     0.43
 6 Калини…  0.6   0.95  1.14  0.46   0.83     1.14    0.98  0.99   0.46     0.75
 7 Мурман…  0.39  0.89  1.01  0.35   1.3      0.98    0.96  0.6    1.02     0.32
 8 Велики…  0.29  0.99  1.17  0.27   0.79     0.99    1.11  0.56   0.22     0.19
 9 Псков    0.27  0.94  1.16  0.22   0.67     1.03    0.83  0.52   0.18     0.17
10 Санкт-…  6.83  0.97  1.01  7.42   1.08     0.97    1.03  4.44   5.76     7.37
# ℹ 7 more variables: minerals <dbl>, manufact <dbl>, engaswat <dbl>,
#   construct <dbl>, apart <dbl>, retail <dbl>, invest <dbl>

Распространенный сценарий, который более удобно реализуется в purrr по сравнению с базовым R — это итерации по множеству аргументов. Например, неоходимо сгенерировать несколько популяций из \(7\) случайных глубин, имеющих равномерное распределение в заданных интервалах от \([-10, -6]\) до \([-5, -1]\). В базовом R для этого мы использовали бы mapply()

set.seed(2020)
zmin = -10:-6
zmax = -5:-1
depths <- mapply(
  runif, min = zmin, max = zmax, 
  MoreArgs = list(n = 7), SIMPLIFY = FALSE
)
str(depths)
List of 5
 $ : num [1:7] -6.77 -8.03 -6.91 -7.62 -9.32 ...
 $ : num [1:7] -7.03 -8.99 -5.9 -5.18 -5.28 ...
 $ : num [1:7] -5.95 -5.3 -3.2 -4.73 -5.27 ...
 $ : num [1:7] -6.61 -2.91 -2.29 -2.58 -6.17 ...
 $ : num [1:7] -3.75 -3.22 -1.18 -5.64 -1.22 ...

При этом обратим внимание на то, что аргументы, по которым не делаются итерации, выносятся в MoreArgs, а чтобы получить на выходе список, а не матрицу, мы применяем SIMPLIFY = FALSE.

При использовании map2 задача решается одним простым выражением:

depths <- map2(zmin, zmax, runif, n = 7)
str(depths)
List of 5
 $ : num [1:7] -8.16 -9.94 -5.33 -7.38 -8.89 ...
 $ : num [1:7] -4.68 -4.58 -5.88 -6.14 -6.65 ...
 $ : num [1:7] -7.17 -5.11 -6.56 -3.49 -5.39 ...
 $ : num [1:7] -4.79 -5.88 -4.28 -4.57 -5.96 ...
 $ : num [1:7] -3.88 -1.68 -2.93 -1.04 -3.12 ...

Если же и количество глубин в каждой популяции должно быть случайным, то нам понадобится функционал, который может принять более двух аргументов — это pmap :

nelem = sample(1:10, 5)
depths <- pmap(list(min = zmin, max = zmax, n = nelem), runif)
str(depths)
List of 5
 $ : num [1:5] -9.71 -6.64 -6.41 -5.91 -9.51
 $ : num [1:6] -7.47 -7.03 -7.09 -8.14 -7.83 ...
 $ : num [1:2] -7.41 -6.88
 $ : num -4
 $ : num [1:3] -2.14 -4.22 -2.01

Еще один полезный сценарий, который проддерживается purrr — это одновременная итерация по индексам аргументов и их значениям. Такая возможность реализуется в функционалах с префиксом i: imap(), iwalk() и их модификациях.

Функционалы семейства walk() в отличие от map не возвращают значения, а только посещают каждый элемент. Их используют, например, чтобы сохранить каждый элемент структуры данных в файл, построить график на его основе или просто вывести инфомацию о нем в консоль.

Например, можем вывести данные по случайным глубинам посредством iwalk() в аннотированном виде:

iwalk(depths, \(z, i) cat('Sample ', i, ': ', z, '\n', sep = ''))
Sample 1: -9.714993-6.636848-6.413566-5.909034-9.511208
Sample 2: -7.466208-7.030675-7.091471-8.139645-7.830458-7.188147
Sample 3: -7.405076-6.881571
Sample 4: -4.000526
Sample 5: -2.143847-4.222869-2.013891

4.3 Квотация аргументов

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

df["salary"]
# A tibble: 10 × 1
   salary
    <dbl>
 1 36268.
 2 39790 
 3 40303.
 4 69884.
 5 31483 
 6 34142 
 7 53240.
 8 32377.
 9 27405.
10 44187 
select(df, salary)
# A tibble: 10 × 1
   salary
    <dbl>
 1 36268.
 2 39790 
 3 40303.
 4 69884.
 5 31483 
 6 34142 
 7 53240.
 8 32377.
 9 27405.
10 44187 

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

Для начала разберемся с тем, что происходит в программном коде:

  • select() представляет собой вызов (call) функции
  • df и salary представляют собой символы, обозначающие объекты в программе.
  • "salary" представляет собой константу

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

a = 78     # стандартная запись
`=`(a, 78) # функциональная запись

a + 4     # стандартная запись
[1] 82
`+`(a, 4) # функциональная запись
[1] 82
df['salary']      # стандартная запись
# A tibble: 10 × 1
   salary
    <dbl>
 1 36268.
 2 39790 
 3 40303.
 4 69884.
 5 31483 
 6 34142 
 7 53240.
 8 32377.
 9 27405.
10 44187 
`[`(df, 'salary') # функциональная запись
# A tibble: 10 × 1
   salary
    <dbl>
 1 36268.
 2 39790 
 3 40303.
 4 69884.
 5 31483 
 6 34142 
 7 53240.
 8 32377.
 9 27405.
10 44187 

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

Программный код a + 4 с точки зрения R является выражением (expression). Выражение может состоять вообще из одного символа, то есть a — это тоже выражение. Когда интерпретатор доходит до выражения a + 4, он выполняет следующее:

  1. Оценка (evaluation) выражения a. Результатом оценки является константа 78
  2. Вызов (call) функции +, которая складывает константы 78 и 4

Не все символы и выражения в программе необходимо оценивать. Некоторые из них необходимо квотировать, то есть использовать в качестве имени объекта. Квотацию можно условно рассматривать как простановку кавычек вокруг выражения. Для обозначения квотации в явном виде используются обратные кавычки: ` Мы уже сталкивались с квотацией при вызове функции сложения: `+`(a, 4). В данном случае квотация была нужна чтобы + интерпретировался как имя функции.

Явная квотация бывает необходима, когда объекты R имеют недопустимые имена, например начинаются с цифры или содержат пробелы. Для начала рассмотрим искусственный пример:

`f + 17` = 87 # создаем объект с именем f + 17
f + 17 # ошибка: переменной f не существует
Error in eval(expr, envir, enclos): object 'f' not found
`f + 17` # обращаемся к объекту путем явной квотации
[1] 87

Теперь более жизнеспособный пример: данные о населении федеральных округов:

library(readr)
(okruga = read_csv('data/okruga.csv'))
# A tibble: 8 × 7
    `№` Регион            `2005` `2010` `2011` `2012` `2013`
  <dbl> <chr>              <dbl>  <dbl>  <dbl>  <dbl>  <dbl>
1     1 Центральный         4341   3761   3613   3651   3570
2     2 Северо-Западный     3192   3088   2866   2877   2796
3     3 Южный федеральный   1409   1446   1436   1394   1321
4     4 Северо-Кавказский    496    390    397    395    374
5     5 Приволжский         3162   2883   2857   2854   2849
6     6 Уральский           1681   1860   1834   1665   1624
7     7 Сибирский           2575   2218   2142   2077   1941
8     8 Дальневосточный      871    870    821    765    713

Обратите внимание на обратные кавычки вокруг названий столбцов. Они проставлены потому что числа в среде R по умолчанию оцениваются, и не могут использоваться в качестве символов для объектов. Чтобы разрешить это, используется принудительная квотация. Как мы уже знаем, чтобы обратиться к такому объекту, надо использовать обратные кавычки:

okruga$2010   # ошибка: 2010 - константа, которая оценивается
Error: <text>:1:8: unexpected numeric constant
1: okruga$2010
           ^
okruga$`2010` # правильно: `2010` -- символ, полученный путем квотации
[1] 3761 3088 1446  390 2883 1860 2218  870

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

Когда функция применяет квотацию аргументов, говорят что осуществляется нестандартная оценка (NSE – non-standard evaluation). Если аргументы функции оцениваются, то происходит стандартная оценка (SE – standard evaluation)

Иногда бывает необходимо использовать строковые названия столбцов (например, если они записаны у вас в переменные). Функция select достаточно умна и в случае если квотация дала несуществующие значения столбцов, произведет оценку аргументов и попытается извлечь из них имена:

f1 = 'salary'
f2 = 'birth'
select(df, f1, f2)        # ОК
# A tibble: 10 × 2
   salary birth
    <dbl> <dbl>
 1 36268.  13  
 2 39790   14.5
 3 40303.  11.8
 4 69884.  18.1
 5 31483   16.2
 6 34142   13.3
 7 53240.  12.4
 8 32377.  13.8
 9 27405.  13.2
10 44187   13.6

Если же передавать в качестве аргумента константы, то они всегда используются по значению, т.к. имени у них нет:

select(df, 'salary', 'birth') # то же самое
# A tibble: 10 × 2
   salary birth
    <dbl> <dbl>
 1 36268.  13  
 2 39790   14.5
 3 40303.  11.8
 4 69884.  18.1
 5 31483   16.2
 6 34142   13.3
 7 53240.  12.4
 8 32377.  13.8
 9 27405.  13.2
10 44187   13.6

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

(d = expression(b^2 - 4*a)) # создаем выражение
expression(b^2 - 4 * a)
a = 2
b = 7
eval(d) # оцениваем значение выражения
[1] 41

Все символы в объекте выражения по умолчанию квотируются и выражение хранится в статичном виде до тех пор пока не будет произведена его оценка (подстановка значений переменных вместо их символов).

Выражения и квотация существуют в R благодаря возможностям метапрограммирования. Это техника программирования, при которой программный код может генерировать другой код. Широкая поддержка и интенсивное использование метапрограммирования – одна из удивительных черт R, которая ставит его особняком на фоне многих других языков, включая Python. Метапрограммирование позволяет во многих случаях сделать код более компактным и подойти к решению задачи элегантным путем.

4.4 Краткий обзор

Для просмотра презентации щелкните на ней один раз левой кнопкой мыши и листайте, используя кнопки на клавиатуре:

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

4.5 Контрольные вопросы и упражнения

4.5.1 Вопросы

  1. Что такое функция?
  2. Какое ключевое слово ипользуется для создания функции?
  3. В каких случаях целесообразно применение функций?
  4. Сколько аргументов может принимать функция?
  5. Можно ли из одной функции вызывать другую функцию?
  6. Как осуществить принудительный выход из функции с возвратом результата?
  7. Что необходимо сделать, если надо передать несколько объектов из функции?
  8. Для чего нужны функционалы семейства apply? В каких задачах они бывают полезны?
  9. Перечислите функции семейства apply, назовите их отличия и сферы применения.
  10. Какая функция семейства apply позволяет обабатывать заданные измерения?
  11. Какой объект первым передается в функцию, подставляемую в параметр FUN, если применяется lapply к фрейму данных?
  12. Назовите аналоги функций apply из пакета purrr
  13. Что такое метапрограммирование?
  14. Из каких объектов состоят выражения в R?
  15. Что из себя по на самом деле представляют бинарные операторы в R?
  16. Как обратиться к объекту, символ которого не является допустимым именем переменной?
  17. Что такое оценка и квотация выражения? Для чего они используются?
  18. Как понять, будет ли используемая вами функция квотрировать или оценивать ее аргументы?
  19. Как называются функции dplyr, осуществляющие оценку, а не квотацию аргументов?
  20. Как создать и оценить объект выражения в R?

4.5.2 Упражнения

  1. Напишите функцию is_leap(year), которая определяет, является ли указанный год високосным. Протестируйте ее в вашем скрипте, используя чтение года из консолии

    Подсказка: високосным считается год, кратный 400, либо кратный 4, но не кратный 100.

  2. Функция Тоблера показывает зависимость скорости пешего маршрута (в км/ч) от угла наклона на местности. Предположим, в вашем распоряжении имеется матрица профиля рельефа, в которой в одном столбце указано расстояние от начала маршрута, а во втором — абсолютная отметка точки. Напишите функцию hiking_time(profile), которая вычисляет время прохождения маршрута на основе переданной ей матрицы. Используйте для тестирования функции маршрут из 10 точек с шагом в 1 км и случайным разбросом высот в диапазоне от 500 до 1000 метров (равномерное распределение).

    Подсказка: угол наклона на каждом участке считается постоянным. Для вычисления экспоненты используйте встроенную функцию exp().

  3. Создайте на основе данных по Москве с сайта pogodaiklimat таблицу Excel с повторяемостью различных направлений ветра. Не преобразовывая структуру данных, вычислите на ее основе с помощью lapply() преобладающее направление для каждого месяца. Представьте результат как фрейм данных.

  4. В текущей лекции мы работали с данными по характеристикам центров субъектов СЗФО. Напишите функцию get_extremes(df), которая определяет названия переменных, по которым каждая строчка фрейма данных (в нашем случае — город) занимает максимальное и минимальное положение относительно среднего значения по всем городам. Например, Петрозаводск имеет максимальный рейтинг по показателю doctors (1.05) — количество врачей на 10000 чел и минимальный по показателю manufact (0.05) — продукции обрабатывающей промышленность (в млн руб). Результирующая таблица должна содержать первое по счету поле, а также поля minvar и maxvar:

    Region        minvar    maxvar
    Петрозаводск doctors  manufact
    ...
    ...

    Подсказка: для начала вам надо нормировать значения всех переменных с помощью lapply(). Затем нужно определить номер столбца, имеющего максимальное и минимальное значений для каждой строки таблицы. Используйте для этого функции which.min() и which.max() возвращающие индекс максимального и минимального элемента вектора, в комбинации с функцией colnames(), возвращающей названия переменных. Применяйте функцию apply(), которая умеет ходить по строкам.

Самсонов Т.Е. Визуализация и анализ географических данных на языке R. М.: Географический факультет МГУ, lubridate::year(Sys.Date()). DOI: 10.5281/zenodo.901911