4.3 Filtro e Subconjunto

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.

4.3.1 Filtro

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

4.3.2 Subconjunto

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 quais subset pertence, tem uma assinatura de função consistente que sempre recebe um DataFrame 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()
Table 6: 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

  1. 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).↩︎



CC BY-NC-SA 4.0 Jose Storopoli, Rik Huijzer, Lazaro Alonso