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)
)Introdução aos data.frames
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.
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() 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.
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.
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.
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() 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 |
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
- Inspeccione
dadoscomdim(),str()esummary(). 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
- Seleccione
iderts_mspara idades inferiores a 40 e preserve o tipodata.frame. Verifique dimensões e primeiras linhas. Explique o papel de cada lado da vírgula e compare o acesso arts_mscom$e com[.
Solução 2
[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"]$ e [,] devolvem aqui o vector da coluna. drop = FALSE impede que a selecção de colunas simplifique seleccionados para outro tipo.
- Crie
dados_verificadosa partir dedados, acrescenterts_s, altere uma célula documentada e escreva duas verificações.
Solução 3
O segundo teste verifica uma propriedade dos valores; o primeiro verifica que a cópia conservou o número de registos.- Apague
generocomNULLnuma cópia e, separadamente, por selecção. Comparenames()nos três objectos.
Solução 4
[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.
- Renomeie
rts_msparatempo_mssem alterardados. Que objecto tem a coluna antiga?
Solução 5
A coluna antiga continua emdados; dados_renomeados tem tempo_ms.
- Construa uma segunda tabela com
ide combine-a commerge(). Teste o efeito de introduzir umidduplicado 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
id == 1, essa linha de dados combina com ambas e o resultado passa a ter seis linhas.
- Explique por que
cbind()pode produzir uma tabela plausível mas errada quando as ordens deiddiferem.