На эксклюзивных условиях представляем для вас полный вариант статьи из журнала Хакер, посвященной разработке на R. Под катом вы узнаете, как выжать максимум скорости при работе с табличными данными в языке R. Примечание: орфография и пунктуация автора сохранены. К чему лишние слова? Ты же читаешь статью про скорость, поэтому давай сразу к сути! Если у тебя в проекте идет работа с большим объемом данных и на трансформацию таблиц тратится больше времени, чем хотелось, то data.table поможет решить эту проблему. Статья будет интересна тем, кто уже немного знаком с языком R, а также разработчикам, которые его активно используют, но еще не открыли для себя пакет data.table. Все необходимое для нашей сегодняшней статьи можно проинсталлить с помощью соответствующих функций: В последние годы язык R заслуженно набирает популярность в среде машинного обучения. Как правило, для работы с этим подразделом искусственного интеллекта необходимо загрузить данные из нескольких источников, провести с ними преобразования для получения обучающей выборки, на ее основе создать модель, а затем использовать эту модель для предсказаний. На словах все просто, но в реальной жизни для формирования «хорошей» и устойчивой модели требуется множество попыток, большинство из которых могут быть абсолютно тупиковыми. Язык R помогает упростить процесс создания такой модели, так как это эффективный инструмент анализа табличных данных. Для работы с ними в R существует встроенный тип данных data.frame и огромное количество алгоритмов и моделей, которые его активно используют. К тому же вся мощь R заключается в возможности расширять базовую функциональность с помощью сторонних пакетов. В момент написания материала их количество в официальном репозитории достигло 8914. Но, как говорится, нет предела совершенству. Большое количество пакетов позволяют облегчить работу с самим типом данных data.frame. Обычно их цель — упростить синтаксис для выполнения наиболее распространенных задач. Здесь нельзя не вспомнить пакет dplyr, который уже стал стандартом де-факто для работы с data.frame, так как за счет него читаемость и удобство работы с таблицами выросли в разы. Перейдем от теории к практике и создадим data.frame DF со столбцами a, b и с. Если мы хотим: базовый синтаксис на чистом data.frame будет такой: С помощью dplyr все гораздо нагляднее: Эти же шаги, но с комментариями: Есть и альтернативный подход для работы с таблицами — data.table. Формально data.table — это тоже data.frame, и его можно использовать с существующими функциями и пакетами, которые зачастую ничего не знают о data.table и работают исключительно с data.frame. Этот «улучшенный» data.frame может выполнять многие типовые задачи в несколько раз быстрее своего прародителя. Возникает законный вопрос: где подвох? Этой самой «засадой» в data.table оказывается его синтаксис, который сильно отличается от оригинального. При этом если dplyr с первых же секунд использования делает код легче для понимания, то data.table превращает код в черную магию, и только годы изучения колдовских книг несколько дней практики с data.tableпозволят полностью понять идею нового синтаксиса и принцип упрощения кода. Для работы с data.table необходимо подключить его пакет. В дальнейших примерах эти вызовы будут опущены и будет считаться, что пакет уже загружен. Так как данные очень часто загружаются из файлов CSV, то уже на этом этапе data.table может удивлять. Для того чтобы показать более измеримые оценки, возьмем какой-нибудь достаточно большой файл CSV. В качестве примера можно привести данные с одного из последних соревнований на Kaggle. Там ты найдешь тренировочный файл CSV размером в 1,27 Гбайт. Структура файла очень простая: Попробуем воспользоваться базовой функцией R — read.csv и измерим время, которое понадобится для загрузки этого файла (для этого обратимся к функции system.time): Время выполнения — 461,349 секунды. Достаточно, чтобы сходить за кофе… Даже если в будущем ты не захочешь пользоваться data.table, все равно старайся реже применять встроенные функции чтения CSV. Есть хорошая библиотека readr, где все реализовано гораздо эффективнее, чем в базовых функциях. Посмотрим ее работу на примере и подключим пакет. Дальше воспользуемся функцией загрузки данных из CSV: Время выполнения — 38,067 секунды — значительно быстрее предыдущего результата! Посмотрим, на что способен data.table: Время выполнения — 20,906 секунды, что почти в два раза быстрее, чем в readr, и в двадцать раз быстрее, чем в базовом методе. В нашем примере разница в скорости загрузки для разных методов получилась достаточно большая. Внутри каждого из используемых методов время линейно зависит от объема файла, но разница в скорости между этими методами сильно зависит от структуры файла (количества и типов столбцов). Ниже указаны тестовые замеры времени загрузки файлов. Для файла из трех текстовых колонок видно явное преимущество fread:  Если же считываются не текстовые, а цифровые колонки, то разница между fread и read_csv менее заметна:  Если после загрузки данных из файла ты собираешься дальше работать с data.table, то fread сразу его возвращает. При других способах загрузки данных будет необходимо сделать data.table из data.frame, хотя это просто: Большинство оптимизаций по скорости в data.table достигается за счет работы с объектами по ссылке, дополнительные копии объектов в памяти не создаются, а значит, экономится время и ресурсы. Например, ту же задачу создать data.table из data.frame можно было бы решить одной командой на «прокачку», но надо помнить, что первоначальное значение переменной будет потеряно. Итак, данные мы загрузили, пора с ними поработать. Будем считать, что в переменной DT уже есть загруженный data.table. Авторы пакета используют следующее обозначение основных блоков DT[i, j, by]: Вспомним самый первый пример, где мы использовали data.frame DF, и на нем протестируем различные блоки. Начнем с создания data.table из data.frame: Это самый понятный из блоков. Он служит для фильтра строк data.table, и, если больше ничего дополнительно не требуется, остальные блоки можно не указывать. В данном блоке выполняется обработка содержимого data.table с отфильтрованными строками. Ты можешь просто попросить вернуть нужные столбцы, указав их в списке list. Для удобства введен синоним list в виде точки (то есть list (a, b) эквивалентно .(a, b)). Все существующие в data.table столбцы доступны как «переменные» — тебе не надо работать с ними как со строками, и можно пользоваться intellisense. Можно также указать дополнительные колонки, которые хочешь создать, и присвоить им необходимые значения: Если все это объединить, можно выполнить первую задачу, которую мы пробовали решать разными способами: Выбор колонок — всего лишь часть возможностей блока j. Также там можно менять существующий data.table. Например, если мы хотим добавить новую колонку в существующем data.table, а не в новой копии (как в прошлом примере), это можно сделать с помощью специального синтаксиса :=. С помощью этого же оператора можно удалять колонки, присваивая им значение NULL. Работа с ресурсами по ссылке здорово экономит мощности, и она гораздо быстрее, так как мы избегаем создания копии одних и тех же таблиц с разными колонками. Но надо понимать, что изменение по ссылке меняет сам объект. Если тебе нужна копия этих данных в другой переменной, то надо явно указать, что это отдельная копия, а не ссылка на тот же объект. Рассмотрим пример: Может показаться, что мы поменяли только DT3, но DT2 и DT3 — это один объект, и, обратившись к DT2, мы увидим там новую колонку. Это касается не только удаления и создания столбцов, так как data.table использует ссылки в том числе и для сортировки. Так что вызов setorder(DT3, "a") повлияет и на DT2. Для создания копии можно воспользоваться функцией: Теперь DT2 и DT3 — это разные объекты, и мы удалили столбец именно у DT3. Этот блок группирует данные наподобие group_by из пакета dplyr или GROUP BY в языке запросов SQL. Логика обращения к data.table с группировкой следующая: Блок заполняется следующим способом: by=list(переменные для группировки), но, как и в блоке j, list может быть заменен на точку, то есть by=list(a, b) эквивалентно by=.(a, b). Если необходимо группировать только по одному полю, можно опустить использование списка и написать напрямую by=a: Самая частая ошибка тех, кто учится работать с data.table, — это применение привычных по data.frame конструкций к data.table. Это очень больное место, и на поиск ошибки можно потратить очень много времени. Если у нас в переменных DF2 (data.frame) и DT2 (data.table) находятся абсолютно одинаковые данные, то указанные вызовы вернут абсолютно разные значения: Причина этого очень проста: Если надо обратиться к data.table в формате data.frame, необходимо явно указать это с помощью дополнительного параметра: Давай убедимся, что изучение этого синтаксиса имеет смысл. Вернемся к примеру с большим файлом CSV. В train_DF загружен data.frame, а в train_DT, соответственно, data.table. В используемом примере place_id является целым числом большой длины (integer64), но об этом «догадался» только fread. Остальные методы загрузили это поле как число с плавающей запятой, и нам надо будет явно провести преобразование поля place_id внутри train_DF, чтобы сравнить скорости. Допустим, перед нами поставлена задача посчитать количество упоминаний каждого place_id в данных. В dplyr с обычным data.frame это заняло 13,751 секунды: При этом data.table делает то же самое за 2,578 секунды: Усложним задачу — для всех place_id посчитаем количество, медиану по x и y, а затем отсортируем по количеству в обратном порядке. data.frame c dplyr справляются с этим за 27,386 секунды: data.table же справился намного быстрее — 12,414 секунды: Тестовые замеры времени выполнения простой группировки данных с помощью dplyr и data.table:  Это все лишь поверхностное описание функциональности data.table, но его достаточно, чтобы начать пользоваться этим пакетом. Сейчас развивается пакет dtplyr, который позиционируется как реализация dplyr для data.table, но пока он еще очень молод (версия 0.0.1). В любом случае понимание особенностей работы data.table необходимо до того, чтобы пользоваться дополнительными «обертками». Станислав Чистяков — эксперт по облачным технологиям и машинному обучению. Очень советую почитать статьи, входящие в состав пакета: Тема языка R не впервые поднимается в нашем журнале. Подкинем тебе еще пару линков на статьи по теме: