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 DataFrames de despacho múltiplo.
OBSERVAÇÃO: A maioria das funções nativas de
DataFrames.jl, as quaissubsetpertence, tem uma assinatura de função consistente que sempre recebe umDataFramecomo 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).↩︎