有两种方式可以选取 DataFrame 中的某些行, 一种是 filter (Section 4.3.1) 而另一种是 subset (Section 4.3.2)。
DataFrames.jl 较早地添加了 filter 函数, 它更强大且与 Julia Base 库的语法保持一致,因此我们先讨论 filter。 subset 是较新的函数,但它通常更简便。
由此开始,接下来将讨论 DataFrames.jl 中非常强大的特性。 在讨论伊始,首先学习一些函数,例如 select 和 filter。 但请不要担心! 可以先松一口气,因为 DataFrames.jl 的总体设计目标就是让用户需学习的函数保持在最低限度15。
与之前一样,从 grades_2020 开始:
grades_2020()
| name | grade_2020 | 
|---|---|
| Sally | 1.0 | 
| Bob | 5.0 | 
| Alice | 8.5 | 
| Hank | 4.0 | 
可以使用 filter(source => f::Function, df) 筛选行。 注意,这个函数与 Julia Base 模块中的 filter(f::Function, V::Vector) 函数非常相似。 这是因为 DataFrames.jl 使用多重派发 (see Section 2.3.3) 扩展filter,以使其能够接收DataFrame 作为参数。
从第一印象来看,实际中定义和使用函数 f 可能有些困难。 但请坚持学习,我们的努力会有超高的回报,因为 这是非常强大的数据筛选方法。 如下是一个简单的例子, 创建函数 equals_alice 来检查输入是否等于 “Alice”:
equals_alice(name::String) = name == "Alice"
equals_alice("Bob")
false
equals_alice("Alice")
true
结合该函数, 可以使用 f 筛选出所有 name 等于 “Alice” 的行:
filter(:name => equals_alice, grades_2020())
| name | grade_2020 | 
|---|---|
| Alice | 8.5 | 
注意这不仅适用于 DataFrame,也适用于向量:
filter(equals_alice, ["Alice", "Bob", "Dave"])
["Alice"]
还可以使用 匿名函数 缩短代码长度 (请查阅 Section 3.2.4.4):
filter(n -> n == "Alice", ["Alice", "Bob", "Dave"])
["Alice"]
它也可用于 grades_2020:
filter(:name => n -> n == "Alice", grades_2020())
| name | grade_2020 | 
|---|---|
| Alice | 8.5 | 
简单来说,上述函数可以理解为 “遍历 :name 列的所有元素,对每一个元素 n,检查 n 是否等于 Alice”。 可能对于某些人来说,这样的代码些许冗长。 幸运的是,Julia 已经扩展了 == 的偏函数应用(partial function application) (译注:指定部分参数的函数)。 其中的细节不重要 – 只需知道能像其他函数一样使用 ==:
filter(:name => ==("Alice"), grades_2020())
| name | grade_2020 | 
|---|---|
| Alice | 8.5 | 
subset 函数的加入使得处理 missing 值 (Section 4.5) 更加容易。 与 filter 相反, subset 对整列进行操作,而不是整行或者单个值。 如果想使用之前的函数,可以将其包装在 ByRow 里:
subset(grades_2020(), :name => ByRow(equals_alice))
| name | grade_2020 | 
|---|---|
| Alice | 8.5 | 
另请注意, DataFrame 是 subset(df, args...) 的第一个参数,而而对于 filter 来说是第二个参数,即 filter(f, df)。 这是因为, Julia 定义 filter 的方式为 filter(f, V::Vector),而 DataFrames.jl 在使用多重派发将其扩展到 DataFrame 类型时,选择与现有函数形式保持一致。
NOTE:
subset所属的大多数原生DataFrames.jl函数都保持着一致的函数签名,即 将DataFrame作为第一个参数。
与 filter 一样,可以在 subset 中使用匿名函数:
subset(grades_2020(), :name => ByRow(name -> name == "Alice"))
| name | grade_2020 | 
|---|---|
| Alice | 8.5 | 
或者使用 == 的偏函数应用:
subset(grades_2020(), :name => ByRow(==("Alice")))
| name | grade_2020 | 
|---|---|
| Alice | 8.5 | 
最后展示 subset 的真正用处。 首先,创建一个含有 missing 值的数据集:
function salaries()
    names = ["John", "Hank", "Karen", "Zed"]
    salary = [1_900, 2_800, 2_800, missing]
    DataFrame(; names, salary)
end
salaries()
| names | salary | 
|---|---|
| John | 1900 | 
| Hank | 2800 | 
| Karen | 2800 | 
| Zed | missing | 
这是一种合理的情况:你想算出同事们的工资,但还没算 Zed 的。 尽管我们不鼓励这么做,但这是一个有趣的例子。 假设我们想知道谁的工资超过了 2000。 如果使用 filter, 但未考虑 missing值,则会失败:
filter(:salary => >(2_000), salaries())
TypeError: non-boolean (Missing) used in boolean context
Stacktrace:
  [1] (::DataFrames.var"#103#104"{Base.Fix2{typeof(>), Int64}})(x::Missing)
    @ DataFrames ~/.julia/packages/DataFrames/58MUJ/src/abstractdataframe/abstractdataframe.jl:1216
  ...
subset 同样会失败,但幸运的是,报错指出一则简单的解决方案:
subset(salaries(), :salary => ByRow(>(2_000)))
ArgumentError: missing was returned in condition number 1 but only true or false are allowed; pass skipmissing=true to skip missing values
Stacktrace:
  [1] _and(x::Missing)
    @ DataFrames ~/.julia/packages/DataFrames/58MUJ/src/abstractdataframe/subset.jl:11
  ...
所以仅需要传递关键字参数 skipmissing=true:
subset(salaries(), :salary => ByRow(>(2_000)); skipmissing=true)
| names | salary | 
|---|---|
| Hank | 2800 | 
| Karen | 2800 | 
 15. 这来自于 Bogumił Kamiński (DataFrames.jl 的首席开发者和维护者) 在 Discourse (https://discourse.julialang.org/t/pull-dataframes-columns-to-the-front/60327/5) 论坛上的发言。↩︎