有两种方式可以选取 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) 论坛上的发言。↩︎