Introdução aos data.frames

Programação
Referência guiada
data.frames
Uma referência guiada para inspeccionar, seleccionar e verificar tabelas.
Autores
Afiliações

João O. Santos

ISPA

Cristina Mendonça

ISPA; WJCR

Vitória Melita

FP-UL

Mariona Pascual Peñas

UIB

João Raposo

ISPA

Marta Barros

FP-UL

0 Introdução aos data.frames

Um data.frame é uma tabela em que cada coluna representa uma variável e cada linha corresponde a uma unidade representada nessa tabela. Se vem da estatística, pode pensar numa folha de cálculo com regras mais explícitas. Mas há uma cautela importante: a forma rectangular da tabela não nos diz, por si só, o que significa uma linha. Pode ser uma pessoa, uma sessão, uma tentativa ou outra unidade. Quando chegarmos a medidas repetidas, essa distinção vai deixar de ser apenas uma questão de organização.

Nesta página vamos fazer quatro coisas, uma de cada vez: inspeccionar, seleccionar, modificar e verificar. Começamos com uma tabela pequena para que seja possível prever o resultado de cada operação. A gramática do R explica os objectos, vectores e índices que usamos aqui. A importação de dados mostra depois como estas tabelas chegam ao R a partir de ficheiros, e a limpeza de dados acrescenta decisões menos arrumadas e mais parecidas com o que acontece num estudo real.

Criar uma Tabela

Neste exemplo, cada vector fornecido a data.frame() tem um elemento por linha. O R consegue reciclar alguns vectores mais curtos, mas não queremos confiar nisso ao construir uma tabela: cada coluna deve descrever os mesmos registos.

dados <- data.frame(
    id = 1:5,
    idade = c(21, 32, 24, 46, 33),
    genero = c("M", "F", "M", "F", "F"),
    rts_ms = c(1435, 4352, 2232, 2820, 4552)
)

Inspeccionar Antes de Alterar

Antes de mexermos na tabela, convém perceber o que ela contém: tamanho, nomes, primeiras e últimas linhas, estrutura e resumos. Não é a parte mais excitante da análise, mas costuma ser muito mais rápida do que descobrir meia hora depois que estivemos a trabalhar na coluna errada. View() pode ajudar numa sessão interactiva, mas não pertence a um script que tenha de correr sem intervenção. Comecemos pelas dimensões: dim() devolve linhas e colunas nessa ordem, e nrow() e ncol() devolvem cada número separadamente.

dim(dados)
[1] 5 4
nrow(dados)
[1] 5
ncol(dados)
[1] 4

dim() devolve 5 4: cinco linhas e quatro colunas. Os dois comandos seguintes pedem cada dimensão separadamente. Agora confirmemos os nomes das colunas. names() e colnames() dão o mesmo resultado num data.frame; basta escolher uma delas no código que escrevermos.

names(dados)
[1] "id"     "idade"  "genero" "rts_ms"
colnames(dados)
[1] "id"     "idade"  "genero" "rts_ms"

Para uma primeira leitura dos registos, head() mostra o início e tail() o fim da tabela. Aqui pedimos apenas as duas últimas linhas a tail().

head(dados)
id idade genero rts_ms
1 21 M 1435
2 32 F 4352
3 24 M 2232
4 46 F 2820
5 33 F 4552
tail(dados, n = 2)
id idade genero rts_ms
4 4 46 F 2820
5 5 33 F 4552

Por fim, str() descreve a estrutura e summary() calcula resumos simples. São inspecções diferentes: uma ajuda a confirmar os tipos dos objectos e a outra ajuda a detectar valores ou intervalos inesperados.

str(dados)
'data.frame':   5 obs. of  4 variables:
 $ id    : int  1 2 3 4 5
 $ idade : num  21 32 24 46 33
 $ genero: chr  "M" "F" "M" "F" ...
 $ rts_ms: num  1435 4352 2232 2820 4552
summary(dados)
       id        idade            genero      rts_ms    
 Min.   :1   Min.   :21.0   Length   :5   Min.   :1435  
 1st Qu.:2   1st Qu.:24.0   N.unique :2   1st Qu.:2232  
 Median :3   Median :32.0   N.blank  :0   Median :2820  
 Mean   :3   Mean   :31.2   Min.nchar:1   Mean   :3078  
 3rd Qu.:4   3rd Qu.:33.0   Max.nchar:1   3rd Qu.:4352  
 Max.   :5   Max.   :46.0                 Max.   :4552  
View(dados)

Seleccionar Linhas e Colunas

A notação [linhas, colunas] permite escolher as duas dimensões. O que fica à esquerda da vírgula selecciona linhas; o que fica à direita selecciona colunas. Quando um dos lados fica vazio, pedimos todas as linhas ou todas as colunas. Comecemos pelas linhas, sem acrescentar já outras condições:

dados[1, ]
id idade genero rts_ms
1 21 M 1435
dados[2:4, ]
id idade genero rts_ms
2 2 32 F 4352
3 3 24 M 2232
4 4 46 F 2820
dados[-2, ]
id idade genero rts_ms
1 1 21 M 1435
3 3 24 M 2232
4 4 46 F 2820
5 5 33 F 4552

O primeiro resultado tem uma linha, o segundo tem três e o terceiro exclui a segunda. Repare que estamos a seleccionar linhas sem ainda decidir que colunas queremos conservar. Para escolher uma coluna, podemos usar a posição ou, de preferência, o nome. O operador $ é uma forma curta de pedir uma coluna pelo nome e devolve o vector que está nessa coluna. As duas formas seguintes pedem a mesma coluna; compare o resultado e note que é um vector.

dados[ , "idade"]
[1] 21 32 24 46 33
dados$idade
[1] 21 32 24 46 33

Quando queremos mais do que uma coluna, colocamos os nomes num vector. Também podemos usar posições ou o sinal - para excluir uma coluna, mas os nomes costumam tornar a intenção mais fácil de ler.

dados[ , c("id", "idade")]
id idade
1 21
2 32
3 24
4 46
5 33
dados[ , 2:3]
idade genero
21 M
32 F
24 M
46 F
33 F
dados[ , -4]
id idade genero
1 21 M
2 32 F
3 24 M
4 46 F
5 33 F

Podemos agora combinar as duas dimensões. O primeiro exemplo pede uma célula; o segundo pede as colunas id e idade das três primeiras linhas.

dados[1, "idade"]
[1] 21
dados[1:3, c("id", "idade")]
id idade
1 21
2 32
3 24

Uma condição lógica no lugar das linhas permite seleccionar registos. Leia o código abaixo como «todas as linhas cuja idade é pelo menos 18». Guardamos o resultado num novo objecto para podermos inspeccioná-lo sem perder dados.

adultas <- dados[dados$idade >= 18, ]
adultas
id idade genero rts_ms
1 21 M 1435
2 32 F 4352
3 24 M 2232
4 46 F 2820
5 33 F 4552

Neste exemplo, todas as cinco pessoas passam o critério. O resultado continua a ser uma tabela, porque deixámos a parte das colunas vazia. Podemos acrescentar uma segunda condição com &. Aqui seleccionamos adultos com tempos abaixo de 4000 ms e, separadamente, pedimos apenas a coluna dos tempos.

seleccionados <- dados[
    dados$idade >= 18 & dados$rts_ms < 4000,
]
seleccionados
id idade genero rts_ms
1 1 21 M 1435
3 3 24 M 2232
4 4 46 F 2820

A mesma condição pode ser usada para pedir apenas o vector rts_ms. Assim, o resultado já não é uma tabela completa, mas os tempos das linhas seleccionadas.

rts_de_adultos <- dados[dados$idade >= 18, "rts_ms"]
rts_de_adultos
[1] 1435 4352 2232 2820 4552

Por defeito, seleccionar uma só coluna pode simplificar o resultado para um vector. drop = FALSE conserva um data.frame, o que faz diferença quando o resultado vai ser passado a uma função que espera uma tabela.

idades_em_tabela <- dados[ , "idade", drop = FALSE]
adultas_em_tabela <- dados[dados$idade >= 18, , drop = FALSE]

Há ainda um cuidado importante quando a condição pode conter NA: a selecção com [ pode criar uma linha de NA. Nesse caso, escreva explicitamente o que deve acontecer aos valores em falta, por exemplo com !is.na().

idades_conhecidas <- dados[
    !is.na(dados$idade) & dados$idade >= 18,
    ,
    drop = FALSE
]

Modificar e Acrescentar

Podemos atribuir a uma coluna inteira, a uma célula ou apenas a algumas linhas. Enquanto estamos a aprender, é muitas vezes boa ideia trabalhar numa cópia. Assim conseguimos comparar o antes e o depois e voltar atrás sem reconstruir o objecto original.

dados_preparados <- dados
dados_preparados$rts_s <- dados_preparados$rts_ms / 1000
dados_preparados$idade_centrada <-
    dados_preparados$idade - mean(dados_preparados$idade)

A cópia tem agora duas colunas novas. Inspeccionemos uma delas antes de alterar valores individuais.

dados_preparados[c("rts_ms", "rts_s", "idade_centrada")]
rts_ms rts_s idade_centrada
1435 1.435 -10.2
4352 4.352 0.8
2232 2.232 -7.2
2820 2.820 14.8
4552 4.552 1.8

Para uma alteração documentada, podemos atribuir um valor a uma célula. O comentário indica a razão hipotética da correcção e deixa um registo para quem ler o código mais tarde.

# Apply a hypothetical documented correction to one cell.
dados_preparados[1, "rts_s"] <- 1.450
dados_preparados[1, c("rts_ms", "rts_s")]
rts_ms rts_s
1435 1.45

Também podemos criar uma categoria para todas as linhas e depois substituir o valor apenas nas linhas que satisfazem uma condição.

# Set a value for every row before modifying selected rows.
dados_preparados$grupo_etario <- "30_ou_mais"
dados_preparados[dados_preparados$idade < 30, "grupo_etario"] <- "menos_de_30"
dados_preparados[c("idade", "grupo_etario")]
idade grupo_etario
21 menos_de_30
32 30_ou_mais
24 menos_de_30
46 30_ou_mais
33 30_ou_mais

Para acrescentar uma linha, rbind() precisa de colunas correspondentes. Para acrescentar uma coluna, cbind() junta por posição. Essa posição pode parecer uma ligação entre tabelas, mas não é uma chave relacional.

dados_extra <- rbind(
    dados,
    data.frame(id = 6, idade = 29, genero = "M", rts_ms = 3100)
)
turnos <- cbind(dados, ordem = seq_len(nrow(dados)))

Apagar e Renomear

Atribuir NULL apaga uma coluna. Também podemos criar uma nova tabela sem essa coluna e deixar o objecto original intacto. Para renomear, names() é directo; seleccionar pelos nomes é útil quando queremos construir uma tabela nova.

dados_sem_genero <- dados[ , setdiff(names(dados), "genero"), drop = FALSE]
dados_copia <- dados
dados_copia$genero <- NULL
names(dados_copia)[names(dados_copia) == "rts_ms"] <- "tempo_ms"

Os row names existem e podem aparecer em tabelas importadas, mas não são uma boa escolha como identificador principal. Prefira uma coluna explícita como id: conseguimos inspeccioná-la, combiná-la e exportá-la sem depender de um atributo escondido.

Verificar Depois de Cada Passo

Código que corre sem erro pode fazer exactamente a coisa errada. Depois duma alteração, verifique aquilo que deveria ter mudado e aquilo que deveria ter ficado igual: dimensões, nomes, classes, intervalos, valores em falta ou chaves, conforme o caso.

nrow(dados_extra)
[1] 6
ncol(dados_preparados)
[1] 7
names(dados_preparados)
[1] "id"             "idade"          "genero"         "rts_ms"        
[5] "rts_s"          "idade_centrada" "grupo_etario"  

Estas verificações rápidas confirmam o tamanho e os nomes. Para perceber se os valores também fazem sentido, inspeccionemos a estrutura, o resumo da nova coluna e algumas linhas.

str(dados_preparados)
'data.frame':   5 obs. of  7 variables:
 $ id            : int  1 2 3 4 5
 $ idade         : num  21 32 24 46 33
 $ genero        : chr  "M" "F" "M" "F" ...
 $ rts_ms        : num  1435 4352 2232 2820 4552
 $ rts_s         : num  1.45 4.35 2.23 2.82 4.55
 $ idade_centrada: num  -10.2 0.8 -7.2 14.8 1.8
 $ grupo_etario  : chr  "menos_de_30" "30_ou_mais" "menos_de_30" "30_ou_mais" ...
summary(dados_preparados$rts_s)
   Min. 1st Qu.  Median    Mean 3rd Qu.    Max. 
  1.450   2.232   2.820   3.081   4.352   4.552 
head(dados_preparados)
id idade genero rts_ms rts_s idade_centrada grupo_etario
1 21 M 1435 1.450 -10.2 menos_de_30
2 32 F 4352 4.352 0.8 30_ou_mais
3 24 M 2232 2.232 -7.2 menos_de_30
4 46 F 2820 2.820 14.8 30_ou_mais
5 33 F 4552 4.552 1.8 30_ou_mais

Podemos ainda transformar expectativas importantes em testes. Se uma delas falhar, stopifnot() interrompe o código e aponta que precisamos de investigar antes de continuar.

stopifnot(nrow(dados_extra) == nrow(dados) + 1)
stopifnot(all(dados_preparados$rts_s >= 0))
stopifnot(anyDuplicated(dados$id) == 0)

stopifnot() permite transformar algumas expectativas em verificações explícitas. Não prova que a decisão científica foi boa. É apenas uma maneira de fazer o código protestar quando uma propriedade que esperávamos deixa de ser verdadeira.

Combinar Tabelas: Posição Não É Chave

rbind() empilha registos com a mesma estrutura. cbind() junta colunas pela posição e pode produzir uma tabela perfeitamente plausível mesmo quando as linhas deixaram de corresponder. Para combinar por identificador usamos merge() ou, mais tarde, joins de dplyr.

medidas <- data.frame(id = 1:5, grupo = c("A", "A", "B", "B", "B"))
stopifnot(anyDuplicated(dados$id) == 0)
stopifnot(anyDuplicated(medidas$id) == 0)

Agora combinamos pelas colunas id. merge() procura os valores correspondentes, em vez de confiar na ordem das linhas.

combinados <- merge(dados, medidas, by = "id")
combinados
id idade genero rts_ms grupo
1 21 M 1435 A
2 32 F 4352 A
3 24 M 2232 B
4 46 F 2820 B
5 33 F 4552 B
stopifnot(nrow(combinados) == nrow(dados))

A chave deve representar a relação que queremos. Se houver chaves duplicadas, uma combinação pode gerar várias linhas para o mesmo registo e aumentar a tabela. Não trate esse aumento como um problema de apresentação. Pare e perceba primeiro se a relação devia ser um-para-um, um-para-muitos ou muitos-para-muitos.

Exercícios

  1. Inspeccione dados com dim(), str() e summary(). Escreva o que cada função acrescenta e o que representa uma linha desta tabela.
Solução 1
dim(dados)       # Number of rows and columns.
[1] 5 4
str(dados)        # Classes and structure of each column.
'data.frame':   5 obs. of  4 variables:
 $ id    : int  1 2 3 4 5
 $ idade : num  21 32 24 46 33
 $ genero: chr  "M" "F" "M" "F" ...
 $ rts_ms: num  1435 4352 2232 2820 4552
summary(dados)    # Compact summaries, including ranges and missing values.
       id        idade            genero      rts_ms    
 Min.   :1   Min.   :21.0   Length   :5   Min.   :1435  
 1st Qu.:2   1st Qu.:24.0   N.unique :2   1st Qu.:2232  
 Median :3   Median :32.0   N.blank  :0   Median :2820  
 Mean   :3   Mean   :31.2   Min.nchar:1   Mean   :3078  
 3rd Qu.:4   3rd Qu.:33.0   Max.nchar:1   3rd Qu.:4352  
 Max.   :5   Max.   :46.0                 Max.   :4552  
Cada linha representa uma pessoa nesta tabela; noutra tabela, a unidade poderia ser diferente.
  1. Seleccione id e rts_ms para idades inferiores a 40 e preserve o tipo data.frame. Verifique dimensões e primeiras linhas. Explique o papel de cada lado da vírgula e compare o acesso a rts_ms com $ e com [.
Solução 2
seleccionados <- dados[
    dados$idade < 40,
    c("id", "rts_ms"),
    drop = FALSE
]
dim(seleccionados)
[1] 4 2
head(seleccionados)
id rts_ms
1 1 1435
2 2 4352
3 3 2232
5 5 4552
rts_com_dolar <- dados$rts_ms
rts_com_colchetes <- dados[, "rts_ms"]
À esquerda da vírgula seleccionamos linhas; à direita, colunas. $ e [,] devolvem aqui o vector da coluna. drop = FALSE impede que a selecção de colunas simplifique seleccionados para outro tipo.
  1. Crie dados_verificados a partir de dados, acrescente rts_s, altere uma célula documentada e escreva duas verificações.
Solução 3
dados_verificados <- dados
dados_verificados$rts_s <- dados_verificados$rts_ms / 1000
# Hypothetical documented correction for the first recorded value.
dados_verificados[1, "rts_s"] <- 1.450

stopifnot(nrow(dados_verificados) == nrow(dados))
stopifnot(all(dados_verificados$rts_s >= 0))
O segundo teste verifica uma propriedade dos valores; o primeiro verifica que a cópia conservou o número de registos.
  1. Apague genero com NULL numa cópia e, separadamente, por selecção. Compare names() nos três objectos.
Solução 4
sem_genero_null <- dados
sem_genero_null$genero <- NULL

sem_genero_seleccao <- dados[
    , setdiff(names(dados), "genero"),
    drop = FALSE
]

names(dados)
[1] "id"     "idade"  "genero" "rts_ms"
names(sem_genero_null)
[1] "id"     "idade"  "rts_ms"
names(sem_genero_seleccao)
[1] "id"     "idade"  "rts_ms"
dados conserva genero; as duas cópias não conservam. Nenhuma operação altera o objecto original.
  1. Renomeie rts_ms para tempo_ms sem alterar dados. Que objecto tem a coluna antiga?
Solução 5
dados_renomeados <- dados
names(dados_renomeados)[names(dados_renomeados) == "rts_ms"] <- "tempo_ms"
names(dados)
[1] "id"     "idade"  "genero" "rts_ms"
names(dados_renomeados)
[1] "id"       "idade"    "genero"   "tempo_ms"
A coluna antiga continua em dados; dados_renomeados tem tempo_ms.
  1. Construa uma segunda tabela com id e combine-a com merge(). Teste o efeito de introduzir um id duplicado e explique o aumento (ou não) de linhas.
Solução 6
medidas <- data.frame(
    id = 1:5,
    grupo = c("A", "A", "B", "B", "B")
)
combinados <- merge(dados, medidas, by = "id")
nrow(combinados)
[1] 5
medidas_dup <- rbind(medidas, data.frame(id = 1, grupo = "C"))
combinados_dup <- merge(dados, medidas_dup, by = "id")
nrow(combinados_dup)
[1] 6
A combinação normal tem cinco linhas. Com duas correspondências para id == 1, essa linha de dados combina com ambas e o resultado passa a ter seis linhas.
  1. Explique por que cbind() pode produzir uma tabela plausível mas errada quando as ordens de id diferem.
Solução 7
medidas_reordenadas <- medidas[c(2, 1, 3, 4, 5), ]
por_posicao <- cbind(dados, medidas_reordenadas["grupo"])
por_posicao[, c("id", "grupo")]
id grupo
2 1 A
1 2 A
3 3 B
4 4 B
5 5 B
cbind() junta a primeira linha com a primeira linha, sem procurar o mesmo id. Assim, os valores podem parecer plausíveis mas pertencer à pessoa errada. Para uma chave, use merge() ou um join.