Julia — один из самых молодых математических языков программирования, претендующий на роль основного языка программирования в этой сфере. К сожалению, на данный момент достаточно мало литературы на русском языке, а материалы, доступные на английском языке, содержат сведения, которые, в силу динамичности развития Julia, не всегда соответствуют текущей версии, но это не очевидно для начинающих Julia-программистов. Постараемся восполнить пробелы и донести идеи Julia до читателей в виде простых примеров.


Целью этой статьи является дать представление читателям об основных способах работы c таблицами в языке программирования Julia с тем, чтобы побудить начать использовать этот язык программирования для обработки реальных данных. Предполагаем, что читатель уже знаком с другими языками программирования, поэтому будем давать лишь минимальные сведения о том, как это делается, но не будем вдаваться в детали методов обработки данных.


Безусловно, одним из важнейших этапов работы программы, выполняющей анализ данных, является их импорт и экспорт. Причем, наиболее распространенный формат представления данных — это таблица. Существуют библиотеки для Julia, которые предоставляют доступ к реляционным СУБД, используют обменные форматы типа HDF5, MATLAB, JLD. Но в данном случае нас будет интересовать только текстовый формат представления таблиц, типа CSV.


Прежде чем рассматривать таблицы, необходимо сделать небольшое введение в особенности представления этой структуры данных. Для Julia таблица может быть представлена как двумерный массив или как DataFrame.


Массивы


Начнём с массивов в Julia. Нумерация элементов начинается с единицы. Это вполне естественно для математиков, а, кроме того, такая же схема используется в Fortran, Pascal, Matlab. Для программистов, никогда не использовавших эти языки, такая нумерация может показаться неудобной и стать причиной ошибок при написании граничных условий, но, в действительности, это лишь вопрос привычки. После пары недель использования Julia вопрос переключения между моделями языков уже не возникает.


Второй существенный момент этого языка — внутреннее представление массивов. Для Julia линейный массив — это столбец. В то же время для языков типа C, Java, одномерный массив — это строка.


Проиллюстрируем это массивом, созданном в командной строке (REPL)


julia> a = [1, 2, 3]
3-element Array{Int64,1}:
 1
 2
 3

Обратите внимание на тип массива — Array{Int64,1}. Массив одномерный, типа Int64. При этом, если мы хотим объединить этот массив с другим массивом, то, поскольку имеем дело со столбцом, мы должны использовать функцию vcat (то есть vertical concatenate). Итогом получаем новый столбец.


julia> b = vcat(a, [5, 6, 7])
7-element Array{Int64,1}:
 1
 2
 3
 5
 6
 7

Если же мы создаём массив как строку, то при записи литерала используем пробелы вместо запятых и получаем двумерный массив с типом Array{Int64,2}. Второй аргумент в декларации типа означает количество координат многомерного массива.


julia> c = [1 2 3]
1?3 Array{Int64,2}:
 1  2  3

То есть мы получили матрицу с одной строкой и тремя столбцами.


Эта схема представления строк и столбцов также характерна для Fortran и Matlab, но, следует лишь напомнить, что Julia — это язык, который именно на их область применения и ориентирован.


Матрица для Julia — это двумерный массив, где все ячейки имеют один тип. Обратим внимание на то, что тип может быть абстрактный Any или вполне конкретный, такой как Int64, Float64 или, даже, String.


Создать матрицу мы можем в форме литерала:


julia> a = [1 2; 3 4]
2?2 Array{Int64,2}:
 1  2
 3  4

Создать при помощи конструктора и выделить память без инициализации (undef):


julia> a = Array{Int64,2}(undef, 2, 3)
2?3 Array{Int64,2}:
 4783881648  4783881712  4782818640
 4783881680  4783881744  4782818576

Или с инициализацией, если вместо undef будет указано любое конкретное значение.


Склеить из отдельных колонок:


julia> a = [1, 2, 3]
3-element Array{Int64,1}:
 1
 2
 3

julia> b = hcat(a, a, a, a)
3?4 Array{Int64,2}:
 1  1  1  1
 2  2  2  2
 3  3  3  3

Инициализировать случайным образом:


julia> x = rand(1:10, 2, 3)
2?3 Array{Int64,2}:
 1  10  2
 9   7  7

Аргументы rand — диапазон от 1 до 10 и размерность 2 х 3.


Или использовать включение (Comprehensions)


julia> x = [min(i, j) for i = 0:2, j = 0:2 ]
3?3 Array{Int64,2}:
 0  0  0
 0  1  1
 0  1  2

Отметим, что тот факт, что для Julia столбцы представляют собой линейный блок памяти, приводит к тому, что перебор элементов именно по столбцу будет существенно быстрее, чем перебор по строкам. В частности, в следующем примере используется матрица 1_000_000 строк и 100 столбцов.


#!/usr/bin/env julia

using BenchmarkTools

x = rand(1:1000, 1_000_000, 100)
#x = rand(1_000_000, 100)

function sumbycolumns(x)
  sum = 0
  rows, cols = size(x)
  for j = 1:cols,
      i = 1:rows
    sum += x[i, j]
  end
  return sum
end

@show @btime sumbycolumns(x)

function sumbyrows(x)
  sum = 0
  rows, cols = size(x)
  for i = 1:rows,
      j = 1:cols
    sum += x[i, j]
  end
  return sum
end

@show @btime sumbyrows(x)

Результаты:


  74.378 ms (1 allocation: 16 bytes)
=# @btime(sumbycolumns(x)) = 50053093495

  206.346 ms (1 allocation: 16 bytes)
=# @btime(sumbyrows(x)) = 50053093495

@btime в примере — многократный запуск функции для расчёта среднего времени её выполнения. Этот макрос предоставляется библиотекой BenchmarkTools.jl. В базовом комплекте Julia имеется макрос time, но он измеряет однократный интервал, что, в данном случае, будет неточно. Макрос show просто выводит в консоль выражение и его вычисленное значение.


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


Синонимом для двумерного массива является тип Matrix. Впрочем, это, скорее, является стилевым удобством, а не необходимостью.


Обращение к элементам матрицы выполняется по индексу. Например для ранее созданной матрицы


julia> x = rand(1:10, 2, 3)
2?3 Array{Int64,2}:
 1  10  2
 9   7  7

Мы можем получить конкретный элемент как x[1, 2] => 10. Так и получить целиком столбец, например второй столбец:


julia> x[:, 2]
2-element Array{Int64,1}:
 10
  7

Или вторую строку:


julia> x[2, :]
3-element Array{Int64,1}:
 9
 7
 7

Также имеется полезная функция selectdim, где можно задать порядковый номер размерности, для которой следует сделать выборку, а также индексы элементов этой размерности. Например сделать выборку по 2-й размерности (столбцы), выбрав 1 и 3-й индексы. Этот подход удобен тогда, когда в зависимости от условий, надо переключаться между строками и столбцами. Впрочем, это верно и для многомерного случая, когда количество размерностей больше 2-х.


julia> selectdim(x, 2, [1, 3])
2?2 view(::Array{Int64,2}, :, [1, 3]) with eltype Int64:
 1  2
 9  7

Функции для статистической обработки массивов
Подробнее об одномерных массивах
Многомерные массивы
Функции линейной алгебры и матрицы специального вида


Чтение таблицы из файла можно выполнить при помощи функции readdlm, реализованной в библиотеке DelimitedFiles. Запись — при помощи writedlm. Эти функции обеспечивают работу с файлами с разделителями, частным случаем которых является формат CSV.


Проиллюстрируем примером из документации:


julia> using DelimitedFiles

julia> x = [1; 2; 3; 4];
julia> y = ["a"; "b"; "c"; "d"];

julia> open("delim_file.txt", "w") do io
           writedlm(io, [x y]) # записываем таблицу с двумя столбцами
       end;

julia> readdlm("delim_file.txt") # Читаем таблицу
4?2 Array{Any,2}:
 1  "a"
 2  "b"
 3  "c"
 4  "d"

В данном случае, следует обратить внимание на то, что таблица содержит данные разных типов. Поэтому при чтении файла создаётся матрица с типом Array{Any,2}.
Другой пример — чтение таблиц, содержащих однородные данные


julia> using DelimitedFiles

julia> x = [1; 2; 3; 4];
julia> y = [5; 6; 7; 8];
julia> open("delim_file.txt", "w") do io
           writedlm(io, [x y]) # пишем таблицу
       end;

julia> readdlm("delim_file.txt", Int64) # читаем ячейки как Int64
4?2 Array{Int64,2}:
 1  5
 2  6
 3  7
 4  8

julia> readdlm("delim_file.txt", Float64) # читаем ячейки как Float64
4?2 Array{Float64,2}:
 1.0  5.0
 2.0  6.0
 3.0  7.0
 4.0  8.0

С точки зрения эффективности обработки, такой вариант предпочтительнее, поскольку данные будут представлены компактно. В то же время, явное ограничение таблиц, представляемых матрицей — требование однородности данных.


Полные возможности функции readdlm рекомендуем посмотреть в документации. Среди дополнительных опций есть возможность указать режим обработки заголовков, пропуск строк, функцию обработки ячеек и пр.


Альтернативным способом чтения таблиц является библиотека CSV.jl По сравнению с readdlm и writedlm эта библиотека предоставляет существенно большие возможности в управлении опциями записи и чтения, а также проверки данных в файлах с разделителями. Однако принципиальное отличие заключается в том, что результат выполнения функции CSV.File может быть материализован в тип DataFrame.


DataFrames


Библиотека DataFrames обеспечивает поддержку структуры данных DataFrame, которая и ориентирована на представление таблиц. Принципиальным отличием от матрицы здесь является то, что каждый столбец хранится индивидуально, причём каждый столбец имеет своё имя. Вспоминаем, что для Julia по-колоночный режим хранения, вообще, является естественным. И, хотя здесь имеем частный случай одномерных массивов, получается оптимальное решение как с точки зрения скорости, так и гибкости представления данных, поскольку тип каждого столбца может быть индивидуальным.


Посмотрим как создать DataFrame.


Любая матрица может быть конвертирована в DataFrame.


julia> using DataFrames
julia> a = [1 2; 3 4; 5 6]
3?2 Array{Int64,2}:
 1  2
 3  4
 5  6

julia> b = convert(DataFrame, a)
3?2 DataFrame
¦ Row ¦ x1    ¦ x2    ¦
¦     ¦ Int64 ¦ Int64 ¦
+-----+-------+-------+
¦ 1   ¦ 1     ¦ 2     ¦
¦ 2   ¦ 3     ¦ 4     ¦
¦ 3   ¦ 5     ¦ 6     ¦

Функция convert обеспечивает преобразование данных в указанный тип. Соответственно, для типа DataFrame методы функции convert определены в библиотеке DataFrames (по терминологии Julia, существуют функции, а многообразие их реализаций с разными аргументами называется методами). Следует обратить внимание на то, что столбцам матрицы автоматически присвоены имена x1, x2. То есть, если теперь мы запросим имена столбцов, то получим их в виде массива:


julia> names(b)
2-element Array{Symbol,1}:
 :x1
 :x2

Причём имена представлены в формате типа Symbol (хорошо знакомом в мире Ruby).


DataFrame может быть создан непосредственно — пустым или содержащим какие-то данные в момент конструирования. Например:


julia> df = DataFrame([collect(1:3), collect(4:6)], [:A, :B])
3?2 DataFrame
¦ Row ¦ A     ¦ B     ¦
¦     ¦ Int64 ¦ Int64 ¦
+-----+-------+-------+
¦ 1   ¦ 1     ¦ 4     ¦
¦ 2   ¦ 2     ¦ 5     ¦
¦ 3   ¦ 3     ¦ 6     ¦

Здесь мы указываем массив со значениями столбцов и массив с именами этих столбцов. Конструкции вида collect(1:3) — это преобразование итератора-диапазона от 1 до 3 в массив значений.


Доступ к столбцам возможен как по их имени, так и по индексу.


Очень легко добавить новый столбец, прописав какое-то значение во всех существующих строках. Например, df выше, хотим добавить столбец Score. Для этого нам надо написать:


julia> df[:Score] = 0.0
0.0

julia> df
3?3 DataFrame
¦ Row ¦ A     ¦ B     ¦ Score   ¦
¦     ¦ Int64 ¦ Int64 ¦ Float64 ¦
+-----+-------+-------+---------+
¦ 1   ¦ 1     ¦ 4     ¦ 0.0     ¦
¦ 2   ¦ 2     ¦ 5     ¦ 0.0     ¦
¦ 3   ¦ 3     ¦ 6     ¦ 0.0     ¦

Также как и в случае простых матриц, мы можем склеивать экземпляры DataFrame при помощи функций vcat, hcat. Однако vcat возможно использовать только при одинаковых колонках в обеих таблицах. Выровнять DataFrame можно, например, при помощи следующей функции:


function merge_df(first::DataFrame, second::DataFrame)::DataFrame
   if (first == nothing)
       return second
   else
       names_first = names(first)
       names_second = names(second)

       sub_names = setdiff(names_first, names_second)
       second[sub_names] = 0

       sub_names = setdiff(names_second, names_first)
       first[sub_names] = 0

       vcat(second, first)
   end
end

Функция names здесь получает массив имён столбцов. Функция setdiff(s1,s2) в примере выявляет все элементы s1, которые не входят в s2. Далее, расширяем DataFrame до этих элементов. vcat склеивает два DataFrame и возвращает результат. Использование return в данном случае не нужно, поскольку результат последней операции очевиден.


Можем проверить результат:


julia> df1 = DataFrame(:A => collect(1:2))
2?1 DataFrame
¦ Row ¦ A     ¦
¦     ¦ Int64 ¦
+-----+-------+
¦ 1   ¦ 1     ¦
¦ 2   ¦ 2     ¦

julia> df2 = DataFrame(:B => collect(3:4))
2?1 DataFrame
¦ Row ¦ B     ¦
¦     ¦ Int64 ¦
+-----+-------+
¦ 1   ¦ 3     ¦
¦ 2   ¦ 4     ¦

julia> df3 = merge_df(df1, df2)
4?2 DataFrame
¦ Row ¦ B     ¦ A     ¦
¦     ¦ Int64 ¦ Int64 ¦
+-----+-------+-------+
¦ 1   ¦ 3     ¦ 0     ¦
¦ 2   ¦ 4     ¦ 0     ¦
¦ 3   ¦ 0     ¦ 1     ¦
¦ 4   ¦ 0     ¦ 2     ¦

Отметим, что с точки зрения соглашения об именовании в Julia не принято использовать подчёркивания, но тогда страдает читаемость. Также не вполне хорошо в этой реализации то, что модифицируются исходные DataFrame. Но, тем не менее, этот пример хорош именно для иллюстрации процесса выравнивания множества столбцов.


Склейка нескольких DataFrame по общим значениям в столбцах возможна при помощи функции join (например склеить две таблицы с разными столбцами по идентификаторам общих пользователей).


DataFrame удобен для просмотра в консоли. Любой способ вывода: при помощи макроса show, при помощи функции println и пр. приведёт к тому, что в консоль будет напечатана таблица в удобной для восприятия форме. Если DataFrame слишком большой, то будут выведены начальные и конечные строки. Впрочем, можно и явно запросить голову и хвост функциями head и tail, соответственно.


Для DataFrame доступны функции группировки данных и агрегации по указанной функции. Имеются различия в том, что они возвращают. Это может быть коллекция с DataFrame, соответствующих критерию группировки, либо один единственный DataFrame, где имена колонок будут образованы от исходного имени и имени функции агрегации. В сущности, реализуется схема разбить-вычислить-объединить (split-apply-combine). См. Подробнее


Воспользуемся примером из документации с таблицей примеров, доступной в составе пакета DataFrames.


julia> using DataFrames, CSV, Statistics

julia> iris = CSV.read(joinpath(dirname(pathof(DataFrames)), "../test/data/iris.csv"));

Выполним группировку при помощи функции groupby. Указываем имя столбца группировки и получаем результат типа GroupedDataFrame, который содержит коллекцию отдельных DataFrame, собранных по значениям столбца группировки.


julia> species = groupby(iris, :Species)
GroupedDataFrame with 3 groups based on key: :Species
First Group: 50 rows
¦ Row ¦ SepalLength ¦ SepalWidth ¦ PetalLength ¦ PetalWidth ¦ Species ¦
¦     ¦ Float64     ¦ Float64    ¦ Float64     ¦ Float64    ¦ String  ¦
+-----+-------------+------------+-------------+------------+---------+
¦ 1   ¦ 5.1         ¦ 3.5        ¦ 1.4         ¦ 0.2        ¦ setosa  ¦
¦ 2   ¦ 4.9         ¦ 3.0        ¦ 1.4         ¦ 0.2        ¦ setosa  ¦
¦ 3   ¦ 4.7         ¦ 3.2        ¦ 1.3         ¦ 0.2        ¦ setosa  ¦

Полученный результат может быть преобразован в массив при помощи ранее упомянутой функции collect:


julia> collect(species)
3-element Array{Any,1}:
 50?5 SubDataFrame{Array{Int64,1}}
¦ Row ¦ SepalLength ¦ SepalWidth ¦ PetalLength ¦ PetalWidth ¦ Species ¦
¦     ¦ Float64     ¦ Float64    ¦ Float64     ¦ Float64    ¦ String  ¦
+-----+-------------+------------+-------------+------------+---------+
¦ 1   ¦ 5.1         ¦ 3.5        ¦ 1.4         ¦ 0.2        ¦ setosa  ¦
¦ 2   ¦ 4.9         ¦ 3.0        ¦ 1.4         ¦ 0.2        ¦ setosa  ¦
¦ 3   ¦ 4.7         ¦ 3.2        ¦ 1.3         ¦ 0.2        ¦ setosa  ¦
…

Выполним группировку при помощи функции by. Укажем имя столбца и функцию обработки полученных DataFrame. Первый этап работы by аналогичен функции groupby — получим коллекцию DataFrame. Для каждого такого DataFrame подсчитаем количество строк и поместим их в столбец N. Результат будет склеен в единственный DataFrame и возвращён как результат функции by.


julia> by(iris, :Species, df -> DataFrame(N = size(df, 1)))
3?2 DataFrame
¦ Row ¦ Species    ¦ N     ¦
¦     ¦ String?    ¦ Int64 ¦
+-----+------------+-------+
¦ 1   ¦ setosa     ¦ 50    ¦
¦ 2   ¦ versicolor ¦ 50    ¦
¦ 3   ¦ virginica  ¦ 50    ¦

Ну и последний вариант — функция aggregate. Указываем столбец для группировки и функцию агрегации для остальных столбцов. Результат — DataFrame, где имена столбцов будут образованы от имени исходных столбцов и имени функции агрегации.


julia> aggregate(iris, :Species, sum)
3?5 DataFrame
¦Row¦Species   ¦SepalLength_sum¦SepalWidth_sum¦PetalLength_sum¦PetalWidth_sum¦
¦   ¦ String   ¦ Float64       ¦ Float64      ¦ Float64       ¦ Float64      ¦
+---+----------+---------------+--------------+---------------+--------------+
¦ 1 ¦setosa    ¦250.3          ¦ 171.4        ¦ 73.1          ¦ 12.3         ¦
¦ 2 ¦versicolor¦296.8          ¦ 138.5        ¦ 213.0         ¦ 66.3         ¦
¦ 3 ¦virginica ¦329.4          ¦ 148.7        ¦ 277.6         ¦ 101.3        ¦

Функция colwise применяет указанную функцию ко всем или только к указанным столбцам DataFrame.


julia> colwise(mean, iris[1:4])
4-element Array{Float64,1}:
 5.843333333333335 
 3.057333333333334 
 3.7580000000000027
 1.199333333333334 

Весьма удобной функцией для получения сводки по таблице, является describe. Пример использования:


julia> describe(iris)
5?8 DataFrame
¦Row¦ variable  ¦mean   ¦min   ¦median¦ max     ¦nunique¦nmissing¦ eltype ¦
¦   ¦ Symbol    ¦Union… ¦Any   ¦Union…¦ Any     ¦Union… ¦Int64   ¦DataType¦
+---+-----------+-------+------+------+---------+-------+--------+--------+
¦ 1 ¦SepalLength¦5.84333¦ 4.3  ¦ 5.8  ¦ 7.9     ¦       ¦ 0      ¦ Float64¦
¦ 2 ¦SepalWidth ¦3.05733¦ 2.0  ¦ 3.0  ¦ 4.4     ¦       ¦ 0      ¦ Float64¦
¦ 3 ¦PetalLength¦3.758  ¦ 1.0  ¦ 4.35 ¦ 6.9     ¦       ¦ 0      ¦ Float64¦
¦ 4 ¦PetalWidth ¦1.19933¦ 0.1  ¦ 1.3  ¦ 2.5     ¦       ¦ 0      ¦ Float64¦
¦ 5 ¦Species    ¦       ¦setosa¦      ¦virginica¦ 3     ¦ 0      ¦ String ¦

Полный список функций пакета DataFrames.


Также, как и для случая Matrix, в DataFrame можно применять все статистические функции, доступные в модуле Statistics. См. https://docs.julialang.org/en/v1/stdlib/Statistics/index.html


Для графического отображения DataFrame используется библиотека StatPlots.jl. См. Подробнее https://github.com/JuliaPlots/StatPlots.jl
Эта библиотека реализует набор макросов, упрощающих визуализацию.


julia> df = DataFrame(a = 1:10, b = 10 .* rand(10), c = 10 .* rand(10))
10?3 DataFrame
¦ Row ¦ a     ¦ b       ¦ c       ¦
¦     ¦ Int64 ¦ Float64 ¦ Float64 ¦
+-----+-------+---------+---------+
¦ 1   ¦ 1     ¦ 0.73614 ¦ 7.11238 ¦
¦ 2   ¦ 2     ¦ 5.5223  ¦ 1.42414 ¦
¦ 3   ¦ 3     ¦ 3.5004  ¦ 2.11633 ¦
¦ 4   ¦ 4     ¦ 1.34176 ¦ 7.54208 ¦
¦ 5   ¦ 5     ¦ 8.52392 ¦ 2.98558 ¦
¦ 6   ¦ 6     ¦ 4.47477 ¦ 6.36836 ¦
¦ 7   ¦ 7     ¦ 8.48093 ¦ 6.59236 ¦
¦ 8   ¦ 8     ¦ 5.3761  ¦ 2.5127  ¦
¦ 9   ¦ 9     ¦ 3.55393 ¦ 9.2782  ¦
¦ 10  ¦ 10    ¦ 3.50925 ¦ 7.07576 ¦

julia> @df df plot(:a, [:b :c], colour = [:red :blue])


В последней строке @df — макрос, df — имя переменной с DataFrame.


Весьма полезной библиотекой может быть Query.jl. Используя механизмы макросов и канала обработки, Query.jl предоставляет специализированный язык запросов. Пример — получить список лиц старше 50 лет и количество детей у них:


julia> using Query, DataFrames

julia> df = DataFrame(name=["John", "Sally", "Kirk"], age=[23., 42., 59.], children=[3,5,2])
3?3 DataFrame
¦ Row ¦ name   ¦ age     ¦ children ¦
¦     ¦ String ¦ Float64 ¦ Int64    ¦
+-----+--------+---------+----------+
¦ 1   ¦ John   ¦ 23.0    ¦ 3        ¦
¦ 2   ¦ Sally  ¦ 42.0    ¦ 5        ¦
¦ 3   ¦ Kirk   ¦ 59.0    ¦ 2        ¦

julia> x = @from i in df begin
           @where i.age>50
           @select {i.name, i.children}
           @collect DataFrame
       end
1?2 DataFrame
¦ Row ¦ name   ¦ children ¦
¦     ¦ String ¦ Int64    ¦
+-----+--------+----------+
¦ 1   ¦ Kirk   ¦ 2        ¦

Или форма с каналом:


julia> using Query, DataFrames

julia> df = DataFrame(name=["John", "Sally", "Kirk"], age=[23., 42., 59.], children=[3,5,2]);

julia> x = df |> @query(i, begin
                   @where i.age>50
                   @select {i.name, i.children}
                 end) |> DataFrame
1?2 DataFrame
¦ Row ¦ name   ¦ children ¦
¦     ¦ String ¦ Int64    ¦
+-----+--------+----------+
¦ 1   ¦ Kirk   ¦ 2        ¦

См. подробнее


Оба примера выше, демонстрируют использование языков запросов, функционально подобных dplyr или LINQ. Причём эти языки не ограничиваются Query.jl. Подробнее об использовании этих языков совместно с DataFrames здесь.


В последнем примере используется оператор «|>». См. Подробнее.


Этот оператор подставляет аргумент в функцию, которая указывается справа от него. Иными словами:


julia> [1:5;] |> x->x.^2 |> sum |> inv
0.01818181818181818

Эквивалентно:


julia> inv(sum( [1:5;] .^ 2 ))
0.01818181818181818

И последнее, что хотелось бы отметить, это возможность записать DataFrame в выходной формат с разделителем при помощи ранее упомянутой библиотеки CSV.jl


julia> df = DataFrame(name=["John", "Sally", "Kirk"], age=[23., 42., 59.], children=[3,5,2])
3?3 DataFrame
¦ Row ¦ name   ¦ age     ¦ children ¦
¦     ¦ String ¦ Float64 ¦ Int64    ¦
+-----+--------+---------+----------+
¦ 1   ¦ John   ¦ 23.0    ¦ 3        ¦
¦ 2   ¦ Sally  ¦ 42.0    ¦ 5        ¦
¦ 3   ¦ Kirk   ¦ 59.0    ¦ 2        ¦

julia> CSV.write("out.csv", df)
"out.csv"

Можем проверить записанный результат:


> cat out.csv 
name,age,children
John,23.0,3
Sally,42.0,5
Kirk,59.0,2

Заключение


Сложно прогнозировать, станет ли Julia таким же распространенным языком программирования как R, например, но в этом году она уже стала самым быстро растущим языком программирования. Если в прошлом году о ней знали лишь единицы, в этом году, после выхода версии 1.0 и стабилизации библиотечных функций, о ней начали писать, почти наверное, в следующем году она станет языком, не знать который будет просто неприлично в области Data Science. А компании, которые, не начали использовать Julia для анализа данных, будут являться откровенными динозаврами, подлежащими замещению более проворными потомками.


Язык Julia является молодым языком программирования. Собственно, после появления пилотных проектов, будет понятно, на сколько готова инфраструктура Julia к реальному промышленному использованию. Разработчики Julia весьма амбициозны и заявляют о готовности уже сейчас. В любом случае, простой, но строгий синтаксис Julia, делает её весьма привлекательным языком программирования для обучения уже сейчас. Высокая производительность позволяет реализовывать алгоритмы, которые пригодны не только для учебных целей, но и для реального использования при анализе данных. Мы же, начнём последовательно пробовать Julia в различных проектах уже сейчас.

Комментарии (10)


  1. rssdev10 Автор
    15.11.2018 22:58

    Идея написать эту статью возникла как результат экспериментов с Julia. Не всем надо решать задачи физики или строить модели машинного обучения, но обработка табличных данных встречается часто. А потому это может стать той самой точкой входа, которая подтолкнёт к исследованию новой технологии.

    В целом, жду отзывы, дополнения, замечания. Есть мысли сделать серию небольших заметок про Julia: по созданию скриптов (акцент на обработку аргументов), веб-сервисов, построению отчётов. Возможно, надо пояснить модель системы типов Julia. Поскольку язык новый, не думаю, эти вопросы совсем очевидны.


    1. lumini
      16.11.2018 09:14

      Спасибо за статью. Было бы очень интересно прочитать сравнение с классическими тулзами (pandas, numpy) и на каких юзкейзах переключение на джулию даёт ощутимые преимущества.


      1. rssdev10 Автор
        16.11.2018 11:22

        Вопрос про сравнение с pandas я уже слышал… Но главный вопрос — критерии сравнения. Если вопрос про производительность — это сделать можно. Только надо подобрать наборы данных и операции, на которых сравнивать. Если же вопрос про языки запросов типа


        x = @from i in df begin
                   @where i.age>50
                   @select {i.name, i.children}
                   @collect DataFrame
               end

        то это отдельный разговор. См. https://juliadata.github.io/DataFrames.jl/stable/man/querying_frameworks.html


        Касаемо сравнения производительности, почти уверен, что у pandas будет провал в том случае, когда добавляются сложные условия выборки, требующие выполнения питон-кода. У Julia всегда будет выполняться родной код.


        Касаемо сравнения с numpy, тот же вопрос о критериях. Если сравнивать производительность прямо, то см. https://julialang.org/benchmarks/ Но вопрос, на сколько это реалистично отражает практические задачи. Если сравнивать чистые вычисления внутри Julia и внутри numpy, думаю, что для numpy не всё плохо. Если же появляются задачи, где данные надо использовать в нескольких библиотеках, которые не понимают numpy, или просто требуется их перепаковка из numpy-объектов в питон-объекты и обратно, получаются точки провала производительности. У Julia их нет, поскольку везде используется native-код.


        Касаемо синтаксиса с numpy и Matlab, рекомендую посмотреть сравнительную таблицу https://cheatsheets.quantecon.org/


        И компактно представленные средства из Julia — https://juliadocs.github.io/Julia-Cheat-Sheet/


        То есть ответ касаемо случаев использования Julia такой. Если нет унаследованного кода на Питоне, то следует писать на Julia.....


    1. Yermack
      16.11.2018 09:45

      Очень интересная и полезная статья, спасибо за старания! Такой тщательный разбор будет очень полезен для начинающих. Я тоже недавно начал знакомиться с Юлией, но меня интересует как раз возможность решать задачи, так как я студент-физик. Крайне здорово, что кто-то начал публиковать здесь статьи в другом ключе, так что надеюсь на продолжение и дальнейшее всестороннее раскрытие аспектов этого интересного языка


      1. rssdev10 Автор
        16.11.2018 12:52

        Благодарю за отзыв! Ваши предыдущие статьи, откровенно говоря, и спровоцировали меня на создание учетной записи здесь и на написание этой статьи. Описание по примерам — это хорошо, но только для тех, кому примеры понятны. Для тех же, кому предметная область далека, весь текст может показаться слишком абстрактным, а возможности языка — какими-то надуманными "фичами", которые нужны только тем, кто понимает эту непонятную предметную область. Имено поэтому я и решил изменить контекст изложения и начать делать его в отрыве от предметной области, но с акцентом на то, как программировать. Впрочем, всего должно быть в меру, потому, безусловно, статьи с конкретными примерами тоже нужны и важны.


        Julia — новый язык. На данный момент вряд ли ошибусь, если скажу, что специалистов по ней пока ещё просто нет. Сам я погрузился в Julia в августе, как раз под версию 1.0. До этого только читал обзоры. В следущем году специалисты уже точно будут. Технологии на Julia имеют явный потенциал. Впрочем, как раз сейчас, пытаемся делать сервисы обработки данных именно на Julia.


        Кроме того, мы задумались о создании учебного курса для студентов 1-го курса технического университета, где Julia станет первым языком программирования. Она видится хорошим языком для изучения принципов алгоритмизации, а также в качестве языка для решения текущих студенческих задач.


  1. ivananashkin
    16.11.2018 13:07

    Спасибо за статью.
    По работе я портировал свою библиотеку на жулию ради скорости работы. Однако с релиза 1 версии они много что поломали. Например мне так и не получилось нормально подключить Plots. Пробовал использовать gadfly но он слишком заморочен.
    Интересно было бы прочитать про то как рисовать графики.


    1. rssdev10 Автор
      16.11.2018 13:26

      Сейчас уже большинство популярных библиотек перенесли на 1.0. К слову, в Julia очень удобно проверять библиотеки при помощи встроенных тестов. Когда устанавливаете новый пакет, надо всего лишь набрать команду test ИмяПакета. Если всё в порядке, то можно использовать.


      Касаемо графиков. Я использую Plots / GR. Каких-либо проблем сейчас не замечаю. Из прочих бакендов, см. http://docs.juliaplots.org/latest/examples/plotlyjs/ — интерактивные диаграммы для jupyter notebook. Из известных мне проблем, в plotlyjs поломаны полярные координаты. В остальном, работает.


      И относительно того, как рисовать, Yermack написал серию статей, где, как раз, акцент на графику. Собственно, см. хаб https://habr.com/hub/julia/


  1. nad_oby
    16.11.2018 13:12

    Julia из года в год всё интересней.
    Несколько лет назад в фирме в которой тогда работал рассматривали постепенный перенос большого Matlab проекта и Julia была одной из альтернатив.
    Предпочли python по нескольким причинам, одна из которых относительная зрелость библиотек, другая — то что публикации по ML тогда часто шли с кодом на python. Код можно брать и запускать на своих данных с минимумом усилий.
    Проблема курицы и яйца.
    Если (когда) наберётся критическая масса публикаций перспективы весьма хороши.


    К сожалению мне в работе Julia никак не может пригодиться не могу представить DevOps задач где бы было выгодно.


    1. rssdev10 Автор
      16.11.2018 13:33

      По-моему Julia уже достигла зрелости. Именно потому, для задач ML и обработки данных мы решили её использовать. Пару лет назад, вероятно, я бы согласился с python, но сейчас смысла его использования уже нет. Да и примеров уже достаточно. Хотя, в большинстве случаев, их надо искать. Иногда, чтобы понять, как сделать что-то для себя, надо смотреть код библиотек. Но тут — лишь вопрос программистской квалификации и способности быстро понять как оно должно работать. В целом, Julia не является каким-то принципиально новым языком программирования. Это просто переосмысление существующих моделей. Потому начать программировать на ней довольно просто.


      В DevOps, не знаю где использовать… Но если есть задачи типа конвертировать какие-либо json, csv, прочие данные в другой формат, откуда-то прочитать, что-то куда-то загрузить, то она вполне может пригодиться. По крайней мере подобные скриптики мы тоже пытаемся делать.


  1. nad_oby
    16.11.2018 16:22

    Насчет зрелости согласен, следующий логичный этап — массовость.
    Для Devops python очень хорош.
    Ключевой момент — взаимодействие с реальным миром и внешними сервисами.
    Причём обычно решение требуется уже вчера.
    Тут решаюшим становится огромное количество готовых библиотек, лёгкость с которой они комбинируются в достаточно хорошее решение.
    Развитый механизм работы с исключениями и утиная типизация облегчают работу в нестабильном окружении.
    Так что питон мой выбор по умолчанию в небольших проектах.


    А вот для игр с числами и ML однозначно буду Julia пытать.