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 contém um registo. Se vem da estatística, pode pensar numa folha de cálculo com regras mais explícitas. Mas o que representa esse registo? Pode ser uma pessoa, uma sessão, uma tentativa ou outra unidade. A forma rectangular da tabela não nos dá essa informação. 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.
Preparar os Pacotes
Vamos usar dplyr para seleccionar linhas e colunas e transformar tabelas. Se necessário, instale-o uma vez na consola, fora do script, como explicado na referência de pacotes. No início do script, carregamos o pacote:
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.
ds <- data.frame(
id = 1:5,
age = c(21, 32, 24, 46, 33),
gender = c("M", "F", "M", "F", "F"),
rts_ms = c(1435, 4352, 2232, 2820, 4552)
)Tibbles
Um tibble é uma forma de data.frame usada nos pacotes do tidyverse. Podemos converter a tabela que já criámos com as_tibble() ou construir uma directamente com tibble(), também disponíveis ao carregar dplyr:
[1] "tbl_df" "tbl" "data.frame"
ds_tbl| id | age | gender | rts_ms |
|---|---|---|---|
| 1 | 21 | M | 1435 |
| 2 | 32 | F | 4352 |
| 3 | 24 | M | 2232 |
| 4 | 46 | F | 2820 |
| 5 | 33 | F | 4552 |
As colunas continuam a descrever os mesmos registos. Na consola, a impressão dum tibble limita as linhas e colunas apresentadas e indica os tipos das colunas. Outra diferença útil: ds_tbl[, "age"] conserva uma tabela, enquanto ds[, "age"] devolve um vector. Para obter o vector em ambos, podemos usar $. As funções de dplyr que vamos usar aceitam ambas as formas de tabela; conservamos ds como data.frame para reconhecer também o comportamento da indexação de base R.
1 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. Podemos explorar estes comandos no interpretador, incluindo View() para abrir a tabela. No script, guardamos as transformações que vamos usar na análise e os comentários que explicam as nossas escolhas. Também podemos conservar inspecções que seja útil repetir, mas View() fica para a sessão interactiva. 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(ds)| id | age | gender | rts_ms |
|---|---|---|---|
| 1 | 21 | M | 1435 |
| 2 | 32 | F | 4352 |
| 3 | 24 | M | 2232 |
| 4 | 46 | F | 2820 |
| 5 | 33 | F | 4552 |
tail(ds, n = 2)| id | age | gender | 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(ds)'data.frame': 5 obs. of 4 variables:
$ id : int 1 2 3 4 5
$ age : num 21 32 24 46 33
$ gender: chr "M" "F" "M" "F" ...
$ rts_ms: num 1435 4352 2232 2820 4552
summary(ds) id age gender 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(ds)2 Seleccionar Linhas e Colunas
filter() e select()
filter() escolhe linhas por condições sobre as colunas; select() escolhe colunas pelos nomes. Podemos escrever age e rts_ms directamente, sem repetir ds$. Comecemos pelas pessoas adultas:
adults <- filter(ds, age >= 18)
adults| id | age | gender | 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 pessoas têm pelo menos 18 anos. Para conservar apenas adultos com tempos abaixo de 4000 ms, acrescentamos outra condição. Condições separadas por vírgulas têm de ser todas satisfeitas:
selected <- ds |>
filter(age >= 18, rts_ms < 4000)
selected| id | age | gender | rts_ms |
|---|---|---|---|
| 1 | 21 | M | 1435 |
| 3 | 24 | M | 2232 |
| 4 | 46 | F | 2820 |
Ficamos com três pessoas. & também exige ambas as condições; | permitiria satisfazer uma ou outra. filter() mantém só condições com resultado TRUE: uma condição com resultado NA deixa a linha de fora. Convém decidir o que fazer aos valores em falta, em vez de os tratar como exclusões automáticas.
Para escolher colunas, usamos select(). O pipe nativo |> passa o resultado de cada operação para o primeiro argumento da seguinte. Leia o segundo exemplo como «partir de ds, filtrar as idades e escolher id e rts_ms»:
Esta será a nossa forma habitual de seleccionar dados nos próximos tutoriais. Vale a pena reconhecer também a indexação de base R, usada para pedir posições concretas e corrigir células.
Reconhecer a Indexação de Base R
A notação [rows, columns] permite escolher as duas dimensões. À esquerda da vírgula seleccionamos linhas e à direita seleccionamos colunas. Quando um dos lados fica vazio, pedimos todas as linhas ou todas as colunas. Comecemos pelas linhas, sem acrescentar já outras condições:
ds[1, ]| id | age | gender | rts_ms |
|---|---|---|---|
| 1 | 21 | M | 1435 |
ds[2:4, ]| id | age | gender | rts_ms | |
|---|---|---|---|---|
| 2 | 2 | 32 | F | 4352 |
| 3 | 3 | 24 | M | 2232 |
| 4 | 4 | 46 | F | 2820 |
ds[-2, ]| id | age | gender | 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.
ds[ , "age"][1] 21 32 24 46 33
ds$age[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.
ds[ , c("id", "age")]| id | age |
|---|---|
| 1 | 21 |
| 2 | 32 |
| 3 | 24 |
| 4 | 46 |
| 5 | 33 |
ds[ , 2:3]| age | gender |
|---|---|
| 21 | M |
| 32 | F |
| 24 | M |
| 46 | F |
| 33 | F |
ds[ , -4]| id | age | gender |
|---|---|---|
| 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 age 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 ds.
adults <- ds[ds$age >= 18, ]
adults| id | age | gender | rts_ms |
|---|---|---|---|
| 1 | 21 | M | 1435 |
| 2 | 32 | F | 4352 |
| 3 | 24 | M | 2232 |
| 4 | 46 | F | 2820 |
| 5 | 33 | F | 4552 |
Neste exemplo, as cinco pessoas satisfazem 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.
selected <- ds[ds$age >= 18 & ds$rts_ms < 4000, ]
selected| id | age | gender | rts_ms | |
|---|---|---|---|---|
| 1 | 1 | 21 | M | 1435 |
| 3 | 3 | 24 | M | 2232 |
| 4 | 4 | 46 | F | 2820 |
Condições com subset()
subset()
Para seleccionar linhas por condições sobre as variáveis da tabela, subset() é mais cómodo: podemos escrever age e rts_ms directamente, sem repetir ds$ em cada condição. O primeiro argumento é a tabela e o segundo é a expressão lógica que cada linha tem de satisfazer.
selected_subset <- subset(ds, age >= 18 & rts_ms < 4000)
selected_subset| id | age | gender | rts_ms | |
|---|---|---|---|---|
| 1 | 1 | 21 | M | 1435 |
| 3 | 3 | 24 | M | 2232 |
| 4 | 4 | 46 | F | 2820 |
Obtemos as mesmas três pessoas do filtro com parênteses rectos. & exige ambas as condições; | permitiria satisfazer uma ou outra. O argumento select permite ainda escolher colunas, conservando uma tabela:
subset() mantém só as linhas cuja condição é TRUE. Se a condição der NA, essa linha fica de fora. Convém decidir se isso corresponde ao critério do estudo, em vez de tratar valores em falta como exclusões automáticas.
Não precisamos de conservar várias versões do mesmo filtro. subset() aparece em código de base R; nos workflows de limpeza, continuaremos com filter() e select().
Uma Coluna como Vector
Para pedir apenas o vector rts_ms, usamos aqui só a condição da idade. Este vector inclui também os tempos de 4000 ms ou mais, que o filtro anterior retirou.
adult_rts <- ds[ds$age >= 18, "rts_ms"]
adult_rts[1] 1435 4352 2232 2820 4552
drop e NA
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.
ages_table <- ds[ , "age", drop = FALSE]
adults_table <- ds[ds$age >= 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().
known_ages <- ds[
!is.na(ds$age) & ds$age >= 18,
,
drop = FALSE
]3 Modificar e Acrescentar
Podemos atribuir valores 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.
prepared <- ds
prepared$rts_s <- prepared$rts_ms / 1000
prepared$age_centered <- prepared$age - mean(prepared$age)A cópia tem agora duas colunas novas. Inspeccionemos as colunas antes de alterar valores individuais. Mantemos a vírgula explícita: o lado das linhas fica vazio e os nomes à direita escolhem as colunas. Também poderá encontrar prepared[c(...)], sem vírgula, que num data.frame selecciona colunas; aqui preferimos mostrar as duas dimensões.
prepared[, c("rts_ms", "rts_s", "age_centered")]| rts_ms | rts_s | age_centered |
|---|---|---|
| 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 |
Podemos também corrigir uma célula, conservando um registo da razão. Suponhamos que a consulta do registo original confirmou 1450 ms para a primeira pessoa. Corrigimos os milissegundos na cópia e voltamos a calcular os segundos, para as duas colunas concordarem. ds conserva o valor recebido, 1435 ms; prepared passa a conter a versão corrigida.
# Hypothetical correction confirmed against the source record: 1450 ms.
prepared[1, "rts_ms"] <- 1450
prepared$rts_s <- prepared$rts_ms / 1000
prepared[1, c("rts_ms", "rts_s")]| rts_ms | rts_s |
|---|---|
| 1450 | 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.
prepared$age_group <- "30_or_older"
prepared[prepared$age < 30, "age_group"] <- "under_30"
prepared[, c("age", "age_group")]| age | age_group |
|---|---|
| 21 | under_30 |
| 32 | 30_or_older |
| 24 | under_30 |
| 46 | 30_or_older |
| 33 | 30_or_older |
Para acrescentar uma linha, rbind() precisa de colunas correspondentes. Para acrescentar uma coluna, cbind() junta por posição. Esta operação associa as linhas pela ordem em que aparecem, sem procurar uma chave relacional.
extra <- rbind(
ds,
data.frame(id = 6, age = 29, gender = "M", rts_ms = 3100)
)
ordered <- cbind(ds, order = seq_len(nrow(ds)))4 Apagar e Renomear
Atribuir NULL apaga uma coluna. Também podemos criar uma nova tabela sem essa coluna e deixar o objecto original intacto. select(ds, -gender) exclui a coluna pelo nome. Para renomear, usamos rename(), com new_name = old_name. Em código de base R, também podemos alterar os nomes através de names():
Os nomes das linhas (row names) 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.
5 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(extra)[1] 6
ncol(prepared)[1] 7
names(prepared)[1] "id" "age" "gender" "rts_ms" "rts_s"
[6] "age_centered" "age_group"
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(prepared)'data.frame': 5 obs. of 7 variables:
$ id : int 1 2 3 4 5
$ age : num 21 32 24 46 33
$ gender : chr "M" "F" "M" "F" ...
$ rts_ms : num 1450 4352 2232 2820 4552
$ rts_s : num 1.45 4.35 2.23 2.82 4.55
$ age_centered: num -10.2 0.8 -7.2 14.8 1.8
$ age_group : chr "under_30" "30_or_older" "under_30" "30_or_older" ...
summary(prepared$rts_s) Min. 1st Qu. Median Mean 3rd Qu. Max.
1.450 2.232 2.820 3.081 4.352 4.552
head(prepared)| id | age | gender | rts_ms | rts_s | age_centered | age_group |
|---|---|---|---|---|---|---|
| 1 | 21 | M | 1450 | 1.450 | -10.2 | under_30 |
| 2 | 32 | F | 4352 | 4.352 | 0.8 | 30_or_older |
| 3 | 24 | M | 2232 | 2.232 | -7.2 | under_30 |
| 4 | 46 | F | 2820 | 2.820 | 14.8 | 30_or_older |
| 5 | 33 | F | 4552 | 4.552 | 1.8 | 30_or_older |
Vejamos também quantos registos havia antes e depois de acrescentar a linha, os tempos convertidos e quantas vezes aparece cada identificador:
nrow(ds)[1] 5
nrow(extra)[1] 6
prepared[, c("id", "rts_ms", "rts_s")]| id | rts_ms | rts_s |
|---|---|---|
| 1 | 1450 | 1.450 |
| 2 | 4352 | 4.352 |
| 3 | 2232 | 2.232 |
| 4 | 2820 | 2.820 |
| 5 | 4552 | 4.552 |
table(ds$id, useNA = "ifany")
1 2 3 4 5
1 1 1 1 1
Devemos encontrar cinco registos antes e seis depois. Os tempos em segundos devem ser positivos e corresponder aos milissegundos divididos por 1000; a primeira linha corrigida tem 1450 ms e 1.45 s. Na tabela de frequências, cada id de 1 a 5 aparece uma vez. useNA = "ifany" mostraria também uma contagem de identificadores em falta, se os houvesse. Se algum resultado for inesperado, investigamos os registos antes de continuar. Estes resultados ajudam a detectar problemas, mas não nos dizem se uma correcção ou exclusão tem justificação científica.
6 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.
measures <- data.frame(id = 1:5, group = c("A", "A", "B", "B", "B"))
table(ds$id, useNA = "ifany")
1 2 3 4 5
1 1 1 1 1
table(measures$id, useNA = "ifany")
1 2 3 4 5
1 1 1 1 1
Em ambas as tabelas, os identificadores de 1 a 5 aparecem uma vez cada, sem valores em falta. É a correspondência um-para-um que esperamos neste exemplo. Agora combinamos pelas colunas id. merge() procura os valores correspondentes, em vez de confiar na ordem das linhas.
combined <- merge(ds, measures, by = "id")
combined| id | age | gender | rts_ms | group |
|---|---|---|---|---|
| 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 |
nrow(ds)[1] 5
nrow(combined)[1] 5
Continuamos com cinco linhas. Na tabela combinada, as pessoas 1 e 2 pertencem ao grupo A e as restantes ao grupo B, como em measures.
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.
7 Exercícios
- Inspeccione
dscomdim(),str()esummary(). Escreva o que cada função acrescenta e o que representa uma linha desta tabela.
Solução 1
dim(ds) # Number of rows and columns.[1] 5 4
str(ds) # Classes and structure of each column.'data.frame': 5 obs. of 4 variables:
$ id : int 1 2 3 4 5
$ age : num 21 32 24 46 33
$ gender: chr "M" "F" "M" "F" ...
$ rts_ms: num 1435 4352 2232 2820 4552
summary(ds) # Compact summaries, including ranges and missing values. id age gender 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
- Use
filter()eselect()para seleccionariderts_mspara idades inferiores a 40. Verifique dimensões e primeiras linhas. Explique o papel de cada função. Depois compare o acesso à colunarts_msdedscom$e com[, "rts_ms"].
Solução 2
[1] 4 2
head(selected)| id | rts_ms |
|---|---|
| 1 | 1435 |
| 2 | 4352 |
| 3 | 2232 |
| 5 | 4552 |
rts_dollar <- ds$rts_ms
rts_brackets <- ds[, "rts_ms"]filter() escolhe as linhas e select() conserva as duas colunas, numa tabela. $ e [, "rts_ms"] devolvem aqui o vector da coluna, porque ds é um data.frame. Num tibble, a selecção com [ conservaria uma tabela.
- Crie
checkeda partir deds, acrescenterts_s, altere uma célula documentada e escreva duas verificações.
Solução 3
checked <- ds
checked$rts_s <- checked$rts_ms / 1000
# Hypothetical source-confirmed correction; keep both units consistent.
checked[1, "rts_ms"] <- 1450
checked$rts_s <- checked$rts_ms / 1000
nrow(ds)[1] 5
nrow(checked)[1] 5
checked[, c("id", "rts_ms", "rts_s")]| id | rts_ms | rts_s |
|---|---|---|
| 1 | 1450 | 1.450 |
| 2 | 4352 | 4.352 |
| 3 | 2232 | 2.232 |
| 4 | 2820 | 2.820 |
| 5 | 4552 | 4.552 |
- Apague
gendercomNULLnuma cópia e, separadamente, por selecção. Comparenames()nos três objectos.
Solução 4
[1] "id" "age" "gender" "rts_ms"
names(without_gender_null)[1] "id" "age" "rts_ms"
names(without_gender_selection)[1] "id" "age" "rts_ms"
ds conserva gender. As duas cópias ficam sem essa coluna. Nenhuma operação altera o objecto original.
- Renomeie
rts_mspararesponse_time_mssem alterards. Que objecto tem a coluna antiga?
Solução 5
A coluna antiga continua emds. Em renamed, chama-se response_time_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
measures <- data.frame(
id = 1:5,
group = c("A", "A", "B", "B", "B")
)
combined <- merge(ds, measures, by = "id")
nrow(combined)[1] 5
measures_dup <- rbind(measures, data.frame(id = 1, group = "C"))
combined_dup <- merge(ds, measures_dup, by = "id")
nrow(combined_dup)[1] 6
id == 1, essa linha de ds 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.
Solução 7
| id | group | |
|---|---|---|
| 3 | 1 | B |
| 2 | 2 | A |
| 1 | 3 | A |
| 4 | 4 | B |
| 5 | 5 | B |
cbind() junta a primeira linha com a primeira linha, sem procurar o mesmo id. Aqui, a pessoa 1 fica com B e a pessoa 3 com A, ao contrário da tabela measures. Os valores parecem plausíveis, mas pertencem à pessoa errada. Para combinar por chave, use merge() ou um join.