Existem duas maneiras de remover linhas de um DataFrame
, uma é filter
(Section 4.3.1) e outra é subset
(Section 4.3.2). filter
foi adicionado à biblioteca DataFrames.jl
anteriormente, é mais poderoso e também tem uma sintaxe mais coerente em relação às bibliotecas básicas de Julia. É por isso que vamos iniciar essa seção discutindo filter
primeiro. subset
é mais recente e, comumente, é mais conveniente de usar.
A partir de agora, nós começaremos a adentrar funcionalidades mais robustas da biblioteca DataFrames.jl
. Para fazer isso, precisaremos aprender sobre algumas funções, como select
e filter
. Mas não se preocupe! Pode ser um alívio saber que o objetivo geral do design de DataFrames.jl
é manter o número de funções que um usuário deve aprender em um mínimo15.
Como antes, retomamos a partir de grades_2020
:
grades_2020()
name | grade_2020 |
---|---|
Sally | 1.0 |
Bob | 5.0 |
Alice | 8.5 |
Hank | 4.0 |
Podemos filtrar linhas usando filter(source => f::Function, df)
. Perceba como essa função é similar à função filter(f::Function, V::Vector)
do módulo Base
de Julia. Isso ocorre porque DataFrames.jl
usa despacho múltiplo (see Section 2.3.3) para definir um novo método de filter
que aceita DataFrame
como argumento.
À primeira vista, definir e trabalhar com uma função f
para filtrar pode ser um pouco difícil de se usar na prática. Aguente firme, esse esforço é bem pago, uma vez que é uma forma muito poderosa de filtrar dados. Como um exemplo simples, podemos criar uma função equals_alice
que verifica se sua entrada é igual “Alice”:
equals_alice(name::String) = name == "Alice"
equals_alice("Bob")
false
equals_alice("Alice")
true
Equipados com essa função, podemos usá-la como nossa função f
para filtrar todas as linhas para as quais name
equivale a “Alice”:
filter(:name => equals_alice, grades_2020())
name | grade_2020 |
---|---|
Alice | 8.5 |
Observe que isso não funciona apenas para DataFrame
, mas também para vetores:
filter(equals_alice, ["Alice", "Bob", "Dave"])
["Alice"]
Podemos torná-lo um pouco menos prolixo usando uma função anônima (veja Section 3.1.4.4):
filter(n -> n == "Alice", ["Alice", "Bob", "Dave"])
["Alice"]
que também podemos usar em grades_2020
:
filter(:name => n -> n == "Alice", grades_2020())
name | grade_2020 |
---|---|
Alice | 8.5 |
Recapitulando, esta chamada de função pode ser lida como “para cada elemento na linha :name
, vamos chamar o elemento n
, e checar se n
se iguala a Alice.” Para algumas pessoas, isso ainda é muito prolixo. Por sorte, Julia adicionou uma aplicação de função parcial de ==
. Os detalhes não são importantes – apenas saiba que você pode usá-la como qualquer outra função:
filter(:name => ==("Alice"), grades_2020())
name | grade_2020 |
---|---|
Alice | 8.5 |
Para obter todas as linhas que não são Alice, ==
(igualdade) pode ser substituído por !=
(desigualdade) em todos os exemplos anteriores:
filter(:name => !=("Alice"), grades_2020())
name | grade_2020 |
---|---|
Sally | 1.0 |
Bob | 5.0 |
Hank | 4.0 |
Agora, para mostrar porque funções anônimas são tão poderosas, podemos criar um filtro um pouco mais complexo. Neste filtro, queremos as pessoas cujos nomes comecem com A ou B e tenham uma nota acima de 6:
function complex_filter(name, grade)::Bool
interesting_name = startswith(name, 'A') || startswith(name, 'B')
interesting_grade = 6 < grade
interesting_name && interesting_grade
end
filter([:name, :grade_2020] => complex_filter, grades_2020())
name | grade_2020 |
---|---|
Alice | 8.5 |
A função subset
foi adicionada para tornar mais fácil trabalhar com valores ausentes (Section 4.5). Em contraste com filter
, subset
funciona em colunas completas ao invés de linhas ou valores únicos. Se quisermos usar nossas funções definidas anteriormente, devemos envolvê-las dentro de ByRow
:
subset(grades_2020(), :name => ByRow(equals_alice))
name | grade_2020 |
---|---|
Alice | 8.5 |
Também perceba que DataFrame
é agora o primeiro argumento subset(df, args...)
, enquanto que em filter
foi o segundo filter(f, df)
. A razão para isso é que Julia define filtro como filter(f, V::Vector)
e DataFrames.jl
optou por manter a consistência com as funções Julia existentes que foram estendidas para tipos de DataFrame
s de despacho múltiplo.
OBSERVAÇÃO: A maioria das funções nativas de
DataFrames.jl
, as quaissubset
pertence, tem uma assinatura de função consistente que sempre recebe umDataFrame
como primeiro argumento.
Assim como com filter
, também podemos usar funções anônimas dentro de subset
:
subset(grades_2020(), :name => ByRow(name -> name == "Alice"))
name | grade_2020 |
---|---|
Alice | 8.5 |
Ou, a aplicação de função parcial para ==
:
subset(grades_2020(), :name => ByRow(==("Alice")))
name | grade_2020 |
---|---|
Alice | 8.5 |
Em última análise, vamos mostrar o verdadeiro poder de subset
. Primeiro, criamos um dataset com alguns valores ausentes:
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 |
Esses dados são sobre uma situação plausível em que você deseja descobrir os salários de seus colegas e ainda não descobriu o do Zed. Embora não queiramos incentivar essas práticas, suspeitamos que seja um exemplo interessante. Suponha que queremos saber quem ganha mais de 2.000. Se usarmos filter
, sem levar em consideração os valores ‘faltantes,’ ele falhará:
filter(:salary => >(2_000), salaries())
TypeError: non-boolean (Missing) used in boolean context
Stacktrace:
[1] (::DataFrames.var"#97#98"{Base.Fix2{typeof(>), Int64}})(x::Missing)
@ DataFrames ~/.julia/packages/DataFrames/MA4YO/src/abstractdataframe/abstractdataframe.jl:1110
...
subset
também falhará, mas felizmente nos apontará para uma solução fácil:
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/MA4YO/src/abstractdataframe/subset.jl:11
...
Então, só precisamos passar o argumento de palavra-chave skipmissing=true
:
subset(salaries(), :salary => ByRow(>(2_000)); skipmissing=true)
names | salary |
---|---|
Hank | 2800 |
Karen | 2800 |
15. De acordo com Bogumił Kamiński (desenvolvedor e mantenedor líder do DataFrames.jl
) no Discourse (https://discourse.julialang.org/t/pull-dataframes-columns-to-the-front/60327/5).↩︎