(Re)Formatação de Bases de Dados

Programação
Tratamento de dados
workflows
Reorganizar tabelas e transformar variáveis no R, da leitura à escrita do resultado.

0 Mudar a Forma dos Dados

Uma tabela pode estar limpa e ainda não ter a forma de que precisamos. Por exemplo, duas fases dum estudo podem estar em colunas separadas, quando nos daria mais jeito ter uma coluna com a fase e outra com a resposta. Vamos fazer essa mudança e guardar o resultado. Depois, reorganizamos uma folha com medidas de duas espécies de briozoários.

Em ambos os casos, a pergunta importante é: o que representa uma linha antes e depois da transformação? Criar mais linhas não cria mais participantes nem mais organismos. Mudamos a organização das mesmas medições.

Use a consola para experimentar: veja algumas linhas, conte os registos, faça a transformação e volte a olhar para o resultado. Guarde no script os passos que quer repetir. As consultas de exploração não precisam de acompanhar todas as execuções. Se mudar os dados ou a transformação, volte a inspeccionar.

Preparar os Ficheiros

Precisamos das operações dos tutoriais de data.frames, de importação e de limpeza. Descarregue demo_ds.csv e bryozoan_raw.csv para a pasta data do seu projecto.

Como nos tutoriais anteriores, os caminhos supõem que trabalhamos a partir de programming, ao lado de data. Em ../data/demo_ds.csv, .. significa subir uma pasta; depois entramos em data e abrimos o ficheiro. Se a sua organização for outra, ajuste os caminhos de leitura e de escrita.

Vamos usar tidyr para reorganizar as tabelas e dplyr para transformar colunas e seleccionar linhas. Se necessário, instale os pacotes uma vez na consola, fora do script, como explicado na referência de pacotes. No início do script, carregamo-los uma só vez:

1 Psicologia: Duas Fases por Pessoa

O ficheiro demo_ds.csv tem uma linha por participante e duas colunas com respostas, phase1 e phase2. Queremos juntar essas respostas numa única coluna, mantendo a indicação da fase a que pertencem. Lemos o ficheiro recebido:

ds <- read.csv("../data/demo_ds.csv")

Na consola, escrever ds imprime a tabela; head(ds) mostra apenas as primeiras linhas. Para tabelas grandes, usamos head(ds) como pré-visualização. nrow() conta as linhas:

head(ds)
pp_number consents age gender cond mood rts phase1 phase2 lab
1 Yes 19 female high 31 2106 18 80 B
2 Yes 23 female high 25 2249 27 83 A
3 Yes 41 female high 28 3248 23 95 B
4 Yes 26 female high 29 1931 30 81 A
5 Yes 26 female high 39 2410 28 88 B
6 Yes 42 female high 27 3451 18 89 A
nrow(ds)
[1] 120

Há 120 linhas. pp_number identifica o participante, enquanto phase1 e phase2 guardam as suas respostas nas duas fases. Chamamos a esta organização formato largo (wide): neste caso, as fases ocupam colunas diferentes. Não vamos aplicar critérios de inclusão neste exemplo; esses passos são tratados na limpeza.

Largo para Longo

Vamos passar a ter uma linha por participante e fase. A função pivot_longer() junta as colunas escolhidas:

ds <- pivot_longer(
  ds, cols = c(phase1, phase2),
  names_to = "phase", values_to = "response"
)

ds é a tabela a transformar. cols indica as duas colunas que vamos alongar. names_to = "phase" dá nome à coluna que recebe os nomes originais, "phase1" e "phase2". values_to = "response" dá nome à coluna que recebe as respostas. As restantes colunas são conservadas e repetidas nas duas linhas de cada pessoa.

Na consola, voltamos a imprimir e a contar, agora depois do pivô:

head(ds)
pp_number consents age gender cond mood rts lab phase response
1 Yes 19 female high 31 2106 B phase1 18
1 Yes 19 female high 31 2106 B phase2 80
2 Yes 23 female high 25 2249 A phase1 27
2 Yes 23 female high 25 2249 A phase2 83
3 Yes 41 female high 28 3248 B phase1 23
3 Yes 41 female high 28 3248 B phase2 95
nrow(ds)
[1] 240

Temos 240 linhas, duas por participante. Este é o formato longo (long) para as fases. pivot_longer() devolve um tibble, uma forma de tabela cuja impressão na consola mostra normalmente só algumas linhas e indica as dimensões no início. Use head(ds[, c("pp_number", "phase", "response")]) para ver juntas as colunas que definem esta mudança. Numa página Quarto, a apresentação de tabelas pode mostrar mais linhas do que a consola; head() limita explicitamente a saída.

Antes, pp_number bastava para identificar uma linha. Agora precisamos de pp_number e phase: são duas respostas da mesma pessoa, não 240 pessoas independentes. Colunas como age ou rts repetem-se porque vieram da linha original. O pivô não recolheu uma nova idade nem um novo tempo de resposta em cada fase.

Esta organização é útil para gráficos e análises com medidas repetidas. Quando quiser analisar as diferenças entre fases, consulte o tutorial de medidas repetidas e a explicação da dependência entre medições.

Guardar o Resultado

Vamos escrever a tabela em ../data/demo_long.csv, num ficheiro separado do recebido. Execute a escrita depois de inspeccionar o resultado. Os blocos de escrita e os scripts completos não são executados automaticamente nesta página.

write.csv(ds, "../data/demo_long.csv", row.names = FALSE)

write.csv() recebe a tabela e o caminho de destino. row.names = FALSE evita acrescentar uma coluna com os números das linhas do R. O identificador pp_number continua nos dados, tal como phase e response.

A escrita não imprime habitualmente uma mensagem de sucesso. Procure demo_long.csv na pasta data. Também podemos voltar a lê-lo na consola:

ds <- read.csv("../data/demo_long.csv")
head(ds)
nrow(ds)
AvisoEscolher o Destino

Se voltar a guardar com o mesmo caminho, write.csv() substitui o ficheiro de destino; não acrescenta linhas no fim. Aqui, demo_ds.csv é apenas lido e demo_long.csv é a saída. Confirme o nome para não gravar por cima do original.

Os exemplos usam data, que já existe. Se preferir uma pasta results, crie-a uma vez pelo gestor de ficheiros ou com dir.create("../results") na consola e ajuste o destino. write.csv() cria o ficheiro, mas não cria a pasta.

O Script Completo

Depois da exploração, guardamos estes passos em wrangle_demo.R. O script é autónomo: com os pacotes já instalados, podemos executá-lo de cima para baixo a partir de programming.

library(tidyr)

ds <- read.csv("../data/demo_ds.csv")

# One row per participant and phase; pp_number identifies the person.
ds <- pivot_longer(
  ds, cols = c(phase1, phase2),
  names_to = "phase", values_to = "response"
)

write.csv(ds, "../data/demo_long.csv", row.names = FALSE)

Ficaram os passos que queremos repetir, sem as consultas da consola. Para recomeçar, lemos novamente o original. Não tentamos alongar uma segunda vez a tabela que já está longa.

2 Biologia: Duas Espécies Lado a Lado

O ficheiro bryozoan_raw.csv contém medidas de estádio (STAGE), corrida (RUN), massa (LOG10MASS) e taxa metabólica (LOG10MR). As medidas estão em dois blocos de colunas, um para Bugula e outro para Watersipora. Queremos uma única tabela com uma coluna species e quatro colunas para as medidas.

Os blocos têm comprimentos diferentes. A posição na folha não emparelha organismos das duas espécies: a primeira linha de Bugula e a primeira de Watersipora apenas ocupam a mesma linha do ficheiro.

Lemos os dados preservando os espaços nos nomes das colunas:

ds <- read.csv(
  "../data/bryozoan_raw.csv", check.names = FALSE
)

Na consola, olhamos para a tabela, os nomes e a contagem:

head(ds)
BUGULA STAGE BUGULA RUN BUGULA LOG10MASS BUGULA LOG10MR WATERSIPORA STAGE WATERSIPORA RUN WATERSIPORA LOG10MASS WATERSIPORA LOG10MR
LARVAE 1 1.124187 -0.0718051 LARVAE 1 1.255952 -0.0615924
LARVAE 1 1.214295 -0.3606221 LARVAE 1 1.289612 -0.0926305
LARVAE 1 1.224337 -0.0769383 LARVAE 1 1.403698 -0.1224853
LARVAE 1 1.282665 -0.1383318 LARVAE 1 1.313851 -0.2449149
LARVAE 1 1.230768 -0.0141452 LARVAE 1 1.289418 0.0110419
LARVAE 1 1.138109 -0.1255545 LARVAE 1 1.200366 -0.2453349
names(ds)
[1] "BUGULA STAGE"          "BUGULA RUN"            "BUGULA LOG10MASS"     
[4] "BUGULA LOG10MR"        "WATERSIPORA STAGE"     "WATERSIPORA RUN"      
[7] "WATERSIPORA LOG10MASS" "WATERSIPORA LOG10MR"  
nrow(ds)
[1] 535

Há 535 linhas e oito colunas. check.names = FALSE impede que read.csv() troque os espaços por pontos. Assim, mantemos nomes como BUGULA STAGE e WATERSIPORA LOG10MASS. Cada nome tem duas partes: a espécie e a medida, separadas por um espaço.

Juntar os Blocos

Vamos usar essas duas partes para organizar a nova tabela:

ds <- pivot_longer(
  ds, cols = everything(),
  names_to = c("species", ".value"), names_sep = " "
)

everything() selecciona todas as colunas, porque todas pertencem aos blocos de medidas. names_sep = " " separa cada nome no espaço literal. A primeira parte vai para species, com valores BUGULA ou WATERSIPORA.

A segunda parte merece atenção: .value é uma indicação especial para usar essa parte como nome duma coluna de valores. Por exemplo, BUGULA STAGE e WATERSIPORA STAGE alimentam a mesma coluna STAGE, mas em linhas de espécies diferentes. O mesmo acontece com RUN, LOG10MASS e LOG10MR. Não precisamos de values_to, porque vamos manter quatro colunas de valores, em vez de juntar medidas diferentes numa só.

Na consola, vemos o que o pivô produziu:

head(ds)
species STAGE RUN LOG10MASS LOG10MR
BUGULA LARVAE 1 1.124187 -0.0718051
WATERSIPORA LARVAE 1 1.255952 -0.0615924
BUGULA LARVAE 1 1.214295 -0.3606221
WATERSIPORA LARVAE 1 1.289612 -0.0926305
BUGULA LARVAE 1 1.224337 -0.0769383
WATERSIPORA LARVAE 1 1.403698 -0.1224853
nrow(ds)
[1] 1070
tail(ds)
species STAGE RUN LOG10MASS LOG10MR
BUGULA LATE 12 -0.8188841 -0.3085966
WATERSIPORA NA NA NA
BUGULA LATE 12 -0.8499565 -0.2873348
WATERSIPORA NA NA NA
BUGULA LATE 12 -0.6472585 -0.3400670
WATERSIPORA NA NA NA

Temos 1070 linhas: duas por posição da folha original. No fim aparecem registos de Bugula intercalados com linhas vazias de Watersipora. Estas últimas têm STAGE igual a "" e as três medidas numéricas iguais a NA. São espaço de preenchimento, necessário para colocar lado a lado blocos com comprimentos diferentes.

Como vimos na limpeza, neste ficheiro os registos de Watersipora terminam na linha 288 do bloco original. Podemos retirar o preenchimento depois de separar as espécies:

# In this file, an empty STAGE marks structural padding, not an organism.
ds <- filter(ds, STAGE != "")

Na consola, imprimimos novamente e contamos o que ficou:

head(ds)
species STAGE RUN LOG10MASS LOG10MR
BUGULA LARVAE 1 1.124187 -0.0718051
WATERSIPORA LARVAE 1 1.255952 -0.0615924
BUGULA LARVAE 1 1.214295 -0.3606221
WATERSIPORA LARVAE 1 1.289612 -0.0926305
BUGULA LARVAE 1 1.224337 -0.0769383
WATERSIPORA LARVAE 1 1.403698 -0.1224853
nrow(ds)
[1] 823
tail(ds)
species STAGE RUN LOG10MASS LOG10MR
BUGULA LATE 12 -0.9563585 -0.3968164
BUGULA LATE 12 -0.8670110 -0.3029143
BUGULA LATE 12 -0.8027612 -0.4486721
BUGULA LATE 12 -0.8188841 -0.3085966
BUGULA LATE 12 -0.8499565 -0.2873348
BUGULA LATE 12 -0.6472585 -0.3400670

Ficaram 823 registos, 535 de Bugula e 288 de Watersipora. Retirámos 247 linhas de preenchimento, não 247 organismos. O filtro usa a estrutura que inspeccionámos neste ficheiro; noutro conjunto de dados, um estádio em falta pode coexistir com medições válidas e não justifica, por si só, excluir o registo.

Cada linha contém agora um registo duma espécie, com estádio, corrida e duas medidas. LOG10MASS e LOG10MR continuam em log10: o pivô não alterou a escala. species identifica a espécie, não um organismo. Nem a posição original, nem a combinação de espécie, estádio e corrida fornecem aqui um identificador individual. Para ligar medições do mesmo organismo ou decidir quais são independentes, precisamos dos registos da recolha; não basta numerar as linhas.

O Script Completo

Guardamos o percurso em wrangle_bryozoans.R. A saída fica em ../data/bryozoans_long.csv, separada de bryozoan_raw.csv. Como no primeiro exemplo, uma nova execução substitui apenas essa saída, desde que conservemos estes caminhos distintos.

library(dplyr)
library(tidyr)

ds <- read.csv(
  "../data/bryozoan_raw.csv", check.names = FALSE
)
ds <- pivot_longer(
  ds, cols = everything(),
  names_to = c("species", ".value"), names_sep = " "
)

# In this file, an empty STAGE marks structural padding, not an organism.
ds <- filter(ds, STAGE != "")

write.csv(ds, "../data/bryozoans_long.csv", row.names = FALSE)

O CSV terá 823 linhas de dados e cinco colunas. Podemos voltar a executar estes scripts para ler os dados recebidos, reorganizar a tabela e guardar o resultado. Quando a forma já servir a sua pergunta, pode continuar com as descritivas e os gráficos. As secções seguintes ficam como referência para outras transformações.

3 Transformar Variáveis

Além de mudar a forma, podemos calcular novas colunas. Uma conversão de unidades é um bom exemplo: rts contém tempos de resposta em milissegundos. Vamos voltar a ler a tabela de psicologia para trabalhar com uma linha por pessoa, sem depender do objecto de biologia:

ds <- read.csv("../data/demo_ds.csv")
head(ds$rts)
[1] 2106 2249 3248 1931 2410 3451

mutate() acrescenta ou transforma colunas. Para conservar os valores originais, damos um nome novo à coluna calculada:

# Keep milliseconds and add seconds.
ds <- mutate(ds, rts_s = rts / 1000)

Na consola, vemos agora os segundos:

head(ds$rts_s)
[1] 2.106 2.249 3.248 1.931 2.410 3.451

Dividimos por 1000 porque há 1000 milissegundos num segundo. Não mudou o número de pessoas nem a duração medida, apenas a unidade. Se quisermos conservar esta transformação no script de psicologia, podemos colocá-la entre a leitura e o pivô. A coluna rts_s será repetida nas duas fases, tal como rts; não passa a ser uma medição específica de cada fase.

Podemos também transformar uma variável sem estar apenas a converter unidades. A escolha deve fazer sentido para a medida e para a análise; não é uma etapa obrigatória da preparação.

scale() permite criar z-scores: subtrai a média e divide pelo desvio-padrão. Cada unidade da nova coluna corresponde a um desvio-padrão. Com os dados de psicologia que acabámos de ler:

ds <- mutate(ds, rts_z = as.numeric(scale(rts)))
head(ds$rts_z)
[1] -1.58639138 -1.40783885 -0.16046836 -1.80489972 -1.20681117  0.09300131

Um valor próximo de 0 está próximo da média; 1 está um desvio-padrão acima e -1 um desvio-padrão abaixo. as.numeric() guarda o resultado como uma coluna numérica simples.

Outra possibilidade é o logaritmo. log() calcula o logaritmo natural, log10() o de base 10. Precisamos de valores positivos: log(0) produz -Inf e números negativos não têm logaritmo real. Neste ficheiro, os tempos são positivos:

range(ds$rts)
[1] 1622 5286
ds <- mutate(ds, log_rts = log(rts))
head(ds$log_rts)
[1] 7.652546 7.718241 8.085795 7.565793 7.787382 8.146419

Ao contrário da conversão para segundos, o logaritmo muda a relação entre distâncias na escala. Não se aplica automaticamente para «corrigir» uma variável. No exemplo de biologia, as duas medidas já estão em log10; não devemos voltar a aplicar o logaritmo por rotina. Para justificar a escala e a interpretação duma transformação, consulte a Teoria da Medida.

4 Longo para Largo

pivot_wider() faz o percurso inverso. Vamos retomar as duas fases, criando a forma longa a partir do ficheiro original. Desta vez guardamos o resultado em phases para o distinguir da versão larga:

phases <- read.csv("../data/demo_ds.csv")
phases <- pivot_longer(
  phases, cols = c(phase1, phase2),
  names_to = "phase", values_to = "response"
)
head(phases)
pp_number consents age gender cond mood rts lab phase response
1 Yes 19 female high 31 2106 B phase1 18
1 Yes 19 female high 31 2106 B phase2 80
2 Yes 23 female high 25 2249 A phase1 27
2 Yes 23 female high 25 2249 A phase2 83
3 Yes 41 female high 28 3248 B phase1 23
3 Yes 41 female high 28 3248 B phase2 95
nrow(phases)
[1] 240

Há 240 linhas. Para voltar a uma linha por participante, escolhemos explicitamente o identificador, a origem dos nomes e a dos valores:

wide <- pivot_wider(
  phases, id_cols = pp_number,
  names_from = phase, values_from = response
)
head(wide)
pp_number phase1 phase2
1 18 80
2 27 83
3 23 95
4 30 81
5 28 88
6 18 89
nrow(wide)
[1] 120

Temos 120 linhas e três colunas: pp_number, phase1 e phase2. id_cols indica o que identifica cada linha do resultado; names_from indica de onde vêm os nomes das novas colunas e values_from indica onde estão os valores. Escolhemos conservar apenas o identificador e as respostas neste exemplo. Outras colunas podem entrar em id_cols se forem constantes dentro da pessoa e quisermos conservá-las.

Se houver várias respostas para o mesmo participante e fase, pivot_wider() pode produzir colunas de listas e um aviso. Primeiro perceba a razão: uma duplicação acidental e vários ensaios legítimos pedem decisões diferentes. No segundo caso, pode faltar uma coluna que identifique o ensaio. Não use values_fn = mean só para fazer desaparecer o aviso; fazer uma média é uma decisão sobre as medições.

5 Referência Avançada

Mergulhar Mais Fundo

Na primeira leitura, continue nos exercícios. Os exemplos abaixo ficam para quando precisar destas técnicas.

As técnicas seguintes ajudam quando os nomes das colunas contêm várias dimensões ou quando precisamos de reorganizar a tabela em etapas. Os dados pequenos desta secção são inventados para mostrar a estrutura. Pode voltar aqui quando encontrar um ficheiro com estes problemas.

names_sep e names_pattern

Quando o separador é regular, names_sep basta, como o espaço nos nomes dos briozoários. names_to pode receber vários nomes, um para cada parte que queremos extrair:

measures <- data.frame(
  pp = 1:3,
  rt_easy_short = c(500, 520, 480),
  rt_easy_long = c(700, 680, 720),
  rt_hard_short = c(650, 640, 670),
  rt_hard_long = c(900, 880, 920)
)
by_separator <- pivot_longer(
  measures, -pp,
  names_to = c("measure", "difficulty", "length"),
  names_sep = "_", values_to = "value"
)
by_separator
pp measure difficulty length value
1 rt easy short 500
1 rt easy long 700
1 rt hard short 650
1 rt hard long 900
2 rt easy short 520
2 rt easy long 680
2 rt hard short 640
2 rt hard long 880
3 rt easy short 480
3 rt easy long 720
3 rt hard short 670
3 rt hard long 920

-pp selecciona todas as colunas excepto pp. Cada nome é dividido nos _: por exemplo, rt_easy_short dá rt, easy e short. Uma linha passa a representar uma pessoa numa combinação de dificuldade e comprimento.

Com names_pattern usamos uma expressão regular (regex), um padrão que descreve o texto. Os parêntesis delimitam partes a extrair, os grupos de captura. O exemplo anterior também pode escrever-se:

by_pattern <- pivot_longer(
  measures, -pp,
  names_pattern = "^(rt)_(easy|hard)_(short|long)$",
  names_to = c("measure", "difficulty", "length"),
  values_to = "value"
)
by_pattern
pp measure difficulty length value
1 rt easy short 500
1 rt easy long 700
1 rt hard short 650
1 rt hard long 900
2 rt easy short 520
2 rt easy long 680
2 rt hard short 640
2 rt hard long 880
3 rt easy short 480
3 rt easy long 720
3 rt hard short 670
3 rt hard long 920

^ marca o início e $ o fim do nome. (rt) captura rt; (easy|hard) captura uma das duas dificuldades, pois | significa «ou». O terceiro grupo captura o comprimento. Os três grupos alimentam, pela mesma ordem, as três colunas de names_to.

Se um nome não corresponder ao padrão, pode aparecer NA na informação extraída. Compare os nomes originais antes de decidir o que fazer. Para experimentar o padrão na consola, grepl() devolve um valor lógico por nome:

grepl(
  "^(rt)_(easy|hard)_(short|long)$",
  c("rt_easy_short", "rt_hard_long", "rt_medium_short", "extra_rt_easy_short")
)
[1]  TRUE  TRUE FALSE FALSE

Obtemos TRUE, TRUE, FALSE, FALSE. As marcas de início e fim impedem que apenas parte dum nome seja aceite. A referência de regex explica outros padrões. Para os explorar visualmente, pode usar regex101, escolhendo PCRE2. Há diferenças entre motores de regex: confirme o padrão final na função de R em que o vai usar.

Separar Medidas e Condições

Nos briozoários, .value usou a segunda parte do nome para criar as colunas de medidas. Também pode ocupar a primeira posição. Aqui, rt e acc são duas medidas, e easy e hard duas dificuldades:

conditions <- data.frame(
  pp = 1:3,
  rt_easy = c(500, 520, 480), acc_easy = c(.90, .85, .95),
  rt_hard = c(800, 760, 820), acc_hard = c(.75, .80, .70)
)
conditions_long <- pivot_longer(
  conditions, -pp,
  names_to = c(".value", "difficulty"), names_sep = "_"
)
conditions_long
pp difficulty rt acc
1 easy 500 0.90
1 hard 800 0.75
2 easy 520 0.85
2 hard 760 0.80
3 easy 480 0.95
3 hard 820 0.70

Temos seis linhas, uma por pessoa e dificuldade, com rt e acc em colunas separadas. Sem .value, juntaríamos tempos e proporções numa coluna de valores, perdendo esta separação entre medidas. O mesmo princípio funciona com regex:

conditions_pattern <- pivot_longer(
  conditions, -pp,
  names_pattern = "^(rt|acc)_(easy|hard)$",
  names_to = c(".value", "difficulty")
)
conditions_pattern
pp difficulty rt acc
1 easy 500 0.90
1 hard 800 0.75
2 easy 520 0.85
2 hard 760 0.80
3 easy 480 0.95
3 hard 820 0.70

Alargar Várias Medidas

values_from pode receber vários nomes. Se names_from também tiver várias colunas, o pivô combina os seus níveis nos nomes finais:

long_with_group <- expand_grid(
  pp = 1:3, difficulty = c("easy", "hard"), block = c("A", "B")
) |>
  mutate(
    rt = 450 + 40 * (difficulty == "hard") + 10 * (block == "B") + pp,
    acc = .95 - .10 * (difficulty == "hard") - .02 * (block == "B")
  )
wide_multiple <- pivot_wider(
  long_with_group, id_cols = pp,
  names_from = c(difficulty, block), values_from = c(rt, acc),
  names_sep = "_"
)
wide_multiple
pp rt_easy_A rt_easy_B rt_hard_A rt_hard_B acc_easy_A acc_easy_B acc_hard_A acc_hard_B
1 451 461 491 501 0.95 0.93 0.85 0.83
2 452 462 492 502 0.95 0.93 0.85 0.83
3 453 463 493 503 0.95 0.93 0.85 0.83

expand_grid() cria todas as combinações dos valores fornecidos; mutate() acrescenta duas medidas inventadas às 12 linhas. O resultado largo tem uma linha por pessoa. Nomes como rt_easy_A indicam a medida, a dificuldade e o bloco. Aqui, names_sep controla o separador dos nomes criados, em vez de separar nomes existentes.

Alongar e Voltar a Alargar

Nem sempre compensa transformar tudo numa única chamada. Esta tabela tem dois momentos e duas medidas por pessoa:

messy <- data.frame(
  pp = 1:3, age = c(25, 30, 35),
  pre_anxiety = c(15, 20, 18), pre_depression = c(10, 15, 12),
  post_anxiety = c(10, 15, 14), post_depression = c(5, 10, 8)
)
messy
pp age pre_anxiety pre_depression post_anxiety post_depression
1 25 15 10 10 5
2 30 20 15 15 10
3 35 18 12 14 8
nrow(messy)
[1] 3

Primeiro separamos momento e medida, ficando com uma pontuação por linha:

clean_long <- pivot_longer(
  messy, -c(pp, age),
  names_pattern = "^(pre|post)_(anxiety|depression)$",
  names_to = c("time", "measure"), values_to = "score"
)
clean_long
pp age time measure score
1 25 pre anxiety 15
1 25 pre depression 10
1 25 post anxiety 10
1 25 post depression 5
2 30 pre anxiety 20
2 30 pre depression 15
2 30 post anxiety 15
2 30 post depression 10
3 35 pre anxiety 18
3 35 pre depression 12
3 35 post anxiety 14
3 35 post depression 8
nrow(clean_long)
[1] 12

Passámos de 3 para 12 linhas: cada pessoa tem dois momentos e duas medidas. Agora podemos escolher que dimensão queremos nas colunas. Para comparar os momentos lado a lado:

time_comparison <- pivot_wider(
  clean_long, id_cols = c(pp, age, measure),
  names_from = time, values_from = score
)
time_comparison
pp age measure pre post
1 25 anxiety 15 10
1 25 depression 10 5
2 30 anxiety 20 15
2 30 depression 15 10
3 35 anxiety 18 14
3 35 depression 12 8
nrow(time_comparison)
[1] 6

Há seis linhas, uma por pessoa e medida, com pre e post em colunas. Se quisermos antes uma linha por pessoa e momento, alargamos a medida:

by_measure <- pivot_wider(
  clean_long, id_cols = c(pp, age, time),
  names_from = measure, values_from = score
)
by_measure
pp age time anxiety depression
1 25 pre 15 10
1 25 post 10 5
2 30 pre 20 15
2 30 post 15 10
3 35 pre 18 12
3 35 post 14 8
nrow(by_measure)
[1] 6

Também há seis linhas, mas o significado é diferente: as colunas de valores são agora anxiety e depression. A contagem ajuda a perceber a mudança; precisamos de olhar para as colunas para saber o que cada linha representa. Os dois percursos são úteis para perguntas diferentes.

Para outros argumentos, consulte a documentação oficial de pivot_longer() e de pivot_wider().

6 Exercícios

Os exercícios 5 e 6 são opcionais e acompanham a referência avançada.

  1. Reconstitua wrangle_demo.R sem copiar o bloco completo. Na consola, veja as primeiras linhas e conte os registos antes e depois do pivô. Explique por que temos 240 linhas mas apenas 120 participantes.
Solução 1 — Participantes e Fases

Cada participante contribui com duas respostas, uma por fase. Por isso há 240 linhas e apenas 120 participantes. pp_number identifica a pessoa, enquanto a combinação pp_number e phase identifica cada linha longa. O pivô mudou a organização dos dados, sem criar novos participantes.

  1. Acrescente rts_s ao script de psicologia, antes do pivô, e guarde num destino novo. Explique as unidades e por que razão o tempo se repete nas duas linhas da mesma pessoa.
Solução 2 — Tempos em Segundos

Dividir rts por 1000 converte milissegundos em segundos. O pivô repete esta informação nas duas linhas da mesma pessoa porque rts pertence à pessoa e não a uma fase específica. Depois da transformação, convém confirmar unidades e valores nas primeiras linhas.

  1. Execute wrangle_bryozoans.R a partir do ficheiro recebido. Abra o CSV de saída. Explique para que serve .value e por que razão retirámos 247 linhas. O que aconteceria se omitíssemos check.names = FALSE mantendo o mesmo names_sep?
Solução 3 — Os Dados de Briozoários

.value mantém STAGE, RUN, LOG10MASS e LOG10MR em colunas diferentes. Ficamos com 823 registos depois de retirar o preenchimento vazio do bloco mais curto. Sem check.names = FALSE, os espaços nos nomes passariam a pontos e names_sep = " " já não os dividiria nas partes pretendidas.

  1. Volte a ler demo_long.csv e use pivot_wider() para obter uma linha por pessoa, apenas com o identificador e as duas respostas. Veja as primeiras linhas e conte os registos antes e depois. Guarde o resultado com outro nome.
Solução 4 — Voltar ao Formato Largo

Depois de ler a saída num objecto phases, podemos escrever pivot_wider(phases, id_cols = pp_number, names_from = phase, values_from = response). Passamos de 240 para 120 linhas, com três colunas. A escrita deve usar um destino diferente e row.names = FALSE.

  1. Na referência avançada, adapte o exemplo de .value a nomes como score_pre, score_post, time_pre e time_post. Explique o que representa cada linha do resultado.
Solução 5 — Separar Medida e Momento

Com nomes como score_pre e time_post, podemos usar names_to = c(".value", "occasion") e names_sep = "_". O resultado tem uma linha por pessoa e momento, com score e time em colunas separadas. A combinação do identificador da pessoa e do momento identifica cada linha.

  1. No exemplo de pivôs em etapas, as duas tabelas finais têm seis linhas. Explique por que razão isso não significa que tenham a mesma organização. Indique as colunas necessárias para identificar uma linha em cada uma.
Solução 6 — A Mesma Contagem, Outra Organização

As duas tabelas finais têm seis linhas, mas time_comparison tem uma linha por pp e measure, enquanto by_measure tem uma linha por pp e time. age descreve a pessoa e não identifica as suas várias linhas. O número de linhas, por si só, não revela a unidade de observação.