03.03.2017 Views

№9 (сентябрь 2016)

Create successful ePaper yourself

Turn your PDF publications into a flip-book with our unique Google optimized e-Paper software.

CODING<br />

DATA.TABLE —<br />

НА ТАБЛИЦЫ<br />

СТЕРОИДАХ<br />

ВЫЖИМАЕМ МАКСИМУМ СКОРОСТИ<br />

ПРИ РАБОТЕ С ТАБЛИЧНЫМИ ДАННЫМИ В ЯЗЫКЕ R<br />

Станислав Чистяков<br />

stas.chistyakov@hotmail.com,<br />

эксперт по облачным технологиям<br />

и машинному обучению<br />

К чему лишние слова? Ты же читаешь<br />

статью про скорость, поэтому давай<br />

сразу к сути! Если у тебя в проекте идет<br />

работа с большим объемом данных<br />

и на трансформацию таблиц тратится<br />

больше времени, чем хотелось, то data.<br />

table поможет решить эту проблему.<br />

Статья будет интересна тем, кто уже немного<br />

знаком с языком R, а также разработчикам,<br />

которые его активно используют,<br />

но еще не открыли для себя пакет<br />

data.table.<br />

WWW<br />

Тема языка R не впервые<br />

поднимается в нашем<br />

журнале. Подкинем тебе<br />

еще пару линков на статьи<br />

по теме:<br />

https://xakep.<br />

ru/2015/07/23/dataanalysis-r-part-1/<br />

https://xakep.<br />

ru/2015/04/20/195-<br />

learning-r-programminglanguage/<br />

УСТАНАВЛИВАЕМ ПАКЕТЫ<br />

Все необходимое для нашей сегодняшней статьи можно проинсталлить с помощью<br />

соответствующих функций:<br />

R АТАКУЕТ<br />

В последние годы язык R заслуженно набирает популярность в среде машинного<br />

обучения. Как правило, для работы с этим подразделом искусственного<br />

интеллекта необходимо загрузить данные из нескольких источников, провести<br />

с ними преобразования для получения обучающей выборки, на ее основе создать<br />

модель, а затем использовать эту модель для предсказаний.<br />

На словах все просто, но в реальной жизни для формирования «хорошей»<br />

и устойчивой модели требуется множество попыток, большинство из которых<br />

могут быть абсолютно тупиковыми. Язык R помогает упростить процесс создания<br />

такой модели, так как это эффективный инструмент анализа табличных<br />

данных. Для работы с ними в R существует встроенный тип данных data.frame<br />

и огромное количество алгоритмов и моделей, которые его активно используют.<br />

К тому же вся мощь R заключается в возможности расширять базовую<br />

функциональность с помощью сторонних пакетов. В момент написания материала<br />

их количество в официальном репозитории достигло 8914.<br />

Но, как говорится, нет предела совершенству. Большое количество пакетов<br />

позволяют облегчить работу с самим типом данных data.frame. Обычно<br />

их цель — упростить синтаксис для выполнения наиболее распространенных<br />

задач. Здесь нельзя не вспомнить пакет dplyr, который уже стал стандартом<br />

де-факто для работы с data.frame, так как за счет него читаемость и удобство<br />

работы с таблицами выросли в разы.<br />

Перейдем от теории к практике и создадим data.frame DF со столбцами a,<br />

b и с.<br />

Если мы хотим:<br />

• выбрать только столбцы a и с,<br />

• отфильтровать строчки, где a = 2 и с > 10,<br />

• создать новую колонку aс, равную сумме a и с,<br />

• записать результат в переменную DF2,<br />

базовый синтаксис на чистом data.frame будет такой:<br />

С помощью dplyr все гораздо нагляднее:<br />

Эти же шаги, но с комментариями:<br />

Есть и альтернативный подход для работы с таблицами — data.table. Формально<br />

data.table — это тоже data.frame, и его можно использовать с существующими<br />

функциями и пакетами, которые зачастую ничего не знают<br />

о data.table и работают исключительно с data.frame. Этот «улучшенный»<br />

data.frame может выполнять многие типовые задачи в несколько раз быстрее<br />

своего прародителя. Возникает законный вопрос: где подвох? Этой самой<br />

«засадой» в data.table оказывается его синтаксис, который сильно отличается<br />

от оригинального. При этом если dplyr с первых же секунд использования<br />

делает код легче для понимания, то data.table превращает код в черную<br />

магию, и только годы изучения колдовских книг несколько дней практики<br />

с data.table позволят полностью понять идею нового синтаксиса и принцип<br />

упрощения кода.<br />

ПРОБУЕМ DATA.TABLE<br />

Для работы с data.table необходимо подключить его пакет.<br />

В дальнейших примерах эти вызовы будут опущены и будет считаться, что пакет<br />

уже загружен.<br />

Так как данные очень часто загружаются из файлов CSV, то уже на этом<br />

этапе data.table может удивлять. Для того чтобы показать более измеримые<br />

оценки, возьмем какой-нибудь достаточно большой файл CSV. В качестве<br />

примера можно привести данные с одного из последних соревнований<br />

на Kaggle. Там ты найдешь тренировочный файл CSV (zip) размером в 1,27<br />

Гбайт. Структура файла очень простая:<br />

• row_id — идентификатор события;<br />

• x, y — координаты;<br />

• accuracy — точность;<br />

• time — время;<br />

• place_id — идентификатор организации.<br />

Попробуем воспользоваться базовой функцией R — read.csv и измерим<br />

время, которое понадобится для загрузки этого файла (для этого обратимся<br />

к функции system.time):<br />

Время выполнения — 461,349 секунды. Достаточно, чтобы сходить за кофе...<br />

Даже если в будущем ты не захочешь пользоваться data.table, все равно<br />

старайся реже применять встроенные функции чтения CSV. Есть хорошая библиотека<br />

readr, где все реализовано гораздо эффективнее, чем в базовых<br />

функциях. Посмотрим ее работу на примере и подключим пакет. Дальше воспользуемся<br />

функцией загрузки данных из CSV:<br />

Время выполнения — 38,067 секунды — значительно быстрее предыдущего<br />

результата! Посмотрим, на что способен data.table:<br />

Время выполнения — 20,906 секунды, что почти в два раза быстрее, чем<br />

в readr, и в двадцать раз быстрее, чем в базовом методе.<br />

В нашем примере разница в скорости загрузки для разных методов получилась<br />

достаточно большая. Внутри каждого из используемых методов время<br />

линейно зависит от объема файла, но разница в скорости между этими методами<br />

сильно зависит от структуры файла (количества и типов столбцов). Ниже<br />

указаны тестовые замеры времени загрузки файлов.<br />

Для файла из трех текстовых колонок видно явное преимущество fread:<br />

Если же считываются не текстовые, а цифровые колонки, то разница между<br />

fread и read_csv менее заметна:<br />

Если после загрузки данных из файла ты собираешься дальше работать с data.<br />

table, то fread сразу его возвращает. При других способах загрузки данных<br />

будет необходимо сделать data.table из data.frame, хотя это просто:<br />

Большинство оптимизаций по скорости в data.table достигается за счет работы<br />

с объектами по ссылке, дополнительные копии объектов в памяти не создаются,<br />

а значит, экономится время и ресурсы.<br />

Например, ту же задачу создать data.table из data.frame можно было бы<br />

решить одной командой на «прокачку», но надо помнить, что первоначальное<br />

значение переменной будет потеряно.<br />

Итак, данные мы загрузили, пора с ними поработать. Будем считать, что в переменной<br />

DT уже есть загруженный data.table. Авторы пакета используют<br />

следующее обозначение основных блоков DT[i, j, by]:<br />

• i — фильтр строк;<br />

• j — выбор колонок или выполнение выражения над содержимым DT;<br />

• by — блок для группировки данных.<br />

Вспомним самый первый пример, где мы использовали data.frame DF,<br />

и на нем протестируем различные блоки. Начнем с создания data.table<br />

из data.frame:<br />

Блок i — фильтр строк<br />

Это самый понятный из блоков. Он служит для фильтра строк data.table, и,<br />

если больше ничего дополнительно не требуется, остальные блоки можно<br />

не указывать.<br />

Блок j — выбор колонок или выполнение выражения<br />

над содержимым data.table<br />

В данном блоке выполняется обработка содержимого data.table с отфильтрованными<br />

строками. Ты можешь просто попросить вернуть нужные столбцы,<br />

указав их в списке list. Для удобства введен синоним list в виде точки<br />

(то есть list (a, b) эквивалентно .(a, b)). Все существующие в data.<br />

table столбцы доступны как «переменные» — тебе не надо работать с ними<br />

как со строками, и можно пользоваться intellisense.<br />

Можно также указать дополнительные колонки, которые хочешь создать,<br />

и присвоить им необходимые значения:<br />

Если все это объединить, можно выполнить первую задачу, которую мы пробовали<br />

решать разными способами:<br />

Выбор колонок — всего лишь часть возможностей блока j. Также там можно<br />

менять существующий data.table. Например, если мы хотим добавить новую<br />

колонку в существующем data.table, а не в новой копии (как в прошлом примере),<br />

это можно сделать с помощью специального синтаксиса :=.<br />

С помощью этого же оператора можно удалять колонки, присваивая им значение<br />

NULL.<br />

Работа с ресурсами по ссылке здорово экономит мощности, и она гораздо<br />

быстрее, так как мы избегаем создания копии одних и тех же таблиц с разными<br />

колонками. Но надо понимать, что изменение по ссылке меняет сам объект.<br />

Если тебе нужна копия этих данных в другой переменной, то надо явно указать,<br />

что это отдельная копия, а не ссылка на тот же объект.<br />

Рассмотрим пример:<br />

Может показаться, что мы поменяли только DT3, но DT2 и DT3 — это один объект,<br />

и, обратившись к DT2, мы увидим там новую колонку. Это касается не только<br />

удаления и создания столбцов, так как data.table использует ссылки в том<br />

числе и для сортировки. Так что вызов setorder(DT3, "a") повлияет и на DT2.<br />

Для создания копии можно воспользоваться функцией:<br />

Теперь DT2 и DT3 — это разные объекты, и мы удалили столбец именно у DT3.<br />

by — блок для группировки данных<br />

Этот блок группирует данные наподобие group_by из пакета dplyr или GROUP<br />

BY в языке запросов SQL. Логика обращения к data.table с группировкой<br />

следующая:<br />

1. Блок i фильтрует строки из полного data.table.<br />

2. Блок by группирует данные, отфильтрованные в блоке i, по требуемым полям.<br />

3. Для каждой группы выполняется блок j, который может либо выбирать, либо<br />

обновлять данные.<br />

Блок заполняется следующим способом: by=list(переменные для группировки),<br />

но, как и в блоке j, list может быть заменен на точку, то есть<br />

by=list(a, b) эквивалентно by=.(a, b). Если необходимо группировать<br />

только по одному полю, можно опустить использование списка и написать напрямую<br />

by=a:<br />

Самая частая ошибка тех, кто учится работать с data.table, — это применение<br />

привычных по data.frame конструкций к data.table. Это очень больное<br />

место, и на поиск ошибки можно потратить очень много времени. Если у нас<br />

в переменных DF2 (data.frame) и DT2 (data.table) находятся абсолютно одинаковые<br />

данные, то указанные вызовы вернут абсолютно разные значения:<br />

Причина этого очень проста:<br />

• логика data.frame следующая — DF2[1:5,1:2] означает, что надо взять<br />

первые пять строк и вернуть для них значения первых двух колонок;<br />

• логика data.table отличается — DT2[1:5,1:2] означает, что надо взять<br />

первые пять строк и передать их в блок j. Блок j просто вернет 1 и 2.<br />

Если надо обратиться к data.table в формате data.frame, необходимо явно<br />

указать это с помощью дополнительного параметра:<br />

СКОРОСТЬ ВЫПОЛНЕНИЯ<br />

Давай убедимся, что изучение этого синтаксиса имеет смысл. Вернемся к примеру<br />

с большим файлом CSV. В train_DF загружен data.frame, а в train_<br />

DT, соответственно, data.table.<br />

В используемом примере place_id является целым числом большой длины<br />

(integer64), но об этом «догадался» только fread. Остальные методы загрузили<br />

это поле как число с плавающей запятой, и нам надо будет явно провести<br />

преобразование поля place_id внутри train_DF, чтобы сравнить скорости.<br />

Допустим, перед нами поставлена задача посчитать количество упоминаний<br />

каждого place_id в данных.<br />

В dplyr с обычным data.frame это заняло 13,751 секунды:<br />

При этом data.table делает то же самое за 2,578 секунды:<br />

Усложним задачу — для всех place_id посчитаем количество, медиану по x<br />

и y, а затем отсортируем по количеству в обратном порядке. data.frame c<br />

dplyr справляются с этим за 27,386 секунды:<br />

data.table же справился намного быстрее — 12,414 секунды:<br />

Тестовые замеры времени выполнения простой группировки данных с помощью<br />

dplyr и data.table:<br />

ВМЕСТО ВЫВОДОВ<br />

Это все лишь поверхностное описание функциональности<br />

data.table, но его достаточно, чтобы<br />

начать пользоваться этим пакетом. Сейчас развивается<br />

пакет dtplyr, который позиционируется<br />

как реализация dplyr для data.table, но пока<br />

он еще очень молод (версия 0.0.1). В любом<br />

случае понимание особенностей работы data.<br />

table необходимо до того, чтобы пользоваться<br />

дополнительными «обертками».<br />

WWW<br />

Очень советую почитать статьи,<br />

входящие в состав пакета:<br />

Небольшой обзор<br />

Распространенные вопросы<br />

и ответы

Hooray! Your file is uploaded and ready to be published.

Saved successfully!

Ooh no, something went wrong!