Post no Medium sobre isso aqui: https://medium.com/psicodata/valores-missing-parte-2-d2e0b832ce14
Esse é um documento feito para ensinar e para reforçar conteúdos de manipulação de dados usando tidyr. Qualquer comentário, erro ou sugestão, é só falar comigo entrando em contato através de qualquer uma das opções listadas em Contato.
O script em R está disponível aqui: https://github.com/GabrielReisR/R/blob/master/estrutura%20de%20dados/tidyr-parte-2.R
Essa publicação possui uma primeira parte em Organizando com tidyr - Parte 1: dados wide e long: https://rpubs.com/reisrgabriel/tidyrPt1
Às vezes, os dados estão desorganizados, o que pode dificultar as análises.
Um banco bem organizado é um no qual:
Dados desorganizados, untidy data, quebram uma ou mais dessas regras acima.
Outros aspectos de bancos de dados desorganizados incluem:
O tidyr é um pacote criado pelo time do tidyverse com a função de organização de um banco de dados.
Ele existe para que as três regras acima sejam cumpridas.
Para maiores informações sobre o tidyr:
Para explicar os conceitos sobre limpeza de missings, pegaremos os dados de bibliotecas públicas ao redor do mundo: https://github.com/GabrielReisR/R/tree/master/estrutura%20de%20dados/dados/libraries.csv
Antes de começar, vou fazer uma mudança no nome com o pacote dplyr.
biblios_original <- read.csv("https://raw.githubusercontent.com/GabrielReisR/R/master/estrutura%20de%20dados/dados/libraries.csv")
library(dplyr)
biblios_original <- biblios_original %>%
rename(id = X)
Vamos utilizar o pacote janitor para limpar o nome das colunas.
Vamos ver antes como estão as colunas:
names(biblios_original)
## [1] "id" "Country"
## [3] "Region" "Expenditures.....US.Dollars."
## [5] "Total.Libraries" "Total.Librarians"
## [7] "Total.Volumes" "Total.Users"
É preciso uniformizar os nomes para facilitar a manipulação do banco. Vamos ler e utilizar janitor, especificamente a função clean_names().
Abaixo, todas as possibilidades dessa função.
library(janitor)
biblios_snake <- biblios_original %>% # note o uso do pipe
clean_names() # se n?o dermos nenhum argumento, ent?o 'case = snake'
names(biblios_snake)
## [1] "id" "country"
## [3] "region" "expenditures_us_dollars"
## [5] "total_libraries" "total_librarians"
## [7] "total_volumes" "total_users"
# lowerCamel
biblios_lowerCamel <- biblios_original %>% # note o uso do pipe
clean_names(case = "lower_camel")
names(biblios_lowerCamel)
## [1] "id" "country" "region"
## [4] "expendituresUsDollars" "totalLibraries" "totalLibrarians"
## [7] "totalVolumes" "totalUsers"
# UpperCamel
biblios_UpperCamel <- biblios_original %>% # note o uso do pipe
clean_names(case = "upper_camel")
names(biblios_UpperCamel)
## [1] "Id" "Country" "Region"
## [4] "ExpendituresUsDollars" "TotalLibraries" "TotalLibrarians"
## [7] "TotalVolumes" "TotalUsers"
# screaming_snake
biblios_SCREAMING_SNAKE <- biblios_original %>% # note o uso do pipe
clean_names(case = "screaming_snake")
names(biblios_SCREAMING_SNAKE)
## [1] "ID" "COUNTRY"
## [3] "REGION" "EXPENDITURES_US_DOLLARS"
## [5] "TOTAL_LIBRARIES" "TOTAL_LIBRARIANS"
## [7] "TOTAL_VOLUMES" "TOTAL_USERS"
# lowerUPPER
biblios_lowerUPPER <- biblios_original %>% # note o uso do pipe
clean_names(case = "lower_upper")
names(biblios_lowerUPPER)
## [1] "id" "country" "region"
## [4] "expendituresUSdollars" "totalLIBRARIES" "totalLIBRARIANS"
## [7] "totalVOLUMES" "totalUSERS"
# UPPERlower
biblios_UPPERlower <- biblios_original %>% # note o uso do pipe
clean_names(case = "upper_lower")
names(biblios_UPPERlower)
## [1] "ID" "COUNTRY" "REGION"
## [4] "EXPENDITURESusDOLLARS" "TOTALlibraries" "TOTALlibrarians"
## [7] "TOTALvolumes" "TOTALusers"
biblios <- biblios_snake # basta dar a biblios o valor de biblios_snake
names(biblios_snake)
## [1] "id" "country"
## [3] "region" "expenditures_us_dollars"
## [5] "total_libraries" "total_librarians"
## [7] "total_volumes" "total_users"
Há diversas formas de lidar com missings. Aqui, vamos ver apenas quatro dessas formas:
drop_na(): escolhendo apenas as linhas sem nenhum missing; pacote tidyr.fill_na(): escolhendo preencher missing com valores do dataset; pacote tidyr.replace_na(): escolhendo valores específicos ou de tendência central (como média, mediana, etc.) para armazenar em missings; pacote tidyr.imputar(): uma função que vamos criar para imputação com regressão.Em primeiro lugar, vamos carregar nosso banco de dados que já vimos, agora com o nome de suas colunas em português.
Novamente: esse é um banco sobre bibliotecas públicas de diversos países, junto com o dinheiro (em dólar) de quanto cada país gastou com suas bibliotecas, a quantidade de pessoas que trabalhavam nas bibliotecas, a quantidade de usuários e de livros dessas bibliotecas.
A função drop_na() elimina todas as linhas que contém missings no dataset.
library(Amelia) # para visualizar missings com 'missmap()'
library(tidyr)
# lendo os dados
url <- "https://raw.githubusercontent.com/GabrielReisR/R/master/estrutura%20de%20dados/dados/biblios.csv"
data <- read.csv(url)[,- 1]
# visualizando dados
head(data)
# visualizando missings com o pacote Amelia
missmap(data)
Show! Entendemos então que nossas três variáveis com mais missings são:
Logo no início podemos fazer algo bem radical e excluir todas as linhas que contenham qualquer tipo de missing (listwise deletion). Fazemos isso usando a função drop_na() do pacote tidyr.
data <- tidyr::drop_na(data) # excluindo com drop_na()
head(data) # primeiros casos
nrow(data) # quantas linhas?
## [1] 140
Agora não temos mais nenhum missing em nenhum lugar. Entretanto, temos 79 casos a menos.
Digamos que ao invés de excluirmos todas as linhas que tivesse ao menos um missing, quiséssemos excluir qualquer linha que tenha missing na coluna total_usuarios.
Para isso, bastaria mudar o código só um pouquinho para tidyr::drop_na(data, total_usuarios)
Quando especificamos uma coluna como argumento (drop_na(banco_de_dados, coluna)), todas as linhas que contém missing nessa coluna são eliminados.
data <- read.csv(url)[,- 1] # relendo o banco inicial
data <- tidyr::drop_na(data, total_usuarios)
head(data) # mostra os primeiros casos
nrow(data) # quantas linhas?
## [1] 180
Filtrando apenas pela coluna total_usuarios, o nosso banco fica com 180 casos.
E se não quisermos eliminar colunas, mas sim preencher os valores?
Uma substituição possível de ser usada é a com a função fill() do pacote tidyr.
A solução provida por essa função é a de pegar os valores que não estão missings mais próximos para preencher os valores missings das variáveis. Não é uma solução muito adequada porque o critério é muito aleatório e pode não ser representativo daquele caso.
data <- read.csv(url)[,- 1] # relendo o banco inicial
library(tidyr)
data <- data %>%
fill(gastos,
total_trabalham,
total_usuarios,
.direction = "up") # preenche de baixo PARA CIMA ('up'))
head(data) # mostra os primeiros casos
nrow(data) # quantas linhas?
## [1] 219
Com .direction = "up", os valores completos passaram para cima.
Por exemplo, em gastos, na primeira linha, Afghanistan recebeu o valor de gastos de Albania.
Agora vamos ver de cima PARA BAIXO. Para isso, precisamos especificar nosso parâmetro como .direction = "down"
data <- read.csv(url)[,- 1] # relendo o banco inicial
library(tidyr)
data <- data %>%
fill(gastos,
total_trabalham,
total_usuarios,
.direction = "down") # preenche de baixo PARA CIMA ('up'))
head(data) # mostra os primeiros casos
nrow(data) # quantas linhas?
## [1] 219
Note que na primeira linha da coluna gastos é NA: isso porque não existe nenhum valor acima dela para a substituir.
Poderíamos também especificar outros dois valores em .direction:
.direction = "updown": primeiro vai para cima, depois para baixo, e assim por diante..direction = "downup": primeiro vai para baixo, depois para cima, e assim por diante.A última função do tidyr para tratar de missings se chama replace_na(). Ela recebe uma coluna ou um dataset e substitui por um valor que informamos em uma lista.
Para usar replace_na() basta nós colocarmos a lista das nossas variáveis junto com a função OU o valor que queremos para essas variáveis.
data <- read.csv(url)[,- 1] # relendo o banco inicial
library(tidyr) # lendo tidyr
data <- data %>%
replace_na(list(
gastos = mean(data$gastos, na.rm = T), # NAs se tornam média
total_trabalham = floor(mean(data$total_trabalham, na.rm = T)), # NAs se tornam média
total_usuarios = floor(mean(data$total_usuarios, na.rm = T)) # NAs se tornam média
))
head(data)
Perceba que aqui é um pouco mais complicado. Isso porque precisamos:
=e começar a especificar qual valor os missings dessa coluna receberão.floor(), que arredonda esse valor para baixo.Olhemos para o resultado agora. Basta olhar para total_trabalham para vermos que a média talvez não seja a melhor representação de tendência central dessa variável. O valor novo para as linhas que continham missing (1, 4 e 6) passou a ser 5002, o que não parece ser a realidade desses países.
Em gastos, o mesmo é percebido. Por exemplo, na linha 4, o país American Samoa tinha um missing em gastos que foi substituído pela média dessa coluna. O novo valor em dólares de gastos com as 32 bibliotecas desse país passou a ser de R$357.736.161. Um valor absurdamente maior do que o provável valor real.
Vimos o problema de se substituir pela média. Ela é facilmente influenciada por valores extremos.
Uma solução poderia ser substituir pela mediana. A função seria basicamente a mesma e o resultado bem diferente:
data <- read.csv(url)[,- 1] # relendo o banco inicial
library(tidyr) # lendo tidyr
data <- data %>%
replace_na(list(
gastos = median(data$gastos, na.rm = T), # NAs se tornam média
total_trabalham = floor(median(data$total_trabalham, na.rm = T)), # NAs se tornam média
total_usuarios = floor(median(data$total_usuarios, na.rm = T)) # NAs se tornam média
))
head(data)
O novo valor para os missings de total_trabalham agora é 175. O valor novo para as linhas que continham missing em gastos passou a ser R$1.810.863, o que parece estar mais adequado à realidade dos países que estamos vendo aqui. Já os missings de total_usuarios receberam o valor de 316.202, também mais condizente com os dados que estamos explorando.
Uma simples mudança da média para a mediana parece ter feito uma grande diferença aqui.
Podemos personalizar ainda mais os valores de missing. Vamos fazer agora com que cada linha receba um valor específico nos missings baseado nos valores de suas outras variáveis.
Podemos fazer isso utilizando-se da regressão linear, um método que busca estimar os valores de y a partir de variáveis x.
Aqui, vamos estimar o valor de gastos, total_trabalham e total_usarios usando as outras variáveis numéricas do nosso banco. Quais são essas?
Em todos os casos, possuímos dois valores numéricos que não se alteram: total_biblios e total_volumes.
Fazendo uma rápida regressão linear (tópico para um próximo post no PsicoData) para cada coluna temos que em:
Para fazer uma imputação simples com regressão, não vamos utilizar nenhum pacote, vamos criar a nossa própria função (o que aprendi a fazer com esse ótimo capítulo sobre missings).
# modelos de regressão
regressao_gastos <- lm(gastos ~ total_biblios + total_volumes - 1,
data = data)
regressao_trabalham <- lm(total_trabalham ~ total_biblios + total_volumes - 1,
data = data)
regressao_usuarios <- lm(total_usuarios ~ total_biblios + total_volumes - 1,
data = data)
# criando predição dos valores para cada coluna
gastos_pred <- predict(regressao_gastos, data) # coluna gastos
trabalham_pred <- predict(regressao_trabalham, data) # coluna gastos
usuarios_pred <- predict(regressao_usuarios, data) # coluna gastos
# criando a função
imputar <- function(coluna, pred){
ifelse(is.na(coluna), pred, coluna)
}
lm()
gastos ~ total_biblios + total_volumes - 1 estamos querendo dizer que os valores de gastos serão preditos pelos valores das colunas total_biblios e total_volumes. O - 1 significa que nós estamos retirando desse cálculo o valor do intercepto, que nesses casos eram bem significativos.predict()imputar()
Agora vamos usar tudo isso que fizemos para imputar esses valores no nosso banco de dados!
# imputando valores
data$gastos <- imputar(data$gastos, gastos_pred)
data$total_trabalham <- floor(imputar(data$total_trabalham, trabalham_pred))
data$total_usuarios <- floor(imputar(data$total_usuarios, usuarios_pred))
head(data)
Por enquanto é isso! :)
Organizando com tidyr - Parte 1: dados wide e long: https://rpubs.com/reisrgabriel/tidyrPt1
Manipulando com dplyr - Parte 1: select() e mutate(): https://rpubs.com/reisrgabriel/dplyrPt1
Manipulando com dplyr - Parte 2: bind() e join(): https://rpubs.com/reisrgabriel/dplyrPt2
Importação de dados e diagnósticos iniciais: https://rpubs.com/reisrgabriel/importdiagn
Para maiores informações sobre o tidyr: https://tidyr.tidyverse.org/ ou https://cran.r-project.org/web/packages/tidyr/tidyr.pdf