4 DataFrames.jl

数据通常以表格格式存储。 在表格格式中,数据由包含行和列的表组成。 每列通常具有相同的数据类型,而每行数据类型不同。 实际上,行表示观测量,而列表示变量。 例如,我们有一个电视节目表,其中包含每个节目的制作国家和大众个人评分,如 表 1 所示。

Table 1: TV shows.
namecountryrating
Game of ThronesUnited States8.2
The CrownEngland7.3
FriendsUnited States7.8

此处的省略号表示这是一张非常长的表,但只显示了少数行。 在分析数据时,我们经常会提出一些关于数据的有趣问题,这也称为 数据查询。 对于大型表格,计算机能够比手工查询更快地回答此类问题。 一些 数据查询 问题的例子如下:

  • 哪个电视节目评分最高?
  • 哪些电视节目由美国制作?
  • 哪些电视节目由相同的国家制作?

但是,作为研究人员,实际的科学往往从多张表格或多个数据源开始。 例如,如果我们也有其他人的电视节目评分数据 (表 2):

Table 2: Ratings.
namerating
Game of Thrones7
Friends6.4

现在则能够提出以下问题:

  • 节目 Game of Thrones 的平均评分是多少?
  • 谁对 Friends 给出了最高的评分?
  • 哪些节目你评分了,但其他人没有?

在本章的其余部分中,我们将展示如何借助 Julia 来轻松地回答这些问题。 因此此,首先说明为什么需要 Julia 包 DataFrames.jl。 下节将展示如何使用此包,最后将展示如何编写快速数据变换的代码 (Section 4.9)。

首先查看如下的成绩表 表 3

Table 3: Grades for 2020.
nameagegrade_2020
Bob175.0
Sally181.0
Alice208.5
Hank194.0

其中 name 列的类型为 string, age 列的类型为 integer,而 grade 列的类型为 float

截至目前,本书只介绍了 Julia 的基础知识。 这些基础能够处理很多东西,但不能处理表。 因此,为了说明我们需要更多类型,让我们尝试将表格数据存储在数组中:

  1. function grades_array()
  2. name = ["Bob", "Sally", "Alice", "Hank"]
  3. age = [17, 18, 20, 19]
  4. grade_2020 = [5.0, 1.0, 8.5, 4.0]
  5. (; name, age, grade_2020)
  6. end

现在,数据以列优先形式存储,当想从行获取数据时,这种形式很麻烦:

  1. function second_row()
  2. name, age, grade_2020 = grades_array()
  3. i = 2
  4. row = (name[i], age[i], grade_2020[i])
  5. end
  6. second_row()
  1. ("Sally", 18, 1.0)

或者,如果想获得 Alice 的成绩,首先需要弄清楚 Alice 所在的行:

  1. function row_alice()
  2. names = grades_array().name
  3. i = findfirst(names .== "Alice")
  4. end
  5. row_alice()
  1. 3

然后才能得到成绩:

  1. function value_alice()
  2. grades = grades_array().grade_2020
  3. i = row_alice()
  4. grades[i]
  5. end
  6. value_alice()
  1. 8.5

DataFrames.jl 可以很容易地处理此类问题。 首先使用 using 加载 DataFrames.jl

  1. using DataFrames

通过 DataFrames.jl,我们可以定义 DataFrame 来存储表格数据:

  1. names = ["Sally", "Bob", "Alice", "Hank"]
  2. grades = [1, 5, 8.5, 4]
  3. df = DataFrame(; name=names, grade_2020=grades)
namegrade_2020
Sally1.0
Bob5.0
Alice8.5
Hank4.0

即此处返回的变量 df 以表格格式存储数据。

NOTE: 这是可行的,但我们需要立即改变一件事。 在本例中,我们在全局作用域定义了变量 namegrade_2020df。 这意味着可以从任何位置访问和修改这些变量。 如果我们继续像这样写这本书,那么我们会在书结尾时拥有上百个变量,即使变量 name 中的数据本应只能通过 DataFrame 访问! 变量 namegrade_2020 不应该持久地保存! 现在,想象一下,我们将会在本书中多次修改 grade_2020。 如果本书只有 PDF 格式, 那么几乎不可能在最后指出变量的内容。

可以使用函数轻松地解决此类问题。

让我们使用函数完成同样的操作:

  1. function grades_2020()
  2. name = ["Sally", "Bob", "Alice", "Hank"]
  3. grade_2020 = [1, 5, 8.5, 4]
  4. DataFrame(; name, grade_2020)
  5. end
  6. grades_2020()
Table 4: Grades 2020.
namegrade_2020
Sally1.0
Bob5.0
Alice8.5
Hank4.0

注意, namegrade_2020 会在函数返回后销毁,即它们仅在函数中可用。 这样做还有两个好处。 首先,读者可以清晰地看到 namegrade_2020 由谁所有:它们属于 2020 成绩表。 其次,很容易在书中的任何地方确定 grades_2020() 的输出。 例如,可以将数据赋给变量 df

  1. df = grades_2020()
namegrade_2020
Sally1.0
Bob5.0
Alice8.5
Hank4.0

改变 df 的内容:

  1. df = DataFrame(name = ["Malice"], grade_2020 = ["10"])
namegrade_2020
Malice10

而且仍然能够无损恢复数据:

  1. df = grades_2020()
namegrade_2020
Sally1.0
Bob5.0
Alice8.5
Hank4.0

当然,此处假设没有重新定义函数。 我们在本书中保证不会这样做,因为这是非常糟糕的做法。 我们不会 “改变” 函数,而是创建一个具有明确名称的新函数。

因此,回到 DataFrames构造器。 如你所见,创建方法是将向量作为参数传递给 DataFrame 构造器。 你可以给定任何合法的 Julia 向量,并且 只要向量长度相同,就能成功构造 DataFrame。 重复的向量、Unicode 符号和任何类型的数字都可以:

  1. DataFrame = ["a", "a", "a"], δ = [π, π/2, π/3])
σδ
a3.141592653589793
a1.5707963267948966
a1.0471975511965976

通常,您在代码中会创建函数来包装一个或多个作用于 DataFrame 的函数。 例如,可以创建函数来获取一个或多个 names 的成绩:

  1. function grades_2020(names::Vector{Int})
  2. df = grades_2020()
  3. df[names, :]
  4. end
  5. grades_2020([3, 4])
namegrade_2020
Alice8.5
Hank4.0

使用函数来包装基本功能的这种方式,在编程语言和包中非常常见。 基本上,你可以把 Julia 和 DataFrames.jl 看作基本模块的提供者。 它们提供了相当 通用的 模块,从而你可以在此基础之上实现一些 特例 ,比如这个成绩例子。 借助这些基本模块,你可以编写数据分析脚本,控制机器人或任何你想要构造的东西。

截至目前,由于必须使用索引,这些例子都非常麻烦。 下节将介绍如何在 DataFrames.jl 中加载和保存数据,以及其它一些强大的基本模块。

CC BY-NC-SA 4.0 Jose Storopoli, Rik Huijzer, Lazaro Alonso, 刘贵欣 (中文翻译), 田俊 (中文审校)