using TidierData
using DataFrames
using Statistics5 Manipulação de dados com Tidier
No livro original, este é o capítulo do tidyverse. Julia tem uma tradução notavelmente fiel: a família Tidier, uma reimplementação em Julia puro da mini-linguagem do tidyverse sobre DataFrames.jl.
5.1 Os dados
Usaremos ao longo do livro um levantamento simulado de anuros em 180 lagoas distribuídas por três regiões. Os dados são simulados de propósito: como o processo gerador é conhecido, cada modelo ajustado nos capítulos Capítulo 8 e Capítulo 9 pode ser confrontado com a verdade. O script gerador está em scripts/make_data.jl.
using CSV
ponds = CSV.read(joinpath("..", "data", "ponds.csv"), DataFrame)
first(ponds, 5)| Row | pond_id | region | hydro | area_m2 | canopy | temp_c | richness | focal | body_size |
|---|---|---|---|---|---|---|---|---|---|
| String7 | String15 | String15 | Float64 | Float64? | Float64 | Int64 | Int64 | Float64 | |
| 1 | P001 | Atlantic | permanent | 174.1 | 70.0 | 22.4 | 8 | 0 | 34.55 |
| 2 | P002 | Amazon | permanent | 161.6 | 42.6 | 23.9 | 12 | 1 | 30.36 |
| 3 | P003 | Cerrado | temporary | 233.9 | 57.5 | 25.2 | 6 | 1 | 34.44 |
| 4 | P004 | Amazon | temporary | 174.5 | 49.4 | 25.6 | 13 | 1 | 39.3 |
| 5 | P005 | Cerrado | permanent | 314.4 | 30.0 | 27.6 | 6 | 0 | 43.65 |
O livro é gerado de dentro da pasta pt/, por isso o ".." (a pasta de cima) neste e nos próximos capítulos. Na sua sessão, com a pasta do livro aberta, use joinpath("data", "ponds.csv"), como no quadro de exercícios abaixo.
describe(ponds, :eltype, :nmissing, :mean, :min, :max)| Row | variable | eltype | nmissing | mean | min | max |
|---|---|---|---|---|---|---|
| Symbol | Type | Int64 | Union… | Any | Any | |
| 1 | pond_id | String7 | 0 | P001 | P180 | |
| 2 | region | String15 | 0 | Amazon | Cerrado | |
| 3 | hydro | String15 | 0 | permanent | temporary | |
| 4 | area_m2 | Float64 | 0 | 240.215 | 10.0 | 1981.8 |
| 5 | canopy | Union{Missing, Float64} | 7 | 49.1422 | 6.4 | 95.8 |
| 6 | temp_c | Float64 | 0 | 24.4406 | 17.0 | 33.9 |
| 7 | richness | Int64 | 0 | 7.13333 | 0 | 31 |
| 8 | focal | Int64 | 0 | 0.45 | 0 | 1 |
| 9 | body_size | Float64 | 0 | 35.6777 | 26.91 | 44.26 |
| Coluna | Significado |
|---|---|
pond_id |
identificador da lagoa |
region |
região (Atlantic, Cerrado, Amazon) |
hydro |
hidroperíodo (permanent, temporary) |
area_m2 |
área da lagoa (m²) |
canopy |
cobertura de dossel (%) |
temp_c |
temperatura da água (°C) |
richness |
riqueza de espécies (contagem) |
focal |
ocorrência da espécie-foco (0/1) |
body_size |
tamanho corporal médio (mm) |
Os quadros Sua vez funcionam como no Capítulo 4. Aqui as respostas são tabelas e números calculados a partir dos dados das lagoas; por isso, comece cada sessão carregando o verificador, os pacotes e os dados, com a pasta do livro aberta no VS Code:
include("exercises/checar.jl")
using TidierData, DataFrames, Statistics, CSV
ponds = CSV.read(joinpath("data", "ponds.csv"), DataFrame)Os nomes das colunas estão em inglês nas duas versões do livro, de modo que o código seja literalmente idêntico em português e em inglês.
5.2 @chain: o pipe
@chain é o eixo de tudo o que vem a seguir. Ele passa o resultado de cada linha como primeiro argumento da próxima — exatamente o papel de |> (ou %>%) no R.
@chain ponds begin
@filter(region == "Amazon")
@select(pond_id, area_m2, canopy, richness)
@arrange(desc(richness))
first(5)
end| Row | pond_id | area_m2 | canopy | richness |
|---|---|---|---|---|
| String7 | Float64 | Float64? | Int64 | |
| 1 | P065 | 1048.6 | 18.0 | 31 |
| 2 | P044 | 568.1 | 31.7 | 24 |
| 3 | P067 | 411.4 | 30.9 | 21 |
| 4 | P085 | 204.6 | 58.2 | 19 |
| 5 | P073 | 140.5 | 17.7 | 18 |
Em R
ponds %>%
filter(region == "Amazon") %>%
select(pond_id, area_m2, canopy, richness) %>%
arrange(desc(richness)) %>%
head(5)A correspondência é linha a linha. A única diferença sintática é o @ e o begin ... end.
Escreva um @chain que retorne as três maiores lagoas (por area_m2) do Cerrado, da maior para a menor. Guarde o resultado em top3 e verifique com checar("5.1", top3).
Três passos, um por linha: fique com as linhas do Cerrado, ordene pela área da maior para a menor e fique com as três primeiras. O bloco acima faz algo bem parecido.
top3 = @chain ponds begin
@filter(region == "Cerrado")
@arrange(desc(area_m2))
first(3)
end| Row | pond_id | region | hydro | area_m2 | canopy | temp_c | richness | focal | body_size |
|---|---|---|---|---|---|---|---|---|---|
| String7 | String15 | String15 | Float64 | Float64? | Float64 | Int64 | Int64 | Float64 | |
| 1 | P130 | Cerrado | temporary | 1981.8 | 84.9 | 26.0 | 9 | 0 | 38.53 |
| 2 | P034 | Cerrado | temporary | 1126.3 | missing | 22.9 | 6 | 0 | 33.53 |
| 3 | P143 | Cerrado | temporary | 932.7 | 39.8 | 25.1 | 11 | 0 | 34.15 |
checar("5.1", top3)✓ Correto!
5.3 Os cinco verbos
5.3.1 @filter — selecionar linhas
@chain ponds begin
@filter(richness > 8 && hydro == "permanent")
nrow
end39
Atenção: dentro de @filter, use && e ||, não & e | como no dplyr.
Quantas lagoas temporárias têm cobertura de dossel acima de 60% e riqueza abaixo da mediana geral de riqueza? Verifique o número com checar("5.2", n).
Calcule a mediana antes, fora da cadeia: med = median(ponds.richness). Dentro de uma macro do Tidier, um nome solto significa uma coluna; para usar a variável med, escreva !!med.
Se aparecer TypeError: non-boolean (Missing) used in boolean context, algumas lagoas não têm valor de dossel, e missing > 60 não é nem verdadeiro nem falso. Remova essas linhas antes com @drop_missing(canopy) (Seção 5.4).
med = median(ponds.richness)
n = @chain ponds begin
@drop_missing(canopy)
@filter(hydro == "temporary" && canopy > 60 && richness < !!med)
nrow
end18
checar("5.2", n)✓ Correto!
Descartar as lagoas sem dossel é a decisão certa aqui: não dá para saber se o dossel delas passa de 60%. Já a mediana é calculada sobre as 180 lagoas, antes de descartar qualquer coisa.
5.3.2 @select — selecionar colunas
@chain ponds begin
@select(pond_id, region, richness)
first(3)
end| Row | pond_id | region | richness |
|---|---|---|---|
| String7 | String15 | Int64 | |
| 1 | P001 | Atlantic | 8 |
| 2 | P002 | Amazon | 12 |
| 3 | P003 | Cerrado | 6 |
# exclusão e intervalos funcionam como no dplyr
@chain ponds begin
@select(-pond_id, -focal)
names
end9-element Vector{String}:
"region"
"hydro"
"area_m2"
"canopy"
"temp_c"
"richness"
"focal"
"body_size"
"pond_id"
5.3.3 @mutate — criar colunas
ponds_t = @chain ponds begin
@mutate(
log_area = log(area_m2),
canopy_std = (canopy - mean(skipmissing(canopy))) / std(skipmissing(canopy)),
open = canopy < 40,
)
end
@chain ponds_t begin
@select(pond_id, area_m2, log_area, canopy, canopy_std, open)
first(5)
end| Row | pond_id | area_m2 | log_area | canopy | canopy_std | open |
|---|---|---|---|---|---|---|
| String7 | Float64 | Float64 | Float64? | Float64? | Bool? | |
| 1 | P001 | 174.1 | 5.15963 | 70.0 | 0.946503 | false |
| 2 | P002 | 161.6 | 5.08512 | 42.6 | -0.296877 | false |
| 3 | P003 | 233.9 | 5.45489 | 57.5 | 0.379268 | false |
| 4 | P004 | 174.5 | 5.16192 | 49.4 | 0.0116988 | false |
| 5 | P005 | 314.4 | 5.75067 | 30.0 | -0.868651 | true |
Lagoas maiores tendem a ter mais espécies. Adicione uma coluna rich_per_100m2 com o número de espécies por 100 m² de lagoa, mantendo todas as linhas e colunas de ponds. Guarde o resultado em ponds_d e verifique com checar("5.3", ponds_d).
richness / area_m2 é espécies por m²; multiplique por 100.
ponds_d = @chain ponds begin
@mutate(rich_per_100m2 = richness / area_m2 * 100)
end
checar("5.3", ponds_d)✓ Correto!
5.3.4 @group_by + @summarize — agregar
@chain ponds begin
@group_by(region, hydro)
@summarize(
n = n(),
mean_rich = mean(richness),
sd_rich = std(richness),
prop_focal = mean(focal),
)
@arrange(region, hydro)
endGroupedDataFrame with 3 groups based on key: region
| Row | region | hydro | n | mean_rich | sd_rich | prop_focal |
|---|---|---|---|---|---|---|
| String15 | String15 | Int64 | Float64 | Float64 | Float64 | |
| 1 | Amazon | permanent | 26 | 11.2692 | 5.99038 | 0.538462 |
| 2 | Amazon | temporary | 22 | 10.9091 | 5.95946 | 0.318182 |
⋮
| Row | region | hydro | n | mean_rich | sd_rich | prop_focal |
|---|---|---|---|---|---|---|
| String15 | String15 | Int64 | Float64 | Float64 | Float64 | |
| 1 | Cerrado | permanent | 31 | 4.77419 | 2.90643 | 0.580645 |
| 2 | Cerrado | temporary | 35 | 4.8 | 2.95854 | 0.4 |
Calcule, para cada região, a proporção de lagoas onde a espécie-foco ocorre, em uma coluna chamada prop_focal, ordenada da maior para a menor. Verifique com checar("5.4", por_regiao).
focal vale 0 ou 1, então a média é a proporção de lagoas onde ela vale 1. Ordenar da maior para a menor é @arrange(desc(...)).
por_regiao = @chain ponds begin
@group_by(region)
@summarize(prop_focal = mean(focal))
@arrange(desc(prop_focal))
end
checar("5.4", por_regiao)✓ Correto!
5.3.5 @arrange — ordenar
@chain ponds begin
@arrange(region, desc(richness))
@select(region, pond_id, richness)
first(6)
end| Row | region | pond_id | richness |
|---|---|---|---|
| String15 | String7 | Int64 | |
| 1 | Amazon | P065 | 31 |
| 2 | Amazon | P044 | 24 |
| 3 | Amazon | P067 | 21 |
| 4 | Amazon | P085 | 19 |
| 5 | Amazon | P073 | 18 |
| 6 | Amazon | P164 | 18 |
5.4 Dados faltantes
A coluna canopy tem valores ausentes propositalmente.
sum(ismissing.(ponds.canopy))7
@chain ponds begin
@drop_missing(canopy)
nrow
end173
# agregação ignorando ausentes
@chain ponds begin
@group_by(region)
@summarize(mean_canopy = mean(skipmissing(canopy)))
end| Row | region | mean_canopy |
|---|---|---|
| String15 | Float64 | |
| 1 | Atlantic | 51.7125 |
| 2 | Amazon | 45.313 |
| 3 | Cerrado | 49.327 |
mean(canopy) devolve missing se qualquer valor for ausente — o mesmo comportamento de mean() em R sem na.rm = TRUE. Seja explícito com skipmissing.
Antes de descartar valores faltantes, vale saber onde eles estão: se estiverem concentrados em uma região, descartá-los muda essa região mais do que as outras. Conte, por região, quantas lagoas não têm valor de dossel, em uma coluna chamada n_missing. Verifique com checar("5.5", lacunas).
ismissing.(canopy) dá true para cada valor faltante, e sum conta os true. Os dois funcionam dentro de @summarize.
lacunas = @chain ponds begin
@group_by(region)
@summarize(n_missing = sum(ismissing.(canopy)))
end
checar("5.5", lacunas)✓ Correto!
Sete valores faltantes espalhados pelas três regiões: nenhuma região é muito mais afetada que as outras, então descartá-los dificilmente distorce comparações entre regiões.
5.5 Formato longo e largo
longo = @chain ponds begin
@select(pond_id, region, area_m2, canopy, temp_c)
@pivot_longer(area_m2:temp_c, names_to = "variable", values_to = "value")
end
first(longo, 6)| Row | pond_id | region | variable | value |
|---|---|---|---|---|
| String7 | String15 | String | Float64? | |
| 1 | P001 | Atlantic | area_m2 | 174.1 |
| 2 | P002 | Amazon | area_m2 | 161.6 |
| 3 | P003 | Cerrado | area_m2 | 233.9 |
| 4 | P004 | Amazon | area_m2 | 174.5 |
| 5 | P005 | Cerrado | area_m2 | 314.4 |
| 6 | P006 | Cerrado | area_m2 | 620.2 |
@chain longo begin
@pivot_wider(names_from = variable, values_from = value)
first(3)
end| Row | pond_id | region | area_m2 | canopy | temp_c |
|---|---|---|---|---|---|
| String7 | String15 | Float64? | Float64? | Float64? | |
| 1 | P001 | Atlantic | 174.1 | 70.0 | 22.4 |
| 2 | P002 | Amazon | 161.6 | 42.6 | 23.9 |
| 3 | P003 | Cerrado | 233.9 | 57.5 | 25.2 |
5.6 Junções
# uma tabela auxiliar com informação em nível de região
regioes = DataFrame(
region = ["Atlantic", "Cerrado", "Amazon"],
biome_type = ["forest", "savanna", "forest"],
mean_rain = [1400, 1500, 2300],
)
@chain ponds begin
@left_join(regioes, region)
@select(pond_id, region, biome_type, mean_rain, richness)
first(5)
end| Row | pond_id | region | biome_type | mean_rain | richness |
|---|---|---|---|---|---|
| String7 | String15 | String? | Int64? | Int64 | |
| 1 | P001 | Atlantic | forest | 1400 | 8 |
| 2 | P002 | Amazon | forest | 2300 | 12 |
| 3 | P003 | Cerrado | savanna | 1500 | 6 |
| 4 | P004 | Amazon | forest | 2300 | 13 |
| 5 | P005 | Cerrado | savanna | 1500 | 6 |
Usando a tabela regioes acima, calcule a riqueza média das lagoas de floresta e de savana, em uma coluna chamada mean_rich. Verifique com checar("5.6", por_bioma).
Primeiro faça a junção, para que cada lagoa ganhe seu biome_type; depois agrupe por biome_type e resuma.
por_bioma = @chain ponds begin
@left_join(regioes, region)
@group_by(biome_type)
@summarize(mean_rich = mean(richness))
end
checar("5.6", por_bioma)✓ Correto!
5.7 DataFrames.jl por baixo
Tidier é uma camada de conveniência. Vale saber a forma nativa, que é o que você vai encontrar na documentação da maior parte do ecossistema:
combine(
groupby(ponds, :region),
:richness => mean => :mean_rich,
:richness => length => :n,
)| Row | region | mean_rich | n |
|---|---|---|---|
| String15 | Float64 | Int64 | |
| 1 | Atlantic | 6.59091 | 66 |
| 2 | Amazon | 11.1042 | 48 |
| 3 | Cerrado | 4.78788 | 66 |
Em Tidier, isso seria @group_by(region) seguido de @summarize(mean_rich = mean(richness), n = n()): a mesma ideia do @group_by/@summarize acima, mas agrupando só por região. :region é um Symbol, o jeito de Julia nomear uma coluna, e :richness => mean => :mean_rich se lê “pegue richness, aplique mean e chame o resultado de mean_rich”. Use Tidier para escrever, e reconheça a forma nativa para ler.
Reescreva sua resposta ao 5.4 com groupby, combine e sort do DataFrames.jl, sem as macros do Tidier. Verifique com checar("5.7", por_regiao_nativo).
sort(df, :coluna, rev = true) ordena da maior para a menor.
por_regiao_nativo = sort(
combine(groupby(ponds, :region), :focal => mean => :prop_focal),
:prop_focal, rev = true)
checar("5.7", por_regiao_nativo)✓ Correto!
Desafios
Estes não têm verificador. São sobre o que os números dizem — e o que não dizem.
- No 5.4 o Cerrado fica em primeiro, mas as três proporções vão só de 0,42 a 0,48, cada uma calculada sobre 48 a 66 lagoas. Abra
scripts/make_data.jl: a região afeta a espécie-foco na simulação? O que isso diz sobre ler um ranking em uma tabela resumo? - No 5.6 as lagoas de floresta são mais ricas que as de savana. Mas “floresta” aqui significa Atlantic mais Amazon. Calcule a riqueza média por região e decida se “floresta” é um agrupamento útil para essa pergunta.
- Use
@pivot_longerpara colocararea_m2,canopyetemp_cem uma única coluna e depois calcule a média de cada variável por região em um único pipeline. Como você lida com os valores faltantes de dossel?