Таблицы¶
В данном примере мы детально разберём таблицы и варианты применения их в скриптах Engee. Начнём с подключения вспомогательной библиотеки и создания таблицы.
Pkg.add("TypedTables")
using TypedTables
t = Table(a = [1, 2, 3], b = [2.0, 4.0, 6.0])
Далее мы подробно разберём весь функционал взаимодействия с таблицами. Обратимся к первой строке и первому столбцу таблицы.
t[1]
t.a
Теперь опишем, что такое таблица и каковы сферы её применения.
Таблица – массив типа Julia, где каждый элемент (строка) представляет собой файл NamedTuple. В частности:
Внешне a Table представляет собой массив именованных кортежей. То есть каждая строка таблицы представлена как один из новых NamedTuple-элементов Julia, которые просты в использовании и очень эффективны. В обозначении подтипа Table <: AbstractArray{<:NamedTuple}.
Внутри Table хранится (именованный) кортеж массивов, который представляет собой удобную структуру для хранения табличных данных на основе столбцов.
Таким образом, манипулировать данными Table очень просто. Равно как и работать с массивами и именованными кортежами – эта эффективность, простота и увлекательность заложена в самой идеологии Julia.
Tables и их столбцы могут относиться к типу AbstractArray любой размерности. Это позволяет вам воспользоваться мощными функциями массивов Julia, такими как, к примеру, многомерное вещание. Каждый столбец должен представлять собой массив той же размерности и размера, что и другие столбцы.
Цель TypedTables.jl – представить очень мало концепций с минимальными затратами на обучение, чтобы вы могли сходу манипулировать табличными данными. Этот Table-тип представляет собой простую обертку над столбцами и представляет собой хорошо известный и чрезвычайно продуктивный AbstractArray-интерфейс. Если вы знакомы с массивами и именованными кортежами, вы сможете написать свою аналитику данных с помощью файла Table.
Однако от этой функциональности было бы мало пользы, будь контейнер данных по своей сути медленным или если бы использование контейнера было подвержено ловушкам падения производительности, когда программист использует идиоматический шаблон. В этом случае for-циклы по строкам a Table возможны со скоростью рукописного кода на статически компилируемом языке, таком как C, поскольку компилятор полностью знает типы каждого столбца. Таким образом, пользователи могут писать общие функции, используя сочетание рукописных циклов и вызовов таких функций, как map, filter, reduce, а также высокоуровневые интерфейсы, предоставляемые пакетами типа groupQuery.jl, и при этом получать оптимальную производительность .innerjoin
Наконец, поскольку Table не имеет никакого мнения о базовом хранилище массивов (и действует скорее как удобный уровень метапрограммирования), массивы, представляющие каждый столбец, могут иметь довольно разные свойства – например, поддержку рабочих нагрузок в памяти, вне ядра и распределенных рабочих нагрузок (см. более подробную информацию в разделе «Представление данных»).
Способы создания таблицы¶
Самый простой способ создать таблицу из столбцов – использовать аргументы ключевых слов.
t = Table(name = ["Alice", "Bob", "Charlie"], age = [25, 42, 37])
Конструктор в равной степени примет a NamedTuple из столбцов as Table((name = ["Alice", "Bob", "Charlie"], age = [25, 42, 37])) (обратите внимание на дополнительные скобки).
Кроме того, легко преобразовать вектор именованных кортежей на основе хранения строк в столбчатое хранилище с помощью Table-конструктора.
Table([(name = "Alice", age = 25), (name = "Bob", age = 42), (name = "Charlie", age = 37)])
Доступ к данным, хранящимся в Tables¶
Начнём со строк. В нашей таблице строка – это просто NamedTuple, к которому, как мы уже видели ранее, легко получить доступ. Обратимся просто по индексу строки.
t[1]
Несколько строк могут быть проиндексированы аналогично стандартным массивам.
t[2:3]
Также мы можем получить размерности таблицы при помощи стандартных функций Engee. Важно отметить, что количество столбцов не отображается при вызове size.
length(t)
size(t)
Доступ к столбцам возможен через обращение к имени столбца.
t.name
Самый простой способ извлечь более одного столбца – создать из столбцов новую таблицу (как в table2 = Table(column1 = table1.column1, column2 = table1.column2, ...)).
Доступ к столбцам можно получить напрямую как к NamedTuple-массивам с помощью функции columns.
columns(t)
Помимо этого мы можем обратиться к функции получения имён столбцов.
columnnames(t)
Из всего вышесказанного выделим два равнозначных способа получения ячейки данных.
t[1].name
t.name[1]
Сравнение TypedTables и DataFrame.¶
Для тех, у кого есть опыт использования пакета DataFrames.jl , это сравнение может быть полезным:
Столбцы, хранящиеся в a Table, неизменяемые: нельзя добавлять, удалять или переименовывать столбцы. Однако создать новую таблицу с разными столбцами очень легко, что поощряет стиль функционального программирования для работы с внешней структурой данных. (см. также FlexTable – более гибкую альтернативу). Для сравнения: это аналогичный подход для IndexedTables и JuliaDB, тогда как DataFrames использует нетипизированный вектор столбцов.
Сами столбцы могут быть изменяемыми. Вы можете модифицировать данные в одном или нескольких столбцах, а также добавить или удалить строки. Таким образом, операции над данными (а не структурой данных) при желании могут принимать императивную форму.
Типы столбцов известны компилятору, что делает прямые операции, такие, как перебор строк Table, очень быстрыми. Программист может свободно писать комбинацию низкоуровневых for-циклов, использовать такие операции, как map, filter, reduce или использовать интерфейс запросов высокого уровня, такой как Query.jl – и все это с высокой производительностью, которую можно ожидать от статически скомпилированного языка group. .innerjoin
И, наоборот, компилятор Julia затрачивает усилия на отслеживание имен и типов всех столбцов таблицы. Если у вас очень большое количество столбцов (многие сотни), Table, возможно, – это не подходящая структура данных (здесь лучше использовать вектор столбцов с динамическим размером и типизацией DataFrame).
Tables может быть массивом любой размерности.
В отличие от DataFrame, вы не можете получить доступ к одной ячейке за один getindex-вызов (сначала вам следует извлечь столбец и проиндексировать ячейку из этого столбца). Аналогично, количество столбцов не участвует в size или .lengthTable.
Вывод¶
Проверить, что больше подходит для вашей задачи, – статически скомпилированный Table или динамический подход DataFrames, можно следующим образом. Посмотрите, имеет ли написанный код тенденцию ссылаться на столбцы по имени или имена столбцов более динамичны (и, например, требуется итерация по столбцам).