数据通常以表格格式存储。 在表格格式中,数据由包含行和列的表组成。 每列通常具有相同的数据类型,而每行数据类型不同。 实际上,行表示观测量,而列表示变量。 例如,我们有一个电视节目表,其中包含每个节目的制作国家和大众个人评分,如 表 1 所示。
name | country | rating |
---|---|---|
Game of Thrones | United States | 8.2 |
The Crown | England | 7.3 |
Friends | United States | 7.8 |
… | … | … |
此处的省略号表示这是一张非常长的表,但只显示了少数行。 在分析数据时,我们经常会提出一些关于数据的有趣问题,这也称为 数据查询。 对于大型表格,计算机能够比手工查询更快地回答此类问题。 一些 数据查询 问题的例子如下:
但是,作为研究人员,实际的科学往往从多张表格或多个数据源开始。 例如,如果我们也有其他人的电视节目评分数据 (表 2):
name | rating |
---|---|
Game of Thrones | 7 |
Friends | 6.4 |
… | … |
现在则能够提出以下问题:
在本章的其余部分中,我们将展示如何借助 Julia 来轻松地回答这些问题。 因此此,首先说明为什么需要 Julia 包 DataFrames.jl
。 下节将展示如何使用此包,最后将展示如何编写快速数据变换的代码 (Section 4.9)。
首先查看如下的成绩表 表 3 :
name | age | grade_2020 |
---|---|---|
Bob | 17 | 5.0 |
Sally | 18 | 1.0 |
Alice | 20 | 8.5 |
Hank | 19 | 4.0 |
其中 name 列的类型为 string
, age 列的类型为 integer
,而 grade 列的类型为 float
。
截至目前,本书只介绍了 Julia 的基础知识。 这些基础能够处理很多东西,但不能处理表。 因此,为了说明我们需要更多类型,让我们尝试将表格数据存储在数组中:
function grades_array()
name = ["Bob", "Sally", "Alice", "Hank"]
age = [17, 18, 20, 19]
grade_2020 = [5.0, 1.0, 8.5, 4.0]
(; name, age, grade_2020)
end
现在,数据以列优先形式存储,当想从行获取数据时,这种形式很麻烦:
function second_row()
name, age, grade_2020 = grades_array()
i = 2
row = (name[i], age[i], grade_2020[i])
end
second_row()
("Sally", 18, 1.0)
或者,如果想获得 Alice 的成绩,首先需要弄清楚 Alice 所在的行:
function row_alice()
names = grades_array().name
i = findfirst(names .== "Alice")
end
row_alice()
3
然后才能得到成绩:
function value_alice()
grades = grades_array().grade_2020
i = row_alice()
grades[i]
end
value_alice()
8.5
DataFrames.jl
可以很容易地处理此类问题。 首先使用 using
加载 DataFrames.jl
:
using DataFrames
通过 DataFrames.jl
,我们可以定义 DataFrame
来存储表格数据:
names = ["Sally", "Bob", "Alice", "Hank"]
grades = [1, 5, 8.5, 4]
df = DataFrame(; name=names, grade_2020=grades)
name | grade_2020 |
---|---|
Sally | 1.0 |
Bob | 5.0 |
Alice | 8.5 |
Hank | 4.0 |
即此处返回的变量 df
以表格格式存储数据。
NOTE: 这是可行的,但我们需要立即改变一件事。 在本例中,我们在全局作用域定义了变量
name
、grade_2020
和df
。 这意味着可以从任何位置访问和修改这些变量。 如果我们继续像这样写这本书,那么我们会在书结尾时拥有上百个变量,即使变量name
中的数据本应只能通过DataFrame
访问! 变量name
和grade_2020
不应该持久地保存! 现在,想象一下,我们将会在本书中多次修改grade_2020
。 如果本书只有 PDF 格式, 那么几乎不可能在最后指出变量的内容。可以使用函数轻松地解决此类问题。
让我们使用函数完成同样的操作:
function grades_2020()
name = ["Sally", "Bob", "Alice", "Hank"]
grade_2020 = [1, 5, 8.5, 4]
DataFrame(; name, grade_2020)
end
grades_2020()
name | grade_2020 |
---|---|
Sally | 1.0 |
Bob | 5.0 |
Alice | 8.5 |
Hank | 4.0 |
注意, name
和 grade_2020
会在函数返回后销毁,即它们仅在函数中可用。 这样做还有两个好处。 首先,读者可以清晰地看到 name
和 grade_2020
由谁所有:它们属于 2020 成绩表。 其次,很容易在书中的任何地方确定 grades_2020()
的输出。 例如,可以将数据赋给变量 df
:
df = grades_2020()
name | grade_2020 |
---|---|
Sally | 1.0 |
Bob | 5.0 |
Alice | 8.5 |
Hank | 4.0 |
改变 df
的内容:
df = DataFrame(name = ["Malice"], grade_2020 = ["10"])
name | grade_2020 |
---|---|
Malice | 10 |
而且仍然能够无损恢复数据:
df = grades_2020()
name | grade_2020 |
---|---|
Sally | 1.0 |
Bob | 5.0 |
Alice | 8.5 |
Hank | 4.0 |
当然,此处假设没有重新定义函数。 我们在本书中保证不会这样做,因为这是非常糟糕的做法。 我们不会 “改变” 函数,而是创建一个具有明确名称的新函数。
因此,回到 DataFrames
构造器。 如你所见,创建方法是将向量作为参数传递给 DataFrame
构造器。 你可以给定任何合法的 Julia 向量,并且 只要向量长度相同,就能成功构造 DataFrame
。 重复的向量、Unicode 符号和任何类型的数字都可以:
DataFrame(σ = ["a", "a", "a"], δ = [π, π/2, π/3])
σ | δ |
---|---|
a | 3.141592653589793 |
a | 1.5707963267948966 |
a | 1.0471975511965976 |
通常,您在代码中会创建函数来包装一个或多个作用于 DataFrame
的函数。 例如,可以创建函数来获取一个或多个 names
的成绩:
function grades_2020(names::Vector{Int})
df = grades_2020()
df[names, :]
end
grades_2020([3, 4])
name | grade_2020 |
---|---|
Alice | 8.5 |
Hank | 4.0 |
使用函数来包装基本功能的这种方式,在编程语言和包中非常常见。 基本上,你可以把 Julia 和 DataFrames.jl
看作基本模块的提供者。 它们提供了相当 通用的 模块,从而你可以在此基础之上实现一些 特例 ,比如这个成绩例子。 借助这些基本模块,你可以编写数据分析脚本,控制机器人或任何你想要构造的东西。
截至目前,由于必须使用索引,这些例子都非常麻烦。 下节将介绍如何在 DataFrames.jl
中加载和保存数据,以及其它一些强大的基本模块。