4.3 Filter 和 Subset

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

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

4.3.1 Filter

由此开始,接下来将讨论 DataFrames.jl 中非常强大的特性。 在讨论伊始,首先学习一些函数,例如 selectfilter。 但请不要担心! 可以先松一口气,因为 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

另请注意, DataFramesubset(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, 刘贵欣 (中文翻译), 田俊 (中文审校)