|
| 1 | +## Indexação e Sumarização |
| 2 | + |
| 3 | +Vamos voltar para o exemplo dos dados `grades_2020()` definidos antes: |
| 4 | + |
| 5 | +```jl |
| 6 | +sco("grades_2020()"; process=without_caption_label) |
| 7 | +``` |
| 8 | + |
| 9 | +Para recuperar um **vetor** para `name`, podemos acessar o `DataFrame` com o `.`, como fizemos anteriormente com `struct`s em @sec:julia_basics: |
| 10 | + |
| 11 | +```jl |
| 12 | +@sco JDS.names_grades1() |
| 13 | +``` |
| 14 | + |
| 15 | +ou podemos indexar um `DataFrame` de modo muito parecido com uma `Array` utilizando símbolos e caracteres especiais. |
| 16 | +O **segundo índice é a indexação da coluna**: |
| 17 | + |
| 18 | +```jl |
| 19 | +@sco JDS.names_grades2() |
| 20 | +``` |
| 21 | + |
| 22 | +Perceba que `df.name` é exatamente o mesmo que o comando `df[!, :name]`, o que você pode verificar fazendo: |
| 23 | + |
| 24 | +``` |
| 25 | +julia> df = DataFrame(id=[1]); |
| 26 | +
|
| 27 | +julia> @edit df.name |
| 28 | +``` |
| 29 | + |
| 30 | +Em ambos os casos, ele dará a coluna `:name`. |
| 31 | +Também existe o comando `df[:, :name]` que copia a coluna `:name`. |
| 32 | +Na maioria dos casos, `df[!, :name]` é a melhor aposta, pois é mais versátil e faz uma modificação no local. |
| 33 | + |
| 34 | +Para qualquer **linha**, digamos a segunda linha, podemos usar o **primeiro índice como indexação de linha**: |
| 35 | + |
| 36 | +```jl |
| 37 | +s = """ |
| 38 | + df = grades_2020() |
| 39 | + df[2, :] |
| 40 | + df = DataFrame(df[2, :]) # hide |
| 41 | + """ |
| 42 | +sco(s; process=without_caption_label) |
| 43 | +``` |
| 44 | + |
| 45 | +ou criar uma função para nos dar qualquer linha `i` que quisermos: |
| 46 | + |
| 47 | +```jl |
| 48 | +@sco process=without_caption_label JDS.grade_2020(2) |
| 49 | +``` |
| 50 | + |
| 51 | +Podemos também obter apenas a coluna `names` para as 2 primeiras linhas usando **fatiamento** (novamente, de modo similar a um `Array`): |
| 52 | + |
| 53 | +```jl |
| 54 | +@sco JDS.grades_indexing(grades_2020()) |
| 55 | +``` |
| 56 | + |
| 57 | +Se assumirmos que todos os nomes na tabela são únicos, também podemos escrever uma função para obter a nota de uma pessoa por meio de seu `name`. |
| 58 | +Para fazer isso, convertemos a tabela de volta para uma das estruturas de dados básicas de Julia (veja @sec:data_structures) que é capaz de criar mapeamentos, a saber `Dict`s: |
| 59 | + |
| 60 | +```jl |
| 61 | +@sco post=output_block grade_2020("Bob") |
| 62 | +``` |
| 63 | + |
| 64 | +que funciona porque `zip` itera pelo `df.name` e `df.grade_2020` ao mesmo tempo como um "zipper": |
| 65 | + |
| 66 | +```jl |
| 67 | +sco(""" |
| 68 | +df = grades_2020() |
| 69 | +collect(zip(df.name, df.grade_2020)) |
| 70 | +""") |
| 71 | +``` |
| 72 | + |
| 73 | +Entretanto, converter um `DataFrame` para `Dict` só é útil quando os elementos são únicos. |
| 74 | +Geralmente esse não é o caso e é por isso que precisamos aprender como `filter` (filtrar) um `DataFrame`. |
| 75 | + |
| 76 | +## Filtro e Subconjunto {#sec:filter_subset} |
| 77 | + |
| 78 | +Existem duas maneiras de remover linhas de um `DataFrame`, uma é `filter` (@sec:filter) e outra é `subset` (@sec:subset). |
| 79 | +`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. |
| 80 | +`subset` é mais recente e, comumente, é mais conveniente de usar. |
| 81 | + |
| 82 | +### Filtro {#sec:filter} |
| 83 | + |
| 84 | +A partir de agora, nós começaremos a adentrar funcionalidades mais robustas da biblioteca `DataFrames.jl`. |
| 85 | +Para fazer isso, precisaremos aprender sobre algumas funções, como `select` e `filter`. |
| 86 | +Mas não se preocupe! |
| 87 | +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ínimo[^verbos]**. |
| 88 | + |
| 89 | +[^verbos]: 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>). |
| 90 | + |
| 91 | +Como antes, retomamos a partir de `grades_2020`: |
| 92 | + |
| 93 | +```jl |
| 94 | +sco("grades_2020()"; process=without_caption_label) |
| 95 | +``` |
| 96 | + |
| 97 | +Podemos filtrar linhas usando `filter(source => f::Function, df)`. |
| 98 | +Perceba como essa função é similar à função `filter(f::Function, V::Vector)` do módulo `Base` de Julia. |
| 99 | +Isso ocorre porque `DataFrames.jl` usa **despacho múltiplo** (see @sec:multiple_dispatch) para definir um novo método de `filter` que aceita `DataFrame` como argumento. |
| 100 | + |
| 101 | +À primeira vista, definir e trabalhar com uma função `f` para filtrar pode ser um pouco difícil de se usar na prática. |
| 102 | +Aguente firme, esse esforço é bem pago, uma vez que **é uma forma muito poderosa de filtrar dados**. |
| 103 | +Como um exemplo simples, podemos criar uma função `equals_alice` que verifica se sua entrada é igual "Alice": |
| 104 | + |
| 105 | +```jl |
| 106 | +@sco post=output_block JDS.equals_alice("Bob") |
| 107 | +``` |
| 108 | + |
| 109 | +```jl |
| 110 | +sco("equals_alice(\"Alice\")"; post=output_block) |
| 111 | +``` |
| 112 | + |
| 113 | +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": |
| 114 | + |
| 115 | +```jl |
| 116 | +s = "filter(:name => equals_alice, grades_2020())" |
| 117 | +sco(s; process=without_caption_label) |
| 118 | +``` |
| 119 | + |
| 120 | +Observe que isso não funciona apenas para `DataFrame`, mas também para vetores: |
| 121 | + |
| 122 | +```jl |
| 123 | +s = """filter(equals_alice, ["Alice", "Bob", "Dave"])""" |
| 124 | +sco(s) |
| 125 | +``` |
| 126 | + |
| 127 | +Podemos torná-lo um pouco menos prolixo usando uma **função anônima** (veja @sec:function_anonymous): |
| 128 | + |
| 129 | +```jl |
| 130 | +s = """filter(n -> n == "Alice", ["Alice", "Bob", "Dave"])""" |
| 131 | +sco(s) |
| 132 | +``` |
| 133 | + |
| 134 | +que também podemos usar em `grades_2020`: |
| 135 | + |
| 136 | +```jl |
| 137 | +s = """filter(:name => n -> n == "Alice", grades_2020())""" |
| 138 | +sco(s; process=without_caption_label) |
| 139 | +``` |
| 140 | + |
| 141 | +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". |
| 142 | +Para algumas pessoas, isso ainda é muito prolixo. |
| 143 | +Por sorte, Julia adicionou uma _aplicação de função parcial_ de `==`. |
| 144 | +Os detalhes não são importantes -- apenas saiba que você pode usá-la como qualquer outra função: |
| 145 | + |
| 146 | +```jl |
| 147 | +sco(""" |
| 148 | +s = "This is here to workaround a bug in books" # hide |
| 149 | +filter(:name => ==("Alice"), grades_2020()) |
| 150 | +"""; process=without_caption_label) |
| 151 | +``` |
| 152 | + |
| 153 | +Para obter todas as linhas que *não* são Alice, `==` (igualdade) pode ser substituído por `!=` (desigualdade) em todos os exemplos anteriores: |
| 154 | + |
| 155 | +```jl |
| 156 | +s = """filter(:name => !=("Alice"), grades_2020())""" |
| 157 | +sco(s; process=without_caption_label) |
| 158 | +``` |
| 159 | + |
| 160 | +Agora, para mostrar **porque funções anônimas são tão poderosas**, podemos criar um filtro um pouco mais complexo. |
| 161 | +Neste filtro, queremos as pessoas cujos nomes comecem com A ou B **e** tenham uma nota acima de 6: |
| 162 | + |
| 163 | +```jl |
| 164 | +s = """ |
| 165 | + function complex_filter(name, grade)::Bool |
| 166 | + interesting_name = startswith(name, 'A') || startswith(name, 'B') |
| 167 | + interesting_grade = 6 < grade |
| 168 | + interesting_name && interesting_grade |
| 169 | + end |
| 170 | + """ |
| 171 | +sc(s) |
| 172 | +``` |
| 173 | + |
| 174 | +```jl |
| 175 | +s = "filter([:name, :grade_2020] => complex_filter, grades_2020())" |
| 176 | +sco(s; process=without_caption_label) |
| 177 | +``` |
| 178 | + |
| 179 | +### Subconjunto {#sec:subset} |
| 180 | + |
| 181 | +A função `subset` foi adicionada para tornar mais fácil trabalhar com valores ausentes (@sec:missing_data). |
| 182 | +Em contraste com `filter`, `subset` funciona em colunas completas ao invés de linhas ou valores únicos. |
| 183 | +Se quisermos usar nossas funções definidas anteriormente, devemos envolvê-las dentro de `ByRow`: |
| 184 | + |
| 185 | +```jl |
| 186 | +s = "subset(grades_2020(), :name => ByRow(equals_alice))" |
| 187 | +sco(s; process=without_caption_label) |
| 188 | +``` |
| 189 | + |
| 190 | +Também perceba que `DataFrame` é agora o primeiro argumento `subset(df, args...)`, enquanto que em `filter` foi o segundo `filter(f, df)`. |
| 191 | +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. |
| 192 | + |
| 193 | +> **_OBSERVAÇÃO:_** |
| 194 | +> 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**. |
| 195 | +
|
| 196 | +Assim como com `filter`, também podemos usar funções anônimas dentro de `subset`: |
| 197 | + |
| 198 | +```jl |
| 199 | +s = "subset(grades_2020(), :name => ByRow(name -> name == \"Alice\"))" |
| 200 | +sco(s; process=without_caption_label) |
| 201 | +``` |
| 202 | + |
| 203 | +Ou, a aplicação de função parcial para `==`: |
| 204 | + |
| 205 | +```jl |
| 206 | +s = "subset(grades_2020(), :name => ByRow(==(\"Alice\")))" |
| 207 | +sco(s; process=without_caption_label) |
| 208 | +``` |
| 209 | + |
| 210 | +Em última análise, vamos mostrar o verdadeiro poder de `subset`. |
| 211 | +Primeiro, criamos um dataset com alguns valores ausentes: |
| 212 | + |
| 213 | +```jl |
| 214 | +@sco salaries() |
| 215 | +``` |
| 216 | + |
| 217 | +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. |
| 218 | +Embora não queiramos incentivar essas práticas, suspeitamos que seja um exemplo interessante. |
| 219 | +Suponha que queremos saber quem ganha mais de 2.000. |
| 220 | +Se usarmos `filter`, sem levar em consideração os valores 'faltantes', ele falhará: |
| 221 | + |
| 222 | +```jl |
| 223 | +s = "filter(:salary => >(2_000), salaries())" |
| 224 | +sce(s, post=trim_last_n_lines(25)) |
| 225 | +``` |
| 226 | + |
| 227 | +`subset` também falhará, mas felizmente nos apontará para uma solução fácil: |
| 228 | + |
| 229 | +```jl |
| 230 | +s = "subset(salaries(), :salary => ByRow(>(2_000)))" |
| 231 | +sce(s, post=trim_last_n_lines(25)) |
| 232 | +``` |
| 233 | + |
| 234 | +Então, só precisamos passar o argumento de palavra-chave `skipmissing=true`: |
| 235 | + |
| 236 | +```jl |
| 237 | +s = "subset(salaries(), :salary => ByRow(>(2_000)); skipmissing=true)" |
| 238 | +sco(s; process=without_caption_label) |
| 239 | +``` |
| 240 | + |
| 241 | +```{=comment} |
| 242 | +Rik, precisamos de um exemplo de filtro e subconjunto com várias condições, como em: |
| 243 | +
|
| 244 | +`filter(row -> row.col1 >= something1 && row.col2 <= something2, df)` |
| 245 | +
|
| 246 | +e: |
| 247 | +
|
| 248 | +`subset(df, :col1 => ByRow(>=(something1)), :col2 => ByRow(<=(something2)>)) |
| 249 | +``` |
0 commit comments