Importação e Exportação de Dados

Programação
Referência guiada
Importação de dados
workflows
Importar, verificar e gravar dados quando os ficheiros não cooperam.
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 Importação e Exportação de Dados

Importar dados não é só escolher uma função que combine com a extensão do ficheiro. O R conseguir abrir um ficheiro não significa que o leu como queríamos. Convém confirmar que é o ficheiro certo, no caminho certo, com o separador, a codificação e os tipos que esperávamos.

Nesta página vamos partir de alguns problemas comuns e ver que verificações os detectam. Se ainda não trabalhou com tabelas no R, a introdução aos data.frames explica o tipo de objecto que estas funções vão criar.

Um ficheiro no disco e um objecto na memória são coisas diferentes:

file on disk  ≠  R object in memory

O working directory é o ponto de partida para caminhos relativos, como ../data/demo_ds.csv; .. significa a pasta acima. Um caminho absoluto indica toda a localização e pode funcionar apenas no computador de quem o escreveu. Num projecto, uma estrutura previsível e caminhos relativos costumam ser mais portáveis.

O objecto vive na memória RAM, dentro da sessão R. Alterá-lo não altera o ficheiro original. Só uma operação de escrita, como write.csv(), cria ou substitui um ficheiro. Esta distinção parece básica, mas resolve muita confusão quando começamos a importar e exportar dados.

O Fluxo Mínimo

Na prática, o fluxo é simples: ler, inspeccionar, só depois escrever. Para não confundirmos a origem com o resultado, damos nomes e caminhos diferentes aos dois. Faça estes passos num script. Abrir a folha de cálculo à mão pode ajudar a explorar, mas não deixa um rasto fácil de repetir.

dados <- read.csv("../data/demo_ds.csv")
head(dados)
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
str(dados)
'data.frame':   120 obs. of  10 variables:
 $ pp_number: int  1 2 3 4 5 6 7 8 9 10 ...
 $ consents : chr  "Yes" "Yes" "Yes" "Yes" ...
 $ age      : int  19 23 41 26 26 42 30 18 18 21 ...
 $ gender   : chr  "female" "female" "female" "female" ...
 $ cond     : chr  "high" "high" "high" "high" ...
 $ mood     : int  31 25 28 29 39 27 31 30 25 30 ...
 $ rts      : int  2106 2249 3248 1931 2410 3451 2450 1820 2168 2743 ...
 $ phase1   : int  18 27 23 30 28 18 24 11 29 15 ...
 $ phase2   : int  80 83 95 81 88 89 90 74 98 76 ...
 $ lab      : chr  "B" "A" "B" "A" ...
# Only after checking and transforming the data:
ficheiro_copia <- tempfile("demo_ds_copia_", fileext = ".csv")
write.csv(dados, ficheiro_copia, row.names = FALSE)

Aqui tempfile() cria um caminho temporário para que o exemplo possa ser renderizado sem alterar a pasta data/. Num projecto real, substitua-o por um caminho de saída documentado, diferente do original, e confirme que tem permissão para escrever nessa pasta.

Erro: Ficheiro Não Encontrado

Um caminho relativo parte do working directory da sessão, não da pasta que está aberta no gestor de ficheiros. Não há uma pasta secreta do R: é apenas o ponto a partir do qual ele começa a interpretar caminhos relativos. Antes de mudar nomes ao acaso, veja onde está e teste se o ficheiro existe.

[1] "/builds/rggd/nrb/programming"
caminho <- "../data/demo_ds.csv"
file.exists(caminho)
[1] TRUE

Se file.exists() devolver FALSE, confirme o directório, a grafia, maiúsculas/minúsculas e a localização do ficheiro. Para uma escolha pontual, file.choose() devolve o caminho seleccionado na interface:

caminho <- file.choose()
dados <- read.csv(caminho)

Num projecto partilhado, é normalmente preferível manter uma estrutura previsível e usar caminhos relativos. setwd() pode ser útil numa sessão, mas um setwd() com o caminho pessoal de quem escreveu o script torna-o frágil noutra máquina.

Erro: Formato ou Separador

CSV significa valores separados por um delimitador, mas esse delimitador nem sempre é uma vírgula. Se todas as colunas aparecerem coladas umas às outras, confirme o separador e a codificação antes de concluir que os dados estão estragados.

# Comma: the usual option for read.csv().
dados_csv <- read.csv("../data/demo_ds.csv")

# A tab-separated file uses read.delim().
dados_tsv <- read.delim("../data/lobsters-2016.tsv")

head(dados_csv)
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
head(dados_tsv)
Year Tonnes Pounds.M. Pounds Value Value.M. Price.lb license.holders Traps.M. Boothbay
1950 8325 18.3526 18352600 6412311 6.4123 0.35 5152 0.430 9.9
1951 9416 20.7595 20759500 7214107 7.2141 0.35 4653 0.383 10.8
1952 9088 20.0363 20036300 8511821 8.5118 0.42 5032 0.417 10.1
1953 10115 22.3003 22300300 8411229 8.4112 0.38 5497 0.490 11.1
1954 9828 21.6677 21667700 8087156 8.0872 0.37 5794 0.488 10.2
1955 10305 22.7181 22718100 8716012 8.7160 0.38 6051 0.532 10.0

Se os acentos ou outros caracteres aparecerem corrompidos, confirme também a codificação em que o ficheiro foi gravado. Funções como read.csv() aceitam o argumento fileEncoding; indique-o apenas depois de identificar a codificação, e volte a inspeccionar nomes e valores em vez de presumir que a leitura ficou correcta.

A extensão .xlsx requer um pacote. Instale-o uma vez, fora do script. Depois pode carregar readxl no início da sessão ou chamar a função com o nome do pacote:

# Run installation once, outside the analysis script.
install.packages("readxl")
# Read a small, real sample so the example remains quick to render.
dados_excel <- readxl::read_excel(
    "../data/global-shark-attack.xlsx",
    n_max = 10,
    .name_repair = "minimal"
)
dim(dados_excel)
[1] 10 21
names(dados_excel)
 [1] "Date"                   "Year"                   "Type"                  
 [4] "Country"                "Area"                   "Location"              
 [7] "Activity"               "Name"                   "Sex"                   
[10] "Age"                    "Injury"                 "Fatal (Y/N)"           
[13] "Time"                   "Species"                "Investigator or Source"
[16] "pdf"                    "href formula"           "href"                  
[19] "Case Number"            "Case Number"            "original order"        

Ficheiros .sav e outros formatos de programas estatísticos também precisam de uma função ou pacote adequado. Consulte a documentação e, sobretudo, inspeccione o objecto depois da leitura. Uma função terminar sem erro só nos diz que terminou sem erro.

Erro: Linhas Extra

Algumas plataformas de recolha acrescentam linhas de metadados entre os nomes das colunas e as respostas dos participantes. Quando isso acontece, números e datas podem entrar no R como character porque as primeiras linhas contêm texto.

raw <- read.csv("../data/raw_qualtrics.csv")
head(raw)
StartDate EndDate Status Progress Duration..in.seconds. Finished RecordedDate ResponseId DistributionChannel UserLanguage consents X1_rating_1 X2_rating_1 X3_rating_1 X4_rating_1 X5_rating_1 X6_rating_1 X7_rating_1 X8_rating_1 X9_rating_1 X10_rating_1 X11_rating_1 X12_rating_1 X13_rating_1 X14_rating_1 X15_rating_1 X16_rating_1 X17_rating_1 X18_rating_1 X19_rating_1 X20_rating_1 X21_rating_1 X22_rating_1 X23_rating_1 X24_rating_1 X25_rating_1 pp_age pp_gender
Start Date End Date Response Type Progress Duration (in seconds) Finished Recorded Date Response ID Distribution Channel User Language Click to write the question text [Field-1] - a - ${lm://Field/1} [Field-1] - b - ${lm://Field/1} [Field-1] - c - ${lm://Field/1} [Field-1] - d - ${lm://Field/1} [Field-1] - e - ${lm://Field/1} [Field-1] - f - ${lm://Field/1} [Field-1] - g - ${lm://Field/1} [Field-1] - h - ${lm://Field/1} [Field-1] - j - ${lm://Field/1} [Field-1] - k - ${lm://Field/1} [Field-1] - l - ${lm://Field/1} [Field-1] - m - ${lm://Field/1} [Field-1] - n - ${lm://Field/1} [Field-1] - o - ${lm://Field/1} [Field-1] - p - ${lm://Field/1} [Field-1] - q - ${lm://Field/1} [Field-1] - r - ${lm://Field/1} [Field-1] - s - ${lm://Field/1} [Field-1] - t - ${lm://Field/1} [Field-1] - u - ${lm://Field/1} [Field-1] - v - ${lm://Field/1} [Field-1] - x - ${lm://Field/1} [Field-1] - w - ${lm://Field/1} [Field-1] - y - ${lm://Field/1} [Field-1] - z - ${lm://Field/1} Indique, por favor, a sua idade (utilize apenas números na resposta) Indique, por favor, o seu género
{“ImportId”:“startDate”,“timeZone”:“Europe/London”} {“ImportId”:“endDate”,“timeZone”:“Europe/London”} {“ImportId”:“status”} {“ImportId”:“progress”} {“ImportId”:“duration”} {“ImportId”:“finished”} {“ImportId”:“recordedDate”,“timeZone”:“Europe/London”} {“ImportId”:“_recordId”} {“ImportId”:“distributionChannel”} {“ImportId”:“userLanguage”} {“ImportId”:“QID1”} {“ImportId”:“1_QID4_1”} {“ImportId”:“2_QID4_1”} {“ImportId”:“3_QID4_1”} {“ImportId”:“4_QID4_1”} {“ImportId”:“5_QID4_1”} {“ImportId”:“6_QID4_1”} {“ImportId”:“7_QID4_1”} {“ImportId”:“8_QID4_1”} {“ImportId”:“9_QID4_1”} {“ImportId”:“10_QID4_1”} {“ImportId”:“11_QID4_1”} {“ImportId”:“12_QID4_1”} {“ImportId”:“13_QID4_1”} {“ImportId”:“14_QID4_1”} {“ImportId”:“15_QID4_1”} {“ImportId”:“16_QID4_1”} {“ImportId”:“17_QID4_1”} {“ImportId”:“18_QID4_1”} {“ImportId”:“19_QID4_1”} {“ImportId”:“20_QID4_1”} {“ImportId”:“21_QID4_1”} {“ImportId”:“22_QID4_1”} {“ImportId”:“23_QID4_1”} {“ImportId”:“24_QID4_1”} {“ImportId”:“25_QID4_1”} {“ImportId”:“QID6_TEXT”} {“ImportId”:“QID7”}
2021-11-24 09:55:35 2021-11-24 09:57:05 IP Address 100 89 True 2021-11-24 09:57:05 R_OCLWvHc3mmom6lP anonymous EN Yes 83 69 87 17 80 36 40 87 75 21 27 83 56 75 51 45 92 37 72 31 41 84 89 72 26 Masculino
2021-11-24 09:57:17 2021-11-24 09:58:22 IP Address 100 64 True 2021-11-24 09:58:23 R_3JlTQBcMudLTmJr anonymous EN Yes 30 19 20 68 86 79 83 89 86 66 80 81 45 74 87 33 24 83 29 20 91 80 91 85 17 Feminino
2021-11-24 09:58:25 2021-11-24 09:58:27 IP Address 100 2 True 2021-11-24 09:58:27 R_28Sb2uxBnGAvdQH anonymous EN No
2021-11-24 09:58:28 2021-11-24 10:00:02 IP Address 100 93 True 2021-11-24 10:00:02 R_2dWJC1L9ystwcv3 anonymous EN Yes 41 25 30 38 53 34 57 65 88 93 70 89 92 31 75 28 59 78 74 82 81 92 81 85 76 19 Feminino
str(raw)
'data.frame':   6 obs. of  38 variables:
 $ StartDate            : chr  "Start Date" "{\"ImportId\":\"startDate\",\"timeZone\":\"Europe/London\"}" "2021-11-24 09:55:35" "2021-11-24 09:57:17" ...
 $ EndDate              : chr  "End Date" "{\"ImportId\":\"endDate\",\"timeZone\":\"Europe/London\"}" "2021-11-24 09:57:05" "2021-11-24 09:58:22" ...
 $ Status               : chr  "Response Type" "{\"ImportId\":\"status\"}" "IP Address" "IP Address" ...
 $ Progress             : chr  "Progress" "{\"ImportId\":\"progress\"}" "100" "100" ...
 $ Duration..in.seconds.: chr  "Duration (in seconds)" "{\"ImportId\":\"duration\"}" "89" "64" ...
 $ Finished             : chr  "Finished" "{\"ImportId\":\"finished\"}" "True" "True" ...
 $ RecordedDate         : chr  "Recorded Date" "{\"ImportId\":\"recordedDate\",\"timeZone\":\"Europe/London\"}" "2021-11-24 09:57:05" "2021-11-24 09:58:23" ...
 $ ResponseId           : chr  "Response ID" "{\"ImportId\":\"_recordId\"}" "R_OCLWvHc3mmom6lP" "R_3JlTQBcMudLTmJr" ...
 $ DistributionChannel  : chr  "Distribution Channel" "{\"ImportId\":\"distributionChannel\"}" "anonymous" "anonymous" ...
 $ UserLanguage         : chr  "User Language" "{\"ImportId\":\"userLanguage\"}" "EN" "EN" ...
 $ consents             : chr  "Click to write the question text" "{\"ImportId\":\"QID1\"}" "Yes" "Yes" ...
 $ X1_rating_1          : chr  "[Field-1] - a - ${lm://Field/1}" "{\"ImportId\":\"1_QID4_1\"}" "83" "30" ...
 $ X2_rating_1          : chr  "[Field-1] - b - ${lm://Field/1}" "{\"ImportId\":\"2_QID4_1\"}" "69" "19" ...
 $ X3_rating_1          : chr  "[Field-1] - c - ${lm://Field/1}" "{\"ImportId\":\"3_QID4_1\"}" "87" "20" ...
 $ X4_rating_1          : chr  "[Field-1] - d - ${lm://Field/1}" "{\"ImportId\":\"4_QID4_1\"}" "17" "68" ...
 $ X5_rating_1          : chr  "[Field-1] - e - ${lm://Field/1}" "{\"ImportId\":\"5_QID4_1\"}" "80" "86" ...
 $ X6_rating_1          : chr  "[Field-1] - f - ${lm://Field/1}" "{\"ImportId\":\"6_QID4_1\"}" "36" "79" ...
 $ X7_rating_1          : chr  "[Field-1] - g - ${lm://Field/1}" "{\"ImportId\":\"7_QID4_1\"}" "40" "83" ...
 $ X8_rating_1          : chr  "[Field-1] - h - ${lm://Field/1}" "{\"ImportId\":\"8_QID4_1\"}" "87" "89" ...
 $ X9_rating_1          : chr  "[Field-1] - j - ${lm://Field/1}" "{\"ImportId\":\"9_QID4_1\"}" "75" "86" ...
 $ X10_rating_1         : chr  "[Field-1] - k - ${lm://Field/1}" "{\"ImportId\":\"10_QID4_1\"}" "21" "66" ...
 $ X11_rating_1         : chr  "[Field-1] - l - ${lm://Field/1}" "{\"ImportId\":\"11_QID4_1\"}" "27" "80" ...
 $ X12_rating_1         : chr  "[Field-1] - m - ${lm://Field/1}" "{\"ImportId\":\"12_QID4_1\"}" "83" "81" ...
 $ X13_rating_1         : chr  "[Field-1] - n - ${lm://Field/1}" "{\"ImportId\":\"13_QID4_1\"}" "56" "45" ...
 $ X14_rating_1         : chr  "[Field-1] - o - ${lm://Field/1}" "{\"ImportId\":\"14_QID4_1\"}" "75" "74" ...
 $ X15_rating_1         : chr  "[Field-1] - p - ${lm://Field/1}" "{\"ImportId\":\"15_QID4_1\"}" "" "87" ...
 $ X16_rating_1         : chr  "[Field-1] - q - ${lm://Field/1}" "{\"ImportId\":\"16_QID4_1\"}" "51" "33" ...
 $ X17_rating_1         : chr  "[Field-1] - r - ${lm://Field/1}" "{\"ImportId\":\"17_QID4_1\"}" "45" "24" ...
 $ X18_rating_1         : chr  "[Field-1] - s - ${lm://Field/1}" "{\"ImportId\":\"18_QID4_1\"}" "92" "83" ...
 $ X19_rating_1         : chr  "[Field-1] - t - ${lm://Field/1}" "{\"ImportId\":\"19_QID4_1\"}" "37" "29" ...
 $ X20_rating_1         : chr  "[Field-1] - u - ${lm://Field/1}" "{\"ImportId\":\"20_QID4_1\"}" "72" "20" ...
 $ X21_rating_1         : chr  "[Field-1] - v - ${lm://Field/1}" "{\"ImportId\":\"21_QID4_1\"}" "31" "" ...
 $ X22_rating_1         : chr  "[Field-1] - x - ${lm://Field/1}" "{\"ImportId\":\"22_QID4_1\"}" "41" "91" ...
 $ X23_rating_1         : chr  "[Field-1] - w - ${lm://Field/1}" "{\"ImportId\":\"23_QID4_1\"}" "84" "80" ...
 $ X24_rating_1         : chr  "[Field-1] - y - ${lm://Field/1}" "{\"ImportId\":\"24_QID4_1\"}" "89" "91" ...
 $ X25_rating_1         : chr  "[Field-1] - z - ${lm://Field/1}" "{\"ImportId\":\"25_QID4_1\"}" "72" "85" ...
 $ pp_age               : chr  "Indique, por favor, a sua idade (utilize apenas números na resposta)" "{\"ImportId\":\"QID6_TEXT\"}" "26" "17" ...
 $ pp_gender            : chr  "Indique, por favor, o seu género" "{\"ImportId\":\"QID7\"}" "Masculino" "Feminino" ...
 [1] "StartDate"             "EndDate"               "Status"               
 [4] "Progress"              "Duration..in.seconds." "Finished"             
 [7] "RecordedDate"          "ResponseId"            "DistributionChannel"  
[10] "UserLanguage"          "consents"              "X1_rating_1"          
[13] "X2_rating_1"           "X3_rating_1"           "X4_rating_1"          
[16] "X5_rating_1"           "X6_rating_1"           "X7_rating_1"          
[19] "X8_rating_1"           "X9_rating_1"           "X10_rating_1"         
[22] "X11_rating_1"          "X12_rating_1"          "X13_rating_1"         
[25] "X14_rating_1"          "X15_rating_1"          "X16_rating_1"         
[28] "X17_rating_1"          "X18_rating_1"          "X19_rating_1"         
[31] "X20_rating_1"          "X21_rating_1"          "X22_rating_1"         
[34] "X23_rating_1"          "X24_rating_1"          "X25_rating_1"         
[37] "pp_age"                "pp_gender"            

Se a inspecção mostrar que há linhas extra, remova-as numa cópia e volte a examinar o resultado. O número de linhas a retirar depende do ficheiro que tem à frente; não transforme o -c(1, 2) deste exemplo numa regra geral.

qualtrics <- raw[-c(1, 2), , drop = FALSE]
head(qualtrics)
StartDate EndDate Status Progress Duration..in.seconds. Finished RecordedDate ResponseId DistributionChannel UserLanguage consents X1_rating_1 X2_rating_1 X3_rating_1 X4_rating_1 X5_rating_1 X6_rating_1 X7_rating_1 X8_rating_1 X9_rating_1 X10_rating_1 X11_rating_1 X12_rating_1 X13_rating_1 X14_rating_1 X15_rating_1 X16_rating_1 X17_rating_1 X18_rating_1 X19_rating_1 X20_rating_1 X21_rating_1 X22_rating_1 X23_rating_1 X24_rating_1 X25_rating_1 pp_age pp_gender
3 2021-11-24 09:55:35 2021-11-24 09:57:05 IP Address 100 89 True 2021-11-24 09:57:05 R_OCLWvHc3mmom6lP anonymous EN Yes 83 69 87 17 80 36 40 87 75 21 27 83 56 75 51 45 92 37 72 31 41 84 89 72 26 Masculino
4 2021-11-24 09:57:17 2021-11-24 09:58:22 IP Address 100 64 True 2021-11-24 09:58:23 R_3JlTQBcMudLTmJr anonymous EN Yes 30 19 20 68 86 79 83 89 86 66 80 81 45 74 87 33 24 83 29 20 91 80 91 85 17 Feminino
5 2021-11-24 09:58:25 2021-11-24 09:58:27 IP Address 100 2 True 2021-11-24 09:58:27 R_28Sb2uxBnGAvdQH anonymous EN No
6 2021-11-24 09:58:28 2021-11-24 10:00:02 IP Address 100 93 True 2021-11-24 10:00:02 R_2dWJC1L9ystwcv3 anonymous EN Yes 41 25 30 38 53 34 57 65 88 93 70 89 92 31 75 28 59 78 74 82 81 92 81 85 76 19 Feminino
str(qualtrics)
'data.frame':   4 obs. of  38 variables:
 $ StartDate            : chr  "2021-11-24 09:55:35" "2021-11-24 09:57:17" "2021-11-24 09:58:25" "2021-11-24 09:58:28"
 $ EndDate              : chr  "2021-11-24 09:57:05" "2021-11-24 09:58:22" "2021-11-24 09:58:27" "2021-11-24 10:00:02"
 $ Status               : chr  "IP Address" "IP Address" "IP Address" "IP Address"
 $ Progress             : chr  "100" "100" "100" "100"
 $ Duration..in.seconds.: chr  "89" "64" "2" "93"
 $ Finished             : chr  "True" "True" "True" "True"
 $ RecordedDate         : chr  "2021-11-24 09:57:05" "2021-11-24 09:58:23" "2021-11-24 09:58:27" "2021-11-24 10:00:02"
 $ ResponseId           : chr  "R_OCLWvHc3mmom6lP" "R_3JlTQBcMudLTmJr" "R_28Sb2uxBnGAvdQH" "R_2dWJC1L9ystwcv3"
 $ DistributionChannel  : chr  "anonymous" "anonymous" "anonymous" "anonymous"
 $ UserLanguage         : chr  "EN" "EN" "EN" "EN"
 $ consents             : chr  "Yes" "Yes" "No" "Yes"
 $ X1_rating_1          : chr  "83" "30" "" "41"
 $ X2_rating_1          : chr  "69" "19" "" "25"
 $ X3_rating_1          : chr  "87" "20" "" "30"
 $ X4_rating_1          : chr  "17" "68" "" "38"
 $ X5_rating_1          : chr  "80" "86" "" "53"
 $ X6_rating_1          : chr  "36" "79" "" "34"
 $ X7_rating_1          : chr  "40" "83" "" "57"
 $ X8_rating_1          : chr  "87" "89" "" "65"
 $ X9_rating_1          : chr  "75" "86" "" "88"
 $ X10_rating_1         : chr  "21" "66" "" "93"
 $ X11_rating_1         : chr  "27" "80" "" "70"
 $ X12_rating_1         : chr  "83" "81" "" "89"
 $ X13_rating_1         : chr  "56" "45" "" "92"
 $ X14_rating_1         : chr  "75" "74" "" "31"
 $ X15_rating_1         : chr  "" "87" "" "75"
 $ X16_rating_1         : chr  "51" "33" "" "28"
 $ X17_rating_1         : chr  "45" "24" "" "59"
 $ X18_rating_1         : chr  "92" "83" "" "78"
 $ X19_rating_1         : chr  "37" "29" "" "74"
 $ X20_rating_1         : chr  "72" "20" "" "82"
 $ X21_rating_1         : chr  "31" "" "" "81"
 $ X22_rating_1         : chr  "41" "91" "" "92"
 $ X23_rating_1         : chr  "84" "80" "" "81"
 $ X24_rating_1         : chr  "89" "91" "" "85"
 $ X25_rating_1         : chr  "72" "85" "" "76"
 $ pp_age               : chr  "26" "17" "" "19"
 $ pp_gender            : chr  "Masculino" "Feminino" "" "Feminino"
ficheiro_qualtrics <- tempfile("qualtrics_sem_cabecalho_", fileext = ".csv")
write.csv(qualtrics, ficheiro_qualtrics, row.names = FALSE)

Se uma variável numérica continuar como texto, veja os valores antes de a converter. Uma conversão cega pode transformar valores mal formados em NA e fazer desaparecer precisamente a pista de que havia um problema.

Erro: Importação Não Verificada

Mesmo quando o ficheiro abre, convém confirmar pelo menos:

  • o caminho e o número de linhas e colunas;
  • os nomes das colunas e as suas classes;
  • algumas linhas do início e do fim;
  • valores em falta, intervalos e identificadores duplicados, quando forem relevantes para a pergunta.
stopifnot(file.exists("../data/demo_ds.csv"))
stopifnot(nrow(dados_csv) > 0)
colnames(dados_csv)
 [1] "pp_number" "consents"  "age"       "gender"    "cond"      "mood"     
 [7] "rts"       "phase1"    "phase2"    "lab"      
str(dados_csv)
'data.frame':   120 obs. of  10 variables:
 $ pp_number: int  1 2 3 4 5 6 7 8 9 10 ...
 $ consents : chr  "Yes" "Yes" "Yes" "Yes" ...
 $ age      : int  19 23 41 26 26 42 30 18 18 21 ...
 $ gender   : chr  "female" "female" "female" "female" ...
 $ cond     : chr  "high" "high" "high" "high" ...
 $ mood     : int  31 25 28 29 39 27 31 30 25 30 ...
 $ rts      : int  2106 2249 3248 1931 2410 3451 2450 1820 2168 2743 ...
 $ phase1   : int  18 27 23 30 28 18 24 11 29 15 ...
 $ phase2   : int  80 83 95 81 88 89 90 74 98 76 ...
 $ lab      : chr  "B" "A" "B" "A" ...
tail(dados_csv)
pp_number consents age gender cond mood rts phase1 phase2 lab
115 115 Yes 30 male low 57 3553 88 36 B
116 116 Yes 28 female low 53 4167 92 17 A
117 117 Yes 26 male low 59 4206 107 29 B
118 118 Yes 19 female low 62 3983 87 9 A
119 119 Yes 18 male low 62 2767 94 43 B
120 120 Yes 18 female low 56 5286 88 50 A
colSums(is.na(dados_csv))
pp_number  consents       age    gender      cond      mood       rts    phase1 
        0         0         0         0         0         0         0         0 
   phase2       lab 
        0         0 

Estas verificações não provam que os dados são válidos. Servem para tornar algumas expectativas explícitas e para nos dar um ponto concreto de partida quando alguma coisa muda. A limpeza de dados começa precisamente quando uma destas perguntas revela algo que exige uma decisão.

Exportar sem Perder a Origem

Depois de documentar as decisões, escrevemos uma nova versão. row.names = FALSE evita acrescentar ao CSV uma coluna com os nomes automáticos das linhas.

ficheiro_verificado <- tempfile("qualtrics_verificado_", fileext = ".csv")
write.csv(qualtrics, ficheiro_verificado, row.names = FALSE)
qualtrics_reimportado <- read.csv(ficheiro_verificado)
stopifnot(identical(names(qualtrics), names(qualtrics_reimportado)))
stopifnot(nrow(qualtrics) == nrow(qualtrics_reimportado))
stopifnot(ncol(qualtrics) == ncol(qualtrics_reimportado))

tempfile() mantém este exemplo seguro durante o render. Um percurso de ida e volta por CSV não preserva necessariamente todas as classes e atributos do R; datas, por exemplo, podem precisar de nova conversão depois da leitura. É por isso que vale a pena verificar mais do que a simples existência do novo ficheiro.

No seu projecto, guarde o original, registe as transformações e escreva a versão nova noutro caminho. Assim pode repetir a análise ou rever uma decisão sem depender da memória ou de cliques que já ninguém sabe reconstruir.

Exercícios

  1. Use file.exists() e getwd() para explicar por que razão um caminho relativo pode funcionar num computador e falhar noutro.
Solução 1
caminho <- "../data/demo_ds.csv"
getwd()
[1] "/builds/rggd/nrb/programming"
file.exists(caminho)
[1] TRUE
Um caminho relativo é interpretado a partir do working directory. Se duas sessões começarem em pastas diferentes, a mesma string pode apontar para ficheiros diferentes ou para nenhum ficheiro.
  1. Importe demo_ds.csv, inspeccione-o e escreva três verificações que confirmem que a importação corresponde à sua expectativa.
Solução 2
dados_demo <- read.csv("../data/demo_ds.csv")
head(dados_demo)
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
str(dados_demo)
'data.frame':   120 obs. of  10 variables:
 $ pp_number: int  1 2 3 4 5 6 7 8 9 10 ...
 $ consents : chr  "Yes" "Yes" "Yes" "Yes" ...
 $ age      : int  19 23 41 26 26 42 30 18 18 21 ...
 $ gender   : chr  "female" "female" "female" "female" ...
 $ cond     : chr  "high" "high" "high" "high" ...
 $ mood     : int  31 25 28 29 39 27 31 30 25 30 ...
 $ rts      : int  2106 2249 3248 1931 2410 3451 2450 1820 2168 2743 ...
 $ phase1   : int  18 27 23 30 28 18 24 11 29 15 ...
 $ phase2   : int  80 83 95 81 88 89 90 74 98 76 ...
 $ lab      : chr  "B" "A" "B" "A" ...
stopifnot(file.exists("../data/demo_ds.csv"))
stopifnot(nrow(dados_demo) > 0)
stopifnot(all(c("pp_number", "consents", "age") %in% names(dados_demo)))
Estas verificações confirmam a origem, a existência de registos e a presença de colunas esperadas. A validade científica dos valores exige outras verificações.
  1. Inspeccione raw_qualtrics.csv. Identifique, com código e não apenas à vista, sinais de cabeçalhos extra ou classes inesperadas. Grave uma cópia corrigida sem substituir o original.
Solução 3
raw_exercicio <- read.csv("../data/raw_qualtrics.csv", stringsAsFactors = FALSE)
linhas_extra <- raw_exercicio$StartDate == "Start Date" |
    grepl("ImportId", raw_exercicio$StartDate, fixed = TRUE)

which(linhas_extra)
[1] 1 2
str(raw_exercicio)
'data.frame':   6 obs. of  38 variables:
 $ StartDate            : chr  "Start Date" "{\"ImportId\":\"startDate\",\"timeZone\":\"Europe/London\"}" "2021-11-24 09:55:35" "2021-11-24 09:57:17" ...
 $ EndDate              : chr  "End Date" "{\"ImportId\":\"endDate\",\"timeZone\":\"Europe/London\"}" "2021-11-24 09:57:05" "2021-11-24 09:58:22" ...
 $ Status               : chr  "Response Type" "{\"ImportId\":\"status\"}" "IP Address" "IP Address" ...
 $ Progress             : chr  "Progress" "{\"ImportId\":\"progress\"}" "100" "100" ...
 $ Duration..in.seconds.: chr  "Duration (in seconds)" "{\"ImportId\":\"duration\"}" "89" "64" ...
 $ Finished             : chr  "Finished" "{\"ImportId\":\"finished\"}" "True" "True" ...
 $ RecordedDate         : chr  "Recorded Date" "{\"ImportId\":\"recordedDate\",\"timeZone\":\"Europe/London\"}" "2021-11-24 09:57:05" "2021-11-24 09:58:23" ...
 $ ResponseId           : chr  "Response ID" "{\"ImportId\":\"_recordId\"}" "R_OCLWvHc3mmom6lP" "R_3JlTQBcMudLTmJr" ...
 $ DistributionChannel  : chr  "Distribution Channel" "{\"ImportId\":\"distributionChannel\"}" "anonymous" "anonymous" ...
 $ UserLanguage         : chr  "User Language" "{\"ImportId\":\"userLanguage\"}" "EN" "EN" ...
 $ consents             : chr  "Click to write the question text" "{\"ImportId\":\"QID1\"}" "Yes" "Yes" ...
 $ X1_rating_1          : chr  "[Field-1] - a - ${lm://Field/1}" "{\"ImportId\":\"1_QID4_1\"}" "83" "30" ...
 $ X2_rating_1          : chr  "[Field-1] - b - ${lm://Field/1}" "{\"ImportId\":\"2_QID4_1\"}" "69" "19" ...
 $ X3_rating_1          : chr  "[Field-1] - c - ${lm://Field/1}" "{\"ImportId\":\"3_QID4_1\"}" "87" "20" ...
 $ X4_rating_1          : chr  "[Field-1] - d - ${lm://Field/1}" "{\"ImportId\":\"4_QID4_1\"}" "17" "68" ...
 $ X5_rating_1          : chr  "[Field-1] - e - ${lm://Field/1}" "{\"ImportId\":\"5_QID4_1\"}" "80" "86" ...
 $ X6_rating_1          : chr  "[Field-1] - f - ${lm://Field/1}" "{\"ImportId\":\"6_QID4_1\"}" "36" "79" ...
 $ X7_rating_1          : chr  "[Field-1] - g - ${lm://Field/1}" "{\"ImportId\":\"7_QID4_1\"}" "40" "83" ...
 $ X8_rating_1          : chr  "[Field-1] - h - ${lm://Field/1}" "{\"ImportId\":\"8_QID4_1\"}" "87" "89" ...
 $ X9_rating_1          : chr  "[Field-1] - j - ${lm://Field/1}" "{\"ImportId\":\"9_QID4_1\"}" "75" "86" ...
 $ X10_rating_1         : chr  "[Field-1] - k - ${lm://Field/1}" "{\"ImportId\":\"10_QID4_1\"}" "21" "66" ...
 $ X11_rating_1         : chr  "[Field-1] - l - ${lm://Field/1}" "{\"ImportId\":\"11_QID4_1\"}" "27" "80" ...
 $ X12_rating_1         : chr  "[Field-1] - m - ${lm://Field/1}" "{\"ImportId\":\"12_QID4_1\"}" "83" "81" ...
 $ X13_rating_1         : chr  "[Field-1] - n - ${lm://Field/1}" "{\"ImportId\":\"13_QID4_1\"}" "56" "45" ...
 $ X14_rating_1         : chr  "[Field-1] - o - ${lm://Field/1}" "{\"ImportId\":\"14_QID4_1\"}" "75" "74" ...
 $ X15_rating_1         : chr  "[Field-1] - p - ${lm://Field/1}" "{\"ImportId\":\"15_QID4_1\"}" "" "87" ...
 $ X16_rating_1         : chr  "[Field-1] - q - ${lm://Field/1}" "{\"ImportId\":\"16_QID4_1\"}" "51" "33" ...
 $ X17_rating_1         : chr  "[Field-1] - r - ${lm://Field/1}" "{\"ImportId\":\"17_QID4_1\"}" "45" "24" ...
 $ X18_rating_1         : chr  "[Field-1] - s - ${lm://Field/1}" "{\"ImportId\":\"18_QID4_1\"}" "92" "83" ...
 $ X19_rating_1         : chr  "[Field-1] - t - ${lm://Field/1}" "{\"ImportId\":\"19_QID4_1\"}" "37" "29" ...
 $ X20_rating_1         : chr  "[Field-1] - u - ${lm://Field/1}" "{\"ImportId\":\"20_QID4_1\"}" "72" "20" ...
 $ X21_rating_1         : chr  "[Field-1] - v - ${lm://Field/1}" "{\"ImportId\":\"21_QID4_1\"}" "31" "" ...
 $ X22_rating_1         : chr  "[Field-1] - x - ${lm://Field/1}" "{\"ImportId\":\"22_QID4_1\"}" "41" "91" ...
 $ X23_rating_1         : chr  "[Field-1] - w - ${lm://Field/1}" "{\"ImportId\":\"23_QID4_1\"}" "84" "80" ...
 $ X24_rating_1         : chr  "[Field-1] - y - ${lm://Field/1}" "{\"ImportId\":\"24_QID4_1\"}" "89" "91" ...
 $ X25_rating_1         : chr  "[Field-1] - z - ${lm://Field/1}" "{\"ImportId\":\"25_QID4_1\"}" "72" "85" ...
 $ pp_age               : chr  "Indique, por favor, a sua idade (utilize apenas números na resposta)" "{\"ImportId\":\"QID6_TEXT\"}" "26" "17" ...
 $ pp_gender            : chr  "Indique, por favor, o seu género" "{\"ImportId\":\"QID7\"}" "Masculino" "Feminino" ...
raw_corrigido <- raw_exercicio[!linhas_extra, , drop = FALSE]
ficheiro_corrigido <- tempfile("raw_qualtrics_corrigido_", fileext = ".csv")
write.csv(raw_corrigido, ficheiro_corrigido, row.names = FALSE)
Aqui o critério identifica as duas linhas de metadados pelos seus valores, em vez de depender apenas da posição. A cópia temporária não substitui o original.
  1. Importe um ficheiro Excel com readxl e explique qual é a dependência do script e que verificações faria antes de o analisar.
Solução 4
excel <- readxl::read_excel(
    "../data/global-shark-attack.xlsx",
    n_max = 10,
    .name_repair = "minimal"
)
dim(excel)
[1] 10 21
names(excel)
 [1] "Date"                   "Year"                   "Type"                  
 [4] "Country"                "Area"                   "Location"              
 [7] "Activity"               "Name"                   "Sex"                   
[10] "Age"                    "Injury"                 "Fatal (Y/N)"           
[13] "Time"                   "Species"                "Investigator or Source"
[16] "pdf"                    "href formula"           "href"                  
[19] "Case Number"            "Case Number"            "original order"        
str(excel)
tibble [10 × 21] (S3: tbl_df/tbl/data.frame)
 $ Date                  : chr [1:10] "2023-07-29" "2023-04-22" "2023-03-02" "2023-02-18" ...
 $ Year                  : chr [1:10] "2023" "2023" "2023" "2023" ...
 $ Type                  : chr [1:10] "Unprovoked" "Unprovoked" "Unprovoked" "Questionable" ...
 $ Country               : chr [1:10] "USA" "AUSTRALIA" "SEYCHELLES" "ARGENTINA" ...
 $ Area                  : chr [1:10] "Florida" "Western Australia" "Praslin Island" "Patagonia" ...
 $ Location              : chr [1:10] "Tampa Bay" "Lucy's Beach" NA "Chubut Province" ...
 $ Activity              : chr [1:10] "Swimming" "Surfing" "Snorkeling" NA ...
 $ Name                  : chr [1:10] "Natalie Branda" "Max Marsden" "Arthur …" "Diego Barría" ...
 $ Sex                   : chr [1:10] "F" "M" "M" "M" ...
 $ Age                   : chr [1:10] "26" "30" "6" "32" ...
 $ Injury                : chr [1:10] "Superficial injuries to abomen and thighs" "Bite to right arm" "Left foot bitten" "Death by misadventure" ...
 $ Fatal (Y/N)           : chr [1:10] "N" "N" "UNKNOWN" "UNKNOWN" ...
 $ Time                  : chr [1:10] "20h00" "07h15" "Afternoon" NA ...
 $ Species               : chr [1:10] NA "Bronze whaler shark, 1.5 m" "Lemon shark" NA ...
 $ Investigator or Source: chr [1:10] "Fox12, 8/1/2023" "The West Australian, 4/22/2023" "Midlibre, 3/18/2023" "El Pais,  2/27/2023" ...
 $ pdf                   : chr [1:10] NA NA NA NA ...
 $ href formula          : chr [1:10] NA NA NA NA ...
 $ href                  : chr [1:10] NA NA NA NA ...
 $ Case Number           : chr [1:10] NA NA NA NA ...
 $ Case Number           : chr [1:10] NA NA NA NA ...
 $ original order        : chr [1:10] NA NA NA NA ...
A dependência é o pacote readxl; a instalação é preparação do ambiente, não parte do script de análise. Antes de analisar, confirmaria o ficheiro, as colunas, as classes, algumas linhas e os valores em falta.
  1. Exporte uma pequena tabela para um caminho criado por tempfile(), volte a importá-la e compare nomes, dimensões e classes. Que propriedades não pode concluir que um CSV preservará sempre?
Solução 5
tabela <- data.frame(id = 1:2, valor = c(2.5, 4.0))
ficheiro <- tempfile("tabela_", fileext = ".csv")
write.csv(tabela, ficheiro, row.names = FALSE)
reimportada <- read.csv(ficheiro)

names(tabela)
[1] "id"    "valor"
names(reimportada)
[1] "id"    "valor"
dim(tabela)
[1] 2 2
dim(reimportada)
[1] 2 2
sapply(tabela, class)
       id     valor 
"integer" "numeric" 
sapply(reimportada, class)
       id     valor 
"integer" "numeric" 
Um CSV conserva texto e uma representação tabular simples, mas não garante todas as classes e atributos do R. Por exemplo, datas, factores e atributos específicos podem precisar de conversão ou reconstrução.