-
Notifications
You must be signed in to change notification settings - Fork 18
/
04-Functions.qmd
398 lines (277 loc) · 38.3 KB
/
04-Functions.qmd
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
# Функции {#controls}
В настоящей главе мы кратко познакомимся с функциональным программированием и метапрограммированием, которые являются одними из основных техник программирования на R.
## Функции {#tech-fun-functions}
Функции в R можно использовать для структурирования кода на логически завершенные, автономные фрагменты кода, каждый из которых выполняет конкретную задачу. Синтаксис функции выглядит следующим образом:
```{r eval=FALSE}
functionName = function(parameter1, parameter2, ...){
...
return(result)
...
last_value = ...
}
```
Функция создается c помощью ключевого слова `function`, за которым в круглых скобках заключается произвольное количество параметров (столько, сколько вам нужно: от нуля и более). С помощью этих параметров вы сможете передавать внутрь функции значения переменных. Созданной функции необходимо дать имя, используя оператор присвоения `<-` или `=`.
Возврат значения функции осуществляется двумя способами:
- Если не указано иное, то будет возвращен результат вычисления последнего выражения, выполненного внутри функции (`last_value` в примере выше)
- Результат можно вернуть принудительно в любом месте функции, передав его в выражение `return()`.
> Выражение `return()` работает аналогично ключевому слову `break` в циклах: оно прерывает выполнение функции и осуществляет выход из нее. Как правило, return() используется, если возврат значения надо сделать где-то посередине или в начале функции. Однако я реккомендую использовать его всегда, поскольку это помогает читателю вашей функции быстро определить, что же именно возвращается из функции
Как правило, функции оказываются полезны, если:
- Вы дублируете один и тот же код в разных частях программы
- Ваш код становится слишком длинным, при этом присутствует очевидная этапность решения задачи, позволяющая разбить программу на автономные блоки
- У вас есть фрагмент кода, который выполняет вспомогательную (второстепенную функцию), и не относится непосредственно к основной логике программы.
Предположим, у нас есть линия, заданная координатами четырех точек, и нам надо вычислить длины каждого из трех отрезков. Без использования функции мы запишем это так:
```{r}
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)
```
В правой части этих выражений стоит один и тот же код, который я скопировал и вставил, а далее заменил названия переменных. Это плохо сразу по двум причинам: дублирование фрагментов программы и возрастание вероятности опечаток в скопированой копии. Улучшить код можно, введя функцию вычисления расстояний:
```{r}
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)
```
Функция всегда возвращает один объект: значение, вектор, список и т.д. Например, мы можем сделать функцию следующего уровня, рассчитывающая сразу все расстояния для множества точек:
```{r}
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)
```
Можно пойти еще дальше, и сделать функцию, выполняющую вычисление длины линии, заданной координатами:
```{r}
line_length = function(x, y) {
sum(distances(x, y))
}
line_length(x, y)
```
Обратите внимание на то, как мы используем одну ранее написанную функцию при создании другой функции! Это весьма распространенная практика: одна и та же функция может быть как самостоятельно полезной (вызываться непосредственно в программе полтьзователя), так и применяться для решения задач внутри других функций. При этом иногда даже относительно простые фрагменты кода имеет смысл оформлять в виде функций, так как это может улучшить читаемость программы и пояснить смысл выполняемых операций. Так, например, `line_length(x, y)` в более явном виде обозначает операцию вычисления длины по координатам, нежели `sum(distances(x, y))`. В то же время, здесь важно не переусердствовать и оформлять короткие фрагменты кода в виде функций только если они будут применяться вами неоднократно.
Если вам нужно вернуть из функции несколько объектов, имеющих разный тип или смысл, заключите их в список и дайте каждому элементу списка "говорящее" имя. Например, помимо периметра, мы можем вернуть также извилистость линии (отношение длины линии к длине отрезка, соединяющего ее первую и последнюю точку):
```{r}
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
result$sinuosity
```
## Функционалы {#tech-fun-functionals}
### Базовые функционалы {#tech-fun-functionals-base}
Данные (в том числе географические) практически всегда носят множественный характер и организованы в определенные структуры (см. главу 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 год:
```{r, collapse=TRUE}
library(readxl)
(df = read_excel("data/sevzap.xlsx", col_types = c('text', rep('numeric', 17))))
```
В данной таблице каждый столбец представляет независимую переменную со своими единицами измерения, поэтому ее необходимо оставить в "широкой" форме, не преобразуя в длинную. Используя `apply`, можно быстро получить максимальные значения каждой переменной:
```{r}
apply(df[-1], 2, max)
```
Что равносильно вызову sapply:
```{r}
sapply(df[-1], max)
```
В качестве функции можно использовать не только стандартные, но и пользовательские функции. Например, нам может быть интересно не максимальное значение показателя, а его отношение к среднему значению среди всех городов. Здесь уже одной функцией не обойдешься, так как нужно каждый столбец поделить на среднее значение по нему. Для этого определим небольшую пользовательскую функцию непосредственно при вызове `sapply()`:
```{r}
(normalized = sapply(df[-1], function(X) { round(X / mean(X, na.rm = TRUE), 2) }))
```
Полученный объект является матрицей. Таким образом, можно видеть, что функционалы бывают полезны не только для агрегирования таблиц, но и для преобразования данных, когда структура таблицы остается прежней.
В приведенном выше коде мы сознательно исключили первый столбец, поскольку он является текстовым. Можно сделать более мощную и универсальную функцию, которая будет нормировать все числовые столбцы таблицы, а текстовые оставлять в оригинале. Для этого проверку типа данных надо внести внутрь функции. Поскольку код функции при этом вырастает, целесообразно определить ее заранее. Поскольку в этом случае часть векторов будет символьной, а не числовой, необходимо применять функцию `lapply()`, которая вернет список из векторов, а не матрицу и таким образом сохранит типы каждого столбца:
```{r}
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())
```
### Функционалы `purrr` {#tech-fun-functionals-purrr}
В качестве альтернативы функциям `apply` можно также воспользоваться вычислениями посредством функций семейства `map` из пакета `purrr` (еще один пакет из [tidyverse](https://www.tidyverse.org/packages/)). Эти функции работают аналогично, но их разнообразие довольно велико. Например, часто используюбся следующие:
| Функция | Тип возвращаемого значения |
|-------------|----------------------------|
| `map()` | список |
| `map_lgl()` | вектор `logical` |
| `map_int()` | вектор `integer` |
| `map_dbl()` | вектор `double` |
| `map_chr()` | вектор `character` |
Основные отличия от базовых функций следующие:
- явное указание типа возвращаемого значения (`apply` могут быть непредсказуемы);
- данные всегда идут первым аргументом (это условие не выполняется для `mapply`).
- поддерживается большое разнообразие сочетаний входных и выходных параметров, в том числе итерации по нескольким векторам одновременно.
Вышеприведенные операции можно осуществить средствами **purrr** вот так:
```{r}
library(purrr)
map_dbl(df[-1], max)
df |> map(normalize) |> as_tibble()
```
Однако **purrr** позволяет выполнять функции не ко всем элементам, а только к тем, которые удовлетворяют заданным условиям. Если задачу нормировки необходимо решить один раз, то можно не создавать новую функцию, а выполнить нормировку по условию непосредственно при вызове функционала:
```{r}
df |> map_if(is.numeric, \(X) round(X / mean(X, na.rm = TRUE), 2)) |> as_tibble()
```
Распространенный сценарий, который более удобно реализуется в **purrr** по сравнению с базовым R --- это итерации по множеству аргументов. Например, неоходимо сгенерировать несколько популяций из $7$ случайных глубин, имеющих равномерное распределение в заданных интервалах от $[-10, -6]$ до $[-5, -1]$. В базовом R для этого мы использовали бы `mapply()`
```{r}
set.seed(2020)
zmin = -10:-6
zmax = -5:-1
depths <- mapply(
runif, min = zmin, max = zmax,
MoreArgs = list(n = 7), SIMPLIFY = FALSE
)
str(depths)
```
При этом обратим внимание на то, что аргументы, по которым не делаются итерации, выносятся в `MoreArgs`, а чтобы получить на выходе список, а не матрицу, мы применяем `SIMPLIFY = FALSE`.
При использовании `map2` задача решается одним простым выражением:
```{r}
depths <- map2(zmin, zmax, runif, n = 7)
str(depths)
```
Если же и количество глубин в каждой популяции должно быть случайным, то нам понадобится функционал, который может принять более двух аргументов --- это `pmap` :
```{r}
nelem = sample(1:10, 5)
depths <- pmap(list(min = zmin, max = zmax, n = nelem), runif)
str(depths)
```
Еще один полезный сценарий, который проддерживается **purrr** --- это одновременная итерация по индексам аргументов и их значениям. Такая возможность реализуется в функционалах с префиксом `i`: `imap()`, `iwalk()` и их модификациях.
> Функционалы семейства `walk()` в отличие от map не возвращают значения, а только посещают каждый элемент. Их используют, например, чтобы сохранить каждый элемент структуры данных в файл, построить график на его основе или просто вывести инфомацию о нем в консоль.
Например, можем вывести данные по случайным глубинам посредством `iwalk()` в аннотированном виде:
```{r}
iwalk(depths, \(z, i) cat('Sample ', i, ': ', z, '\n', sep = ''))
```
## Квотация аргументов {#tech-meta}
Вы уже сталкивались с квотацией, когда работали с функциями `dplyr`. Например, сравните следующие два способа извлечь столбец из фрейма данных:
```{r}
df["salary"]
select(df, salary)
```
В обоих случаях мы получили один и тот же результат. В первом случае было указано название столбца в кавычках. Во втором мы передали название столбца без кавычек, точно так же как было передано название фрейма данных. Как R догадался, что параметр `salary` --- это не имя переменной, которая живет на одном уровне с
Для начала разберемся с тем, что происходит в программном коде:
- `select()` представляет собой **вызов** (call) функции
- `df` и `salary` представляют собой **символы**, обозначающие объекты в программе.
- `"salary"` представляет собой **константу**
R --- это *функциональный* язык программирования. Любая программа на R состоит из вызовов функций, которые применяются к символам и константам. Привычные нам арифметические операции на самом деле тоже являются вызовами функций. Это выглядит довольно неожиданно:
```{r}
a = 78 # стандартная запись
`=`(a, 78) # функциональная запись
a + 4 # стандартная запись
`+`(a, 4) # функциональная запись
df['salary'] # стандартная запись
`[`(df, 'salary') # функциональная запись
```
Таким образом, бинарный оператор в R представляет собой *вызов функции с двумя аргументами*.
Программный код `a + 4` с точки зрения R является **выражением** *(expression)*. Выражение может состоять вообще из одного символа, то есть `a` --- это тоже выражение. Когда интерпретатор доходит до выражения `a + 4`, он выполняет следующее:
1. **Оценка** *(evaluation)* выражения `a`. Результатом оценки является константа `78`
2. **Вызов** *(call)* функции `+`, которая складывает константы 78 и 4
Не все символы и выражения в программе необходимо оценивать. Некоторые из них необходимо **квотировать**, то есть использовать в качестве *имени* объекта. Квотацию можно условно рассматривать как простановку кавычек вокруг выражения. Для обозначения квотации в явном виде используются обратные кавычки: `` ` `` Мы уже сталкивались с квотацией при вызове функции сложения: `` `+`(a, 4) ``. В данном случае квотация была нужна чтобы `+` интерпретировался как *имя* функции.
Явная квотация бывает необходима, когда объекты R имеют недопустимые имена, например начинаются с цифры или содержат пробелы. Для начала рассмотрим искусственный пример:
```{r, error=TRUE}
`f + 17` = 87 # создаем объект с именем f + 17
f + 17 # ошибка: переменной f не существует
`f + 17` # обращаемся к объекту путем явной квотации
```
Теперь более жизнеспособный пример: данные о населении федеральных округов:
```{r}
library(readr)
(okruga = read_csv('data/okruga.csv'))
```
Обратите внимание на обратные кавычки вокруг названий столбцов. Они проставлены потому что числа в среде R по умолчанию *оцениваются*, и не могут использоваться в качестве символов для объектов. Чтобы разрешить это, используется принудительная квотация. Как мы уже знаем, чтобы обратиться к такому объекту, надо использовать обратные кавычки:
```{r, error=TRUE}
okruga$2010 # ошибка: 2010 - константа, которая оценивается
```
```{r, error=TRUE}
okruga$`2010` # правильно: `2010` -- символ, полученный путем квотации
```
Теперь вернемся к примеру с использованием функции `select()`. Данная функция *оценивает* первый аргумент (фрейм данных) и *квотирует* все оставшиеся аргументы, которые отвечают за названия столбцов. Это позволяет избежать использования кавычек и использовать символы для наименования объектов. Чтобы понять, использует ли функция оценку или квотацию ее аргументов, необходимо ознакомиться с ее справкой.
> Когда функция применяет квотацию аргументов, говорят что осуществляется **нестандартная оценка** *(NSE -- non-standard evaluation)*. Если аргументы функции оцениваются, то происходит **стандартная оценка** *(SE -- standard evaluation)*
Иногда бывает необходимо использовать строковые названия столбцов (например, если они записаны у вас в переменные). Функция select достаточно умна и в случае если квотация дала несуществующие значения столбцов, произведет оценку аргументов и попытается извлечь из них имена:
```{r, error=TRUE}
f1 = 'salary'
f2 = 'birth'
select(df, f1, f2) # ОК
```
Если же передавать в качестве аргумента константы, то они всегда используются по значению, т.к. имени у них нет:
```{r}
select(df, 'salary', 'birth') # то же самое
```
Иногда выражения бывает необходимо создать, а оценивать уже потом. Такой подход иногда используется в статистическом анализе. Для этого существуеют **объекты выражений**, которые создаются с помощью функции `expression()`:
```{r}
(d = expression(b^2 - 4*a)) # создаем выражение
a = 2
b = 7
eval(d) # оцениваем значение выражения
```
Все символы в объекте выражения по умолчанию *квотируются* и выражение хранится в статичном виде до тех пор пока не будет произведена его оценка (подстановка значений переменных вместо их символов).
> Выражения и квотация существуют в R благодаря возможностям **метапрограммирования**. Это техника программирования, при которой программный код может генерировать другой код. Широкая поддержка и интенсивное использование метапрограммирования -- одна из удивительных черт **R**, которая ставит его особняком на фоне многих других языков, включая Python. Метапрограммирование позволяет во многих случаях сделать код более компактным и подойти к решению задачи элегантным путем.
## Краткий обзор {#tech_review}
Для просмотра презентации щелкните на ней один раз левой кнопкой мыши и листайте, используя кнопки на клавиатуре:
```{r, echo=FALSE}
knitr::include_url('https://tsamsonov.github.io/r-geo-course-slides/04_Functions.html#1', height = '390px')
```
> Презентацию можно открыть в отдельном окне или вкладке браузере. Для этого щелкните по ней правой кнопкой мыши и выберите соответствующую команду.
## Контрольные вопросы и упражнения {#tech_questions}
### Вопросы {#tech_questions_questions}
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?
### Упражнения {#tech_questions_tasks}
1. Напишите функцию `is_leap(year)`, которая определяет, является ли указанный год високосным. Протестируйте ее в вашем скрипте, используя чтение года из консолии
> **Подсказка**: високосным считается год, кратный 400, либо кратный 4, но не кратный 100.
2. [**Функция Тоблера**](https://en.wikipedia.org/wiki/Tobler%27s_hiking_function) показывает зависимость скорости пешего маршрута (в км/ч) от угла наклона на местности. Предположим, в вашем распоряжении имеется матрица профиля рельефа, в которой в одном столбце указано расстояние от начала маршрута, а во втором --- абсолютная отметка точки. Напишите функцию `hiking_time(profile)`, которая вычисляет время прохождения маршрута на основе переданной ей матрицы. Используйте для тестирования функции маршрут из 10 точек с шагом в 1 км и случайным разбросом высот в диапазоне от 500 до 1000 метров (равномерное распределение).
> **Подсказка:** угол наклона на каждом участке считается постоянным. Для вычисления экспоненты используйте встроенную функцию `exp()`.
3. Создайте на основе данных по Москве с сайта [pogodaiklimat](http://www.pogodaiklimat.ru/climate/27612.htm) таблицу *Excel* с повторяемостью различных направлений ветра. Не преобразовывая структуру данных, вычислите на ее основе с помощью `lapply()` преобладающее направление для каждого месяца. Представьте результат как фрейм данных.
4. В текущей лекции мы работали с [**данными**](https://github.com/tsamsonov/r-geo-course/blob/master/data/sevzap.xlsx) по характеристикам центров субъектов СЗФО. Напишите функцию `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](https://doi.org/10.5281/zenodo.901911) |