(Re)Formatação de Bases de Dados

Programação
Tratamento de dados
workflows
Transformar variáveis e passar dados entre formatos largo e longo no R.
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 Reformatação: Mudar a Forma sem Perder o Sentido

Depois de limpar os dados, ainda podemos precisar de os reorganizar. Nesta página vamos transformar variáveis e passar entre formatos largo (wide) e longo (long). Há uma pergunta que vale a pena fazer antes e depois de cada transformação: o que representa uma linha desta tabela?

Mudar a forma da tabela não muda, por si só, aquilo que foi medido. O tidyr é especialmente útil quando condições, momentos ou medidas ficaram codificados nos nomes das colunas. Mas nenhuma função consegue decidir por nós qual é a unidade observacional, a chave ou a escala que fazem sentido no estudo. A limpeza de dados trata da origem e dos critérios de inclusão. A introdução aos data.frames cobre a selecção básica antes de chegarmos aos pivôs.

Instale o pacote uma vez, fora do script, se necessário:

Um Pequeno Conjunto de Ensaios

Cada linha representa uma pessoa e t1, t2 e t3 são tempos de resposta em milissegundos. Os valores são inventados: servem para mostrar a estrutura, não para tirar conclusões sobre respostas humanas. Por agora, uma linha significa «uma pessoa». Depois do pivô, vai passar a significar «uma pessoa num ensaio».

set.seed(123)
ds <- data.frame(
  pp = paste0("pp", sprintf("%02d", 1:10)),
  t1 = round(rnorm(10, 1000, 100)),
  t2 = round(rnorm(10, 1000, 100)),
  t3 = round(rnorm(10, 1000, 100)),
  cond = rep(c("controlo", "experimental"), each = 5)
)

head(ds)
pp t1 t2 t3 cond
pp01 944 1122 893 controlo
pp02 977 1036 978 controlo
pp03 1156 1040 897 controlo
pp04 1007 1011 927 controlo
pp05 1013 944 937 controlo
pp06 1172 1179 831 experimental

Transformações: Preservar o Original e Documentar a Escala

Uma conversão de unidade pode criar uma coluna nova para que o antes e o depois continuem lado a lado. O mesmo cuidado ajuda com qualquer variável derivada: transformar não é uma etapa obrigatória para tornar os dados “melhores”. Tem de haver uma razão para mudar a escala.

# Create seconds while preserving the original millisecond variables.
ds$t1_seg <- ds$t1 / 1000
ds$t2_seg <- ds$t2 / 1000
ds$t3_seg <- ds$t3 / 1000

# Create a condition indicator with an explicit, inspectable rule.
ds$cond_experimental <- ds$cond == "experimental"

# Reverse-score a 0--100 item without overwriting the original.
item <- c(0, 25, 50, 75, 100)
item_invertido <- 100 - item

# A log transform changes the response scale; check positivity first.
stopifnot(all(ds$t1 > 0))
ds$log_t1 <- log(ds$t1)
ds[, c("pp", "t1", "t1_seg", "cond_experimental", "log_t1")]
pp t1 t1_seg cond_experimental log_t1
pp01 944 0.944 FALSE 6.850126
pp02 977 0.977 FALSE 6.884487
pp03 1156 1.156 FALSE 7.052721
pp04 1007 1.007 FALSE 6.914731
pp05 1013 1.013 FALSE 6.920672
pp06 1172 1.172 TRUE 7.066467
pp07 1046 1.046 TRUE 6.952729
pp08 873 0.873 TRUE 6.771936
pp09 931 0.931 TRUE 6.836259
pp10 955 0.955 TRUE 6.861711

Também podemos padronizar uma coluna. scale() transforma os valores em z-scores: a nova variável fica centrada em 0 e cada unidade corresponde a um desvio-padrão. Isto é muitas vezes útil quando queremos pôr variáveis em escalas comparáveis.

# Transform t1 into z-scores.
ds$t1_z <- as.numeric(scale(ds$t1))

ds[, c("t1", "t1_z")]
t1 t1_z
944 -0.6630503
977 -0.3179295
1156 1.5540895
1007 -0.0041833
1013 0.0585660
1172 1.7214208
1046 0.4036868
873 -1.4055830
931 -0.7990070
955 -0.5480100

Por exemplo, um t1_z próximo de 1 indica um valor cerca de um desvio-padrão acima da média. Um valor próximo de -1, cerca de um desvio-padrão abaixo. Para já, é esta a ideia importante. Se mais tarde precisar de reutilizar exactamente a mesma média e o mesmo desvio-padrão noutro conjunto de dados, aí vale a pena olhar com mais detalhe para o que scale() guarda.

Largo para Longo: Uma Linha Passa a Ser uma Medição

O formato largo é cómodo para leitura: uma pessoa por linha e uma medição por coluna. Para representar ensaios, fazer gráficos ou ajustar muitos modelos, é frequentemente mais claro ter uma linha por medição. O ponto importante é não confundir a nova forma da tabela com novas unidades independentes. Três linhas da mesma pessoa continuam a ser três medições da mesma pessoa.

ImportanteLinha, Chave e Duplicação

Antes do pivô, uma linha representa uma pessoa e pp é a chave esperada. Depois do pivô, uma linha representa uma pessoa num ensaio e a chave esperada passa a ser pp + ensaio. Uma combinação repetida pode indicar uma duplicação acidental ou uma dimensão que falta na chave. Não a agregue só para fazer desaparecer um aviso.

Escreva o significado da linha e a chave antes de transformar, e confirme-os outra vez no resultado.

ds <- pivot_longer(ds, cols = c(t1, t2, t3), names_to = "ensaio",
                   values_to = "tempo_ms")

dplyr::glimpse(ds)
Rows: 30
Columns: 10
$ pp                <chr> "pp01", "pp01", "pp01", "pp02", "pp02", "pp02", "pp0…
$ cond              <chr> "controlo", "controlo", "controlo", "controlo", "con…
$ t1_seg            <dbl> 0.944, 0.944, 0.944, 0.977, 0.977, 0.977, 1.156, 1.1…
$ t2_seg            <dbl> 1.122, 1.122, 1.122, 1.036, 1.036, 1.036, 1.040, 1.0…
$ t3_seg            <dbl> 0.893, 0.893, 0.893, 0.978, 0.978, 0.978, 0.897, 0.8…
$ cond_experimental <lgl> FALSE, FALSE, FALSE, FALSE, FALSE, FALSE, FALSE, FAL…
$ log_t1            <dbl> 6.850126, 6.850126, 6.850126, 6.884487, 6.884487, 6.…
$ t1_z              <dbl> -0.663050308, -0.663050308, -0.663050308, -0.3179294…
$ ensaio            <chr> "t1", "t2", "t3", "t1", "t2", "t3", "t1", "t2", "t3"…
$ tempo_ms          <dbl> 944, 1122, 893, 977, 1036, 978, 1156, 1040, 897, 100…
dplyr::count(ds, pp)
pp n
pp01 3
pp02 3
pp03 3
pp04 3
pp05 3
pp06 3
pp07 3
pp08 3
pp09 3
pp10 3

glimpse() mostra os tipos e as dimensões sem despejar todas as linhas na página. count(ds, pp) confirma que cada participante tem três medições. Não é uma contagem de participantes independentes. pivot_longer() devolve um tibble. Quando o imprimimos na consola, a sua representação compacta começa habitualmente por algo como # A tibble: 30 × 10: isto indica 30 linhas e 10 colunas neste exemplo, não 30 pessoas independentes. No Quarto, a saída pode aparecer como uma tabela formatada. Por isso usamos também glimpse(), que explicita as dimensões e os tipos das colunas. count(ds, pp) resume quantas linhas pertencem a cada participante: neste exemplo, esperamos que cada pp tenha 3.

Agora cada participante tem três linhas: ensaio diz-nos de que coluna veio o valor e tempo_ms guarda a medição. O grão da linha mudou de pessoa para pessoa × ensaio. Não criámos três participantes. pp continua a identificar a pessoa e é a unidade que agrupa as medidas repetidas.

A contagem ajuda a inspeccionar o resultado, mas não substitui uma verificação explícita da chave. Aqui, a combinação pp + ensaio deve aparecer uma só vez:

chave <- c("pp", "ensaio")
stopifnot(anyDuplicated(ds[chave]) == 0)

Da Forma ao Modelo

Para dados completos e equilibrados, com uma observação por pessoa × ensaio, a nova forma permite escrever uma única variável de resposta, tempo_ms, e uma variável que identifica o ensaio. Tornamos pp e ensaio factores explícitos. Como estas são medidas repetidas da mesma pessoa, a análise deve reservar essa estrutura no modelo. Não devemos tratar as 30 linhas como 30 pessoas independentes.

ds$pp <- factor(ds$pp)
ds$ensaio <- factor(ds$ensaio)

modelo_ensaios <- afex::aov_4(tempo_ms ~ ensaio + (ensaio | pp), ds)

modelo_ensaios
Anova Table (Type 3 tests)

Response: tempo_ms
  Effect          df      MSE    F  ges p.value
1 ensaio 1.44, 12.99 14761.80 1.05 .080    .354
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '+' 0.1 ' ' 1

Sphericity correction method: GG 

Este exemplo liga a reformatação ao workflow de medidas repetidas. Não é uma regra para todos os delineamentos. A fórmula diz que ensaio varia dentro de pp, que agrupa as três medições de cada pessoa. Para compreender a estrutura do modelo e as suas limitações, consulte a página teórica. A referência prática de medidas repetidas mostra como continuar quando há mais factores, dados incompletos ou uma pergunta sobre as mudanças entre pessoas.

A atribuição a ds substituiu a forma larga pela forma longa neste exemplo. As medições são as mesmas. Mudou o grão de cada linha. Quando precisarmos da forma larga outra vez, pivot_wider() criará um novo objecto. names_to e values_to dão nomes legíveis às duas colunas que o pivô cria.

Longo para Largo: A Chave Determina o Resultado

pivot_wider() usa names_from para escolher os nomes das novas colunas e values_from para escolher os valores. id_cols permite dizer explicitamente que colunas identificam cada linha que queremos obter.

largos <- pivot_wider(
  ds,
  id_cols = c(pp, cond),
  names_from = ensaio,
  values_from = tempo_ms
)

Podemos verificar a estrutura e a dimensão do resultado assim. Como largos é um tibble, a sua impressão também mostrará de forma compacta o número de linhas e colunas:

print(largos)
# A tibble: 10 × 5
   pp    cond            t1    t2    t3
   <fct> <chr>        <dbl> <dbl> <dbl>
 1 pp01  controlo       944  1122   893
 2 pp02  controlo       977  1036   978
 3 pp03  controlo      1156  1040   897
 4 pp04  controlo      1007  1011   927
 5 pp05  controlo      1013   944   937
 6 pp06  experimental  1172  1179   831
 7 pp07  experimental  1046  1050  1084
 8 pp08  experimental   873   803  1015
 9 pp09  experimental   931  1070   886
10 pp10  experimental   955   953  1125
stopifnot(nrow(largos) == 10)
names(largos)
[1] "pp"   "cond" "t1"   "t2"   "t3"  

Em dados reais, vários valores para a mesma combinação de pp e ensaio dizem-nos que a chave está incompleta ou que há duplicações. Conte-os antes de alargar. Separadamente, confirme que cada pp tem uma só cond, porque a condição é constante dentro da pessoa neste exemplo:

duplicados <- ds[duplicated(ds[c("pp", "ensaio")]) |
                 duplicated(ds[c("pp", "ensaio")], fromLast = TRUE), ]
nrow(duplicados)
[1] 0
cond_por_pp <- unique(ds[c("pp", "cond")])
stopifnot(anyDuplicated(cond_por_pp["pp"]) == 0)

Não resolva automaticamente este diagnóstico com values_fn = mean. Primeiro perceba porque existem vários valores e o que significaria fazer a média deles. Depois de qualquer pivô, confira dimensões, nomes e a unicidade da chave pp + ensaio que espera encontrar.

Um pivô correr sem erro não prova que a reformatação está correcta. Compare o número esperado de linhas e colunas antes e depois e investigue qualquer combinação repetida da chave indicada no aviso acima.

As técnicas seguintes são úteis quando os nomes das colunas codificam mais de uma dimensão ou medida. Se um pivô simples já resolve o seu problema, pode saltar esta secção e voltar quando encontrar uma tabela mais teimosa.

Padrões nos Nomes: names_sep e names_pattern

Quando os nomes têm várias dimensões, names_to pode receber várias colunas. Com names_sep separamos por um delimitador fixo. Com names_pattern usamos os grupos de captura duma expressão regular.

medidas <- 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)
)

# Split each name into measure, difficulty, and length with names_sep.
por_sep <- pivot_longer(
  medidas,
  -pp,
  names_to = c("medida", "dificuldade", "comprimento"),
  names_sep = "_",
  values_to = "valor"
)
por_sep
pp medida dificuldade comprimento valor
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
# Capture the same dimensions with a regular expression.
por_padrao <- pivot_longer(
  medidas,
  -pp,
  names_pattern = "(rt)_(easy|hard)_(short|long)",
  names_to = c("medida", "dificuldade", "comprimento"),
  values_to = "valor"
)

A expressão regular funciona como um contrato: confirme que todos os nomes seleccionados correspondem ao padrão. Um nome que não corresponda pode produzir NA na informação extraída ou denunciar que seleccionámos uma coluna que não devia estar ali.

Várias Colunas de Valores e .value

Às vezes uma parte do nome não é um nível da variável, mas o nome da própria coluna de valores. O marcador especial .value diz ao pivot_longer() para criar uma coluna para essa parte.

condicoes <- 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)
)

# Make rt and acc value columns, with difficulty as the repeated dimension.
condicoes_longas <- pivot_longer(
  condicoes,
  -pp,
  names_to = c(".value", "dificuldade"),
  names_sep = "_"
)
condicoes_longas
pp dificuldade 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

O mesmo princípio pode ser escrito com names_pattern, o que é útil quando o separador não é regular. Em vez de guardar tudo numa coluna valor, esta forma mantém separadas medidas como rt e acc.

condicoes_padrao <- pivot_longer(
  condicoes,
  -pp,
  names_pattern = "(rt|acc)_(easy|hard)",
  names_to = c(".value", "dificuldade")
)

Para alargar várias medidas, passe vários nomes em values_from. names_from também pode ter várias colunas. names_sep controla como os níveis aparecem nos nomes finais.

longas_com_grupo <- expand.grid(
  pp = 1:3,
  dificuldade = c("easy", "hard"),
  bloco = c("A", "B")
)
longas_com_grupo$rt <- 450 + 40 * (longas_com_grupo$dificuldade == "hard") +
  10 * (longas_com_grupo$bloco == "B") + longas_com_grupo$pp
longas_com_grupo$acc <- .95 - .10 * (longas_com_grupo$dificuldade == "hard") -
  .02 * (longas_com_grupo$bloco == "B")

largas_multiplas <- pivot_wider(
  longas_com_grupo,
  id_cols = pp,
  names_from = c(dificuldade, bloco),
  values_from = c(rt, acc),
  names_sep = "_"
)
names(largas_multiplas)
[1] "pp"         "rt_easy_A"  "rt_hard_A"  "rt_easy_B"  "rt_hard_B" 
[6] "acc_easy_A" "acc_hard_A" "acc_easy_B" "acc_hard_B"

A escolha de id_cols, names_from e values_from descreve a chave e as medidas, não apenas o aspecto final da tabela. Se a combinação não for única, pivot_wider() pode criar uma lista ou pedir uma função de agregação. Pare e investigue antes de escolher uma função só para fazer o aviso desaparecer.

Pivôs em Etapas: Tornar o Caminho Legível

Nem sempre compensa transformar tudo numa única chamada. Às vezes é mais fácil auditar o que fizemos se alongarmos primeiro para limpar os nomes e só depois alargarmos as dimensões de que precisamos. Este exemplo começa com dois momentos e duas medidas numa tabela larga.

confusos <- data.frame(
  pp = 1:3,
  idade = c(25, 30, 35),
  pre_ansiedade = c(15, 20, 18),
  pre_depressao = c(10, 15, 12),
  pos_ansiedade = c(10, 15, 14),
  pos_depressao = c(5, 10, 8)
)

# Stage 1: extract time and measure from the column names.
longos_limpos <- pivot_longer(
  confusos,
  -c(pp, idade),
  names_pattern = "(pre|pos)_(ansiedade|depressao)",
  names_to = c("momento", "medida"),
  values_to = "pontuacao"
)

# Stage 2: widen only the dimension needed by the next analysis.
comparacao_momentos <- pivot_wider(
  longos_limpos,
  id_cols = c(pp, idade, medida),
  names_from = momento,
  values_from = pontuacao
)
comparacao_momentos
pp idade medida pre pos
1 25 ansiedade 15 10
1 25 depressao 10 5
2 30 ansiedade 20 15
2 30 depressao 15 10
3 35 ansiedade 18 14
3 35 depressao 12 8

Também podemos fazer o percurso inverso: alargar primeiro duas medidas e alongar depois para uma coluna de valores. O importante é que, em cada etapa, consigamos dizer o que significa uma linha e inspeccionar se essa interpretação continua a fazer sentido.

# Widen by measure, keeping one row per person and moment.
por_medida <- pivot_wider(
  longos_limpos,
  id_cols = c(pp, idade, momento),
  names_from = medida,
  values_from = pontuacao
)

# Then gather the selected measures again for a downstream comparison.
novamente_longos <- pivot_longer(
  por_medida,
  c(ansiedade, depressao),
  names_to = "medida",
  values_to = "pontuacao"
)
stopifnot(nrow(novamente_longos) == nrow(longos_limpos))

Salvaguardas Antes e Depois

Antes de transformar, responda a estas perguntas. Vale a pena escrevê-las num comentário junto do código: uma tabela muito elegante também consegue contar uma história errada.

  1. O que representa cada linha neste momento?
  2. Que colunas identificam inequivocamente essa linha?
  3. Que nomes contêm dimensões e quais contêm medidas?
  4. Quantas linhas e combinações esperamos no resultado?

Depois, inspeccione names(), dim(), alguns casos e a unicidade da nova chave. Para uma chave pp, momento e medida, por exemplo:

chave <- c("pp", "momento", "medida")
anyDuplicated(longos_limpos[chave])
[1] 0
# A tibble printout gives a compact dimensions check after each pivot.
longos_limpos
pp idade momento medida pontuacao
1 25 pre ansiedade 15
1 25 pre depressao 10
1 25 pos ansiedade 10
1 25 pos depressao 5
2 30 pre ansiedade 20
2 30 pre depressao 15
2 30 pos ansiedade 15
2 30 pos depressao 10
3 35 pre ansiedade 18
3 35 pre depressao 12
3 35 pos ansiedade 14
3 35 pos depressao 8
comparacao_momentos
pp idade medida pre pos
1 25 ansiedade 15 10
1 25 depressao 10 5
2 30 ansiedade 20 15
2 30 depressao 15 10
3 35 ansiedade 18 14
3 35 depressao 12 8

anyDuplicated() == 0 diz-nos que a chave é única. Não demonstra que essa é a chave cientificamente correcta. Compare também alguns valores conhecidos antes e depois. Uma coluna trocada pode continuar a produzir uma tabela perfeitamente válida aos olhos do R.

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

Exercícios

  1. Crie t1_min e t2_min, preservando os milissegundos. Explique o factor de conversão e indique as unidades antes e depois da transformação.
  2. Passe ds para longo e escreva uma frase que defina exactamente uma linha do resultado. Verifique a chave mínima pp–ensaio e confirme separadamente que cada pp tem uma só cond antes de voltar a largo. Passe o resultado a um lm() como no exemplo, explique por que pp está na fórmula e confirme que o objecto largo não foi alterado.
  3. Construa uma tabela com nomes como score_pre, score_pos, tempo_pre e tempo_pos. Use .value para obter colunas score e tempo, e confirme que cada pessoa e momento aparece uma vez.
  4. Use names_pattern para separar nomes com três dimensões, por exemplo rt_easy_short. Faça uma tabela de contagens dos níveis extraídos e investigue qualquer NA.
  5. Faça um pivot_wider() com vários names_from e values_from. Antes de agregar duplicados, crie um pequeno exemplo que contenha uma chave repetida; explique por que a média, a soma ou nenhuma agregação seria apropriada.
  6. Reorganize confusos em duas etapas diferentes das mostradas. Compare as dimensões e as chaves depois de cada etapa e descreva qual versão é mais fácil de auditar.
  7. Use scale() para transformar uma variável quantitativa em z-scores. Veja alguns valores antes e depois e explique o que significam aproximadamente z = 0, z = 1 e z = -1.
  8. Se usar um colega ou um LLM para diagnosticar uma chave duplicada, mostre primeiro o código que tentou e peça ajuda para identificar a unidade observacional e a chave antes de pedir uma solução pronta.

Soluções

Exercício 1
base_wide <- largos
base_wide$t1_min <- base_wide$t1 / 60000
base_wide$t2_min <- base_wide$t2 / 60000
base_wide[, c("t1", "t1_min", "t2", "t2_min")]
t1 t1_min t2 t2_min
944 0.0157333 1122 0.0187000
977 0.0162833 1036 0.0172667
1156 0.0192667 1040 0.0173333
1007 0.0167833 1011 0.0168500
1013 0.0168833 944 0.0157333
1172 0.0195333 1179 0.0196500
1046 0.0174333 1050 0.0175000
873 0.0145500 803 0.0133833
931 0.0155167 1070 0.0178333
955 0.0159167 953 0.0158833
Há 60000 milissegundos num minuto. As colunas originais continuam em milissegundos; as novas estão em minutos.
Exercício 2

Aqui usamos o objecto largo guardado anteriormente para não alterar o original. Uma linha de dados_longos representa uma pessoa num ensaio.

largo_original <- largos

dados_longos <- pivot_longer(
  largo_original,
  cols = c(t1, t2, t3),
  names_to = "ensaio",
  values_to = "tempo_ms"
)

stopifnot(anyDuplicated(dados_longos[c("pp", "ensaio")]) == 0)
stopifnot(anyDuplicated(largo_original[c("pp", "cond")]) == 0)

modelo_lm <- lm(tempo_ms ~ ensaio + pp, data = dados_longos)
modelo_lm

Call:
lm(formula = tempo_ms ~ ensaio + pp, data = dados_longos)

Coefficients:
(Intercept)     ensaiot2     ensaiot3       pppp02       pppp03       pppp04  
    998.567       13.400      -50.100       10.667       44.667       -4.667  
     pppp05       pppp06       pppp07       pppp08       pppp09       pppp10  
    -21.667       74.333       73.667      -89.333      -24.000       24.667  
volta_largo <- pivot_wider(
  dados_longos, id_cols = c(pp, cond),
  names_from = ensaio, values_from = tempo_ms
)
stopifnot(isTRUE(all.equal(volta_largo, largo_original)))
stopifnot(identical(largo_original, largos))
pp entra na fórmula para representar diferenças sistemáticas entre pessoas; as três linhas da mesma pessoa não são observações independentes. Um modelo misto, como o aov_4() mostrado acima, é uma alternativa mais apropriada quando queremos modelar explicitamente as medidas repetidas.
Exercício 3
medidas <- data.frame(
  pp = 1:3,
  score_pre = c(12, 15, 11), score_pos = c(14, 16, 13),
  tempo_pre = c(600, 550, 620), tempo_pos = c(580, 530, 610)
)

medidas_longas <- pivot_longer(
  medidas, -pp,
  names_to = c(".value", "momento"),
  names_sep = "_"
)
medidas_longas
pp momento score tempo
1 pre 12 600
1 pos 14 580
2 pre 15 550
2 pos 16 530
3 pre 11 620
3 pos 13 610
stopifnot(anyDuplicated(medidas_longas[c("pp", "momento")]) == 0)
.value transforma a primeira parte do nome em duas colunas de valores, score e tempo, em vez de juntar ambas numa única coluna valor.
Exercício 4
nomes <- data.frame(
  pp = 1:4,
  rt_easy_short = c(500, 520, 480, 510),
  rt_easy_long  = c(700, 680, 720, 710),
  rt_hard_short = c(650, 640, 670, 660),
  rt_hard_long  = c(900, 880, 920, 910)
)

longos <- pivot_longer(
  nomes, -pp,
  names_to = c("medida", "dificuldade", "comprimento"),
  names_pattern = "^(rt)_(easy|hard)_(short|long)$",
  values_to = "valor"
)
table(longos$dificuldade, longos$comprimento, useNA = "ifany")
      
       long short
  easy    4     4
  hard    4     4
colSums(is.na(longos[c("medida", "dificuldade", "comprimento")]))
     medida dificuldade comprimento 
          0           0           0 

Se surgirem NA, inspeccionamos as linhas correspondentes e comparamos os nomes originais com o padrão, em vez de os remover automaticamente:

longos[is.na(longos$dificuldade) | is.na(longos$comprimento), ]
pp medida dificuldade comprimento valor
Também seria possível usar names_sep = "_" neste exemplo; names_pattern é mais útil quando a separação não é regular.
Exercício 5
repetida <- data.frame(
  pp = c(1, 1, 1, 2), momento = c("pre", "pre", "pos", "pre"),
  grupo = c("A", "A", "A", "B"),
  score = c(10, 12, 14, 9), tempo = c(600, 620, 580, 700)
)

chave <- c("pp", "momento", "grupo")
repetida[duplicated(repetida[chave]) | duplicated(repetida[chave], fromLast = TRUE), ]
pp momento grupo score tempo
1 pre A 10 600
1 pre A 12 620
larga <- pivot_wider(
  repetida, id_cols = pp,
  names_from = c(grupo, momento), values_from = c(score, tempo),
  names_sep = "_"
)
larga
pp score_A_pre score_A_pos score_B_pre tempo_A_pre tempo_A_pos tempo_B_pre
1 10, 12 14 NULL 600, 620 580 NULL
2 NULL NULL 9 NULL NULL 700
A primeira combinação pp–momento–grupo tem duas medições. Não há uma resposta universal: nenhuma agregação é adequada se a repetição for um erro ou se representar unidades que deviam permanecer separadas; a média pode ser adequada para réplicas que queremos resumir, e a soma apenas quando a medida é aditiva. Depois de justificar a decisão, poderíamos usar, por exemplo, values_fn = mean.
Exercício 6

Este percurso extrai primeiro o nome completo e separa-o numa etapa distinta.

por_nome <- pivot_longer(
  confusos, -c(pp, idade),
  names_to = "nome", values_to = "pontuacao"
)
por_nome <- separate(por_nome, nome, into = c("momento", "medida"), sep = "_")

resultado <- pivot_wider(
  por_nome,
  id_cols = c(pp, idade, momento),
  names_from = medida,
  values_from = pontuacao
)

stopifnot(anyDuplicated(por_nome[c("pp", "momento", "medida")]) == 0)
stopifnot(anyDuplicated(resultado[c("pp", "idade", "momento")]) == 0)
dim(por_nome)
[1] 12  5
dim(resultado)
[1] 6 5
names(resultado)
[1] "pp"        "idade"     "momento"   "ansiedade" "depressao"
A versão com names_pattern pode ser mais compacta; esta torna explícita a etapa de separar os nomes, o que pode facilitar a auditoria.
Exercício 7
x <- largos$t1
z <- as.numeric(scale(x))
data.frame(t1 = x, t1_z = z)
t1 t1_z
944 -0.6630503
977 -0.3179295
1156 1.5540895
1007 -0.0041833
1013 0.0585660
1172 1.7214208
1046 0.4036868
873 -1.4055830
931 -0.7990070
955 -0.5480100
z = 0 corresponde aproximadamente à média, z = 1 a um desvio-padrão acima dela e z = -1 a um desvio-padrão abaixo. Poderíamos padronizar outra variável quantitativa, desde que a escolha da média e do desvio-padrão seja adequada à pergunta.
Exercício 8

Primeiro mostramos o diagnóstico que tentámos, sem esconder a estrutura dos dados:

tentativa <- dados_longos[duplicated(dados_longos[c("pp", "ensaio")]) |
                           duplicated(dados_longos[c("pp", "ensaio")],
                                      fromLast = TRUE), ]
tentativa
pp cond ensaio tempo_ms
pedido_ajuda <- paste(
  "Tentei verificar a chave pp-ensaio com duplicated().",
  "Que unidade observacional representam as linhas e que coluna falta,",
  "se alguma, para tornar a chave única? Não proponha ainda uma agregação."
)
pedido_ajuda
[1] "Tentei verificar a chave pp-ensaio com duplicated(). Que unidade observacional representam as linhas e que coluna falta, se alguma, para tornar a chave única? Não proponha ainda uma agregação."
Uma alternativa é pedir a um colega que reveja primeiro a definição da unidade e só depois comparar soluções de código. O objectivo é diagnosticar a chave, não apenas eliminar o aviso.