朱莉娅 使用表格


Julia是最年轻的数学编程语言之一,自称是该领域的主要编程语言。 不幸的是,目前没有足够的俄文文献,并且由于朱莉娅的动态发展,英文资料中包含的信息并不总是与当前版本相对应,但这对于初学者朱莉娅程序员而言并不明显。 我们将尝试填补空白,以简单示例的形式向读者传达朱莉娅的想法。


本文的目的是使读者了解使用Julia编程语言处理表的基本方法,以鼓励他们开始使用此编程语言来处理实际数据。 我们假设读者已经熟悉其他编程语言,因此我们将仅提供有关如何完成此操作的最少信息,但是我们将不涉及数据处理方法的详细信息。


当然,执行数据分析的程序中最重要的阶段之一就是它们的导入和导出。 此外,最常见的数据表示格式是表格。 Julia的某些库提供对关系DBMS的访问,并使用交换格式,例如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 

注意数组的类型-数组{Int64,1}。 该数组是一维的,键入Int64。 而且,如果我们想将此数组与另一个数组组合,那么,由于我们要处理一列,因此必须使用vcat函数(即,垂直串联)。 结果是一个新列。


 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 x 3。


或使用包含(理解)


 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基本工具包具有一个时间宏,但它只能测量一个时间间隔,在这种情况下,这是不准确的。 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时,这是正确的。


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

数组的统计处理功能
关于一维数组的更多信息
多维数组
线性代数的函数和特殊形式的矩阵


可以使用DelimitedFiles库中实现的readdlm函数从文件中读取表。 记录-使用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, [xy]) #      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, [xy]) #   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库是读取表的另一种方法,与r​​eaddlm和writedlm相比,该库提供了对写入和读取以及检查定界文件中的数据的更多控制。 但是,根本的区别在于CSV.File函数的结果可以具体化为DataFrame类型。


数据框


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 │ │ │ Int64Int64 │ ├─────┼───────┼───────┤ │ 112 │ │ 234 │ │ 356 

convert函数将数据转换为指定的类型。 因此,对于DataFrame类型,转换函数的方法在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 │ ├─────┼───────┼───────┤ │ 114 │ │ 225 │ │ 336 

在这里,我们指示具有列值的数组和具有这些列的名称的数组。 collect(1:3)形式的构造是将迭代器范围从1到3转换为值数组。


可以通过列名和索引访问列。


通过在所有现有行中写入一些值来添加新列非常容易。 例如,上面的df,我们要添加“分数”列。 为此,我们需要编写:


 julia> df[:Score] = 0.0 0.0 julia> df 3×3 DataFrame │ Row │ A │ B │ Score │ │ │ Int64Int64Float64 │ ├─────┼───────┼───────┼─────────┤ │ 1140.0 │ │ 2250.0 │ │ 3360.0 

与简单矩阵一样,我们可以使用vcat,hcat函数粘合DataFrame实例。 但是,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 

名称函数在这里获取列名称的数组。 示例中的setdiff(s1,s2)函数检测s1中不在s2中的所有元素。 接下来,将DataFrame扩展到这些元素。 vcat将两个DataFrame粘合在一起并返回结果。 在这种情况下,不必使用return,因为最后一个操作的结果显而易见。


我们可以检查结果:


 julia> df1 = DataFrame(:A => collect(1:2)) 2×1 DataFrame │ Row │ A │ │ │ Int64 │ ├─────┼───────┤ │ 11 │ │ 22 │ julia> df2 = DataFrame(:B => collect(3:4)) 2×1 DataFrame │ Row │ B │ │ │ Int64 │ ├─────┼───────┤ │ 13 │ │ 24 │ julia> df3 = merge_df(df1, df2) 4×2 DataFrame │ Row │ B │ A │ │ │ Int64 │ Int64 │ ├─────┼───────┼───────┤ │ 130 │ │ 240 │ │ 301 │ │ 402 

请注意,就Julia的命名约定而言,通常不使用下划线,但这样会损害可读性。 在此实现中也不是很好,是对原始DataFrame进行了修改。 但是,尽管如此,该示例还是有助于说明对齐多列的过程。


使用连接功能可以按列中的公共值连接多个DataFrames(例如,通过公共用户的标识符将具有不同列的两个表粘合在一起)。


DataFrame便于在控制台中查看。 任何输出方式:使用show宏,使用println函数等,都将导致表格以易于阅读的形式打印到控制台。 如果DataFrame太大,将显示开始和结束行。 但是,您可以分别使用head和tail函数显式请求head和tail。


对于DataFrame,可以使用指定功能的数据分组和聚合功能。 它们返回的内容有所不同。 这可以是一个具有符合分组条件的DataFrame的集合,也可以是一个单独的DataFrame,其中的列名称将由原始名称和聚合函数的名称组成。 本质上,实现了拆分应用组合方案。 查看详情


我们将使用文档中的示例,并将示例表作为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 rowsRow │ SepalLength │ SepalWidth │ PetalLength │ PetalWidth │ Species │ │ │ Float64 │ Float64 │ Float64 │ Float64 │ String │ ├─────┼─────────────┼────────────┼─────────────┼────────────┼─────────┤ │ 15.13.51.40.2 │ setosa │ │ 24.93.01.40.2 │ setosa │ │ 34.73.21.30.2 │ setosa │ 

可以使用前面提到的collect函数将结果转换为数组:


 julia> collect(species) 3-element Array{Any,1}: 50×5 SubDataFrame{Array{Int64,1}} │ Row │ SepalLength │ SepalWidth │ PetalLength │ PetalWidth │ Species │ │ │ Float64Float64Float64Float64String │ ├─────┼─────────────┼────────────┼─────────────┼────────────┼─────────┤ │ 15.13.51.40.2 │ setosa │ │ 24.93.01.40.2 │ setosa │ │ 34.73.21.30.2 │ setosa │ … 

使用by函数分组。 指定接收到的DataFrame的列名和处理功能。 工作的第一阶段类似于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 

好吧,最后一个选项是聚合函数。 我们指定一个用于分组的列,并为其余的列指定一个聚合函数。 结果是一个DataFrame,其中将代表源列和聚合函数的名称形成列名称。


 julia> aggregate(iris, :Species, sum) 3×5 DataFrameRowSpeciesSepalLength_sumSepalWidth_sumPetalLength_sumPetalWidth_sum│ │ │ StringFloat64Float64Float64Float64 │ ├───┼──────────┼───────────────┼──────────────┼───────────────┼──────────────┤ │ 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 

描述了一个非常方便的功能来汇总表。 用法示例:


 julia> describe(iris) 5×8 DataFrameRowvariablemeanminmedianmaxnuniquenmissingeltype │ │ │ SymbolUnion… │AnyUnion…│ AnyUnion… │Int64DataType│ ├───┼───────────┼───────┼──────┼──────┼─────────┼───────┼────────┼────────┤ │ 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


StatPlots.jl库用于以图形方式显示DataFrame。 查看更多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 │ ├─────┼───────┼─────────┼─────────┤ │ 110.736147.11238 │ │ 225.52231.42414 │ │ 333.50042.11633 │ │ 441.341767.54208 │ │ 558.523922.98558 │ │ 664.474776.36836 │ │ 778.480936.59236 │ │ 885.37612.5127 │ │ 993.553939.2782 │ │ 10103.509257.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 │ Rowname │ age │ children │ │ │ String │ Float64 │ Int64 │ ├─────┼────────┼─────────┼──────────┤ │ 1 │ John │ 23.03 │ │ 2 │ Sally │ 42.05 │ │ 3 │ Kirk │ 59.02 │ julia> x = @from i in df begin @where i.age>50 @select {i.name, i.children} @collect DataFrame end 1×2 DataFrame │ Rowname │ 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 │ Rowname │ 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 

最后我要指出的是使用前面提到的CSV.jl库使用分隔符将DataFrame写入输出格式的能力


 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.03 │ │ 2 │ Sally │ 42.05 │ │ 3 │ Kirk │ 59.02 │ 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版本发布和库功能稳定之后,他们就开始写它了。几乎可以肯定的是,明年它将成为一种在数据科学领域不了解的语言。 那些没有开始使用Julia来分析数据的公司将是彻底的恐龙,将被更加敏捷的后代所取代。


Julia是一门年轻的编程语言。 实际上,在试点项目出现之后,很明显,Julia基础设施准备好用于实际的工业用途。 Julia开发人员非常有野心,现在就准备好了。 无论如何,Julia的简单但严格的语法使其成为一种非常吸引人的编程语言,适合立即学习。 高性能使您可以实现不仅适用于教育目的,而且还适用于数据分析的算法。 现在,我们将开始在各种项目中持续尝试Julia。

Source: https://habr.com/ru/post/zh-CN429994/


All Articles