4.3 Filter 和 Subset

有两种方式可以选取 DataFrame 中的某些行, 一种是 filter (Section 4.3.1) 而另一种是 subset (Section 4.3.2)。

DataFrames.jl 较早地添加了 filter 函数, 它更强大且与 Julia Base 库的语法保持一致,因此我们先讨论 filter。 subset 是较新的函数,但它通常更简便。

4.3.1 Filter

由此开始,接下来将讨论 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

4.3.2 Subset

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()
Table 6: 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

  1. 15. 这来自于 Bogumił Kamiński (DataFrames.jl 的首席开发者和维护者) 在 Discourse (https://discourse.julialang.org/t/pull-dataframes-columns-to-the-front/60327/5) 论坛上的发言。↩︎



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