Взаимодействие с другими языками программирования
Нередко возникает необходимость использовать скрипты и алгоритмы, написанные на других, часто низкоуровневых языках. Причин этому может быть множество: как ожидаемый прирост скорости (как в случае с 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 x) {
NumericVector timesTworeturn 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
5) my_f_python(
## 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
и в целом пользовательские шаблоны помогают следовать большинству или почти всем этим правилам при написании статьи.