Взаимодействие с другими языками программирования
Нередко возникает необходимость использовать скрипты и алгоритмы, написанные на других, часто низкоуровневых языках. Причин этому может быть множество: как ожидаемый прирост скорости (как в случае с C++), так и необходимость работать с уже готовыми библиотеками - например, некоторыми алгоритмами, написанными на Fortran или Python. Точно так же в работе многих исследователей и аналитиков жизненно необходимы инструменты для работы с базами данных — возможность подключиться к базе данных и отправить SQL-запрос, импортировать/экспортировать данные. Ещё одна важная область, в которой используются другие языки программирования, — это презентация результатов исследований и, соответственно, связь с языками разметки и возможностью конвертировать отчёты в разные форматы, инструменты для воспроизводимых исследований, использование HTML и JavaScript для интерактивных графиков и таблиц, а также дашбордов.
Низкоуровневые языки
C и Fortran
R в своей основе написан на C, точно так же многие статистические алгоритмы используют скрипты на Fortran. Тем не менее, можно функции и рутины на этих языках использовать и в пользовательских скриптах на R. Схема импорта скриптов на C и Fortran идентична — необходимо скомпилировать код и потом подключить динамическую библиотеку (dll, совместно используемый объект).
Например, у нас есть функция, написанная на C (для Fortran это будут подпрограммы, subroutines). В функции первый аргумент — длина массива, второй аргумент — сам массив, в теле функции происходит возведение в квадрат элементов массива (точнее, столько первых элементов, сколько было передано в первом аргументе).
cat ./src/c_example.c## void my_c_f(int *nin, double *x)
## {
## int n = nin[0];
##
## int i;
##
## for (i=0; i<n; i++)
## x[i] = x[i] * x[i];
## }
Скомпилировать этот скрипт можно с помощью консольной команды R (для этого потребуется компилятор С или Fortran, оба есть в gcc или в RTools для Windows):
R CMD SHLIB ./src/c_example.c## make: Nothing to be done for 'all'.
Далее следует подключить скомпилированный код (расширение файла может зависеть от компилятора и операционной системы):
dyn.load("./src/c_example.so")Следующим этапом будет обращение к этому коду, для C и Fortran есть соответствующие функции в базовом R, функции .C() и .Fortran():
.C('my_c_f', as.integer(5), as.double(1:5))## [[1]]
## [1] 5
##
## [[2]]
## [1] 1 4 9 16 25
Следует иметь в виду одну особенность: при выполнении кода будут возвращены все объекты, которые были созданы во время его выполнения. Соответственно, для красоты и большей понятности кода лучше писать функции-обертки (врапперы), которые разбирают результат скрипта на C/Fortran и возвращают только необходимое значение. Также во враппер можно добавить сообщения об ошибках и скрыть некоторые дополнительные вычисления, которые необходимы для C/Fortran:
c_wrapper <- function(x) {
if (!is.numeric(x))
stop('argument x must be numeric')
dyn.load("./src/c_example.so")
out <- .C('my_c_f',
n = as.integer(length(x)),
x = as.double(x))
return(out$x)
}
c_wrapper(1:5)## [1] 1 4 9 16 25
C++
В настоящее время для низкоуровневой оптимизации чаще всего используются вставки кода на C++. Основной пакет для работы с C++ в R — Rcpp и связанные с ним пакеты, дающие возможность работать с разными библиотеками C++. Влияние Rcpp растёт постоянно, в настоящее время более нескольких тысяч R пакетов используют Rcpp и реализации алгоритмов на C++. Для работы в Windows дополнительно должен быть установлен набор инструментов Rtools (XCode для MacOS).
Включить код на C++ в свой рабочий код на R можно двумя путями (плюс чанки в R Markdown). Первый и более элегантный — создать внешний файл, в котором будет использован заголовочный файл Rcpp.h, соответствующее пространство имён using namespace Rcpp и указан префикс // [[Rcpp::export]]. Напишем простую функцию, которая умножает на два переданное значение:
#include <Rcpp.h>
using namespace Rcpp;
// [[Rcpp::export]]
NumericVector timesTwo(NumericVector x) {
return x * 2;
}
/*** R
timesTwo(42)
*/Для того чтобы можно было обращаться к функциям из R-окружения, необходимо их импортировать, для этого потребуется функция sourceCpp(). Стоит учитывать, что импортированные таким образом функции не сохраняются в .RData, поэтому их при старте новой сессии необходимо будет импортировать заново.
В конце .cpp-файла можно сделать вставку на R, в виде /* R <код на R> */. Тогда при компиляции и импорте скрипта в R этот код будет выполнен уже интерпретатором R. Это полезный инструмент для тестирования написанной на C++ функции.
##
## > timesTwo(42)
## [1] 84
# применяем импортированную функцию
timesTwo(9)## [1] 18
Второй способ — написанный код на C++ передать в виде строки в функцию cppFunction(). Так как это работа уже явно в R-окружении, не надо указывать заголовочные файлы и т. д.
# создаём объект с кодом на C++ в виде строки
cpp_code <- '
NumericVector my_cpp_f(NumericVector x) {
return x * 2;
}
'
# создаем R-функцию
cppFunction(cpp_code)
# смотрим, как работает
my_cpp_f(9)## [1] 18
Оба варианта, sourceCpp() и cppFunction(), компилируют код на C++ и на его основе создают совместно используемый объект, который подгружается в R-окружение и потом вызывается с помощью .Call().
Java
Обращения к Java из R — весьма редкое событие в практике большинства пользователей. Обычно пользователи сталкиваются с Java при работе с пакетами, которые работают с программами MS Office, в частности, MS Excel.
Работать с Java из R можно несколькими способами: через обращение к командной строке с помощью system() или с помощью соответствующих пакетов (например, rJava). Сам язык также надо установить, обычно это делается с помощью java developer kit (JDK или OpenJDK).
Простейший класс, при инстанцировании которого выводится на печать сообщение Hello, World. либо, если был использован дополнительный аргумент, текст этого аргумента:
cat ./src/HelloWorld.java## public class HelloWorld {
##
## public String sayHello() {
## String result = new String("Hello Java World!");
## return result;
## }
##
## public static void main(String[] args) {
## String stringArg = "Hello, World.";
##
## if (args.length > 0) {
## stringArg = args[0];
## }
##
## System.out.println(stringArg);
## }
## }
Наиболее примитивный способ — компиляция класса и создание экземпляра класса в командной строке. Для этого необходимо использовать функцию system() и команды javac (для компиляции класса, это создаёт одноимённый файл) и java (для создания экземпляра класса). Так как у нас в main есть аргументы и System.out.println(), то при создании экземпляра класса на печать выводится соответствующее сообщение.
# компилируем класс
system('javac ./src/HelloWorld.java')
# переходим в директорию с файлом класса и создаём экземпляр
system('cd ./src && java HelloWorld')
# создаём экземпляр класса и указываем дополнительный аргумент
system('cd ./src && java HelloWorld "new string"')Более сложный, но более гибкий способ работать с Java — это использовать пакет rJava, который помогает из R запускать виртуальную машину (.jinit() вернёт 0, если машина инициализирована) и использовать скомпилированные классы.
После инициализации виртуальной машины необходимо указать, где лежат архивы скомпилированных классов (jar) и создать экземпляр класса в окружении R. Для этого используются функции .jaddClassPath() и .jclassPath():
# создаём архив из файла класса
system('jar cvf ./src/HelloWorld.jar ./src/HelloWorld.class')
# указываем путь к папке с архивом
.jaddClassPath(paste0(getwd(), '/src'))
.jclassPath()## [1] "/home/konhis/R/x86_64-pc-linux-gnu-library/4.1/rJava/java"
## [2] "/home/konhis/Documents/Rprojects/r_textbook/src"
## Formal class 'jobjRef' [package "rJava"] with 2 slots
## ..@ jobj :<externalptr>
## ..@ jclass: chr "HelloWorld"
Методы и поля класса можно посмотреть с помощью функций .jfields() и .jmethods().:
.jfields(obj)## NULL
.jmethods(obj)## [1] "public static void HelloWorld.main(java.lang.String[])"
## [2] "public java.lang.String HelloWorld.sayHello()"
## [3] "public final void java.lang.Object.wait(long,int) throws java.lang.InterruptedException"
## [4] "public final void java.lang.Object.wait() throws java.lang.InterruptedException"
## [5] "public final native void java.lang.Object.wait(long) throws java.lang.InterruptedException"
## [6] "public boolean java.lang.Object.equals(java.lang.Object)"
## [7] "public java.lang.String java.lang.Object.toString()"
## [8] "public native int java.lang.Object.hashCode()"
## [9] "public final native java.lang.Class java.lang.Object.getClass()"
## [10] "public final native void java.lang.Object.notify()"
## [11] "public final native void java.lang.Object.notifyAll()"
Вызов методов можно сделать тремя путями. Первый и наиболее быстрый — с помощью функции .jcall(), однако необходимо прямо указывать, какого типа будет результат, притом тип в JNI-нотации. Более простой метод — с помощью J(), в котором необходимо только указать Java-объект и название метода. Третий более привычен для пользователей R и напоминает вызов методов R6-классов, так как использует оператор $. Функция .DollarNames() вернёт список методов класса в том виде, который необходим для их вызова с помощью $:
# вызываем метод sayHello класса HelloWorld
.jcall(obj, returnSig = 'S', method = 'sayHello')## [1] "Hello Java World!"
J(obj, method = 'sayHello')## [1] "Hello Java World!"
obj$sayHello()## [1] "Hello Java World!"
Python
reticulate — относительно недавний пакет, который прямо предназначен для работы с Python из R. Пакет разработан компанией RStudio, которая активно старается расширить количество поддерживаемых языков хотя бы на уровне markdown-чанков и скриптов. Его авторы даже с энтузиазмом говорят, что пакет reticulate позволяет писать пакеты, гармонично использующие оба языка.
Работать с Python с помощью пакета reticulate можно в нескольких ситуациях:
выполнение скриптов на Python и загрузка функций и объектов в рабочее окружение R (по аналогии с
source());подгрузка модулей и виртуальных окружений Python в рабочее окружение R;
реализация интерактивной консоли Python (REPL) в консоли R;
чанки в R Markdown по аналогии с другими языками чанков (
r,bash,stanи т. д.).
Версия Python
При начале работы стоит с помощью базовой функции Sys.which() уточнить, какая версия Python прописана в системных путях (PATH) и есть ли вообще такая запись (т. е. установлен ли язык). Для более подробной информации о всех версиях языка можно воспользоваться py_config() или py_discover_config().
library(reticulate)
# смотрим, какая версия есть
Sys.which('python')## python
## ""
Sys.which('python3')## python3
## "/usr/bin/python3"
# смотрим более подробную информацию
py_config()## python: /usr/bin/python3
## libpython: /usr/lib/python3.8/config-3.8-x86_64-linux-gnu/libpython3.8.so
## pythonhome: //usr://usr
## version: 3.8.10 (default, Nov 26 2021, 20:14:08) [GCC 9.3.0]
## numpy: /home/konhis/.local/lib/python3.8/site-packages/numpy
## numpy_version: 1.19.4
В том случае, когда есть несколько версий, можно указать какую-то определенную. Это особенно актуально, когда предполагается использовать модули, актуальные для версий 2.7, а обычно используются версии 3.*. Для этого есть функция use_python(). Пакет reticulate также позволяет работать с виртуальными окружениями (use_virtualenv()), а также с окружением Conda (use_condaenv()). Впрочем, если для работы с Python напрямую (не через вызовы из R) не используется Anaconda, то лучше не усложнять систему и ограничиться просто pip и модулями или же отдельным виртуальным окружением.
Импорт модулей и скриптов
Один из самых важных функционалов пакета reticulate — импорт модулей Python в рабочее окружение R и работа с ними как с объектами R. Для импорта модулей используется функция import(). Получить доступ к функциям и объектам модуля можно с помощью оператора $, как при работе со списками:
# импорт пакета platform
py_platform <- import('platform')
# вызываем функцию platform модуля platform
py_platform$platform()## [1] "Linux-5.4.0-96-generic-x86_64-with-glibc2.29"
Аналогично можно выполнять Python-скрипты и импортировать результат их работы в R. Допустим, у нас есть скрипт python_example.py, в котором мы объявляем функцию возведения объекта в четвёртую степень.
cat ./src/python_example.py## def my_f_python(x):
## return x ** 4
Пример выполнения этого скрипта и доступа к полученным объектам, для чистоты запишем их в новое окружение:
# создаём новое окружение и выполняем python-скрипт
my_env <- new.env()
source_python('./src/python_example.py', envir = my_env)
# смотрим результат выполнения скрипта
ls(my_env)## [1] "my_f_python" "r"
# применяем функцию
my_env$my_f_python(5)## [1] 625
Преобразование типов
При импорте модулей или объектов происходит преобразование типов Python-объектов в R-объекты. При вызове R-объектов из Python происходит аналогичное преобразование. Конечно, конвертируются только самые основные классы объектов — функции, словари, списки, массивы и таблицы. Обычно соответствия типов и классов иллюстрируют следующей таблицей:
| Объект | Python | Примеры на R |
|---|---|---|
| Вектор из одного элемента | Scalar | 1, 1L, TRUE, “foo” |
| Вектор | List | c(1.0, 2.0, 3.0), c(1L, 2L, 3L) |
| Список | Tuple | list(1L, TRUE, “foo”) |
| Именованный список | Dict | list(a = 1L, b = 2.0) |
| Матрица/массив | NumPy ndarray | matrix(c(1,2,3,4), nrow = 2, ncol = 2) |
| Таблица | Pandas DataFrame | data.frame(x = c(1,2,3), y = c(“a”, “b”, “c”)) |
| Функция | Python function | function(x) x + 1 |
| Логические значения | True, False | TRUE, FALSE |
| NULL | None | NULL |
Интерактивная консоль
Переключение в интерактивную консоль Python в консоли R можно сделать с помощью функции repl_python(), будет использоваться та версия Python, которая стоит в системе по умолчанию. Выход из консоли — с помощью команды exit.
Примеры создания и использования функции даны в виде копий консоли:
> repl_python()
Python 3.7.5 (/usr/bin/python3)
Reticulate 1.16 REPL -- A Python interpreter in R.
>>> def my_f_python(x):
... return x ** 3
...
>>> my_f_python(5)
125
>>> exit
>
После того, как будет завершена Python-сессия (после exit и возвращения в консоль R), созданные объекты можно вызвать как элементы объекта py в глобальном окружении:
> py$my_f_python(5)
[1] 125
Чанки в R Markdown
Переключение на Python в чанках традиционное, как и с другими языками (language engines), указанием языка в заголовке чанка (python). Если в скрипте предполагается использовать одновременно и R, и Python, то в начале скрипта необходимо подключить пакет reticulate.
def my_f_python(x):
return x ** 4
my_f_python(5)## 625
Если в скрипте был подключен пакет reticulate, то созданные в Python-чанках объекты будут доступны и в R-чанках, так же как и в объекте py при работе с интерактивной консолью:
res_from_py <- py$my_f_python(5)
res_from_py## [1] 625
Аналогично в Python-чанках можно работать с R-объектами (если reticulate был явно подключен). Все R-объекты хранятся в объекте r:
r.res_from_py## 625.0
SQL
SQL, наверное, относится к той категории инструментов, которые нужно знать всем, кто работает с задачами анализа данных. При этом в академическом мире хранить данные исследований в базах данных не самая распространенная практика. В то же время в индустрии фактически всё информация, с которой работает аналитик, доступна как раз в виде баз данных. Конечно, вариаций может быть много, но чаще всего в практике встречаются базы, поддерживающие тот или иной диалект SQL.
Сочетание SQL и R весьма простое. Основой служит пакет DBI, который является интерфейсом к базе данных. С помощью пакета создаётся коннектор к базе, и потом уже с использованием этого коннектора происходит сбор данных, загрузка таблиц и т. д. Второй компонент — это собственно драйвер для подключения. Как правило, для разных баз данных существуют свои пакеты (RPostgreSQL, RPresto и т. д.). Также есть общий пакет odbc, который разрабатывается и поддерживается командой RStudio, в ледствие чего интегрирован в IDE, — при создании подключения есть возможность просматривать таблицы базы данных, а также писать и выполнять запросы, как если бы это был отдельный клиент для работы с БД.
Иногда встречаются рекомендации использовать пакет dbplyr, который включает в себя бэкенды нескольких наиболее популярных баз данных и позволяет обращаться к таблицам с помощью tidyverse-синтаксиса (переводит глагольные выражения на R в SQL-запрос). На мой взгляд, это удобно только в том случае, если запрос осуществляется к небольшой таблице или сам по себе простой. А для каких-то более сложных вещей, таких как CTE, сложных джойнов с условиями, использования представлений, оконных функции и т. д., лучше писать и оптимизировать запрос самостоятельно.
Последовательность действий при работе с базой данных выглядит следующим образом. Сначала подключаются пакеты и открывается подключение (создаётся коннектор). После чего, как правило, отправляется запрос к таблице и импорт данных из базы данных в рабочее окружение. Завершающим этапом необходимо закрыть соединение. Конечно, это лишь общая канва, при работе с боевыми базами данных может возникнуть множество нюансов или деталей. Большинство из них так или иначе решены в пакетах, но с ними надо будет разбираться по факту.
Для примера воспользуемся пакетом RSQLite, который в оперативной памяти эмулирует базу данных. Попробуем создать подключение и выполнить стандартные задачи — запись таблицы в базу данных и простой запрос к таблице с фильтрацией. Все пакеты бэкендов так или иначе используют DBI, поэтому явно его подключать необязательно. В этом примере при создании коннектора не указываются дополнительные параметры, при работе с реальной базой данных обычно дополнительно указываются ещё имя пользователя, пароль, название базы данных и прочие возможные параметры, в зависимости от БД и её настроек.
# подключаем бэкенд
library(RSQLite)
# создаём коннектор/открываем соединение
con <- dbConnect(RSQLite::SQLite(), ":memory:")
# записываем таблицу в базу данных
dbWriteTable(conn = con, name = 'mtcars_db', value = mtcars)Запись таблицы в базу данных осуществляется с помощью функции dbWriteTable(), в аргументы которой мы передаём коннектор, указываем название новой таблицы и какая таблица из рабочего окружения в R должна быть записана. Стоит учитывать, что при записи в базу данных теряются названия строк, если они были в таблице (впрочем, это больше характерно для data.frame-синтаксиса, который встречается всё реже). Проверить, что таблица была создана, можно с помощью dbListTables() или полноценного запроса:
dbListTables(con)## [1] "mtcars_db"
Запросы к базе данных выполняются схожим образом — функциями dbGetQuery() или dbSendQuery(), в аргументы которых передаются коннектор и строка запроса. Первая функция используется в ситуациях, когда нужно именно импортировать данные в рабочее окружение R, вторая — когда необходимо выполнить что-то на сервере. Строка запроса выглядит как стандартный SQL-запрос на соответствующем диалекте. В тех случаях, когда необходимо в запросе использовать какие-то изменяемые параметры (например, даты), полезно формировать запросы с помощью paste() или glue::glue(). В примере ниже я импортирую три строки из таблицы mtcars_db, в которых в поле cyl значения равны значению переменной my_cyl:
# пользовательская переменная
my_cyl <- 6
# пишем запрос с использованием переменной
query <- paste('select * from mtcars_db where cyl =', my_cyl, 'limit 3')
# делаем запрос к БД и импортируем результат
dbGetQuery(conn = con, statement = query)## mpg cyl disp hp drat wt qsec vs am gear carb
## 1 21.0 6 160 110 3.90 2.620 16.46 0 1 4 4
## 2 21.0 6 160 110 3.90 2.875 17.02 0 1 4 4
## 3 21.4 6 258 110 3.08 3.215 19.44 1 0 3 1
После завершения работы с базой данных необходимо закрывать соединение с помощью функции dbDisconnect(). В целом проще и правильнее всего будет создать собственную функцию для импорта данных, в которой выполняются все этапы, включая открытие соединения, импорт и первичный процессинг данных (например, преобразование в data.table) и разрыв соединения. В данном случае, так как база данных у нас существует только когда открыто соединение (потому что создаётся виртуально), добавим строчку записи данных в виртуальную БД. Естественно, в работе с существующими базами данных это лишний шаг.
# объявляем функцию
my_connector <- function(q) {
con <- dbConnect(RSQLite::SQLite(), ":memory:")
# только для того, чтобы работал пример
dbWriteTable(conn = con, name = 'mtcars_db', value = mtcars)
res <- dbGetQuery(con, q)
on.exit(dbDisconnect(conn = con))
return(res)
}
# делаем запрос к БД
mtcars_6c <- my_connector(query)
mtcars_6c## mpg cyl disp hp drat wt qsec vs am gear carb
## 1 21.0 6 160 110 3.90 2.620 16.46 0 1 4 4
## 2 21.0 6 160 110 3.90 2.875 17.02 0 1 4 4
## 3 21.4 6 258 110 3.08 3.215 19.44 1 0 3 1
Markdown
Одна из неотъемлемых частей современного R — поддержка языка разметки markdown. В отличие от многих других языков, в которых реализовано взаимодействие с markdown, в R это превратилось в целую систему взаимосвязанных пакетов и практик.
В R markdown используется в виде .Rmd-документов, в которых совмещены текст в markdown-разметке и блоки (чанки) кода. Основной язык чанков — R, однако также могут быть чанки на bash, C++, Python, SQL, stan и некоторых других языках. При рендере на основе этих .Rmd-документов могут быть получены файлы в форматах md, pdf, tex, HTML-страницы и документы MS Office. Всё это превращает R и R Markdown в мощный инструмент для составления отчётов, BI-аналитики и прочих направлений, где требуется презентация результатов анализа данных.
В основе всего функционала R Markdown лежит несколько технологий. Сами Rmd-документы состоят из текста в markdown разметке, а также из чанков кода. При рендере документа с помощью пакета knitr происходит выполнение кода и встраивание результатов кода в текст, в результате получается уже классический markdown-документ (md). Далее этот документ конвертируется в требуемый формат с помощью pandoc, консольной утилиты для форматирования текстов и конвертирования их в разные форматы. При конверсии в pdf необходимо, чтобы в системе также был установлен какой-нибудь дистрибутив TeX.
Чаще всего Rmd конвертируются в HTML-страницы, так как это позволяет использовать их в самых разных местах (от презентаций до сайтов). Вторая важная причина — за счёт использования htmlwidgets в таких HTML-страницах сохраняются интерактивные графики (plotly, higcharts и т. д.) и таблицы.
Основные формы использования R Markdown:
Ноутбуки (по аналогии с Jupyter notebook). Ячейки формируются как чанки, обратными апострофами. В отличие от Jupyter, это именно текст с вставками кода, а не разные чанки для текста и кода.
Презентации.
Rmd-документы можно преобразовать вhtml-презентации (ioslides,slidy,xaringan), вpdf-презентации (beamer) и даже в презентации в формате MS PowerPoint.Дашборды. Пакет
flexdashboardпозволяет формировать как статичные (обновляющиеся по расписанию), так и интерактивные дашборды (при сочетании сShiny). В подобных дашбордах могут быть как графики, так и таблицы, тексты и прочие форматы представления данных.Сайты. Пакет
blogdownпозволяет сочетать генераторы статических сайтов (Hugo, Jekyll),Rmd-документы и пользовательские темы (чистыйcssилиcss-препроцессоры). В результате можно создать собственный блог, посты которого будут исходно в виде R Markdown, личную страницу и т. д.Книги. Немалая часть онлайн-учебников по R или по отдельным пакетам написана и опубликована с помощью пакета
bookdown, который позволяет собирать множествоRmd-документов в один большой документ (со всей сопутствующей навигацией, системой ссылок на литературу и перекрестных ссылок и т. д.). Формат книг также может быть разным — как HTML, так и EPUB, PDF и Kindle.Документация. Примерами будут сайты с документацией по пакетам (пакет
pkgdown), виньетки к пакетам или страницы для документации к пакету на GitHub.Шаблоны статей. Все академические журналы имеют свои требования к структуре статей, порядку и правилам указания аффилиации авторов, форматированию ссылок и т. д. Пакет
rticlesи в целом пользовательские шаблоны помогают следовать большинству или почти всем этим правилам при написании статьи.