= function(parameter1, parameter2, ...){
functionName
...return(result)
...= ...
last_value }
4 Функции
В настоящей главе мы кратко познакомимся с функциональным программированием и метапрограммированием, которые являются одними из основных техник программирования на R.
4.1 Функции
Функции в R можно использовать для структурирования кода на логически завершенные, автономные фрагменты кода, каждый из которых выполняет конкретную задачу. Синтаксис функции выглядит следующим образом:
Функция создается c помощью ключевого слова function
, за которым в круглых скобках заключается произвольное количество параметров (столько, сколько вам нужно: от нуля и более). С помощью этих параметров вы сможете передавать внутрь функции значения переменных. Созданной функции необходимо дать имя, используя оператор присвоения <-
или =
.
Возврат значения функции осуществляется двумя способами:
Если не указано иное, то будет возвращен результат вычисления последнего выражения, выполненного внутри функции (
last_value
в примере выше)Результат можно вернуть принудительно в любом месте функции, передав его в выражение
return()
.
Выражение
return()
работает аналогично ключевому словуbreak
в циклах: оно прерывает выполнение функции и осуществляет выход из нее. Как правило, return() используется, если возврат значения надо сделать где-то посередине или в начале функции. Однако я реккомендую использовать его всегда, поскольку это помогает читателю вашей функции быстро определить, что же именно возвращается из функции
Как правило, функции оказываются полезны, если:
- Вы дублируете один и тот же код в разных частях программы
- Ваш код становится слишком длинным, при этом присутствует очевидная этапность решения задачи, позволяющая разбить программу на автономные блоки
- У вас есть фрагмент кода, который выполняет вспомогательную (второстепенную функцию), и не относится непосредственно к основной логике программы.
Предположим, у нас есть линия, заданная координатами четырех точек, и нам надо вычислить длины каждого из трех отрезков. Без использования функции мы запишем это так:
= rnorm(4)
x = rnorm(4)
y = sqrt((x[1] - x[2]) ^ 2 + (y[1] - y[2]) ^ 2)
d12 = sqrt((x[2] - x[3]) ^ 2 + (y[2] - y[3]) ^ 2)
d23 = sqrt((x[3] - x[4]) ^ 2 + (y[3] - y[4]) ^ 2)
d31 cat(d12, d23, d31)
1.78599 1.780432 2.739223
В правой части этих выражений стоит один и тот же код, который я скопировал и вставил, а далее заменил названия переменных. Это плохо сразу по двум причинам: дублирование фрагментов программы и возрастание вероятности опечаток в скопированой копии. Улучшить код можно, введя функцию вычисления расстояний:
= function(x1, y1, x2, y2) {
distance sqrt((x1 - x2) ^ 2 + (y1 - y2) ^ 2)
}
= distance(x[1], y[1], x[2], y[2])
d12 = distance(x[2], y[2], x[3], y[3])
d23 = distance(x[3], y[3], x[4], y[4])
d31 cat(d12, d23, d31)
1.78599 1.780432 2.739223
Функция всегда возвращает один объект: значение, вектор, список и т.д. Например, мы можем сделать функцию следующего уровня, рассчитывающая сразу все расстояния для множества точек:
= function(x, y) {
distances = length(x)
n 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
Можно пойти еще дальше, и сделать функцию, выполняющую вычисление длины линии, заданной координатами:
= function(x, y) {
line_length sum(distances(x, y))
}
line_length(x, y)
[1] 6.305645
Обратите внимание на то, как мы используем одну ранее написанную функцию при создании другой функции! Это весьма распространенная практика: одна и та же функция может быть как самостоятельно полезной (вызываться непосредственно в программе полтьзователя), так и применяться для решения задач внутри других функций. При этом иногда даже относительно простые фрагменты кода имеет смысл оформлять в виде функций, так как это может улучшить читаемость программы и пояснить смысл выполняемых операций. Так, например, line_length(x, y)
в более явном виде обозначает операцию вычисления длины по координатам, нежели sum(distances(x, y))
. В то же время, здесь важно не переусердствовать и оформлять короткие фрагменты кода в виде функций только если они будут применяться вами неоднократно.
Если вам нужно вернуть из функции несколько объектов, имеющих разный тип или смысл, заключите их в список и дайте каждому элементу списка “говорящее” имя. Например, помимо периметра, мы можем вернуть также извилистость линии (отношение длины линии к длине отрезка, соединяющего ее первую и последнюю точку):
= function(x, y) {
line_params = length(x)
n
= line_length(x, y)
l = l / distance(x[1], y[1], x[n], y[n])
s
list(length = l, sinuosity = s)
}
= line_params(x, y)
result $length result
[1] 6.305645
$sinuosity result
[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)
= function(X) {
normalize 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
|> map(normalize) |> as_tibble() df
# 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 позволяет выполнять функции не ко всем элементам, а только к тем, которые удовлетворяют заданным условиям. Если задачу нормировки необходимо решить один раз, то можно не создавать новую функцию, а выполнить нормировку по условию непосредственно при вызове функционала:
|> map_if(is.numeric, \(X) round(X / mean(X, na.rm = TRUE), 2)) |> as_tibble() df
# 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)
= -10:-6
zmin = -5:-1
zmax <- mapply(
depths min = zmin, max = zmax,
runif, 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
задача решается одним простым выражением:
<- map2(zmin, zmax, runif, n = 7)
depths 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
:
= sample(1:10, 5)
nelem <- pmap(list(min = zmin, max = zmax, n = nelem), runif)
depths 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
. Например, сравните следующие два способа извлечь столбец из фрейма данных:
"salary"] df[
# 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 состоит из вызовов функций, которые применяются к символам и константам. Привычные нам арифметические операции на самом деле тоже являются вызовами функций. Это выглядит довольно неожиданно:
= 78 # стандартная запись
a `=`(a, 78) # функциональная запись
+ 4 # стандартная запись a
[1] 82
`+`(a, 4) # функциональная запись
[1] 82
'salary'] # стандартная запись df[
# 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
, он выполняет следующее:
- Оценка (evaluation) выражения
a
. Результатом оценки является константа78
- Вызов (call) функции
+
, которая складывает константы 78 и 4
Не все символы и выражения в программе необходимо оценивать. Некоторые из них необходимо квотировать, то есть использовать в качестве имени объекта. Квотацию можно условно рассматривать как простановку кавычек вокруг выражения. Для обозначения квотации в явном виде используются обратные кавычки: `
Мы уже сталкивались с квотацией при вызове функции сложения: `+`(a, 4)
. В данном случае квотация была нужна чтобы +
интерпретировался как имя функции.
Явная квотация бывает необходима, когда объекты R имеют недопустимые имена, например начинаются с цифры или содержат пробелы. Для начала рассмотрим искусственный пример:
`f + 17` = 87 # создаем объект с именем f + 17
+ 17 # ошибка: переменной f не существует 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 по умолчанию оцениваются, и не могут использоваться в качестве символов для объектов. Чтобы разрешить это, используется принудительная квотация. Как мы уже знаем, чтобы обратиться к такому объекту, надо использовать обратные кавычки:
$2010 # ошибка: 2010 - константа, которая оценивается okruga
Error: <text>:1:8: unexpected numeric constant
1: okruga$2010
^
$`2010` # правильно: `2010` -- символ, полученный путем квотации okruga
[1] 3761 3088 1446 390 2883 1860 2218 870
Теперь вернемся к примеру с использованием функции select()
. Данная функция оценивает первый аргумент (фрейм данных) и квотирует все оставшиеся аргументы, которые отвечают за названия столбцов. Это позволяет избежать использования кавычек и использовать символы для наименования объектов. Чтобы понять, использует ли функция оценку или квотацию ее аргументов, необходимо ознакомиться с ее справкой.
Когда функция применяет квотацию аргументов, говорят что осуществляется нестандартная оценка (NSE – non-standard evaluation). Если аргументы функции оцениваются, то происходит стандартная оценка (SE – standard evaluation)
Иногда бывает необходимо использовать строковые названия столбцов (например, если они записаны у вас в переменные). Функция select достаточно умна и в случае если квотация дала несуществующие значения столбцов, произведет оценку аргументов и попытается извлечь из них имена:
= 'salary'
f1 = 'birth'
f2 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)
= 2
a = 7
b eval(d) # оцениваем значение выражения
[1] 41
Все символы в объекте выражения по умолчанию квотируются и выражение хранится в статичном виде до тех пор пока не будет произведена его оценка (подстановка значений переменных вместо их символов).
Выражения и квотация существуют в R благодаря возможностям метапрограммирования. Это техника программирования, при которой программный код может генерировать другой код. Широкая поддержка и интенсивное использование метапрограммирования – одна из удивительных черт R, которая ставит его особняком на фоне многих других языков, включая Python. Метапрограммирование позволяет во многих случаях сделать код более компактным и подойти к решению задачи элегантным путем.
4.4 Краткий обзор
Для просмотра презентации щелкните на ней один раз левой кнопкой мыши и листайте, используя кнопки на клавиатуре:
Презентацию можно открыть в отдельном окне или вкладке браузере. Для этого щелкните по ней правой кнопкой мыши и выберите соответствующую команду.
4.5 Контрольные вопросы и упражнения
4.5.1 Вопросы
- Что такое функция?
- Какое ключевое слово ипользуется для создания функции?
- В каких случаях целесообразно применение функций?
- Сколько аргументов может принимать функция?
- Можно ли из одной функции вызывать другую функцию?
- Как осуществить принудительный выход из функции с возвратом результата?
- Что необходимо сделать, если надо передать несколько объектов из функции?
- Для чего нужны функционалы семейства apply? В каких задачах они бывают полезны?
- Перечислите функции семейства apply, назовите их отличия и сферы применения.
- Какая функция семейства apply позволяет обабатывать заданные измерения?
- Какой объект первым передается в функцию, подставляемую в параметр
FUN
, если применяется lapply к фрейму данных? - Назовите аналоги функций apply из пакета purrr
- Что такое метапрограммирование?
- Из каких объектов состоят выражения в R?
- Что из себя по на самом деле представляют бинарные операторы в R?
- Как обратиться к объекту, символ которого не является допустимым именем переменной?
- Что такое оценка и квотация выражения? Для чего они используются?
- Как понять, будет ли используемая вами функция квотрировать или оценивать ее аргументы?
- Как называются функции dplyr, осуществляющие оценку, а не квотацию аргументов?
- Как создать и оценить объект выражения в R?
4.5.2 Упражнения
Напишите функцию
is_leap(year)
, которая определяет, является ли указанный год високосным. Протестируйте ее в вашем скрипте, используя чтение года из консолииПодсказка: високосным считается год, кратный 400, либо кратный 4, но не кратный 100.
Функция Тоблера показывает зависимость скорости пешего маршрута (в км/ч) от угла наклона на местности. Предположим, в вашем распоряжении имеется матрица профиля рельефа, в которой в одном столбце указано расстояние от начала маршрута, а во втором — абсолютная отметка точки. Напишите функцию
hiking_time(profile)
, которая вычисляет время прохождения маршрута на основе переданной ей матрицы. Используйте для тестирования функции маршрут из 10 точек с шагом в 1 км и случайным разбросом высот в диапазоне от 500 до 1000 метров (равномерное распределение).Подсказка: угол наклона на каждом участке считается постоянным. Для вычисления экспоненты используйте встроенную функцию
exp()
.Создайте на основе данных по Москве с сайта pogodaiklimat таблицу Excel с повторяемостью различных направлений ветра. Не преобразовывая структуру данных, вычислите на ее основе с помощью
lapply()
преобладающее направление для каждого месяца. Представьте результат как фрейм данных.В текущей лекции мы работали с данными по характеристикам центров субъектов СЗФО. Напишите функцию
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 |