R:Оптимизация/Импорт данных — различия между версиями

Материал Psylab.info - энциклопедии психодиагностики
Перейти к: навигация, поиск
м (Формат RData/Rds)
м (Загрузка 2814-import-data.wiki)
 
(не показана одна промежуточная версия этого же участника)
Строка 1: Строка 1:
 +
<!-- R:Оптимизация/Импорт данных -->
 +
 +
 +
 +
{{CC-BY-4.0|author=автором Артём Клевцов}}
 +
 +
{{Pkg-req-notice}}
 +
 
При обработке данных большого объёма имеет смысл импортировать только ту часть данных, которая непосредственно участвует в обработке. Это целесообразно как с точки зрения расхода оперативной памяти, так и скорости выполнения операций поиска, сортировки и фильтрации данных.
 
При обработке данных большого объёма имеет смысл импортировать только ту часть данных, которая непосредственно участвует в обработке. Это целесообразно как с точки зрения расхода оперативной памяти, так и скорости выполнения операций поиска, сортировки и фильтрации данных.
  
Строка 13: Строка 21:
 
Создадим таблицу данных содержащую <math>10^{6}</math> строк и 6 столбцов:
 
Создадим таблицу данных содержащую <math>10^{6}</math> строк и 6 столбцов:
  
{{r-code|code=<nowiki>> set.seed(123) # начальная точка для генератора случайных чисел для воспроизводимости результатов
+
{{r-code|code=<nowiki>set.seed(123) # начальная точка генератора случайных чисел для воспроизводимости результатов
> N <- 10^6L # задаём количество наблюдений
+
n.rows <- 10^4L # задаём количество наблюдений
> DF <- data.frame(a = sample(1:10^3L, N, replace = TRUE),
+
data.df <- data.frame(a = sample(1:10^3L, n.rows, replace = TRUE),
+                  b = sample(1:10^3L, N, replace = TRUE),
+
                      b = sample(1:10^3L, n.rows, replace = TRUE),
+                  c = sample(LETTERS[1:10], N, replace = TRUE),
+
                      c = sample(LETTERS[1:10], n.rows, replace = TRUE),
+                  e = rnorm(N),
+
                      e = rnorm(n.rows),
+                  d = rnorm(N, 100, 15),
+
                      d = rnorm(n.rows, 100, 15),
+                  f = runif(N, 0, 10^3L))
+
                      f = runif(n.rows, 0, 10^3L))
 
</nowiki>}}
 
</nowiki>}}
  
 
Рассмотрим таблицу более подробно. Начало таблицы:
 
Рассмотрим таблицу более подробно. Начало таблицы:
  
{{r-code|code=<nowiki>> head(DF)
+
{{r-code|code=<nowiki>head(data.df)
     a  b c      e      d    f
+
#>     a  b c      e      d    f
1 288 604 B  1.4534 107.38 545.7
+
#> 1 288 311 J -1.3538  88.07 187.3
2 789 242 C -1.4479 82.32 949.2
+
#> 2 789 325 D -0.5794 82.97 167.6
3 409 65 J  0.4182 113.11 489.9
+
#> 3 409 871 E -0.8610 108.70 640.7
4 884 700 J -0.9194  98.54 824.6
+
#> 4 884 329 B  0.9727 107.77 962.4
5 941 594 D 0.7360 122.02 483.5
+
#> 5 941 126 I 0.6191 103.12 663.5
6  46 843 I -0.8354 84.24 322.7</nowiki>}}
+
#> 6  46 357 C  1.3854 92.75 306.4
 +
</nowiki>}}
  
 
Структура данных:
 
Структура данных:
  
{{r-code|code=<nowiki>> str(DF)
+
{{r-code|code=<nowiki>str(data.df)
'data.frame': 1000000 obs. of  6 variables:
+
#> 'data.frame': 10000 obs. of  6 variables:
  $ a: int  288 789 409 884 941 46 529 893 552 457 ...
+
#> $ a: int  288 789 409 884 941 46 529 893 552 457 ...
  $ b: int  604 242 65 700 594 843 746 303 416 862 ...
+
#> $ b: int  311 325 871 329 126 357 931 876 821 22 ...
  $ c: Factor w/ 10 levels "A","B","C","D",..: 2 3 10 10 4 9 10 10 7 4 ...
+
#> $ c: Factor w/ 10 levels "A","B","C","D",..: 10 4 5 2 9 3 3 6 6 5 ...
  $ e: num  1.453 -1.448 0.418 -0.919 0.736 ...
+
#> $ e: num  -1.354 -0.579 -0.861 0.973 0.619 ...
  $ d: num  107.4 82.3 113.1 98.5 122 ...
+
#> $ d: num  88.1 83 108.7 107.8 103.1 ...
  $ f: num  546 949 490 825 484 ...</nowiki>}}
+
#> $ f: num  187 168 641 962 664 ...
 +
</nowiki>}}
  
 
Типы переменных:
 
Типы переменных:
  
{{r-code|code=<nowiki>> sapply(DF, class)
+
{{r-code|code=<nowiki>sapply(data.df, class)
         a        b        c        e        d        f  
+
#>         a        b        c        e        d        f  
"integer" "integer"  "factor" "numeric" "numeric" "numeric"</nowiki>}}
+
#> "integer" "integer"  "factor" "numeric" "numeric" "numeric"
 +
</nowiki>}}
  
 
Размер объекта в оперативной памяти:
 
Размер объекта в оперативной памяти:
  
{{r-code|code=<nowiki>> print(object.size(DF), units = "auto")
+
{{r-code|code=<nowiki>print(object.size(data.df), units = "auto")
34.3 Mb</nowiki>}}
+
#> 353.7 Kb
 +
</nowiki>}}
  
 
Сохраним таблицу в csv-файл следующими командами:
 
Сохраним таблицу в csv-файл следующими командами:
  
{{r-code|code=<nowiki>> tmp.csv <- tempfile(fileext=".csv") # генерируем имя и путь для временного файла
+
{{r-code|code=<nowiki>tmp.csv <- tempfile(fileext=".csv") # генерируем имя и путь для временного файла
> write.table(DF, tmp.csv, sep = ";", row.names = FALSE)</nowiki>}}
+
write.csv(data.df, tmp.csv, row.names = FALSE)
 +
</nowiki>}}
  
 
Размер полученного файла составил:
 
Размер полученного файла составил:
  
{{r-code|code=<nowiki>> file.info(tmp.csv)$size # размер файла в байтах
+
{{r-code|code=<nowiki>file.info(tmp.csv)$size # размер файла в байтах
[1] 61724411
+
#> [1] 637264
> file.info(tmp.csv)$size / 1024^2 # размер файла в мегабайтах
+
file.info(tmp.csv)$size / 1024^2 # размер файла в мегабайтах
[1] 58.86</nowiki>}}
+
#> [1] 0.6077
 +
</nowiki>}}
  
 
Теперь мы можем сравнить производительность функции <code>read.table()</code> с параметрами по умолчанию и парамтерами, рекомендованными для увеличения производительности данной функции. Для этого нам понадобится пакет {{r-package|microbenchmark}}.
 
Теперь мы можем сравнить производительность функции <code>read.table()</code> с параметрами по умолчанию и парамтерами, рекомендованными для увеличения производительности данной функции. Для этого нам понадобится пакет {{r-package|microbenchmark}}.
  
{{r-code|code=<nowiki>> microbenchmark(defaults = read.table(tmp.csv, sep = ";", header = TRUE),
+
{{r-code|code=<nowiki>microbenchmark(defaults = read.csv(tmp.csv, header = TRUE),
+               ompimize = read.table(tmp.csv, sep = ";", header = TRUE, nrows = N, comment.char = "",  
+
               ompimize = read.csv(tmp.csv, header = TRUE, nrows = n.rows, comment.char = "",  
+                                      colClasses = c("integer", "integer", "factor", "numeric", "numeric", "numeric")))
+
                                  colClasses = c("integer", "integer", "factor", "numeric", "numeric", "numeric")))
Unit: seconds
+
#> Unit: milliseconds
     expr  min    lq median    uq  max neval
+
#>     expr  min    lq median    uq  max neval
  defaults 3.951 4.043 4.068 4.169 4.850   100
+
#> defaults 26.34 26.73 27.00 27.35 30.35   100
  ompimize 2.852 2.927 2.931 2.943 3.631   100</nowiki>}}
+
#> ompimize 18.46 18.55 18.63 18.73 19.09   100
 +
</nowiki>}}
  
Отметим, что чтение файла осуществлялось непосредственно из оперативной памяти, т.к. файл находился в файловой системе [http://ru.wikipedia.org/wiki/Tmpfs <code>tmpfs</code>].
+
Отметим, что чтение файла осуществлялось непосредственно из оперативной памяти, т.к. файл находился в файловой системе [<code>tmpfs</code>](http://ru.wikipedia.org/wiki/Tmpfs).
  
Значения для аргумента <code>colClasses</code> мы получили ранее с помощью команды <code>sapply(DF, class)</code>.
+
Значения для аргумента <code>colClasses</code> мы получили ранее с помощью команды <code>sapply(data.df, class)</code>.
  
 
Обратим внимание на то, что результат работы сравниваемых вариантов функции <code>read.table()</code> абсолютно идентичен.
 
Обратим внимание на то, что результат работы сравниваемых вариантов функции <code>read.table()</code> абсолютно идентичен.
  
{{r-code|code=<nowiki>> identical(read.table(tmp.csv, sep = ";", header = TRUE),
+
{{r-code|code=<nowiki>identical(read.csv(tmp.csv, header = TRUE),
+           read.table(tmp.csv, sep = ";", header = TRUE,
+
           read.csv(tmp.csv, header = TRUE, nrows = n.rows, comment.char = "",
+                      colClasses = c("integer", "integer", "factor", "numeric", "numeric", "numeric"),
+
                  colClasses = c("integer", "integer", "factor", "numeric", "numeric", "numeric")))
+                      nrows = N, comment.char = ""))
+
#> [1] TRUE
[1] TRUE</nowiki>}}
+
</nowiki>}}
  
 
Таким образом, по результатам сравнения, можем заключить, что указание специфических аргументов функции <code>read.table()</code> позволяет существенно ускорить процесс импорта данных в формате CSV.
 
Таким образом, по результатам сравнения, можем заключить, что указание специфических аргументов функции <code>read.table()</code> позволяет существенно ускорить процесс импорта данных в формате CSV.
Строка 94: Строка 109:
 
=== Функция <code>scan</code> ===
 
=== Функция <code>scan</code> ===
  
Проведя [[R:Профилирование кода|профилирование]] выполнения функции <code>read.table()</code> мы получили следующие результаты:
+
Проведя [[R:Профилирование кода|профилирование]] выполнения функции <code>read.table()</code>, мы получили следующие результаты:
  
{{r-code|code=<nowiki>> tmp.log <- tempfile(pattern = "prof-", fileext = ".log")
+
{{r-code|code=<nowiki>tmp.log <- tempfile(pattern = "prof-", fileext = ".log")
> source("http://git.psylab.info/r-scripts/raw/master/proftable.R")
+
source("http://git.psylab.info/r-scripts/raw/master/proftable.R")
> Rprof(tmp.log, interval = 0.01)
+
Rprof(tmp.log, interval = 0.01)
> DF <- read.table(tmp.csv, sep = ";", header = TRUE)
+
data.df <- read.csv(tmp.csv, header = TRUE)
> Rprof(NULL)
+
Rprof(NULL)
> proftable(tmp.log)
+
proftable(tmp.log)
 
+
#> Calls:
Calls:
+
#> RealTime PctTime Call                     
  PctTime Call                     
+
#> 0.04    80      scan                     
  86.520  scan                     
+
#> 0.01    20      type.convert > .External2
  13.235  type.convert > .External2
+
#>
  0.245                         
+
#> Files: None
 +
#>
 +
#> Parent Call: knit > process_file > withCallingHandlers > process_group > process_group.block > call_block > block_exec > in_dir > evaluate > evaluate_call > handle > withCallingHandlers > withVisible > eval > eval > read.csv > read.table
 +
#>
 +
#> Total Time: 0.05 seconds
 +
#>
 +
#> Percent of run time represented: 100%
 +
</nowiki>}}
  
Files:
+
В данном выводе хорошо видно, что большую часть времени затрачено на чтение файла с помощью функции <code>scan()</code>. Мы задались вопросом: а возможно ли получить результат, идентичный функции <code>read.table()</code>, пользуясь только функцией <code>scan()</code> и приведёт ли это к увеличению производительности. После некоторых экспериментов нам подобрать команду, приводящую к нужному результату:
/tmp/RtmpAvhCfO/file23b65745e7ba.R
+
  
Parent Call: read.table
+
}}{r eval = FALSE}
 
+
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)),
Total Time: 4.08 seconds
+
                 nmax = n.rows, sep = ",", quote = "\"", multi.line = FALSE, skip = 1L, quiet = TRUE))
 
+
}}
Percent of run time represented: 100%</nowiki>}}
+
 
+
В данном выводе хорошо видно, что большую часть времени затрачено на чтение файла с помощью функции <code>scan()</code>. Мы задались вопросом: а возможно ли получить результат, идентичный функции <code>read.table()</code>, пользуясь только функцией и <code>scan()</code> и приведёт ли это к увеличению производительности. После некоторых экспериментов нам подобрать команду, приводящую к нужному результату:
+
 
+
{{r-code|code=<nowiki>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))</nowiki>}}
+
  
 
Поясним некоторые моменты:
 
Поясним некоторые моменты:
Строка 127: Строка 142:
 
* <code>skip = 1L</code>: пропускаем первую строку, т.к. она содержит названия столбцов и при её обработка функция <code>scan()</code> выдаст ошибку;
 
* <code>skip = 1L</code>: пропускаем первую строку, т.к. она содержит названия столбцов и при её обработка функция <code>scan()</code> выдаст ошибку;
 
* <code>what = list(a = integer(0), b = integer(0), c = character(0), d = numeric(0), e = numeric(0), f = numeric(0))</code>; аналог аргумента <code>colClasses</code> для <code>read.table()</code>;
 
* <code>what = list(a = integer(0), b = integer(0), c = character(0), d = numeric(0), e = numeric(0), f = numeric(0))</code>; аналог аргумента <code>colClasses</code> для <code>read.table()</code>;
* <code>nmax = N</code>: аналог <code>nrows</code> для <code>read.table()</code>.
+
* <code>nmax = n.rows</code>: аналог <code>nrows</code> для <code>read.table()</code>.
  
 
Ниже приведены результаты сравнения работы функций <code>read.table()</code> и <code>scan()</code>:
 
Ниже приведены результаты сравнения работы функций <code>read.table()</code> и <code>scan()</code>:
  
{{r-code|code=<nowiki>> microbenchmark(defaults = read.table(tmp.csv, sep = ";", header = TRUE),
+
{{r-code|code=<nowiki>microbenchmark(defaults = read.csv(tmp.csv, header = TRUE),
+               ompimize = read.table(tmp.csv, sep = ";", header = TRUE, nrows = N, comment.char = "",  
+
               ompimize = read.csv(tmp.csv, header = TRUE, nrows = n.rows, comment.char = "",
+                                      colClasses = c("integer", "integer", "factor", "numeric", "numeric", "numeric")),
+
                                  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,
+
               scan = data.frame(scan(tmp.csv, nmax = n.rows, 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)))))
+
                                       what = list(a = integer(0), b = integer(0), c = character(0), d = numeric(0), e = numeric(0), f = numeric(0)))))
Unit: seconds
+
#> Unit: milliseconds
     expr  min    lq median    uq  max neval
+
#>     expr  min    lq median    uq  max neval
  defaults 3.948 4.037 4.109 4.220 4.757   100
+
#> defaults 26.86 27.25 27.46 27.63 30.91   100
  ompimize 2.851 2.863 2.931 3.005 3.686   100
+
#> ompimize 18.78 18.96 19.08 19.17 22.10   100
     scan 2.970 2.985 3.058 3.653 3.822   100</nowiki>}}
+
#>     scan 18.90 19.16 19.26 19.34 25.63   100
 +
</nowiki>}}
  
 
Как видим, функций <code>scan()</code> работает быстрее <code>read.table()</code> с параметрами по умолчанию и может сравниться по скорости работы с <code>read.table()</code> с оптимизированными аргументами.
 
Как видим, функций <code>scan()</code> работает быстрее <code>read.table()</code> с параметрами по умолчанию и может сравниться по скорости работы с <code>read.table()</code> с оптимизированными аргументами.
Строка 150: Строка 166:
 
Сравним результаты работы функций <code>read.table()</code> и <code>fread()</code>:
 
Сравним результаты работы функций <code>read.table()</code> и <code>fread()</code>:
  
{{r-code|code=<nowiki>> library(data.table)
+
{{r-code|code=<nowiki>microbenchmark(defaults = read.csv(tmp.csv, header = TRUE),
> microbenchmark(defaults = read.table(tmp.csv, sep = ";", header = TRUE),
+
               ompimize = read.csv(tmp.csv, header = TRUE, nrows = n.rows, comment.char = "",  
+               ompimize = read.table(tmp.csv, sep = ";", header = TRUE, nrows = N, comment.char = "",  
+
                                  colClasses = c("integer", "integer", "factor", "numeric", "numeric", "numeric")),
+                                      colClasses = c("integer", "integer", "factor", "numeric", "numeric", "numeric")),
+
               fread = fread(tmp.csv))
+               fread = fread(tmp.csv))
+
#> Unit: milliseconds
Unit: milliseconds
+
#>     expr    min    lq median    uq   max neval
     expr    min    lq median    uq max neval
+
#> defaults 26.922 27.605  27.77 28.166 30.482   100
  defaults 3957.5 4049.0  4066 4166.3 4797   100
+
#> ompimize 18.771 18.964  19.06 19.194 19.898   100
  ompimize 2844.9 2853.6  2928 2932.0 3557   100
+
#>     fread  5.854 5.952  6.02 6.123 6.479   100
     fread  532.2 533.4    534 535.5 616   100</nowiki>}}
+
</nowiki>}}
  
 
Таким образом, функция <code>fread()</code> является более быстрым и более удобным (за счёт автоматического определения входных параметров) инструментом по сравнению со штатной функцией <code>read.table()</code> (даже при использовании оптимальных параметров).
 
Таким образом, функция <code>fread()</code> является более быстрым и более удобным (за счёт автоматического определения входных параметров) инструментом по сравнению со штатной функцией <code>read.table()</code> (даже при использовании оптимальных параметров).
  
== Формат RData/Rds ==
+
Приведём ещё один интересный факт. Попытка оптимизировать аргументы для функции <code>fread()</code> по аналогии с <code>read.table()</code> не привела к каким либо значимым изменениям, что свидетельствует о высокой оптимизации алгоритмов, задействованных в функции <code>fread()</code>.
 +
 
 +
{{r-code|code=<nowiki>microbenchmark(defaults = fread(tmp.csv),
 +
              ompimize = fread(tmp.csv, sep = ",", header = TRUE, nrows = n.rows,
 +
                                colClasses = c("integer", "integer", "factor", "numeric", "numeric", "numeric")))
 +
#> Unit: milliseconds
 +
#>      expr  min    lq median    uq  max neval
 +
#>  defaults 5.798 5.896  5.992 6.064 6.245  100
 +
#>  ompimize 5.800 5.877  5.937 6.040 7.221  100
 +
</nowiki>}}
 +
 
 +
== Формат RData/RDS ==
  
 
Форматы RData/Rds являются внутренними форматами в R. Данные форматы характеризуются тем, что имеют двоичный вид и предполагают сжатие данных. Хотя CSV являются наиболее универсальным форматом, для работы с данными в R предпочтительнее использовать внутренние форматы R.
 
Форматы RData/Rds являются внутренними форматами в R. Данные форматы характеризуются тем, что имеют двоичный вид и предполагают сжатие данных. Хотя CSV являются наиболее универсальным форматом, для работы с данными в R предпочтительнее использовать внутренние форматы R.
Строка 169: Строка 196:
 
Сравним скорость записи данных в формат CSV в двоичный формат RData:
 
Сравним скорость записи данных в формат CSV в двоичный формат RData:
  
{{r-code|code=<nowiki>> tmp.csv <- tempfile(fileext = "csv")
+
{{r-code|code=<nowiki>tmp.csv <- tempfile(fileext = "csv")
> tmp.RData <- tempfile(fileext = "RData")
+
tmp.RDS <- tempfile(fileext = "RDS")
> microbenchmark(text = write.table(DF, file = tmp.csv, sep = ";", row.names = FALSE),
+
microbenchmark(text = write.csv(data.df, tmp.csv, row.names = FALSE),
+               binary = save(DF, file = tmp.RData))
+
               binary = saveRDS(data.df, tmp.RDS))
Unit: seconds
+
#> Unit: milliseconds
   expr  min    lq median    uq  max neval
+
#>   expr  min    lq median    uq  max neval
   text 4.658 4.737 4.755 4.825 5.526   100
+
#>   text 44.40 44.72 44.79 44.93 45.96   100
  binary 2.202 2.203 2.205 2.210 2.248   100</nowiki>}}
+
#> binary 21.14 21.33 21.43 21.56 21.99   100
 +
</nowiki>}}
 +
 
 +
Удостоверимся в идентичности исходных и импортированных данных:
 +
 
 +
{{r-code|code=<nowiki>identical(read.csv(tmp.csv, header = TRUE),
 +
          readRDS(tmp.RDS))
 +
#> [1] TRUE
 +
</nowiki>}}
  
 
Размеры полученных файлов в мегабайтах:
 
Размеры полученных файлов в мегабайтах:
  
{{r-code|code=<nowiki>> file.info(tmp.RData)$size / 1024^2
+
{{r-code|code=<nowiki>file.info(tmp.csv)$size / 1024^2
[1] 25
+
#> [1] 0.6077
> file.info(tmp.csv)$size / 1024^2
+
file.info(tmp.RDS)$size / 1024^2
[1] 60.77</nowiki>}}
+
#> [1] 0.2552
 +
</nowiki>}}
 +
 
 +
Теперь сравним скорость чтения данных с помощью функций <code>read.table()</code>, <code>fread()</code> и <code>readRDS()</code>:
 +
 
 +
{{r-code|code=<nowiki>microbenchmark(defaults = read.csv(tmp.csv, header = TRUE),
 +
              ompimize = read.csv(tmp.csv, header = TRUE, nrows = n.rows, comment.char = "",
 +
                                  colClasses = c("integer", "integer", "factor", "numeric", "numeric", "numeric")),
 +
              fread = fread(tmp.csv),
 +
              readRDS = readRDS(tmp.RDS))
 +
#> Unit: milliseconds
 +
#>      expr    min    lq median    uq    max neval
 +
#>  defaults 27.074 27.482 27.663 27.939 29.320  100
 +
#>  ompimize 18.783 19.014 19.089 19.222 20.867  100
 +
#>    fread  5.834  5.965  6.026  6.116  7.519  100
 +
#>  readRDS  1.920  1.966  1.983  2.014  3.400  100
 +
</nowiki>}}
 +
 
 +
Отметим, что запись и чтение производилась в файловую систему [<code>tmpfs</code>](http://ru.wikipedia.org/wiki/Tmpfs), размещённую непосредственно в оперативной памяти.
 +
 
 +
Таким образом, среди преимуществ форматов RData/RDS можно выделить:
  
Теперь сравним скорость чтения данных с помощью функций <code>read.table()</code>, <code>fread()</code> и <code>load()</code>:
+
* Скорость записи превышает скорость записи в текстовый формат;
 +
* Скорость чтения значительно превышает скорость чтения текстового формата;
 +
* Размер полученного файла значительного меньше текстового формата<ref>Начиная с версии 2.10 функция <code>read.table()</code> может также читать сжатые gz, xz, bzip файлы. Но даже при максимальной степени сжатия размер сжатого csv-файла едва приближается к размеру RData-файла. При этом скорость чтения сжатого csv-файла приблизительно такая же как при чтении несжатого csv-файла.</ref>;
 +
* Возможность сохранять любые объекты: функции, векторы, таблицы данных, матрицы, списки.
  
{{r-code|code=<nowiki>> 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),
+
+                load = load(tmp.RData))
+
Unit: milliseconds
+
    expr    min    lq median    uq    max neval
+
defaults 3940.6 4032.5 4067.2 4155.4 4903.4  100
+
ompimize 2847.8 2923.3 2929.5 2941.6 3632.5  100
+
    fread  533.1  534.6  535.1  536.3 1220.1  100
+
    load  177.1  177.2  177.2  177.6  268.6  100</nowiki>}}
+
  
Отметим, что запись и чтение производилась в файловую систему [http://ru.wikipedia.org/wiki/Tmpfs tmpfs], размещённую непосредственно в оперативной памяти.
+
* Возможность использования данного формата исключительно в среде R.
  
 
== Примечания ==
 
== Примечания ==

Текущая версия на 17:45, 28 мая 2014




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

Формат 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.rows <- 10^4L # задаём количество наблюдений data.df <- data.frame(a = sample(1:10^3L, n.rows, replace = TRUE), b = sample(1:10^3L, n.rows, replace = TRUE), c = sample(LETTERS[1:10], n.rows, replace = TRUE), e = rnorm(n.rows), d = rnorm(n.rows, 100, 15), f = runif(n.rows, 0, 10^3L)) </syntaxhighlight>

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

КодR

<syntaxhighlight lang="r">head(data.df) #> a b c e d f #> 1 288 311 J -1.3538 88.07 187.3 #> 2 789 325 D -0.5794 82.97 167.6 #> 3 409 871 E -0.8610 108.70 640.7 #> 4 884 329 B 0.9727 107.77 962.4 #> 5 941 126 I 0.6191 103.12 663.5 #> 6 46 357 C 1.3854 92.75 306.4 </syntaxhighlight>

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

КодR

<syntaxhighlight lang="r">str(data.df) #> 'data.frame': 10000 obs. of 6 variables: #> $ a: int 288 789 409 884 941 46 529 893 552 457 ... #> $ b: int 311 325 871 329 126 357 931 876 821 22 ... #> $ c: Factor w/ 10 levels "A","B","C","D",..: 10 4 5 2 9 3 3 6 6 5 ... #> $ e: num -1.354 -0.579 -0.861 0.973 0.619 ... #> $ d: num 88.1 83 108.7 107.8 103.1 ... #> $ f: num 187 168 641 962 664 ... </syntaxhighlight>

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

КодR

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

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

КодR

<syntaxhighlight lang="r">print(object.size(data.df), units = "auto") #> 353.7 Kb </syntaxhighlight>

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

КодR

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

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

КодR

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

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

КодR

<syntaxhighlight lang="r">microbenchmark(defaults = read.csv(tmp.csv, header = TRUE), ompimize = read.csv(tmp.csv, header = TRUE, nrows = n.rows, comment.char = "", colClasses = c("integer", "integer", "factor", "numeric", "numeric", "numeric"))) #> Unit: milliseconds #> expr min lq median uq max neval #> defaults 26.34 26.73 27.00 27.35 30.35 100 #> ompimize 18.46 18.55 18.63 18.73 19.09 100 </syntaxhighlight>

Отметим, что чтение файла осуществлялось непосредственно из оперативной памяти, т.к. файл находился в файловой системе [tmpfs](http://ru.wikipedia.org/wiki/Tmpfs).

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

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

КодR

<syntaxhighlight lang="r">identical(read.csv(tmp.csv, header = TRUE), read.csv(tmp.csv, header = TRUE, nrows = n.rows, comment.char = "", colClasses = c("integer", "integer", "factor", "numeric", "numeric", "numeric"))) #> [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) data.df <- read.csv(tmp.csv, header = TRUE) Rprof(NULL) proftable(tmp.log) #> Calls: #> RealTime PctTime Call #> 0.04 80 scan #> 0.01 20 type.convert > .External2 #> #> Files: None #> #> Parent Call: knit > process_file > withCallingHandlers > process_group > process_group.block > call_block > block_exec > in_dir > evaluate > evaluate_call > handle > withCallingHandlers > withVisible > eval > eval > read.csv > read.table #> #> Total Time: 0.05 seconds #> #> Percent of run time represented: 100% </syntaxhighlight>

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

}}{r eval = FALSE} 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.rows, sep = ",", quote = "\"", multi.line = FALSE, skip = 1L, quiet = TRUE))

}}

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

  • 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.rows: аналог nrows для read.table().

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

КодR

<syntaxhighlight lang="r">microbenchmark(defaults = read.csv(tmp.csv, header = TRUE), ompimize = read.csv(tmp.csv, header = TRUE, nrows = n.rows, comment.char = "", colClasses = c("integer", "integer", "factor", "numeric", "numeric", "numeric")), scan = data.frame(scan(tmp.csv, nmax = n.rows, 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: milliseconds #> expr min lq median uq max neval #> defaults 26.86 27.25 27.46 27.63 30.91 100 #> ompimize 18.78 18.96 19.08 19.17 22.10 100 #> scan 18.90 19.16 19.26 19.34 25.63 100 </syntaxhighlight>

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

Функция fread

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

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

КодR

<syntaxhighlight lang="r">microbenchmark(defaults = read.csv(tmp.csv, header = TRUE), ompimize = read.csv(tmp.csv, header = TRUE, nrows = n.rows, comment.char = "", colClasses = c("integer", "integer", "factor", "numeric", "numeric", "numeric")), fread = fread(tmp.csv)) #> Unit: milliseconds #> expr min lq median uq max neval #> defaults 26.922 27.605 27.77 28.166 30.482 100 #> ompimize 18.771 18.964 19.06 19.194 19.898 100 #> fread 5.854 5.952 6.02 6.123 6.479 100 </syntaxhighlight>

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

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

КодR

<syntaxhighlight lang="r">microbenchmark(defaults = fread(tmp.csv), ompimize = fread(tmp.csv, sep = ",", header = TRUE, nrows = n.rows, colClasses = c("integer", "integer", "factor", "numeric", "numeric", "numeric"))) #> Unit: milliseconds #> expr min lq median uq max neval #> defaults 5.798 5.896 5.992 6.064 6.245 100 #> ompimize 5.800 5.877 5.937 6.040 7.221 100 </syntaxhighlight>

Формат RData/RDS

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

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

КодR

<syntaxhighlight lang="r">tmp.csv <- tempfile(fileext = "csv") tmp.RDS <- tempfile(fileext = "RDS") microbenchmark(text = write.csv(data.df, tmp.csv, row.names = FALSE), binary = saveRDS(data.df, tmp.RDS)) #> Unit: milliseconds #> expr min lq median uq max neval #> text 44.40 44.72 44.79 44.93 45.96 100 #> binary 21.14 21.33 21.43 21.56 21.99 100 </syntaxhighlight>

Удостоверимся в идентичности исходных и импортированных данных:

КодR

<syntaxhighlight lang="r">identical(read.csv(tmp.csv, header = TRUE), readRDS(tmp.RDS)) #> [1] TRUE </syntaxhighlight>

Размеры полученных файлов в мегабайтах:

КодR

<syntaxhighlight lang="r">file.info(tmp.csv)$size / 1024^2 #> [1] 0.6077 file.info(tmp.RDS)$size / 1024^2 #> [1] 0.2552 </syntaxhighlight>

Теперь сравним скорость чтения данных с помощью функций read.table(), fread() и readRDS():

КодR

<syntaxhighlight lang="r">microbenchmark(defaults = read.csv(tmp.csv, header = TRUE), ompimize = read.csv(tmp.csv, header = TRUE, nrows = n.rows, comment.char = "", colClasses = c("integer", "integer", "factor", "numeric", "numeric", "numeric")), fread = fread(tmp.csv), readRDS = readRDS(tmp.RDS)) #> Unit: milliseconds #> expr min lq median uq max neval #> defaults 27.074 27.482 27.663 27.939 29.320 100 #> ompimize 18.783 19.014 19.089 19.222 20.867 100 #> fread 5.834 5.965 6.026 6.116 7.519 100 #> readRDS 1.920 1.966 1.983 2.014 3.400 100 </syntaxhighlight>

Отметим, что запись и чтение производилась в файловую систему [tmpfs](http://ru.wikipedia.org/wiki/Tmpfs), размещённую непосредственно в оперативной памяти.

Таким образом, среди преимуществ форматов RData/RDS можно выделить:

  • Скорость записи превышает скорость записи в текстовый формат;
  • Скорость чтения значительно превышает скорость чтения текстового формата;
  • Размер полученного файла значительного меньше текстового формата[2];
  • Возможность сохранять любые объекты: функции, векторы, таблицы данных, матрицы, списки.

Среди минусов можно отметить:

  • Возможность использования данного формата исключительно в среде R.

Примечания

  1. Синтаксис работы с классом data.table отличен от синтаксиса работы с матрицами и таблицами данных в R.
  2. Начиная с версии 2.10 функция read.table() может также читать сжатые gz, xz, bzip файлы. Но даже при максимальной степени сжатия размер сжатого csv-файла едва приближается к размеру RData-файла. При этом скорость чтения сжатого csv-файла приблизительно такая же как при чтении несжатого csv-файла.