统计数据分析

Julia比较适合用作数值计算,
编程既有Python、R、Matlab这样的语言的简洁,
又有C++这样的编译语言的运行效率。
统计数据分析、作图需要用到许多复杂的算法,
有些算法耗时很多,
比如MCMC等。
大量数据的分析、计算、测试都需要易用的编程和高效的运行效率,
Julia在这两点都很适合。

Julia用作统计数据分析,
缺点是其问世时间还比较短,
许多统计模型还没有现成的软件包。
但是,
基本的数据管理、作图、统计分布、随机模拟、最优化等统计计算必须的功能已经很完善,
所以能够自己编写的统计计算任务很适合用Julia编写。

Julia目前的版本是1.7.3。

这一部分在基本Julia语言语法基础上,
从统计数据分析角度简单介绍如何用Julia读入数据,
进行数据清理,汇总,简单的统计分析,
以及一些统计模型和机器学习方法。

参考:

  • 数据框(DataFrames)的手册:
    https://juliapackages.com/p/dataframes
  • McNicholas and Tait(2019) Data Science Using Julia, CRC Press.
  • Jose Storopoli, Rik Huijzer, Lazaro Alonso(2021) Julia Data Science.
    https://cn.julialang.org/JuliaDataScience/

数据分析任务的大部分时间并不是消耗在建模、计算上面,
而是对数据进行整理。
这包括从网络、文本文件、关系数据库、NoSQL数据库等位置获取数据,
将其转换成统计过程需要的数据输入形式,
清理无效数据,
将数据格式进行一致化,
对数据进行简单的探索性分析(EDA),
比如各个属性(变量)的类型,分布,属性之间的关系,
观测之间的分类,
异常值(离群值)识别等。

统计分析最常见的输入数据格式是数据框(Data Frame),
这是与矩阵类似的数据结构,
但是可以同时保存不同类型的数据,
同一列的数据类型相同。
R语言中称为数据框,
SAS语言中称为数据集(dataset),
数据库中称为一个表(table)。
数据框的每一行称为一个观测
每一列称为一个变量

Julia语言的DataFrames包(package)提供了数据框数据类型。
如果没有安装此包可用如下命令安装:

import Pkg
Pkg.add("DataFrames")

参考:

  • https://juliapackages.com/p/dataframes
  • https://github.com/JuliaData/DataFramesMeta.jl
  • https://github.com/queryverse/Query.jl

数据框

数据框中的列与统计学中随机变量的分类类似,
分为类别(categorical)变量与数值(continuous)变量。
类别变量主要用来对观测分组,
也可以作为模型中的自变量和因变量。
对类别变量不能进行通常的四则运算和其它数学计算。
字符型列只能作为类别变量,
某些离散取值的数值型列在建模时也可以作为类别变量,
比如,用1代表男性,2代表女性,
这样的列就应该当作类别变量使用。
对数值变量则可以进行各种数学运算。

统计数据与一般的数值计算问题有一个重要差别,
就是统计数据中常常含有缺失值,
如记录遗失、无应答、无交易、不适用等。
用来保存统计数据的数据结构必须能表示缺失值。
Julia语言的missing对象表示缺失值,
缺失值不区分具体类型,
属于Missing数据类型。

4-element Array{Union{Missing, Int64},1}:
 1
 2
  missing
 4

数据框生成

数据框各列常用的类型是字符型、整数型和浮点型。
DataFrame()可以直接生成小的数据框,如

using DataFrames
da0 = DataFrame(
    name=["张三", "李四", "王五", "赵六"], 
    age=[33, 42, missing, 51],
    sex=["M", "F", "M", "M"])
da1 = copy(da0)
4×3 DataFrame
 Row │ name    age      sex    
     │ String  Int64?   String 
─────┼─────────────────────────
   1 │ 张三         33  M
   2 │ 李四         42  F
   3 │ 王五    missing  M
   4 │ 赵六         51  M

其中copy()函数调用制作了一个da0数据框的副本。
数据框属于可变数据类型(mutable type),
如果直接将da0赋值给一个变量da1
则这两个变量实际指向同一个数据框,
改变其中一个变量会同时修改另一个变量的值。

构造数据框的DataFrame()函数也可以写成对的输入形式,如:

da0 = DataFrame(
    "name" => ["张三", "李四", "王五", "赵六"], 
    "age" => [33, 42, missing, 51],
    "sex" => ["M", "F", "M", "M"])

这时允许列名不符合Julia变量名要求。

可以从一个字典转换为数据框,如:

di = Dict(
    "name" => ["张三", "李四", "王五", "赵六"], 
    "age" => [33, 42, missing, 51],
    "sex" => ["M", "F", "M", "M"])
da0 = DataFrame(di)

当所有列都是数值型时,
也可以用一个矩阵转换成数据框,
这时列名自动命名,
然后可以用rename!()指定列名。
如:

mat1 = [[1, 3, 5, 7] [2, 3, 3, 6]]
4×2 Matrix{Int64}:
 1  2
 3  3
 5  3
 7  6
dam1 = DataFrame(mat1, :auto)
4×2 DataFrame
 Row │ x1     x2    
     │ Int64  Int64 
─────┼──────────────
   1 │     1      2
   2 │     3      3
   3 │     5      3
   4 │     7      6
rename!(dam1, ["colma", "colmb"])
4×2 DataFrame
 Row │ colma  colmb 
     │ Int64  Int64 
─────┼──────────────
   1 │     1      2
   2 │     3      3
   3 │     5      3
   4 │     7      6

数据框信息

df为数据框,
可以用nrow(df)求数据框行数(观测个数),
ncol(df)求数据框列数(变量个数)。
size(df)返回行列数。

(nrow(da1), ncol(da1))
## (4, 3)

names(df)返回数据框的变量名字符串数组,
propertynames(df)返回变量名的符号数组,如:

@show names(da1);
## names(da1) = ["name", "age", "sex"]
@show propertynames(da1);
## propertynames(da1) = [:name, :age, :sex]

获取每列的类型如:

zip(names(da1), string.(eltype.(eachcol(da1)))) |> 
    DataFrame |>
    d -> rename!(d, ["Variable", "Type"])
3×2 DataFrame
 Row │ Variable  Type
     │ String    String
─────┼─────────────────────────────────
   1 │ name      String
   2 │ age       Union{Missing, Int64}
   3 │ sex       String

访问数据框内容

访问单个元素

可以像对矩阵那样取数据框元素或子集。
比如, df[2,1]表示数据框df第2行第1列元素:

列序号也可以用变量名符号或者变量名字符串表示,
变量名符号就是将变量名前面添加冒号,
不用字符串表示,如

da1[2,:name]
## "李四"
da1[2,"name"]
## "李四"

vname = "name"
da1[2,vname]
## "李四"

可以修改单个元素的值,
如:

4×3 DataFrame
 Row │ name    age      sex    
     │ String  Int64?   String 
─────┼─────────────────────────
   1 │ 张三         33  M
   2 │ 孙七         42  F
   3 │ 王五    missing  M
   4 │ 赵六         51  M

访问一列

在行下标位置写单独的叹号表示所有行,
在列下标指定一列后可以访问数据框的这一列,
不制作副本。
比如,df[!,2], df[!, "age"]df[!, :age]都可以取出df的第二列作为一个一维数组:

4-element Vector{Union{Missing, Int64}}:
 33
 42
   missing
 51

列序号或列名也可以保存在变量中,如:

也可以用“数据框名.列名”的格式访问一列,如da1.ageda1."age"

冒号方式

在行下标处使用叹号不制作列向量的副本,
效率较高,
可以修改提取的列的值。
可以用冒号:作为行下标,
这会生成一列的副本。
如:

da1 = copy(da0)
x = da1[:, :age]
x[:] = x .+ 100
da1
4×3 DataFrame
 Row │ name    age      sex    
     │ String  Int64?   String 
─────┼─────────────────────────
   1 │ 张三         33  M
   2 │ 李四         42  F
   3 │ 王五    missing  M
   4 │ 赵六         51  M

可见原始的数据框未被修改。
如果使用叹号格式,则会被修改:

da1 = copy(da0)
x = da1[!, :age]
x[:] = x .+ 100
da1
4×3 DataFrame
 Row │ name    age      sex    
     │ String  Int64?   String 
─────┼─────────────────────────
   1 │ 张三        133  M
   2 │ 李四        142  F
   3 │ 王五    missing  M
   4 │ 赵六        151  M

叹号格式与冒号格式的选择

当列下标为单个整数、符号、字符串、字符串变量时,
行下标写!或者写:都可以取出一列作为数组。

  • 叹号格式为“视图”,不制作副本,
    修改取出数组同时也会修改原始数据框。
  • 冒号格式会制作一列的副本,
    除非将冒号格式直接放在赋值的左侧,
    都不会修改原始数据框。
  • 为安全起见,应使用冒号格式;
    对于很大的数据框,
    复制会造成较大开销,
    可以谨慎地使用叹号格式。

用select选列子集

select可以挑选列子集,返回副本:

da1 = copy(da0)
select(da1, :name, :age)
4×2 DataFrame
 Row │ name    age     
     │ String  Int64?  
─────┼─────────────────
   1 │ 张三         33
   2 │ 李四         42
   3 │ 王五    missing 
   4 │ 赵六         51

可以用Not()说明要去掉的列:

select(da1, Not([:name, :age]))
4×1 DataFrame
 Row │ sex    
     │ String 
─────┼────────
   1 │ M
   2 │ F
   3 │ M
   4 │ M

即使结果只有一列,
select也会返回数据框。

如下的做法可以将某一列提前到第一列:

4×3 DataFrame
 Row │ age      name    sex    
     │ Int64?   String  String 
─────┼─────────────────────────
   1 │      33  张三    M
   2 │      42  李四    F
   3 │ missing  王五    M
   4 │      51  赵六    M

select!则会修改输入的数据框,
仅保留指定列。

访问行子集

firstlast函数

可以用first(df,k)取出df前面k行组成的子集,
last(df,k)取出df最后k行组成的子集。

da1 = copy(da0)
first(da1, 2)
2×3 DataFrame
 Row │ name    age     sex    
     │ String  Int64?  String 
─────┼────────────────────────
   1 │ 张三        33  M
   2 │ 李四        42  F
2×3 DataFrame
 Row │ name    age      sex    
     │ String  Int64?   String 
─────┼─────────────────────────
   1 │ 王五    missing  M
   2 │ 赵六         51  M

逻辑下标

除了用行序号取行子集,
还可以用条件取行子集,
如:

1×3 DataFrame
 Row │ name    age     sex    
     │ String  Int64?  String 
─────┼────────────────────────
   1 │ 李四        42  F

注意其中的比较用了加点广播形式。

可以用in.()判断某列的值是否属于某个子集,
此子集使用Ref()函数指定。
在使用广播时,用Ref()使得其自变量被看作标量,
而不需要按元素对应运算。
如:

da0[in.(da0.name, Ref(["张三", "李四"])), :]
2×3 DataFrame
 Row │ name    age     sex    
     │ String  Int64?  String 
─────┼────────────────────────
   1 │ 张三        33  M
   2 │ 李四        42  F

subset函数

subset()函数输入一个数据框和若干个变量名和关于该变量选择行子集的示性函数(返回逻辑值的函数),
返回满足条件的子集副本。
如:

subset(da0, :sex => x -> x .== "M")
3×3 DataFrame
 Row │ name    age      sex    
     │ String  Int64?   String 
─────┼─────────────────────────
   1 │ 张三         33  M
   2 │ 王五    missing  M
   3 │ 赵六         51  M

如果某一列有缺失值,
则对此列的逻辑判断结果也会有缺失值。
这时可以用coallese()函数指定缺失时的结果替换值,如:

subset(da0, :age => x -> coalesce.(x .< 45, false), 
    :sex => x -> x .== "M")
1×3 DataFrame
 Row │ name    age     sex    
     │ String  Int64?  String 
─────┼────────────────────────
   1 │ 张三        33  M

一个更方便的办法是给subset()函数加skipmissing=true选项:

subset(da0, :age => x -> x .< 45, 
    :sex => x -> x .== "M",
    skipmissing=true)
1×3 DataFrame
 Row │ name    age     sex    
     │ String  Int64?  String 
─────┼────────────────────────
   1 │ 张三        33  M

filter函数

可以用filter(f, df)取出df的满足条件的行子集,
其中f是一个以一行作为有名元组类型自变量的示性函数,
如:

filter(row -> row.sex == "F", da1)
1×3 DataFrame
 Row │ name    age     sex    
     │ String  Int64?  String 
─────┼────────────────────────
   1 │ 李四        42  F

filter!则会修改输入的数据框,
仅保留满足条件的行。

访问行列子集

下标方法

对行、列下标可以指定范围,
用下标向量或者变量名符号向量,

da1 = copy(da0)
da1[2:4, [1,3]]
3×2 DataFrame
 Row │ name    sex    
     │ String  String 
─────┼────────────────
   1 │ 李四    F
   2 │ 王五    M
   3 │ 赵六    M

注意,da1[2:4, [1,3]]da1[2:4, 1:3]是允许的,
da1[2:4, (1,3)]不允许。
多个列下标必须是向量或范围,
不允许使用其它序列。

3×2 DataFrame
 Row │ name    sex    
     │ String  String 
─────┼────────────────
   1 │ 李四    F
   2 │ 王五    M
   3 │ 赵六    M
da1[2:4, ["name", "sex"]]
3×2 DataFrame
 Row │ name    sex    
     │ String  String 
─────┼────────────────
   1 │ 李四    F
   2 │ 王五    M
   3 │ 赵六    M

如果仅取一列,
结果将不再是数据框,
而是普通数组:

    3-element Vector{String}:
     "F"
     "M"
     "M"

但是,
如果列下标位置使用数组记号,
则可以取出仅有一列的数据框:

3×1 DataFrame
 Row │ sex    
     │ String 
─────┼────────
   1 │ F
   2 │ M
   3 │ M

取行列子集的结果一般不是视图,而是副本。
但是,
在行下标写叹号或者用行序号仅取一行时,
取出的是视图。
视图不生成副本,
修改视图就会修改原始数据框。

视图

视图是数据框的一个子集,
但不制作副本,
修改视图也会修改原始数据框。
对大型数据访问效率更高,
但访问其中一部分时序号进行下标转换,
有一些额外开销。
如:

da1 = copy(da0)
da2v = @view da1[2:4, [:sex, :age]]
3×2 SubDataFrame
 Row │ sex     age     
     │ String  Int64?  
─────┼─────────────────
   1 │ F            42
   2 │ M       missing 
   3 │ M            51

其它列子集

在列下标位置使用Not()可以取排除掉某些列后的列子集。如:

da1 = copy(da0)
da1[!, :class] .= 1
da1[!, :ctn] .= "Zhang"
da1
4×5 DataFrame
 Row │ name    age      sex     class  ctn    
     │ String  Int64?   String  Int64  String 
─────┼────────────────────────────────────────
   1 │ 张三         33  M           1  Zhang
   2 │ 李四         42  F           1  Zhang
   3 │ 王五    missing  M           1  Zhang
   4 │ 赵六         51  M           1  Zhang
da1[:, Not([:age, :sex])]
4×3 DataFrame
 Row │ name    class  ctn    
     │ String  Int64  String 
─────┼───────────────────────
   1 │ 张三        1  Zhang
   2 │ 李四        1  Zhang
   3 │ 王五        1  Zhang
   4 │ 赵六        1  Zhang

在列下标位置用Between()指定两列,
选择这两列之间的列:

da1[:, Between(:age, :class)]
4×3 DataFrame
 Row │ age      sex     class 
     │ Int64?   String  Int64 
─────┼────────────────────────
   1 │      33  M           1
   2 │      42  F           1
   3 │ missing  M           1
   4 │      51  M           1

可以用Cols()将不同的列选择方式合并使用,如:

da1[:, Cols(:name, Between(:sex, :ctn))]
4×4 DataFrame
 Row │ name    sex     class  ctn    
     │ String  String  Int64  String 
─────┼───────────────────────────────
   1 │ 张三    M           1  Zhang
   2 │ 李四    F           1  Zhang
   3 │ 王五    M           1  Zhang
   4 │ 赵六    M           1  Zhang

df[:, Cols(:name, :)]name列调到最前面;
df[:, Cols(Not(:name), :)]name列调到最后面。

可以在列下标位置使用正则表达式,
选择能匹配的变量名。如:

4×2 DataFrame
 Row │ class  ctn    
     │ Int64  String 
─────┼───────────────
   1 │     1  Zhang
   2 │     1  Zhang
   3 │     1  Zhang
   4 │     1  Zhang

在程序中,
正则表达式r"^c"选择以字母c开头的变量名。

将行列子集转换为矩阵

若取出的某个行列子集A的元素都是数值型,
可以用hcat(eachcol(A)...)的方法转换为数值型矩阵。
如:

mat1 = [[1, 3, 5, 7] [2, 3, 3, 6]]
dam1 = DataFrame(mat1, :auto)
mat2sub = hcat(eachcol(dam1[1:2, 1:2])...)
2×2 Matrix{Int64}:
 1  2
 3  3

添加列

下标方法

可以从一个空数据框逐列地添加数据,如:

da2 = DataFrame()
da2.name = ["张三", "李四", "王五", "赵六"]
da2[!, :age] = [33, 42, missing, 51]
da2[!, "sex"] = ["M", "F", "M", "M"]
da2
4×3 DataFrame
 Row │ name    age      sex    
     │ String  Int64?   String 
─────┼─────────────────────────
   1 │ 张三         33  M
   2 │ 李四         42  F
   3 │ 王五    missing  M
   4 │ 赵六         51  M

如果给不存在的一列赋值,
就生成了新的列,
如:

da1 = copy(da0)
da1[!,:group] = [1,1,2,2]
da1
4×4 DataFrame
 Row │ name    age      sex     group 
     │ String  Int64?   String  Int64 
─────┼────────────────────────────────
   1 │ 张三         33  M           1
   2 │ 李四         42  F           1
   3 │ 王五    missing  M           2
   4 │ 赵六         51  M           2

transform函数

如果需要添加一些统计量列,
可以用transform()函数,
格式是transform(df, 列名 => 变换函数 => 结果变量名)
如:

using Statistics
da1 = copy(da0)
da1[!,:height] .= [166, 182, 173, 171]
da2 = transform(da1, :height => maximum => :max_height)
4×5 DataFrame
 Row │ name    age      sex     height  max_height 
     │ String  Int64?   String  Int64   Int64      
─────┼─────────────────────────────────────────────
   1 │ 张三         33  M          166         182
   2 │ 李四         42  F          182         182
   3 │ 王五    missing  M          173         182
   4 │ 赵六         51  M          171         182

也可以对某一列的每一行进行变化,
这时用ByRow()说明要进行的变换。
可以对多列分别变换。
如:

da1 = copy(da0)
da1[!,:height] .= [166, 182, 173, 171]
da2 = transform(da1, 
    :height => maximum => :max_height,
    :age => ByRow(x -> x + 20) => :newage)
4×6 DataFrame
 Row │ name    age      sex     height  max_height  newage  
     │ String  Int64?   String  Int64   Int64       Int64?  
─────┼──────────────────────────────────────────────────────
   1 │ 张三         33  M          166         182       53
   2 │ 李四         42  F          182         182       62
   3 │ 王五    missing  M          173         182  missing 
   4 │ 赵六         51  M          171         182       71

添加行

可以用push!()给一个数据框添加新的行,如:

da1 = copy(da0)
push!(da1, ("钱多", 59, "M"))
5×3 DataFrame
 Row │ name    age      sex         
     │ String  Int64?   String      
─────┼─────────────────────────     
   1 │ 张三         33  M
   2 │ 李四         42  F
   3 │ 王五    missing  M
   4 │ 赵六         51  M
   5 │ 钱多         59  M

下面这些写法也可以用来添加行:

push!(da1, ["王芳", 40, "F"])
push!(da1, Dict(:name => "邓丽", :age => 18, :sex => "F"))

逐行添加数据是比较低效率的方法,
正常情况下还是应该一次性生成整个数据框。

CSV文件读写

读入

用CSV包的CSV.read()函数可以读入CSV文件到数据框,
CSV.write()函数将数据框保存到CSV文件。

设当前工作目录下有如下的文件class9.csv

name,sex,age,height,weight
Sandy,F,11,130.0,23.0
Karen,F,12,143.0,35.0
Kathy,F,12,152.0,38.0
Alice,F,13,144.0,38.0
Thomas,M,11,146.0,39.0
James,M,12,146.0,38.0
John,M,12,150.0,45.0
Robert,M,12,165.0,58.0
Jeffrey,M,13,159.0,38.0

用如下命令将其读入为数据框:

using CSV, DataFrames
d_class = CSV.read("class9.csv", DataFrame)
9×5 DataFrame
 Row │ name     sex      age    height   weight  
     │ String7  String1  Int64  Float64  Float64 
─────┼───────────────────────────────────────────
   1 │ Sandy    F           11    130.0     23.0
   2 │ Karen    F           12    143.0     35.0
   3 │ Kathy    F           12    152.0     38.0
   4 │ Alice    F           13    144.0     38.0
   5 │ Thomas   M           11    146.0     39.0
   6 │ James    M           12    146.0     38.0
   7 │ John     M           12    150.0     45.0
   8 │ Robert   M           12    165.0     58.0
   9 │ Jeffrey  M           13    159.0     38.0

选项

程序自动判断每一列的类型。
这里String1String7等是固定宽度的字符串类型,
可以提高效率。

为了保险,
用户可以用types参数指定一个从变量名字符串到类型的字典,如:

using CSV
d_class = CSV.read("class9.csv", DataFrame,
        types=Dict(
            "name" => String, 
            "sex" => String, 
            "height" => Float64,
            "weight" => Float64))

如果数据项之间是用一个空格分隔的,
可以加选项用delim=' '的方式读入。
不支持多个空格作为分隔。
如果数据项之间是用一个制表符分隔的,
可以加选项delim='\t')的方式读入。

如果数据中没有列名,
可以加选项header=0

对于很大的文件,
可以先尝试读入一部分,
这时可以用limit=选项指定一个读入的行数。
可以用skipto=指定从那一行开始读入。

更多选项可查看函数的帮助,
命令如“?CSV.Read”。

从网络读入

可以从网络直接下载CSV文件读入为数据框。
可以用Downloads标准库的download函数下载临时文件读入,
还可以用HTTP包直接读取。

例如,读入UCI网站的Cleveland心脏病数据数据集:

using Downloads
urlf = "https://archive.ics.uci.edu/ml/machine-learning-databases/heart-disease/processed.cleveland.data"
dht = CSV.read(Downloads.download(urlf), DataFrame,
    header=0)
rename!(dht, ["age", "sex", "cp", "trestbps", "chol", 
    "fbs", "restecg", "thalach", "exang", "oldpeak",    
    "slope", "ca", "thal", "num"])

该数据集文件的前几行如:

3.0,1.0,1.0,145.0,233.0,1.0,2.0,150.0,0.0,2.3,3.0,0.0,6.0,0
67.0,1.0,4.0,160.0,286.0,0.0,2.0,108.0,1.0,1.5,2.0,3.0,3.0,2
67.0,1.0,4.0,120.0,229.0,0.0,2.0,129.0,1.0,2.6,2.0,2.0,7.0,1

文件第一行没有变量名,
所以用了rename!添加变量名。

写出

将数据框写入CSV文件class2.csv,命令如

CSV.write("class2.csv", d_class)

如果文件不在当前工作路径,
可以使用全路径如““C:/work/proj/data/class2.csv””。

可以设置当前工作路径,
用“cd()”命令可以设置当前工作路径,
用“pwd()”显示当前工作路径,
用“homedir()”显示用户在操作系统中的主目录。

参考:

  • https://csv.juliadata.org/latest/

Excel文件读写

XLSX.jl是完全用Julia实现的读写Excel文件的扩展包。

设当前目录下有class9.xlsx文件,
数据与class9.csv的数据相同。
读入如:

d_class2 = DataFrame(XLSX.readtable("class9.xlsx", 1))
9×5 DataFrame
 Row │ name     sex  age  height  weight 
     │ Any      Any  Any  Any     Any    
─────┼───────────────────────────────────
   1 │ Sandy    F    11   130     23
   2 │ Karen    F    12   143     35
   3 │ Kathy    F    12   152     38
   4 │ Alice    F    13   144     38
   5 │ Thomas   M    11   146     39
   6 │ James    M    12   146     38
   7 │ John     M    12   150     45
   8 │ Robert   M    12   165     58
   9 │ Jeffrey  M    13   159     38

转换的数据框没有自动识别各列的类型。
使用identity()函数对各列的类型进行转换:

d_class2 = identity.(d_class2)
9×5 DataFrame
 Row │ name     sex     age    height  weight 
     │ String   String  Int64  Int64   Int64  
─────┼────────────────────────────────────────
   1 │ Sandy    F          11     130      23
   2 │ Karen    F          12     143      35
   3 │ Kathy    F          12     152      38
   4 │ Alice    F          13     144      38
   5 │ Thomas   M          11     146      39
   6 │ James    M          12     146      38
   7 │ John     M          12     150      45
   8 │ Robert   M          12     165      58
   9 │ Jeffrey  M          13     159      38

可以将这两步写在一起:

d_class2 = identity.(DataFrame(XLSX.readtable("class9.xlsx", 1)))

XLSX包可以读取多个工作簿的Excel文件,
可以生成Excel文件。
详见:

  • https://felipenoris.github.io/XLSX.jl/stable/tutorial/

数据框变量概括

describe函数

describe()函数对数据框各个变量进行简单概括,
结果为数据框格式。如

5×7 DataFrame
 Row │ variable  mean     min    median  max     nmissing  eltype   
     │ Symbol    Union…   Any    Union…  Any     Int64     DataType 
─────┼──────────────────────────────────────────────────────────────
   1 │ name               Alice          Thomas         0  String
   2 │ sex                F              M              0  String
   3 │ age       12.0     11     12.0    13             0  Int64
   4 │ height    148.333  130.0  146.0   165.0          0  Float64
   5 │ weight    39.1111  23.0   38.0    58.0           0  Float64

describe()对每个变量给出了数据类型,
缺失值个数,
最大值和最小值,
对数值型变量还给出了均值、中位数。
类似于R语言的summary()函数。

直接调用统计函数

计算指定的统计量:

using Statistics
mean(d_class[!,"age"])
## 12.0

常用统计函数有mean,
median,
var, std, iqr,
minimum, maximum,
quantile.

summarystats(x)函数对数值型变量x计算各种简单统计量,
以纯文本格式显示。如:

using Statistics, StatsBase
summarystats(d_class[!,"age"])
## 12.0
Summary Stats:
Length:         9
Missing Count:  0
Mean:           12.000000
Minimum:        11.000000
1st Quartile:   12.000000
Median:         12.000000
3rd Quartile:   12.000000
Maximum:        13.000000

两个变量可以用cov(x,y)计算协方差,
cor(x,y)计算相关系数。
对于矩阵形式的随机向量观测值

X,
cov(X)估计协方差阵,
cor(X)估计相关系数矩阵。
如:

cov(hcat(eachcol(dcl)...))
3×3 Matrix{Float64}:
 0.5      3.375    1.75
 3.375  100.25    79.2083
 1.75    79.2083  84.1111

combine函数

可以用combine函数计算统计量。
输入数据框,
以及用变量名 => 统计函数或者变量名 => 统计函数 => 结果变量名方式指定的一个或多个要汇总的内容。

比如,计算身高和体重的平均值:

combine(d_class, :height => mean, :weight => mean)
1×2 DataFrame
 Row │ height_mean  weight_mean 
     │ Float64      Float64     
─────┼──────────────────────────
   1 │     148.333      39.1111

上例可以用.=>的语法合并写在一起:

combine(d_class, [:height, :weight] .=> mean)

可以用names(df) .=> 统计函数的格式指定对所有列计算同一统计量。
如:

subdf = d_class[:,Cols(:age, :height, :weight)]
combine(subdf, 
        names(subdf) .=> mean)
1×3 DataFrame
 Row │ age_mean  height_mean  weight_mean 
     │ Float64   Float64      Float64     
─────┼────────────────────────────────────
   1 │     12.0      148.333      39.1111

计算三个变量的平均值和最大值,写成链式调用,
names(df)方法指定所有变量:

using Statistics
select(d_class, :age, :height, :weight) |>
    subdf -> combine(subdf, 
        names(subdf) .=> mean,
        names(subdf) .=> maximum)
1×6 DataFrame
 Row │ age_mean  height_mean  weight_mean  age_maximum  height_maximum  weight_maximum 
     │ Float64   Float64      Float64      Int64        Float64         Float64        
─────┼─────────────────────────────────────────────────────────────────────────────────
   1 │     12.0      148.333      39.1111           13           165.0            58.0

上例的另一种写法:

combine(d_class,
    ([:age, :height, :weight] .=> [mean maximum])...)

简单修改

修改变量名

rename!()函数可以为数据框变量改名,

da1 = copy(da0)
rename!(da1, [:sex => :性别, :age => :年龄])
4×3 DataFrame
 Row │ name    年龄     性别   
     │ String  Int64?   String 
─────┼─────────────────────────
   1 │ 张三         33  M
   2 │ 李四         42  F
   3 │ 王五    missing  M
   4 │ 赵六         51  M

或:

da1 = copy(da0)
rename!(da1, ["name", "年龄", "性别"])

修改或添加变量

可以直接修改变量或者添加新变量,如

dcl = copy(d_class)
dcl[!,:height] ./= 100
first(dcl, 3)
3×5 DataFrame
 Row │ name    sex     age    height   weight  
     │ String  String  Int64  Float64  Float64 
─────┼─────────────────────────────────────────
   1 │ Sandy   F          11     1.3      23.0
   2 │ Karen   F          12     1.43     35.0
   3 │ Kathy   F          12     1.52     38.0
dcl = copy(d_class)
dcl[!,:ratio] = dcl[!,:weight] ./ dcl[!,:height]
dcl[!,:classno] .= 1
first(dcl,3)
3×7 DataFrame
 Row │ name    sex     age    height   weight   ratio     classno 
     │ String  String  Int64  Float64  Float64  Float64   Int64   
─────┼────────────────────────────────────────────────────────────
   1 │ Sandy   F          11    130.0     23.0  0.176923        1
   2 │ Karen   F          12    143.0     35.0  0.244755        1
   3 │ Kathy   F          12    152.0     38.0  0.25            1

其中dcl[!,:classno] .= 1用了广播的方法将一列赋值为同一个值,
并生成了新的列。

dcl[!,:ratio] = dcl[!,:weight] ./ dcl[!,:height]添加了新变量,
=或者用.=赋值都是同样效果,
!或者用:也是相同的效果。

但是,如果这一列已经存在,
赋值左侧用!格式时,
不论用=还是用.=都是重新绑定此列到赋值的右侧,
可以改变此列的类型。
这时如果左侧是用的冒号,
则用.=不改变此列的元素类型,
=可以修改此列的元素类型。

如果将dcl[!,:ratio]赋值为一个保存了向量值的变量x
则这时执行的是一个重复绑定,
不制作x的副本,
所以修改彼此的值会同时修改对方。

函数select!将数据框变成列子集,
filter!将数据框变成行子集。

transform!修改列

可以用transform!修改原有的列,
renamecols = false选项。
比如:

dcl = copy(d_class)
transform!(dcl, :height => (x -> x ./ 100),
    renamecols = false)
9×5 DataFrame
 Row │ name     sex     age    height   weight  
     │ String   String  Int64  Float64  Float64 
─────┼──────────────────────────────────────────
   1 │ Sandy    F          11     1.3      23.0
   2 │ Karen    F          12     1.43     35.0
   3 │ Kathy    F          12     1.52     38.0
   4 │ Alice    F          13     1.44     38.0
   5 │ Thomas   M          11     1.46     39.0
   6 │ James    M          12     1.46     38.0
   7 │ John     M          12     1.5      45.0
   8 │ Robert   M          12     1.65     58.0
   9 │ Jeffrey  M          13     1.59     38.0

上面的程序将数据框中的身高改成了以米为单位。
如果希望修改后使用新变量名,
方法如:

dcl = copy(d_class)
transform!(dcl, :height => (x -> x ./ 100) => :height_m)
9×6 DataFrame
 Row │ name     sex     age    height   weight   height_m 
     │ String   String  Int64  Float64  Float64  Float64  
─────┼────────────────────────────────────────────────────
   1 │ Sandy    F          11    130.0     23.0      1.3
   2 │ Karen    F          12    143.0     35.0      1.43
   3 │ Kathy    F          12    152.0     38.0      1.52
   4 │ Alice    F          13    144.0     38.0      1.44
   5 │ Thomas   M          11    146.0     39.0      1.46
   6 │ James    M          12    146.0     38.0      1.46
   7 │ John     M          12    150.0     45.0      1.5
   8 │ Robert   M          12    165.0     58.0      1.65
   9 │ Jeffrey  M          13    159.0     38.0      1.59

修改多列

可以用mapcols()对数据框的所有列应用某种变换,
返回变换结果。
如:

dc1 = copy(d_class)
dc2 = mapcols(x -> x ./100, dc1[!, ["age", "height", "weight"]])
first(dc2, 3)
3×3 DataFrame
 Row │ age      height   weight  
     │ Float64  Float64  Float64 
─────┼───────────────────────────
   1 │    0.11     1.3      0.23
   2 │    0.12     1.43     0.35
   3 │    0.12     1.52     0.38

替换值

replace!()函数对数据框指定的列的某些值进行替换。如:

da1 = copy(da0)
replace!(da1.sex, "F" => "女", "M" => "男")
da1
4×3 DataFrame
 Row │ name    age      sex    
     │ String  Int64?   String 
─────┼─────────────────────────
   1 │ 张三         33  男
   2 │ 李四         42  女
   3 │ 王五    missing  男
   4 │ 赵六         51  男
replace!(da1.age, missing => 0); da1
4×3 DataFrame
 Row │ name    age     sex    
     │ String  Int64?  String 
─────┼────────────────────────
   1 │ 张三        33  男
   2 │ 李四        42  女
   3 │ 王五         0  男
   4 │ 赵六        51  男

replace!函数也可以输入一个替换函数,
按该替换函数的映射将第二自变量进行值替换,如:

da1 = copy(da0)
replace!(x -> ismissing(x) ? 0 : x, da1[!, :age])
da1
4×3 DataFrame
 Row │ name    age     sex    
     │ String  Int64?  String 
─────┼────────────────────────
   1 │ 张三        33  M
   2 │ 李四        42  F
   3 │ 王五         0  M
   4 │ 赵六        51  M

将某些值替换为missing
需要先用allowmissing!将整个数据框的所有列(或指定列)允许有缺失值,
然后使用.=赋值。如:

da1 = copy(da0)
allowmissing!(da1)
replace!(x -> x == "F" ? missing : x, da1[!, :sex])
da1
4×3 DataFrame
 Row │ name     age      sex     
     │ String?  Int64?   String? 
─────┼───────────────────────────
   1 │ 张三          33  M
   2 │ 李四          42  missing 
   3 │ 王五     missing  M
   4 │ 赵六          51  M

对所有列如果要统一替换,
可以选择整个数据框。如:

da2 = DataFrame(x = [1, 2, 0, 4],
  y = [11, 0, 33, 44])
allowmissing!(da2)
da2 .= ifelse.(da2 .== 0, missing, da2)
4×2 DataFrame
 Row │ x        y       
     │ Int64?   Int64?  
─────┼──────────────────
   1 │       1       11
   2 │       2  missing 
   3 │ missing       33
   4 │       4       44

排序

sort!()函数将数据框安装某一列或某几列排序。
这个函数会修改其输入,
输入的数据框会被修改为排序后的值。

用第二个自变量指定按哪一列排序:

dcl = copy(d_class)
typeof(dcl)
## DataFrame
sort!(dcl, :age)
dcl
9×5 DataFrame
 Row │ name     sex     age    height   weight  
     │ String   String  Int64  Float64  Float64 
─────┼──────────────────────────────────────────
   1 │ Sandy    F          11    130.0     23.0
   2 │ Thomas   M          11    146.0     39.0
   3 │ Karen    F          12    143.0     35.0
   4 │ Kathy    F          12    152.0     38.0
   5 │ James    M          12    146.0     38.0
   6 │ John     M          12    150.0     45.0
   7 │ Robert   M          12    165.0     58.0
   8 │ Alice    F          13    144.0     38.0
   9 │ Jeffrey  M          13    159.0     38.0

可以指定多列,当前一列相同时按后一列排序:

sort!(dcl, [:sex, :age])
dcl
9×5 DataFrame
 Row │ name     sex     age    height   weight  
     │ String   String  Int64  Float64  Float64 
─────┼──────────────────────────────────────────
   1 │ Sandy    F          11    130.0     23.0
   2 │ Karen    F          12    143.0     35.0
   3 │ Kathy    F          12    152.0     38.0
   4 │ Alice    F          13    144.0     38.0
   5 │ Thomas   M          11    146.0     39.0
   6 │ James    M          12    146.0     38.0
   7 │ John     M          12    150.0     45.0
   8 │ Robert   M          12    165.0     58.0
   9 │ Jeffrey  M          13    159.0     38.0

在指定排序变量时,
可以用order()函数指定选项,
rev=true指定降序,
by=uppercase指定按转换为大写后排序。
例如

sort!(dcl, [:sex, order(:age, rev=true)])
dcl
9×5 DataFrame
 Row │ name     sex     age    height   weight  
     │ String   String  Int64  Float64  Float64 
─────┼──────────────────────────────────────────
   1 │ Alice    F          13    144.0     38.0
   2 │ Karen    F          12    143.0     35.0
   3 │ Kathy    F          12    152.0     38.0
   4 │ Sandy    F          11    130.0     23.0
   5 │ Jeffrey  M          13    159.0     38.0
   6 │ James    M          12    146.0     38.0
   7 │ John     M          12    150.0     45.0
   8 │ Robert   M          12    165.0     58.0
   9 │ Thomas   M          11    146.0     39.0

函数sort()对输入数据框排序并输出排序后的副本,
不改变输入的数据框。

纵向合并

如果两个数据框df1和df2结构相同,
只是保存了不同的观测,
可以用vcat(df1, df2)返回上下合并的结果,
如:

df1 = DataFrame(
    id = [1, 2], 
    x = [101, 102])
df2 = DataFrame(
    id = [3, 4], 
    x = [201, 202])
vcat(df1, df2)
4×2 DataFrame
 Row │ id     x     
     │ Int64  Int64 
─────┼──────────────
   1 │     1    101
   2 │     2    102
   3 │     3    201
   4 │     4    202

可以用append!(df1, df2)将内容合并到df1中。
如:

4×2 DataFrame
 Row │ id     x     
     │ Int64  Int64 
─────┼──────────────
   1 │     1    101
   2 │     2    102
   3 │     3    201
   4 │     4    202

横向合并

如果两个数据框行数相同,
按行号一对一左右合并,
可以用hcat(x, y)合并。

innerjoin()等函数可以用来按照关键列对两个数据框进行横向合并。

一对一横向合并

dfj1 = DataFrame(id=["a", "b"], X=[11,12])
2×2 DataFrame
 Row │ id      X     
     │ String  Int64 
─────┼───────────────
   1 │ a          11
   2 │ b          12
dfj2 = DataFrame(id=["b", "a"], Y=[21,22])
 Row │ id      Y     
     │ String  Int64 
─────┼───────────────
   1 │ b          21
   2 │ a          22
dfj3 = innerjoin(dfj1, dfj2, on = :id)
2×3 DataFrame
 Row │ id      X      Y     
     │ String  Int64  Int64 
─────┼──────────────────────
   1 │ b          12     21
   2 │ a          11     22

如果用来连接的关键列名字不一样,
可以提供对作为on的输入,如on = :id => :num
如果有多个关键列,
可以用对的向量作为on的输入。

如果确信应该是作一对一合并,
想检查实际数据是否满足如此要求,
可以加选项validate = (true, true)进行验证,
验证失败则出错停止。

如果两个数据框有非关键列但是变量名相同,
可以加选项makeunique = true使得后面的数据框自动修改变量名以避免列名冲突。

一对多横向合并

关键变量的一个值可以和多个值匹配。如:

dfj4 = DataFrame(id=["a", "a", "b"], Y=[41,42,43])
3×2 DataFrame
 Row │ id      Y     
     │ String  Int64 
─────┼───────────────
   1 │ a          41
   2 │ a          42
   3 │ b          43
dfj5 = innerjoin(dfj1, dfj4, on = :id)
3×3 DataFrame
 Row │ id      X      Y     
     │ String  Int64  Int64 
─────┼──────────────────────
   1 │ a          11     41
   2 │ a          11     42
   3 │ b          12     43

多对多横向合并

关键变量的多个值可以和多个值两两搭配。如:

dfj6 = DataFrame(id=["a", "a", "b"], X=[61,62,63])
3×2 DataFrame
 Row │ id      X     
     │ String  Int64 
─────┼───────────────
   1 │ a          61
   2 │ a          62
   3 │ b          63
dfj7 = DataFrame(id=["a", "a", "b"], Y=[71,72,73])
3×2 DataFrame
 Row │ id      Y     
     │ String  Int64 
─────┼───────────────
   1 │ a          71
   2 │ a          72
   3 │ b          73
dfj8 = innerjoin(dfj6, dfj7, on = :id)
5×3 DataFrame
 Row │ id      X      Y     
     │ String  Int64  Int64 
─────┼──────────────────────
   1 │ a          61     71
   2 │ a          62     71
   3 │ a          61     72
   4 │ a          62     72
   5 │ b          63     73

连接

  • innerjoin(a,b, on=...)默认将左右能匹配的观测保留,
    不能匹配的删除,
    这在数据库术语中称为“内连接”(inner join)。
  • leftjoin作左连接,即保留左侧数据框所有观测,
    右侧数据框仅保留能匹配的观测。
  • rightjoin作右连接,保留右侧数据框所有观测,
    左侧数据框仅保留能匹配的观测。
  • outerjoin作全外连接。保留两侧数据框所有观测。
  • semijoin仅保留左侧数据框能匹配的观测,
    不保留右侧数据框内容,
    实际是用右侧数据框的键值来选择左侧数据框的内容。
  • antijoin仅保留左侧数据框不能匹配的观测,
    不保留右侧数据框内容,
    也是用右侧数据框的键值来排除左侧数据框的内容。
dfj9 = DataFrame(id=["a", "b"], X=[91,92])
2×2 DataFrame
 Row │ id      X     
     │ String  Int64 
─────┼───────────────
   1 │ a          91
   2 │ b          92
dfj10 = DataFrame(id=["a", "c"], Y=[101,102])
2×2 DataFrame
 Row │ id      Y     
     │ String  Int64 
─────┼───────────────
   1 │ a         101
   2 │ c         102
leftjoin(dfj9, dfj10, on=:id)
2×3 DataFrame
 Row │ id      X      Y       
     │ String  Int64  Int64?  
─────┼────────────────────────
   1 │ a          91      101
   2 │ b          92  missing 
rightjoin(dfj9, dfj10, on=:id)
2×3 DataFrame
 Row │ id      X        Y     
     │ String  Int64?   Int64 
─────┼────────────────────────
   1 │ a            91    101
   2 │ c       missing    102
outerjoin(dfj9, dfj10, on=:id)
3×3 DataFrame
 Row │ id      X        Y       
     │ String  Int64?   Int64?  
─────┼──────────────────────────
   1 │ a            91      101
   2 │ b            92  missing 
   3 │ c       missing      102

在左连接、右连接或外连接时,
可以用source = 列名选项使得输出中增加一项用来表示当前观测是否仅来自左侧、仅来自右侧还是来自两侧。如:

outerjoin(dfj9, dfj10, on=:id, source = :source)
3×4 DataFrame
 Row │ id      X        Y        source     
     │ String  Int64?   Int64?   String     
─────┼──────────────────────────────────────
   1 │ a            91      101  both
   2 │ b            92  missing  left_only
   3 │ c       missing      102  right_only

crossjoin则对输入的两个或多个数据框找到观测的所有组合,
输出组合的结果。
不使用on自变量。
如:

dfc1 = DataFrame(a = [1,2,3], b=["a", "a", "b"])
3×2 DataFrame
 Row │ a      b      
     │ Int64  String 
─────┼───────────────
   1 │     1  a
   2 │     2  a
   3 │     3  b
dfc2 = DataFrame(c = [21,22])
2×1 DataFrame
 Row │ c     
     │ Int64 
─────┼───────
   1 │    21
   2 │    22
6×3 DataFrame
 Row │ a      b       c     
     │ Int64  String  Int64 
─────┼──────────────────────
   1 │     1  a          21
   2 │     1  a          22
   3 │     2  a          21
   4 │     2  a          22
   5 │     3  b          21
   6 │     3  b          22

长宽表转换

stackunstack

对于多个个体(subject)的若干个变量的多次测量值,
如果将多次观测值放在不同行保存,
就称为长表格式。
如果将多次测量值放在同一行保存,
就称为宽表格式。
统计建模程序一般使用长表格式。

Julia的DataFrames包具有比较一般的长宽表转换功能,
能够解决最常见的长宽表转换问题。
目前还没有R的dplyr包、SAS的proc transpose功能那么强。

stack函数

stack函数将同一行的不同列堆叠到同一列中,
实现宽表到长表的转换。
比如,下面是一个宽表:

dw1 = DataFrame(id=["a", "b", "c"], 
    x1 = [11, 12, 13], x2=[21, 22, 23])
3×3 DataFrame
 Row │ id      x1     x2    
     │ String  Int64  Int64 
─────┼──────────────────────
   1 │ a          11     21
   2 │ b          12     22
   3 │ c          13     23
dw1t = stack(dw1, [:x1, :x2], :id)
6×3 DataFrame
 Row │ id      variable  value 
     │ String  String    Int64 
─────┼─────────────────────────
   1 │ a       x1           11
   2 │ b       x1           12
   3 │ c       x1           13
   4 │ a       x2           21
   5 │ b       x2           22
   6 │ c       x2           23

stack第一自变量是要转换的宽表,
第二自变量用向量给出要转换的列集合,
第三自变量指定一个或多个分组变量,
长宽表转换是针对相同的分组变量值进行转换的。
结果数据框中自动命名的variable变量用来表示原来的列名,
value表示该列的值。

上例的结果中variable表示时间,
如果要转换成数值的时间,
可以用:

transform!(dw1t, :variable => ByRow(s -> parse(Int, s[2:2])) => :time)
select!(dw1t, Not(:variable))
6×3 DataFrame
 Row │ id      value  time  
     │ String  Int64  Int64 
─────┼──────────────────────
   1 │ a          11      1
   2 │ b          12      1
   3 │ c          13      1
   4 │ a          21      2
   5 │ b          22      2
   6 │ c          23      2

unstack函数

unstack函数将放在同一列的多个测量值转为存放在同一行,
并适当命名。如:

d1n = DataFrame(id=["a", "a", "b", "b", "c", "c"],
    time = [1,2,1,2,1,2],
  value=[11,12, 21,22, 31,32])
6×3 DataFrame
 Row │ id      time   value 
     │ String  Int64  Int64 
─────┼──────────────────────
   1 │ a           1     11
   2 │ a           2     12
   3 │ b           1     21
   4 │ b           2     22
   5 │ c           1     31
   6 │ c           2     32

将每个id两次测量值(value)转为存放到同一行:

d1nw = unstack(d1n, :id, :time, :value)
3×3 DataFrame
 Row │ id      1       2      
     │ String  Int64?  Int64? 
─────┼────────────────────────
   1 │ a           11      12
   2 │ b           21      22
   3 │ c           31      32

unstack()的第一自变量是要转换的长表,
第二自变量是分组变量,
转换在分组变量指定的每组内进行,
第三自变量是用来区分不同测量值的标识,
第四自变量是实际测量值。

上面的转换结果用了1, 2这样的列名,
为了使用合法变量名,程序如:

d1n2 = transform(d1n, 
    :time => ByRow(s -> string("x", s)) => :variable)
select!(d1n2, Not(:time))
d1nw = unstack(d1n2, :id, :variable, :value)
3×3 DataFrame
 Row │ id      x1      x2     
     │ String  Int64?  Int64? 
─────┼────────────────────────
   1 │ a           11      12
   2 │ b           21      22
   3 │ c           31      32

转换实例

考虑如下数据:

"subject","x_1","x_2","x_3","y_1","y_2","y_3"
1,5,7,8,9,7,6
2,8,2,10,1,1,9
3,7,2,5,10,8,3
4,1,5,6,10,1,1
5,9,7,10,8,8,10

读入为数据框:

using CSV
dwide = CSV.read("widetab.csv", DataFrame)
5×7 DataFrame
 Row │ subject  x_1    x_2    x_3    y_1    y_2    y_3   
     │ Int64    Int64  Int64  Int64  Int64  Int64  Int64 
─────┼───────────────────────────────────────────────────
   1 │       1      5      7      8      9      7      6
   2 │       2      8      2     10      1      1      9
   3 │       3      7      2      5     10      8      3
   4 │       4      1      5      6     10      1      1
   5 │       5      9      7     10      8      8     10

这是5位病人3次去医院的记录数据,
每次去有xy两个测量值。

stack()函数将其中的测量值合并到一列value
并增加一列表示值所对应的变量名的字符型列。
第二自变量指定要合并的列,可以用列号范围、列号向量、列号名称。
第三自变量可选,指定保持不变的区分病人的变量。

dlong4w = stack(dwide, 2:7, :subject)
30×3 DataFrame
 Row │ subject  variable  value 
     │ Int64    String    Int64 
─────┼──────────────────────────
   1 │       1  x_1           5
   2 │       2  x_1           8
   3 │       3  x_1           7
   4 │       4  x_1           1
  ⋮  │    ⋮        ⋮        ⋮
  27 │       2  y_3           9
  28 │       3  y_3           3
  29 │       4  y_3           1
  30 │       5  y_3          10
                 22 rows omitted

也可以写成

dlong4w = stack(dwide, [:x_1, :x_2, :x_3, :y_1, :y_2, :y_3], :subject)

dlong4w = stack(dwide, Cols(r"^x_", r"^y_"), :subject)

这个表格的形式虽然是长表,
但并不是我们最终所求。
我们希望将变量名与随访时间分为两个变量,
先写一个按下划线拆分变量名与时间的函数:

function make_split(sep="_")
    function f(x)
        y = split(x, sep)
        return (string(y[1]), parse(Int, y[2]))
    end
end
transform!(dlong4w, 
    :variable => (x -> make_split("_").(x)) => [:varname, :time])
select!(dlong4w, Not(:variable))
30×4 DataFrame
 Row │ subject  value  varname  time  
     │ Int64    Int64  String   Int64 
─────┼────────────────────────────────
   1 │       1      5  x            1
   2 │       2      8  x            1
   3 │       3      7  x            1
   4 │       4      1  x            1
  ⋮  │    ⋮       ⋮       ⋮       ⋮
  27 │       2      9  y            3
  28 │       3      3  y            3
  29 │       4      1  y            3
  30 │       5     10  y            3
                       22 rows omitted

按照病人号、随访时间、测量值名排序,长表现在变成了:

sort!(dlong4w, [:subject, :time, :varname] )
dlong4w
30×4 DataFrame
 Row │ subject  value  varname  time  
     │ Int64    Int64  String   Int64 
─────┼────────────────────────────────
   1 │       1      5  x            1
   2 │       1      9  y            1
   3 │       1      7  x            2
   4 │       1      7  y            2
  ⋮  │    ⋮       ⋮       ⋮       ⋮
  27 │       5      7  x            2
  28 │       5      8  y            2
  29 │       5     10  x            3
  30 │       5     10  y            3

unstack()可以将放在同一列的变量值合并到一行中。
第二自变量表示按照那些变量分组,
第三自变量表示保存了变量名的列(类型是Symbol),
第四自变量是保存的值。

dlong4w2 = unstack(dlong4w, [:subject, :time], :varname, :value)
15×4 DataFrame
 Row │ subject  time   x       y      
     │ Int64    Int64  Int64?  Int64? 
─────┼────────────────────────────────
   1 │       1      1       5       9
   2 │       1      2       7       7
   3 │       1      3       8       6
   4 │       2      1       8       1
  ⋮  │    ⋮       ⋮      ⋮       ⋮
  12 │       4      3       6       1
  13 │       5      1       9       8
  14 │       5      2       7       8
  15 │       5      3      10      10
                        7 rows omitted

在获得了长表格式后,可以进一步用groupbycombine作分组汇总。
比如,计算每个病人x, y的平均值:

groupby(dlong4w2, :subject) |>
  gdf -> combine(gdf, :x => mean, :y => mean)
5×3 DataFrame
 Row │ subject  x_mean   y_mean  
     │ Int64    Float64  Float64 
─────┼───────────────────────────
   1 │       1  6.66667  7.33333
   2 │       2  6.66667  3.66667
   3 │       3  4.66667  7.0
   4 │       4  4.0      4.0
   5 │       5  8.66667  8.66667

计算每个时间的测量值均值:

groupby(dlong4w2, :time) |>
  gdf -> combine(gdf, :x => mean, :y => mean)
3×3 DataFrame
 Row │ time   x_mean   y_mean  
     │ Int64  Float64  Float64 
─────┼─────────────────────────
   1 │     1      6.0      7.6
   2 │     2      4.6      5.0
   3 │     3      7.8      5.8

总结

如果使用R的tidyr包,
可以用pivot_longerpivot_wider更容易地完成转换。
SAS的PROC TRANSPOSE也可以比较容易地完成这个数据的转换。

总之,
Julia DataFrames的宽表变长表用stack()函数,
长表变宽表用unstack()函数,
拆分变量名与时间可以用字符串功能以及类型转换功能来实现。

变量名在Julia中是Symbol数据类型,
不是字符串。
String()将Symbol类型转换成字符串类型,
parse(Int, s)将字符串s转换成整数,
parse(Float64, s)将字符串s转换成浮点数。

缺失值管理

Julia语言提供了missing表示缺失值,
missing的功能与R语言中的NA,
数据库SQL语言中的null类似。
其数据类型为Missing

数据框中可以用missing表示缺失元素,
见上面的da1数据框。
但是,如果数据框中某列原来没有缺失值,
则不能将其中的值赋值为missing

ismissing()判断某个值是否缺失值,
加点以后可以判断某个向量的每个元素,如

da1 = copy(da0)
ismissing.(da1[!,:age]) |> show
## Bool[0, 0, 1, 0]

函数any()可以判断布尔值向量元素是否存在真值,
all()可以判断布尔值向量元素是否都是真值。如

any(ismissing.(da1[!,:age]))
## true

missing参与的四则运算、比较、数学计算一般返回缺失值。
skipmissing()可以在计算时删除指定列中的缺失值进行计算,

4-element Vector{Union{Missing, Int64}}:
 33
 42
   missing
 51
sum(da1[!,:age])
## missing
sum(skipmissing(da1[!,:age]))
## 126

coalesce.()可以将缺失值替换为指定的值:

coalesce.(da1[!,:age], 0) |> show
## [33, 42, 0, 51]

Missings.replace()输出一个迭代器,
可以在计算中用指定值替换缺失值,如

sum(Missings.replace(da1[!,:age], 0))
## 126

为了获得整个数据框df中每行是否不含任何缺失值的示性值(1表示没有缺失值,0表示有),
DataFrames.completecases(df)函数。
如:

DataFrames.completecases(da1) |> show
## Bool[1, 1, 0, 1]

将整个数据框中含有缺失值的观测都删去,
DataFrames.dropmissing!(df)函数。
DataFrames.dropmissing(df)则返回副本。

da1 = copy(da0)
dropmissing!(da1)
da1
3×3 DataFrame
 Row │ name    age    sex    
     │ String  Int64  String 
─────┼───────────────────────
   1 │ 张三       33  M
   2 │ 李四       42  F
   3 │ 赵六       51  M

skipmissing()的输出是一个迭代器,
可以当作一个普通向量参与运算;
为了将去掉缺失值后的内容转换成一个普通向量,
skipmissing()collect()函数复合,如

da1 = copy(da0)
collect(skipmissing(da1[!,:age])) |> show
## [33, 42, 51]

也可以用函数复合运算写成:

(collect ∘ skipmissing)(da1[!,:age]) |> show
## [33, 42, 51]

含有缺失值的向量实际是Array{Union{Missing, T},1}类型,
其中T是String, Int64, Float64等值类型。如

typeof(da1[!,:age])
## Vector{Union{Missing, Int64}} 
## (alias for Array{Union{Missing, Int64}, 1})

Missings.nonmissingtype(eltype(x))函数可以求带有missing值的向量x中非缺失值的数据类型,如

Missings.nonmissingtype(eltype(da1[!,:age]))
## Int64

如果df是数据框,则DataFrames.allowmissing!(df)使得数据框中所有列都允许取缺失值。
DataFrames.allowmissing!(df, col)可以使得第col列取缺失值,
DataFrames.allowmissing!(df, cols)可以使得下标向量cols指定的那些列允许取缺失值。
DataFrames.disallowmissing!(df)函数起到反向的作用。

da1 = copy(da0)
DataFrames.allowmissing!(da1, 2:3)
typeof(da1[!,:sex])
## Vector{Union{Missing, String}} 
## (alias for Array{Union{Missing, String}, 1})

分类变量

介绍

某些统计模型允许使用分类变量,
比如,
逻辑斯谛回归的因变量是分类变量。
用字符串表示分类变量很直观,
但是不方便用在数学模型计算当中,
在数据量很大时保存许多有重复的字符串往往也比较浪费存储空间。

CategoricalArrays包提供了CategoricalArray(分类数组)类型,
专门用来表示分类变量。
这种类型类似于R的因子(factor)类型,
将分类变量的不同值编码为整数号码,
然后保存这些号码以及号码与变量实际值之间的对应表。
参见:

  • https://categoricalarrays.juliadata.org/stable/

生成

categorical()函数将分类变量转换成分类数组类型,
允许有缺失值(missing)。

using CategoricalArrays
dcl = copy(d_class);
dcl[!,:sex] = categorical(dcl[!,:sex]);
show(dcl[!,:sex])
## CategoricalValue{String, UInt32}[
##    "F", "F", "F", "F", "M", "M", "M", "M", "M"]
typeof(dcl[!,:sex])
## CategoricalVector{String, UInt32, String, 
##    CategoricalValue{String, UInt32}, Union{}} 
## (alias for CategoricalArray{String, 1, UInt32, String, 
##    CategoricalValue{String, UInt32}, Union{}})

水平值

levels()求分类数组的各个不同值,如

levels(dcl[!,:sex]) |> show
## ["F", "M"]

levels!()修改因子水平的次序,如:

sexc = dcl[:,:sex] # 副本
levels!(sexc, ["M", "F"])
sexc |> show
## CategoricalValue{String, UInt32}[
##   "F", "F", "F", "F", "M", "M", "M", "M", "M"]

生成时加ordered=true选项使分类变量为有序,
可以比较次序。
也可以用ordered!(x, true)设置为有序。
不论是否有序都可以按照分类变量排序,
排序时按因子水平(levels)排列。

recode!()给已有的分类变量重新指定标签,
如:

recode!(sexc, "F" => "女", "M" => "男")
sexc |> show
## CategoricalValue{String, UInt32}[
##   "女", "女", "女", "女", "男", "男", "男", "男", "男"]

转换

许多Julia软件包不支持分类变量,
这时,可以用levelcode.()将分类变量转换成其编码的整数值,如:

sexi = levelcode.(dcl[:,:sex]);
show(sexi)
## [1, 1, 1, 1, 2, 2, 2, 2, 2]

将类别变量转换为其显示字符串的函数:

cat2string = x -> levels(x)[levelcode.(x)]
cat2string(dcl[:,:sex]) |> show
## ["F", "F", "F", "F", "M", "M", "M", "M", "M"]

频数统计

x为整型变量,
StatsBase.counts(x)x的最小值到最大值的所有整数值计算频数,
如:

using StatsBase
dcl = copy(d_class)
StatsBase.counts(dcl[:,:age]) |> show
## [2, 5, 2]

将值与频数对应:

dcl[:,:age] |> x -> DataFrame(
    age = minimum(x):maximum(x),
    count = StatsBase.counts(x))
3×2 DataFrame
 Row │ age    count 
     │ Int64  Int64 
─────┼──────────────
   1 │    11      2
   2 │    12      5
   3 │    13      2

函数StatsBase.frequency(x)计算比例。
对于更一般的类型如字符串,
countsfrequency不支持,
可以使用更一般的StatsBase.countmap(x)函数,
结果为变量值到频数值的字典,
如:

StatsBase.countmap(dcl[:,:age]) |> show
## Dict(13 => 2, 11 => 2, 12 => 5)

countmap输出一个字典,
缺点是不能排序。
利用这个函数写一个频数函数:

function freqd(x)
    di = StatsBase.countmap(x)
    d = DataFrame(x = collect(keys(di)), count = collect(values(di)))
    sort!(d)
    return d
end
freqd(dcl[:,:age])
3×2 DataFrame
 Row │ x      count 
     │ Int64  Int64 
─────┼──────────────
   1 │    11      2
   2 │    12      5
   3 │    13      2
2×2 DataFrame
 Row │ x       count 
     │ String  Int64 
─────┼───────────────
   1 │ F           4
   2 │ M           5
dcl[!,:sex] = categorical(dcl[!,:sex])
freqd(dcl[:,:sex])
2×2 DataFrame
 Row │ x     count 
     │ Cat…  Int64 
─────┼─────────────
   1 │ F         4
   2 │ M         5

日期和时间类型

有些数据中包括日期,
或者日期时间。
Julia的Dates包提供了Date类型和DateTime类型。
日期不包含时区,
可以调用TimeZones包的功能来支持时区。

日期

Dates.today()返回当天日期。

可以从年月日的整数值用Dates.Date()转换成日期,
也可以从日期字符串按照某个模板转换成日期。
如:

Dates.Date(2018)
## 2018-01-01
Dates.Date(2018, 10)
## 2018-10-01
Dates.Date(2018, 10, 31)
## 2018-10-31
Dates.Date("2018-10-31")
## 2018-10-31
Dates.Date("2018-10-31", "y-m-d")
## 2018-10-31
Dates.Date("20181031", "yyyymmdd")
## 2018-10-31
Dates.Date.([2018, 2018], [3, 10], [15, 31])
2-element Array{Dates.Date,1}:
 2018-03-15
 2018-10-31
Dates.Date.(["20180315", "20181031"], "yyyymmdd")
2-element Array{Dates.Date,1}:
 2018-03-15
 2018-10-31

日期时间

可以用DateTime()函数将年、月、日、时、分、秒、毫秒整数值转换成日期时间,
精确到1毫秒。

Dates.DateTime(2018, 10, 31)
## 2018-10-31T00:00:00
Dates.DateTime(2018, 10, 31, 12, 15, 30)
## 2018-10-31T12:15:30
Dates.DateTime(2018, 10, 31, 12, 15, 30, 136)
## 2018-10-31T12:15:30.136

提取成分

  • Dates.year(d)提取年,Dates.month(d)提取月份数值,
    Dates.day(d)提取日数值。
  • Dates.yearmonth(d)提取年、月元组,
    Dates.monthday(d)提取月、日元组,
    Dates.yearmonthday(d)提取年、月、日元组。
  • Dates.dayofweek(d)提取星期几的数值,
    星期一返回1,星期日返回7。
  • Dates.dayname(d)返回星期几的名称,如"Monday"
  • Dates.dayofmonth(d)返回当前的星期号码是本月的第几个。

日期运算

两个日期或者时间可以比较大小,
可以相减。结果带有单位,
Dates.value()转换为表示天数或者毫秒数的整数值。

Dates.Date(2018, 10, 31) - Dates.Date(2018)
## 303 days
Dates.Date(2018, 10, 31) - Dates.Date(2018) |> Dates.value
## 303
Dates.DateTime(2018, 10, 31, 12, 15, 30, 136) - Dates.DateTime(2018)
## 26223330136 milliseconds
Dates.DateTime(2018, 10, 31, 12, 15, 30, 136) - 
  Dates.DateTime(2018) |> Dates.value 
## 26223330136
Dates.DateTime(2018, 10, 31, 12, 15, 30, 136) - 
  Dates.DateTime(2018) |> 
  Dates.value |> 
  x -> x / (24*3600*1000)
## 303.510765462963

日期和日期时间不能直接加减数值,
而需要用单位表示,类型为Dates.Period
比如,
加一天应该加Dates.Day(1)
单位包括:

  • Dates.Year()
  • Dates.Month()
  • Dates.Day()
  • Dates.Hour()
  • Dates.Minute()
  • Dates.Second()
  • Dates.Millisecond(): 毫秒。

如:

Dates.Date("2020-01-01", "y-m-d") + 
  Dates.Year(2) + Dates.Month(6) + Dates.Day(15)
## 2022-07-16

日期序列

可以用start:step:end格式生成一系列日期,
其中step用日期单位。
如:

Dates.Date("2020-01-01"):Dates.Day(1):Dates.Date("2020-01-07")
## Dates.Date("2020-01-01"):Dates.Day(1):Dates.Date("2020-01-07")
Dates.Date("2020-01-01"):Dates.Day(1):Dates.Date("2020-01-07") |> collect
7-element Vector{Dates.Date}:
 2020-01-01
 2020-01-02
 2020-01-03
 2020-01-04
 2020-01-05
 2020-01-06
 2020-01-07

使用DataFramesMeta包

DataFramesMeta包实现了与R的dplyr的类似功能,
能查询、排序、计算新变量、计算汇总统计量、分组汇总。
主要好处是可以直接使用符号形式的变量名,
以及可以用“@chain”写出链式的查询。
与DataFrames的transform, combine, groupby等函数相比,
DataFramesMeta的做法更简洁。
参见:

  • https://github.com/JuliaData/DataFramesMeta.jl

查询、排序、计算新变量

using DataFrames, DataFramesMeta

@chain d_class begin
    @subset(:sex .== "M")
    @transform(:bmi = :weight ./ (:height ./ 100.0).^2)
    @select(:name, :years = :age, :bmi)
    @orderby(:name)
end

5 rows × 3 columns

name years bmi
String Int64 Float64
1 James 12 17.827
2 Jeffrey 13 15.0311
3 John 12 20.0
4 Robert 12 21.3039
5 Thomas 11 18.2961
  • @chain指定进行一系列的数据框操作,
    beginend界定。
  • @subset取行子集,
    注意关于列向量的比较运算需要使用加点的广播形式。
    多个条件用逗号分隔。
  • @transform计算新变量,
    注意使用列向量进行计算时要用广播形式。
    多个新变量定义用逗号分隔。
  • @select选择列子集,
    并且可以用新变量名=老变量名的格式改名。
  • @orderby排序。
    可以使用多个变量。
    如果某个变量x需要降序排列,
    sortperm(:x, rev=true)

在系列操作中末尾可以有一般的函数调用,
该函数以系列末尾的结果为第一自变量。
比如,
上述程序末尾的end之前添加first(3)行,
将仅显示结果的前3行。

@combine汇总计算

用法如:

@chain d_class begin
    @combine(:n = length(:height), 
        :mean = mean(:height),
        :std = std(:height))
end

1 rows × 3 columns

n mean std
Int64 Float64 Float64
1 9 148.333 10.0125

groupby@combine汇总计算

@chain d_class begin
    groupby(:sex)
    @combine(:n = length(:height), 
        :mean = mean(:height),
        :std = std(:height))
end

2 rows × 4 columns

sex n mean std
String Int64 Float64 Float64
1 F 4 142.25 9.10586
2 M 5 153.2 8.46759

分组后也可以做变换。
比如,按性别分组,每组的身高减去本组的最小值,
使用@transform

@chain d_class begin
    groupby(:sex)
    @transform(:hextra = :height .- minimum(:height))
    @orderby(:sex, :hextra)
end

9 rows × 6 columns

name sex age height weight hextra
String String Int64 Float64 Float64 Float64
1 Sandy F 11 130.0 23.0 0.0
2 Karen F 12 143.0 35.0 13.0
3 Alice F 13 144.0 38.0 14.0
4 Kathy F 12 152.0 38.0 22.0
5 Thomas M 11 146.0 39.0 0.0
6 James M 12 146.0 38.0 0.0
7 John M 12 150.0 45.0 4.0
8 Jeffrey M 13 159.0 38.0 13.0
9 Robert M 12 165.0 58.0 19.0

用Query包进行查询

利用Query包可以实现从各种数据源的信息查询,
这也包括从数据框查询,
语法参照了C#语言的LINQ工具。

参见:

  • https://github.com/queryverse/Query.jl

Query查询以@from宏开始,
由若干个查询命令连接而成,
@from宏可以指定行遍历,
@where宏指定行筛选条件,
@select宏可以指定列子集并为结果列变量改名,
@orderby宏指定排序方式,
@collect宏使行遍历结果组合为数据框输出。

Query和DataFramesMeta都定义了@select@orderby
所以不适合在同一程序中或同一会话同时使用。

d_class数据框中查询所有性别为女、年龄在12岁及以下的人的姓名、体重,
按体重排序:

using Query

df_tmp = @from i in d_class begin
    @where i.sex=="F" && i.age <= 12
    @select {i.name, 体重=i.weight}
    @orderby 体重
    @collect DataFrame
end
name 体重
“Sandy” 23.0
“Karen” 35.0
“Kathy” 38.0

查询的结果保存成了一个数据框。
如果省略@collect命令,
结果会是一个Julia迭代器,
可以用在for循环中遍历各行。

分组汇总

对数据框经常需要按某一个或几个分类变量进行分类汇总。
可以将数据框分成若干个子数据框,
对每一自数据进行一些汇总处理,
然后将汇总结果合并为一个数据框,
这样的流程称为“分组-操作-合并”(Split-Apply-Combine)。

groupby()函数分组。
分组后,
可以用combine()进行汇总统计然后合并结果;
可以用select, select!生成与每个子数据框行数相同的仅包含新生成列的结果;
可以用transform, transform!对每个子数据框生成与每个子数据框行数相同的结果数据框,
其中包括原有列与新生成的列。
最后将分组的结果合并。
对每个子数据框汇总或者变换时,
用“cols => func”或格式“cols => func => newcols”格式指定那些了列需要进行什么操作。
实际上,
部分组也可以使用这些函数进行操作,
这可以看成是仅有一个组。

例如,
将分组结果保存为一个变量,
后续可以对利用此变量进行多种分组汇总或变换。
先求各组的观测数:

using DataFrames, Statistics
dcl = copy(d_class)
gdf = groupby(d_class, :sex)
combine(gdf, nrow)

2 rows × 2 columns

sex nrow
String Int64
1 F 4
2 M 5

如果不需要对分组进行多种汇总,
也可以用“|>”写成链式调用:

dcl |>
    df -> groupby(df, :sex) |>
    subdf -> combine(subdf, nrow)

2 rows × 2 columns

sex nrow
String Int64
1 F 4
2 M 5

对每组求平均身高:

dcl |>
    df -> groupby(df, :sex) |>
    subdf -> combine(subdf, :height => mean)

2 rows × 2 columns

sex height_mean
String Float64
1 F 142.25
2 M 153.2

在对每组汇总时,用“cols => function”的格式指定对某些列使用那些汇总,
结果数据框的统计量自动命名,
也可以用“cols => function => target_col”指定结果列名,如:

dcl |>
    df -> groupby(df, :sex) |>
    subdf -> combine(subdf, :height => mean => :均值)

2 rows × 2 columns

sex 均值
String Float64
1 F 142.25
2 M 153.2

combine中指定多个统计函数:

dcl |>
    df -> groupby(df, :sex) |>
    subdf -> combine(subdf, 
    nrow => :观测数, 
    :height => mean => :平均身高, 
    :weight => mean => :平均体重)

2 rows × 4 columns

sex 观测数 平均身高 平均体重
String Int64 Float64 Float64
1 F 4 142.25 33.5
2 M 5 153.2 43.6

也可以利用多列计算一个统计量,
如两列的相关系数:

dcl |>
    df -> groupby(df, :sex) |>
    subdf -> combine(subdf, [:height, :weight] => cor => :corr)

2 rows × 2 columns

sex corr
String Float64
1 F 0.930356
2 M 0.711767

如果函数要返回多列结果,
就需要用“cols => func => AsTable”模式。

例如:

combine(gdf, :height => (x -> (mean = mean(x), std = std(x))) => AsTable )

2 rows × 3 columns

sex mean std
String Float64 Float64
1 F 142.25 9.10586
2 M 153.2 8.46759

如果一个统计函数得到多个结果,
比如Statistics.extrema返回最小值和最大值,
只要将返回结果包装成一个向量,
并对应到不同变量名,如:

combine(gdf, :height => (x -> [extrema(x)]) => [:min, :max])

2 rows × 3 columns

sex min max
String Float64 Float64
1 F 130.0 152.0
2 M 146.0 165.0

可以利用多列计算,得到多个结果,如:

combine(gdf, [:height, :weight] => 
    ((x, y) -> (m1 = mean(x), m2=mean(y), cor = cor(x,y))) => AsTable)

2 rows × 4 columns

sex m1 m2 cor
String Float64 Float64 Float64
1 F 142.25 33.5 0.930356
2 M 153.2 43.6 0.711767

在指定列时,仍可以使用Cols(), Not(), Between(), All()这些函数。

也可以用一个子数据框作为输入,
写成combine(subdf -> func, gdf)的形式,如:

combine(subdf -> (
        meanrat = mean(subdf.height)/mean(subdf.weight), 
        stdrat = std(subdf.height)/std(subdf.weight)), 
    gdf)

2 rows × 3 columns

sex meanrat stdrat
String Float64 Float64
1 F 4.24627 1.27508
2 M 3.51376 0.989026

数据框的其它功能

DataFrames包中还有许多函数,详见其文档的函数部分。
可用于DataFrame类型的函数包括(未特别说明都属于DataFrames包):

size(df)返回行数和列数,
size(df, 1)返回行数,
size(df, 2)返回列数:

size(d_class), size(d_class, 1), size(d_class, 2)
((9, 5), 9, 5)

eltype.(eachcol(df))获得每列的数据类型:

eltype.(eachcol(d_class))
5-element Array{DataType,1}:
 String
 String
 Int64
 Float64
 Float64

first(df, k), last(df, k)返回数据框头部或者尾部的若干行:

3 rows × 5 columns

name sex age height weight
String String Int64 Float64 Float64
1 Sandy F 11 130.0 23.0
2 Karen F 12 143.0 35.0
3 Kathy F 12 152.0 38.0

3 rows × 5 columns

name sex age height weight
String String Int64 Float64 Float64
1 John M 12 150.0 45.0
2 Robert M 12 165.0 58.0
3 Jeffrey M 13 159.0 38.0

eachcol(df)提供对每一列的遍历,
如:

import Statistics.mean
[mean(y) for y in eachcol(d_class[!, [:age, :height, :weight]])]
3-element Array{Float64,1}:
  12.0
 148.33333333333334
  39.111111111111114

select!选择仅保留指定的列, 如:

dtmp = copy(d_class)
select!(dtmp, [:age, :height, :weight])
names(dtmp)
3-element Array{String,1}:
 "age"
 "height"
 "weight"

为了删除指定的列,可以在select!中用Not()指定若干列,如:

dtmp = copy(d_class)
select!(dtmp, Not([:age, :height, :weight]))
names(dtmp)
2-element Array{String,1}:
 "name"
 "sex"

rename!修改变量名,如:

dtmp = copy(d_class)
rename!(dtmp, :age => :years, :height => :h)
names(dtmp)
5-element Array{String,1}:
 "name"
 "sex"
 "years"
 "h"
 "weight"

filter(f, df)筛选行,
其中f是以行作为元组类型自变量的示性函数。
如:

dtmp = copy(d_class)
filter(row -> row.sex == "F", dtmp)

4 rows × 5 columns

name sex age height weight
String String Int64 Float64 Float64
1 Sandy F 11 130.0 23.0
2 Karen F 12 143.0 35.0
3 Kathy F 12 152.0 38.0
4 Alice F 13 144.0 38.0

其它一些函数:

  • Base中的filter, filter!, join, similar, sort, sort!, unique
  • aggregate
  • allowmissing!, disallowmissing!调整列使得其允许或不允许包含缺失值
  • by将观测分组汇总
  • combine, groupby可以将数据框分组处理后合并处理结果
  • completecases, dropmissing, dropmissing!用于获得完全观测
  • eachrow用来逐行遍历数据框
  • eltype.(eachcol(df))获得每列的数据类型
  • melt, meltdf, stack, stackdf用于将宽表变成长表, unstack用于将长表变成宽表
  • names!为数据框修改所有变量名
  • unique, unique!, nonunique用于处理重复行或重复的变量值组合
  • describe对每列进行简单描述统计