R:Оптимизация/Импорт данных

Материал Psylab.info - энциклопедии психодиагностики
Перейти к: навигация, поиск

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

Формат CSV

Функция read.table

Для импорта файлов CSV в R предусмотрена функция read.table() и функци-обёртка (wrapper) read.csv(). С точки зрения скорости работы, параметры функции read.table(), заданные по умолчанию, не являются оптимальными. Приведём несколько рекомендаций по использованию функции read.table():

  • указать тип переменных, содержащихся в таблице с помощью аргумента colClasses;
  • указать количество импортируемых строк с помощью аргумента nrows;
  • отключить поиск комментариев с помощью аргумента comment.char = "".

Создадим таблицу данных содержащую [math]10^{6}[/math] строк и 6 столбцов:

КодR

<syntaxhighlight lang="r">> set.seed(123) # начальная точка для генератора случайных чисел для воспроизводимости результатов > N <- 10^6L # задаём количество наблюдений > DF <- data.frame(a = sample(1:10^3L, N, replace = TRUE), + b = sample(1:10^3L, N, replace = TRUE), + c = sample(LETTERS[1:10], N, replace = TRUE), + e = rnorm(N), + d = rnorm(N, 100, 15), + f = runif(N, 0, 10^3L)) </syntaxhighlight>

Рассмотрим таблицу более подробно. Начало таблицы:

КодR

<syntaxhighlight lang="r">> head(DF) a b c e d f 1 288 604 B 1.4534 107.38 545.7 2 789 242 C -1.4479 82.32 949.2 3 409 65 J 0.4182 113.11 489.9 4 884 700 J -0.9194 98.54 824.6 5 941 594 D 0.7360 122.02 483.5 6 46 843 I -0.8354 84.24 322.7</syntaxhighlight>

Структура данных:

КодR

<syntaxhighlight lang="r">> str(DF) 'data.frame': 1000000 obs. of 6 variables: $ a: int 288 789 409 884 941 46 529 893 552 457 ... $ b: int 604 242 65 700 594 843 746 303 416 862 ... $ c: Factor w/ 10 levels "A","B","C","D",..: 2 3 10 10 4 9 10 10 7 4 ... $ e: num 1.453 -1.448 0.418 -0.919 0.736 ... $ d: num 107.4 82.3 113.1 98.5 122 ... $ f: num 546 949 490 825 484 ...</syntaxhighlight>

Типы переменных:

КодR

<syntaxhighlight lang="r">> sapply(DF, class) a b c e d f "integer" "integer" "factor" "numeric" "numeric" "numeric"</syntaxhighlight>

Размер объекта в оперативной памяти:

КодR

<syntaxhighlight lang="r">> print(object.size(DF), units = "auto") 34.3 Mb</syntaxhighlight>

Сохраним таблицу в csv-файл следующими командами:

КодR

<syntaxhighlight lang="r">> tmp.csv <- tempfile(fileext=".csv") # генерируем имя и путь для временного файла > write.table(DF, tmp.csv, sep = ";", row.names = FALSE)</syntaxhighlight>

Размер полученного файла составил:

КодR

<syntaxhighlight lang="r">> file.info(tmp.csv)$size # размер файла в байтах [1] 61724411 > file.info(tmp.csv)$size / 1024^2 # размер файла в мегабайтах [1] 58.86</syntaxhighlight>

Теперь мы можем сравнить производительность функции read.table() с параметрами по умолчанию и парамтерами, рекомендованными для увеличения производительности данной функции. Для этого нам понадобится пакет microbenchmark.

КодR

<syntaxhighlight lang="r">> microbenchmark(defaults = read.table(tmp.csv, sep = ";", header = TRUE), + ompimize = read.table(tmp.csv, sep = ";", header = TRUE, nrows = N, comment.char = "", + colClasses = c("integer", "integer", "factor", "numeric", "numeric", "numeric"))) Unit: seconds expr min lq median uq max neval defaults 3.951 4.043 4.068 4.169 4.850 100 ompimize 2.852 2.927 2.931 2.943 3.631 100</syntaxhighlight>

Отметим, что чтение файла осуществлялось непосредственно из оперативной памяти, т.к. файл находился в файловой системе tmpfs.

Значения для аргумента colClasses мы получили ранее с помощью команды sapply(DF, class).

Обратим внимание на то, что результат работы сравниваемых вариантов функции read.table() абсолютно идентичен.

КодR

<syntaxhighlight lang="r">> identical(read.table(tmp.csv, sep = ";", header = TRUE), + read.table(tmp.csv, sep = ";", header = TRUE, + colClasses = c("integer", "integer", "factor", "numeric", "numeric", "numeric"), + nrows = N, comment.char = "")) [1] TRUE</syntaxhighlight>

Таким образом, по результатам сравнения, можем заключить, что указание специфических аргументов функции read.table() позволяет существенно ускорить процесс импорта данных в формате CSV.

Функция scan

Проведя профилирование выполнения функции read.table() мы получили следующие результаты:

КодR

<syntaxhighlight lang="r">> tmp.log <- tempfile(pattern = "prof-", fileext = ".log") > source("http://git.psylab.info/r-scripts/raw/master/proftable.R") > Rprof(tmp.log, interval = 0.01) > DF <- read.table(tmp.csv, sep = ";", header = TRUE) > Rprof(NULL) > proftable(tmp.log) Calls: PctTime Call 86.520 scan 13.235 type.convert > .External2 0.245 Files: /tmp/RtmpAvhCfO/file23b65745e7ba.R Parent Call: read.table Total Time: 4.08 seconds Percent of run time represented: 100%</syntaxhighlight>

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

КодR

<syntaxhighlight lang="r">data.frame(scan(tmp.csv, what = list(a = integer(0), b = integer(0), c = character(0), d = numeric(0), e = numeric(0), f = numeric(0)), nmax = N, sep = ";", quote = "\"", multi.line = FALSE, skip = 1L, quiet = TRUE))</syntaxhighlight>

Поясним некоторые моменты:

  • skip = 1L: пропускаем первую строку, т.к. она содержит названия столбцов и при её обработка функция scan() выдаст ошибку;
  • what = list(a = integer(0), b = integer(0), c = character(0), d = numeric(0), e = numeric(0), f = numeric(0)); аналог аргумента colClasses для read.table();
  • nmax = N: аналог nrows для read.table().

Ниже приведены результаты сравнения работы функций read.table() и scan():

КодR

<syntaxhighlight lang="r">> microbenchmark(defaults = read.table(tmp.csv, sep = ";", header = TRUE), + ompimize = read.table(tmp.csv, sep = ";", header = TRUE, nrows = N, comment.char = "", + colClasses = c("integer", "integer", "factor", "numeric", "numeric", "numeric")), + scan = data.frame(scan(tmp.csv, nmax = N, sep = ";", quote = "\"", multi.line = FALSE, skip = 1L, quiet = TRUE, + what = list(a = integer(0), b = integer(0), c = character(0), d = numeric(0), e = numeric(0), f = numeric(0))))) Unit: seconds expr min lq median uq max neval defaults 3.948 4.037 4.109 4.220 4.757 100 ompimize 2.851 2.863 2.931 3.005 3.686 100 scan 2.970 2.985 3.058 3.653 3.822 100</syntaxhighlight>

Как видим, функций scan() работает быстрее read.table() с параметрами по умолчанию и может сравниться по скорости работы с read.table() с оптимизированными аргументами.

Функция fread

Пакет data.table, помимо прочего, включает в себя функцию для чтения csv-файлов - fread(). Стоит отметить, что полученная в результате импорта переменная будет иметь класс data.table, что предполагает определённую специфику работы с ней[1].

Сравним результаты работы функций read.table() и fread():

КодR

<syntaxhighlight lang="r">> library(data.table) > microbenchmark(defaults = read.table(tmp.csv, sep = ";", header = TRUE), + ompimize = read.table(tmp.csv, sep = ";", header = TRUE, nrows = N, comment.char = "", + colClasses = c("integer", "integer", "factor", "numeric", "numeric", "numeric")), + fread = fread(tmp.csv)) Unit: milliseconds expr min lq median uq max neval defaults 3957.5 4049.0 4066 4166.3 4797 100 ompimize 2844.9 2853.6 2928 2932.0 3557 100 fread 532.2 533.4 534 535.5 616 100</syntaxhighlight>

Таким образом, функция fread() является более быстрым и более удобным (за счёт автоматического определения входных параметров) инструментом по сравнению со штатной функцией read.table() (даже при использовании оптимальных параметров).

Формат RData/Rds

Форматы RData/Rds являются внутренними форматами в R. Данные форматы характеризуются тем, что имеют двоичный вид и предполагают сжатие данных. Хотя CSV являются наиболее универсальным форматом, для работы с данными в R предпочтительнее использовать внутренние форматы R.

Сравним скорость записи данных в формат CSV и двоичный формат RData:

КодR

<syntaxhighlight lang="r">> tmp.csv <- tempfile(fileext = "csv") > tmp.RData <- tempfile(fileext = "RData") > microbenchmark(text = write.table(DF, file = tmp.csv, sep = ";", row.names = FALSE), + binary = save(DF, file = tmp.RData)) Unit: seconds expr min lq median uq max neval text 4.658 4.737 4.755 4.825 5.526 100 binary 2.202 2.203 2.205 2.210 2.248 100</syntaxhighlight>

Отметим, что запись производилась в файловую систему tmpfs, размещённую непосредственно в оперативной памяти.

Примечания

  1. Синтаксис работы с классом data.table отличен от синтаксиса работы с матрицами и таблицами данных в R.