
Com o tidyverse: o percurso inteiro dentro do mutate()
Source:vignettes/articles/tidyverse.Rmd
tidyverse.RmdToda função de tradução deste pacote recebe um vetor e devolve um
vetor do mesmo comprimento, sem estado e sem efeito colateral. É a única
propriedade que importa para o dplyr: uma função assim entra em
mutate(), em summarise(), em
across() e em group_by() sem adaptação
nenhuma. Este artigo percorre um pipeline inteiro, do código bruto do
TSE à tabela agregada, sem sair do pipe.
Os outros artigos cobrem outras coisas. Comece aqui explica o que cada medida significa, e Os percursos mostra onde cada tradução se interrompe. Aqui o assunto é só a mecânica de aplicar essas funções a um banco de verdade.
A moldura de dados
O recorte abaixo usa os nomes de coluna do cadastro de candidaturas
do TSE e códigos de ocupação reais, escolhidos para cobrir estratos
diferentes. As pessoas são fictícias e identificadas só pelo
SQ_CANDIDATO; os rótulos nunca são digitados, e sim pedidos
ao pacote com tse_para_rotulo().
candidaturas <- tibble(
SQ_CANDIDATO = 10001:10012,
ANO_ELEICAO = c(2026L, 2026L, 2022L, 2022L, 2022L, 2026L,
2026L, 2022L, 2026L, 2026L, 2010L, 1998L),
CD_OCUPACAO = c("111", "131", "601", "169", "257", "298",
"265", "532", "709", "278", "999", "214"),
DS_CARGO = c("DEPUTADO FEDERAL", "DEPUTADO FEDERAL", "DEPUTADO ESTADUAL",
"DEPUTADO ESTADUAL", "SENADOR", "DEPUTADO ESTADUAL",
"DEPUTADO ESTADUAL", "DEPUTADO ESTADUAL", "DEPUTADO ESTADUAL",
"DEPUTADO ESTADUAL", "DEPUTADO FEDERAL", "DEPUTADO FEDERAL"),
SG_UF = c("RJ", "SP", "MG", "BA", "SP", "RJ",
"CE", "PR", "PE", "GO", "AM", "RS")
)
candidaturas |>
mutate(rotulo = tse_para_rotulo(CD_OCUPACAO, ano = ANO_ELEICAO)) |>
select(CD_OCUPACAO, ANO_ELEICAO, rotulo)
#> # A tibble: 12 × 3
#> CD_OCUPACAO ANO_ELEICAO rotulo
#> <chr> <int> <chr>
#> 1 111 2026 MÉDICO
#> 2 131 2026 ADVOGADO
#> 3 601 2022 AGRICULTOR
#> 4 169 2022 COMERCIANTE
#> 5 257 2022 EMPRESARIO
#> 6 298 2026 SERVIDOR PÚBLICO MUNICIPAL
#> 7 265 2026 PROFESSOR DE ENSINO FUNDAMENTAL
#> 8 532 2022 MOTORISTA DE VEÍCULOS DE TRANSPORTE DE CARGA
#> 9 709 2026 TRABALHADOR DE CONSTRUÇÃO CIVIL
#> 10 278 2026 VEREADOR
#> 11 999 2010 OUTROS
#> 12 214 1998 DELEGADO DE POLICIAA última linha já mostra a razão de o ano existir: o
código 214 em 1998 não é o 214 de hoje.
Uma coluna por régua, num só mutate()
Cada função de tradução vira uma coluna. Elas não conversam entre si,
então a ordem dentro do mutate() é indiferente e todas
podem entrar de uma vez.
medidas <- candidaturas |>
mutate(
isco88 = tse_para_isco(CD_OCUPACAO, ano = ANO_ELEICAO),
isco08 = tse_para_isco08(CD_OCUPACAO, ano = ANO_ELEICAO),
isei = tse_para_isei(CD_OCUPACAO, ano = ANO_ELEICAO),
prestigio = tse_para_prestigio(CD_OCUPACAO, ano = ANO_ELEICAO),
egp = tse_para_egp(CD_OCUPACAO, ano = ANO_ELEICAO, avisar = FALSE),
classe = tse_para_classe(CD_OCUPACAO, ano = ANO_ELEICAO),
estrato = tse_para_estrato(CD_OCUPACAO, ano = ANO_ELEICAO)
)
#> Warning: There were 7 warnings in `mutate()`.
#> The first warning was:
#> ℹ In argument: `isco88 = tse_para_isco(CD_OCUPACAO, ano =
#> ANO_ELEICAO)`.
#> Caused by warning:
#> ! 1 candidatura(s) usam código(s) que o TSE REUTILIZOU depois (214): naquele ano designavam outra ocupação, e voltam NA.
#> Veja ?tse_vigencia.
#> ℹ Run `dplyr::last_dplyr_warnings()` to see the 6 remaining warnings.
medidas |> select(CD_OCUPACAO, isco88, isco08, isei, prestigio)
#> # A tibble: 12 × 5
#> CD_OCUPACAO isco88 isco08 isei prestigio
#> <chr> <chr> <chr> <dbl> <dbl>
#> 1 111 2221 2210 88 78
#> 2 131 2421 2611 85 73
#> 3 601 6100 6100 23 38
#> 4 169 1300 1400 51 50
#> 5 257 1200 1200 68 60
#> 6 298 NA NA NA NA
#> 7 265 2330 2340 66 57
#> 8 532 8300 8300 32 33
#> 9 709 7100 7100 31 34
#> 10 278 1100 1110 70 67
#> 11 999 NA NA NA NA
#> 12 214 NA NA NA NAO aviso que veio junto é da última linha, o 214 de 1998, e a próxima
seção é sobre ele. Ele reaparece a cada chamada que toca aquele código;
da seção do across() em diante este artigo o omite, para
não repetir dez vezes a mesma frase.
As colunas categóricas são largas e ficam melhor à parte:
medidas |>
mutate(rotulo = tse_para_rotulo(CD_OCUPACAO, ano = ANO_ELEICAO)) |>
select(CD_OCUPACAO, classe, estrato) |>
print(n = Inf)
#> # A tibble: 12 × 3
#> CD_OCUPACAO classe estrato
#> <chr> <chr> <chr>
#> 1 111 Profissionais de nível superior Classe alta
#> 2 131 Profissionais de nível superior Classe alta
#> 3 601 Trabalhadores rurais Classes populares
#> 4 169 Proprietários e empregadores Classe alta
#> 5 257 Proprietários e empregadores Classe alta
#> 6 298 Vínculo público não especificado Vínculo público não es…
#> 7 265 Profissionais de nível superior Classe alta
#> 8 532 Operários e trabalhadores manuais Classes populares
#> 9 709 Operários e trabalhadores manuais Classes populares
#> 10 278 Dirigentes e políticos Classe alta
#> 11 999 Não informado Fora da PEA / não info…
#> 12 214 NA NAO médico tem ISEI 88 e o trabalhador de construção civil, 31. As
linhas de servidor público municipal, “outros” e do 214 de 1998 saem com
NA no ISEI, e mesmo assim recebem classe: são os três
motivos diferentes de o pacote se abster, e Comece aqui explica cada um.
O ano é o que evita o erro silencioso
Sem o ano, o pacote aplica o cadastro vigente a partir
de 2002 a todas as linhas, inclusive às de 1998 e 2000. Compare as duas
chamadas na mesma tabela:
candidaturas |>
filter(ANO_ELEICAO == 1998) |>
mutate(
rotulo_sem_ano = tse_para_rotulo(CD_OCUPACAO),
rotulo_com_ano = tse_para_rotulo(CD_OCUPACAO, ano = ANO_ELEICAO),
isei_sem_ano = tse_para_isei(CD_OCUPACAO),
isei_com_ano = tse_para_isei(CD_OCUPACAO, ano = ANO_ELEICAO)
) |>
select(starts_with("rotulo"), starts_with("isei")) |>
glimpse()
#> Warning: There was 1 warning in `mutate()`.
#> ℹ In argument: `isei_com_ano = tse_para_isei(CD_OCUPACAO, ano =
#> ANO_ELEICAO)`.
#> Caused by warning:
#> ! 1 candidatura(s) usam código(s) que o TSE REUTILIZOU depois (214): naquele ano designavam outra ocupação, e voltam NA.
#> Veja ?tse_vigencia.
#> Rows: 1
#> Columns: 4
#> $ rotulo_sem_ano <chr> "ESCULTOR E PINTOR"
#> $ rotulo_com_ano <chr> "DELEGADO DE POLICIA"
#> $ isei_sem_ano <dbl> 54
#> $ isei_com_ano <dbl> NAA versão sem ano devolve um número plausível para uma
ocupação que aquela pessoa não tinha. Para descobrir isso antes de
traduzir, checa_periodo() marca as posições atingidas e
avisa. Ela devolve o vetor lógico invisivelmente, o que a torna útil
dentro do pipe:
candidaturas |>
mutate(atingido = checa_periodo(CD_OCUPACAO, ANO_ELEICAO)) |>
count(atingido)
#> Warning: There was 1 warning in `mutate()`.
#> ℹ In argument: `atingido = checa_periodo(CD_OCUPACAO, ANO_ELEICAO)`.
#> Caused by warning:
#> ! 1 candidatura(s) até 2000 usam código(s) que o TSE reutilizou em 2002 (214). Elas estão classificadas pelo cadastro NOVO e portanto erradas; veja ?checa_periodo.
#> # A tibble: 2 × 2
#> atingido n
#> <lgl> <int>
#> 1 FALSE 11
#> 2 TRUE 1
across(), quando a mesma transformação vale para várias
colunas
Como as funções são vetorizadas puras, elas entram em
across() como qualquer outra. Duas formas são úteis. A
primeira aplica várias traduções à mesma coluna de códigos, nomeando o
resultado pelo nome da função:
candidaturas |>
mutate(across(
CD_OCUPACAO,
list(isei = ~ tse_para_isei(.x, ano = ANO_ELEICAO),
prestigio = ~ tse_para_prestigio(.x, ano = ANO_ELEICAO),
isei08 = ~ tse_para_isei08(.x, ano = ANO_ELEICAO)),
.names = "{.fn}"
)) |>
select(CD_OCUPACAO, isei, prestigio, isei08)
#> # A tibble: 12 × 4
#> CD_OCUPACAO isei prestigio isei08
#> <chr> <dbl> <dbl> <dbl>
#> 1 111 88 78 88.7
#> 2 131 85 73 86.7
#> 3 601 23 38 19.4
#> 4 169 51 50 51.0
#> 5 257 68 60 72.9
#> 6 298 NA NA NA
#> 7 265 66 57 71.4
#> 8 532 32 33 26.8
#> 9 709 31 34 25.4
#> 10 278 70 67 74.5
#> 11 999 NA NA NA
#> 12 214 NA NA NAA segunda arruma de uma vez as colunas já traduzidas:
Agregar por cargo, ano ou partido
Daqui em diante é dplyr comum. O cuidado que permanece é com o
NA: ele precisa aparecer na contagem, e não sumir dentro de
um na.rm = TRUE que ninguém relata.
medidas |>
group_by(DS_CARGO) |>
summarise(
n = n(),
com_isei = sum(!is.na(isei)),
isei_medio = round(mean(isei, na.rm = TRUE), 1),
.groups = "drop"
)
#> # A tibble: 3 × 4
#> DS_CARGO n com_isei isei_medio
#> <chr> <int> <int> <dbl>
#> 1 DEPUTADO ESTADUAL 7 6 45.5
#> 2 DEPUTADO FEDERAL 4 2 86.5
#> 3 SENADOR 1 1 68A coluna com_isei ao lado de n é o mínimo
que se publica junto de uma média de ISEI. Sem ela, a média descreve um
subconjunto que o leitor não conhece.
Nas variáveis categóricas, count() já mostra o
NA sem ajuda:
medidas |> count(classe, sort = TRUE)
#> # A tibble: 8 × 2
#> classe n
#> <chr> <int>
#> 1 Profissionais de nível superior 3
#> 2 Operários e trabalhadores manuais 2
#> 3 Proprietários e empregadores 2
#> 4 Dirigentes e políticos 1
#> 5 Não informado 1
#> 6 Trabalhadores rurais 1
#> 7 Vínculo público não especificado 1
#> 8 NA 1A junção com o dicionário inteiro
Quando o que se quer não é uma medida e sim todas, mais a régua de
qualidade da tradução, o caminho é juntar com o dicionário.
crosswalk_tse() sem argumento devolve as 275 linhas do
cadastro.
candidaturas |>
left_join(crosswalk_tse(), by = c("CD_OCUPACAO" = "cod_tse")) |>
select(CD_OCUPACAO, isco88, isei88, qualidade, nivel)
#> # A tibble: 12 × 5
#> CD_OCUPACAO isco88 isei88 qualidade nivel
#> <chr> <chr> <dbl> <chr> <int>
#> 1 111 2221 88 ambígua 4
#> 2 131 2421 85 exata 4
#> 3 601 6100 23 agregada 2
#> 4 169 1300 51 agregada 2
#> 5 257 1200 68 agregada 2
#> 6 298 NA NA NA NA
#> 7 265 2330 66 agregada 3
#> 8 532 8300 32 agregada 2
#> 9 709 7100 31 agregada 2
#> 10 278 1100 70 agregada 2
#> 11 999 NA NA NA NA
#> 12 214 2452 54 exata 4A qualidade diz como cada tradução foi obtida, e serve
para restringir a análise ao que é exato:
candidaturas |>
left_join(crosswalk_tse(), by = c("CD_OCUPACAO" = "cod_tse")) |>
count(qualidade)
#> # A tibble: 4 × 2
#> qualidade n
#> <chr> <int>
#> 1 agregada 7
#> 2 ambígua 1
#> 3 exata 2
#> 4 NA 2Há uma ressalva importante nessa junção, e ela é o preço de usar a
tabela em vez das funções: a junção não conhece o
ano. O dicionário tem uma linha por código, com o
cadastro vigente a partir de 2002, então a candidatura de 1998 recebe
calada o rótulo novo do 214.
candidaturas |>
filter(ANO_ELEICAO == 1998) |>
left_join(crosswalk_tse(), by = c("CD_OCUPACAO" = "cod_tse")) |>
select(ANO_ELEICAO, CD_OCUPACAO, rotulo, isei88)
#> # A tibble: 1 × 4
#> ANO_ELEICAO CD_OCUPACAO rotulo isei88
#> <int> <chr> <chr> <dbl>
#> 1 1998 214 ESCULTOR E PINTOR 54Para dado que cruza 2000 e 2002, junte pelo dicionário para
inspecionar e traduza pelas funções com ano para
analisar.
EGP com o que o TSE conhece
O EGP distingue quem supervisiona de quem não supervisiona, e o
cadastro do TSE não registra isso. tse_para_egp() calcula
assim mesmo e avisa; o avisar = FALSE acima serviu para não
repetir o mesmo aviso em cada chunk deste artigo, depois de ele ter sido
explicado. Os dois argumentos que mudam o resultado são
n_classes, que colapsa o esquema, e
usa_proprietario, que decide se a marca de proprietário do
dicionário entra na classificação.
egps <- candidaturas |>
mutate(
egp11 = tse_para_egp(CD_OCUPACAO, ano = ANO_ELEICAO, avisar = FALSE),
egp7 = tse_para_egp(CD_OCUPACAO, ano = ANO_ELEICAO, n_classes = 7,
avisar = FALSE),
egp7_sem_prop = tse_para_egp(CD_OCUPACAO, ano = ANO_ELEICAO, n_classes = 7,
usa_proprietario = FALSE, avisar = FALSE)
)
egps |> select(CD_OCUPACAO, egp11) |> print(n = Inf)
#> # A tibble: 12 × 2
#> CD_OCUPACAO egp11
#> <chr> <chr>
#> 1 111 I: dirigentes e profissionais superiores
#> 2 131 I: dirigentes e profissionais superiores
#> 3 601 IVc: proprietário rural
#> 4 169 IVb: conta própria sem empregados
#> 5 257 I: dirigentes e profissionais superiores
#> 6 298 NA
#> 7 265 II: dirigentes e profissionais inferiores
#> 8 532 VIIa: trabalhador manual não qualificado
#> 9 709 VI: trabalhador manual qualificado
#> 10 278 I: dirigentes e profissionais superiores
#> 11 999 NA
#> 12 214 NAO usa_proprietario muda a classe só de quem o dicionário
marca como proprietário, e o filter() isola essas
linhas:
egps |>
filter(egp7 != egp7_sem_prop) |>
select(CD_OCUPACAO, egp7, egp7_sem_prop) |>
as.data.frame()
#> CD_OCUPACAO egp7
#> 1 601 IVc: agricultores por conta própria
#> 2 169 IVab: pequena burguesia
#> egp7_sem_prop
#> 1 VIIb: trabalhadores agrícolas
#> 2 I+II: classe de serviçoAgricultor e comerciante mudam de lado. É uma decisão de medida, e ela deve ser declarada no texto do artigo que a usa.
Cobertura, antes de analisar
checa_cobertura() falha com erro quando algum código do
seu vetor não está no dicionário, ou quando não há código nenhum. Ela é
uma barreira, não uma transformação, então o lugar dela no pipe é antes
do mutate(), alimentada por um pull():
candidaturas |> pull(CD_OCUPACAO) |> checa_cobertura()
#> cobertura ok: 12 códigos observados, todos no dicionário.Com um código que não existe no cadastro, o pipeline para:
candidaturas |>
mutate(CD_OCUPACAO = if_else(row_number() == 1, "812", CD_OCUPACAO)) |>
pull(CD_OCUPACAO) |>
checa_cobertura()
#> Error:
#> ! código(s) de ocupação sem entrada no dicionário: 812.
#> Se forem códigos novos do TSE, abra uma issue no repositório do pacote — o dicionário precisa ser estendido, e não contornado.Vale a pena parar assim. A alternativa é um vetor de NA
que atravessa a análise inteira sem chamar atenção, e quase sempre a
causa é de formato do código, não de dicionário: zeros à esquerda
perdidos numa leitura de CSV, ou um fator convertido para inteiro.
Série longa, com a mesma pessoa em várias eleições
Quando a mesma pessoa se candidata mais de uma vez, um registro sem
ocupação classificável pode herdar o escore do registro anterior dela
própria. isei_retrospectivo() recebe escore,
id e tempo, e devolve um
data.frame com o escore final, a marca de herança e a
defasagem.
O ponto que costuma escapar: não use
group_by(). A função já corta na fronteira de cada
id, e receber o painel inteiro é o que lhe permite fazer
isso. O que ela precisa é da ordenação, que o arrange()
garante.
painel <- tibble(
SQ_CANDIDATO = c(rep(20001L, 4), rep(20002L, 4)),
ANO_ELEICAO = rep(c(2010L, 2014L, 2018L, 2022L), 2),
CD_OCUPACAO = c("131", "999", "999", "278",
"999", "111", "931", "999")
)
carregado <- painel |>
arrange(SQ_CANDIDATO, ANO_ELEICAO) |>
mutate(isei = tse_para_isei(CD_OCUPACAO, ano = ANO_ELEICAO)) |>
mutate(ret = isei_retrospectivo(isei, SQ_CANDIDATO, ANO_ELEICAO,
cod = CD_OCUPACAO, excluir = "931")) |>
mutate(isei_cheio = ret$escore,
herdado = ret$herdado,
defasagem = ret$defasagem) |>
select(-ret)
carregado |>
select(SQ_CANDIDATO, ANO_ELEICAO, CD_OCUPACAO, isei,
isei_cheio, herdado, defasagem) |>
print(n = Inf)
#> # A tibble: 8 × 7
#> SQ_CANDIDATO ANO_ELEICAO CD_OCUPACAO isei isei_cheio herdado
#> <int> <int> <chr> <dbl> <dbl> <lgl>
#> 1 20001 2010 131 85 85 FALSE
#> 2 20001 2014 999 NA 85 TRUE
#> 3 20001 2018 999 NA 85 TRUE
#> 4 20001 2022 278 70 70 FALSE
#> 5 20002 2010 999 NA NA FALSE
#> 6 20002 2014 111 88 88 FALSE
#> 7 20002 2018 931 NA NA FALSE
#> 8 20002 2022 999 NA 88 TRUE
#> # ℹ 1 more variable: defasagem <dbl>Os códigos do painel são 131 para advogado, 111 para médico, 278 para vereador, 931 para estudante e 999 para “outros”, que é o que não se classifica.
O excluir = "931" deixa de fora estudante e estagiário,
cuja posição raramente se explica pelo passado. O primeiro registro de
cada pessoa nunca herda, porque só o passado da própria pessoa é
usado.
A herança tem custo, e ele é grande: o escore herdado supõe que a
ocupação não mudou, e nas candidaturas ela muda em cerca de metade dos
casos, sempre com o erro apontando para o passado. Por isso
herdado e defasagem saem no resultado. Relate
quantos escores são herdados e repita a análise só com os
observados:
carregado |> count(herdado)
#> # A tibble: 2 × 2
#> herdado n
#> <lgl> <int>
#> 1 FALSE 5
#> 2 TRUE 3A documentação de ?isei_retrospectivo traz as taxas
medidas no painel completo de candidaturas, e a decisão de usar ou não o
carregamento deve passar por elas.
O resumo, função por coluna
| Chamada dentro do mutate() | O que vira |
|---|---|
| tse_para_isco(cod, ano = ) | código ISCO-88, texto |
| tse_para_isco08(cod, ano = ) | código ISCO-08, texto |
| tse_para_isei(cod, ano = ) | ISEI, numérico de 16 a 90 |
| tse_para_prestigio(cod, ano = ) | prestígio de Treiman, numérico |
| tse_para_egp(cod, ano = ) | classe do EGP, texto; com rotulo = FALSE, o código |
| tse_para_classe(cod, ano = ) | classe do esquema do pacote, texto |
| tse_para_estrato(cod, ano = ) | estrato, texto |
| tse_para_rotulo(cod, ano = ) | rótulo do cadastro no ano, texto |
| checa_periodo(cod, ano) | lógico: a linha é atingida pela quebra de 2002 |
| isei_retrospectivo(escore, id, tempo) | data.frame com escore, herdado e defasagem |
Fora do pipe ficam duas: checa_cobertura(), que é
barreira e recebe o vetor por pull(), e
crosswalk_tse(), que é tabela e entra por
left_join().
Onde continuar
A escolha entre ISEI, prestígio, EGP e o esquema de classes está em Qual régua responde à sua pergunta. Se a medida vai sustentar um resultado publicado, A medida se sustenta? mostra como confrontá-la com um critério externo que não entrou na sua construção.