Limpeza de Bases de Dados

Programação
Tratamento de dados
workflows
Um workflow reproduzível para inspeccionar e limpar dados 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 Limpar É Decidir

Dados reais raramente chegam prontos para análise. Nesta página vamos seguir um único workflow com um ficheiro exportado do Qualtrics: importar, inspeccionar, registar decisões, filtrar e guardar uma nova versão. Limpar não é apagar observações sem justificação. É decidir, com critérios que possamos explicar, o que entra e o que fica de fora.

A limpeza inclui tarefas rotineiras, como retirar respostas usadas para testar o formulário, metadados acrescentados pelo exportador e registos tecnicamente inutilizáveis. Inclui também exclusões que afectam a amostra, como aplicar um critério de consentimento ou de elegibilidade. Nem todos os pormenores técnicos precisam de ocupar espaço num artigo; quando forem rotineiros, pode bastar uma descrição breve ou podem ficar implícitos, conforme o contexto. Ainda assim, devem continuar visíveis no script e nos registos do projecto.

Acompanhe o código num script. Guarde sempre o ficheiro original e não grave por cima dele. Não precisa de saber estatística avançada para seguir os passos, mas cada filtro deve ter uma razão que conseguimos recuperar. O tutorial de importação ajuda quando o problema ainda é trazer o ficheiro para o R; a referência de data.frames ajuda a inspeccionar a tabela.

Uma Importação com Problemas de Estrutura

O ficheiro raw_qualtrics.csv tem duas linhas de metadados depois dos nomes das colunas. Se as importarmos como se fossem observações, o R deixa de conseguir interpretar muitas colunas como números. Começamos por conservar o ficheiro recebido e olhar para aquilo que entrou, sem alterar nada.

raw <- read.csv("../data/raw_qualtrics.csv", stringsAsFactors = FALSE)

nrow(raw)
[1] 6
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" ...
head(raw[, c("StartDate", "consents", "Progress", "pp_age")])
StartDate consents Progress pp_age
Start Date Click to write the question text Progress Indique, por favor, a sua idade (utilize apenas números na resposta)
{“ImportId”:“startDate”,“timeZone”:“Europe/London”} {“ImportId”:“QID1”} {“ImportId”:“progress”} {“ImportId”:“QID6_TEXT”}
2021-11-24 09:55:35 Yes 100 26
2021-11-24 09:57:17 Yes 100 17
2021-11-24 09:58:25 No 100
2021-11-24 09:58:28 Yes 100 19

Inspeccionar faz parte da análise. Aqui, as duas primeiras linhas são cabeçalhos do exportador, não participantes. Registamos essa decisão no script para que outra pessoa, ou nós próprios daqui a três meses, consiga reconstituir o caminho.

Aviso

Não remova linhas só porque parecem estranhas. Primeiro escreva o critério, confirme-o com a documentação do instrumento e, se possível, mantenha uma cópia ou um registo da decisão.

NotaTrês Tipos de Decisão

Uma correcção técnica rotineira seria retirar as duas linhas de metadados que o exportador acrescentou: não estamos a excluir uma pessoa, estamos a corrigir a estrutura do ficheiro. Uma exclusão prevista no protocolo seria retirar respostas sem consentimento ou fora da idade elegível. Uma escolha exploratória seria experimentar um novo limiar de progresso depois de ver os dados.

As três decisões devem ficar registadas, mas não têm o mesmo estatuto. Na última, guarde a análise planeada, identifique a alternativa como exploratória e compare as conclusões quando isso for relevante.

Um Workflow de Limpeza

O R não arruma a casa por nós. Neste caso, pelo menos, deixa a limpeza dos dados num script que conseguimos repetir.

Começamos por remover os cabeçalhos extra. Depois verificamos classes, valores e critérios de inclusão. O filtro abaixo é apenas um exemplo de decisão de estudo: consentimento, idade mínima e pelo menos 10% de progresso. No seu trabalho, os critérios devem vir do protocolo e ser documentados quando mudam.

# Decision 1: the first two rows are not responses.
dados <- raw[-c(1, 2), ]

# Check: after removing the metadata, inspect the classes.
str(dados)
'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"
# These columns were read as character because of the extra rows.
# Keep their original values so that the conversion can be audited.
colunas_numericas <- c("Progress", "pp_age")
valores_originais <- dados[, colunas_numericas, drop = FALSE]
na_antes_coercao <- colSums(is.na(valores_originais))

dados$Progress <- as.numeric(dados$Progress)
dados$pp_age <- as.numeric(dados$pp_age)

# Identify cells that became NA, rather than checking only the total.
valores_convertidos <- dados[, colunas_numericas, drop = FALSE]
novos_na <- !is.na(valores_originais) & is.na(valores_convertidos)
auditoria_coercao <- data.frame(
  coluna = colunas_numericas,
  em_falta_antes = na_antes_coercao,
  em_falta_depois = colSums(is.na(valores_convertidos)),
  novos_na = colSums(novos_na),
  row.names = NULL
)
auditoria_coercao
coluna em_falta_antes em_falta_depois novos_na
Progress 0 0 0
pp_age 0 1 1
posicoes_novos_na <- which(novos_na, arr.ind = TRUE)
data.frame(
  linha = rownames(valores_originais)[posicoes_novos_na[, "row"]],
  coluna = colnames(valores_originais)[posicoes_novos_na[, "col"]],
  valor_original = valores_originais[novos_na],
  row.names = NULL
)
linha coluna valor_original
5 pp_age
# Decision 2: keep only consented adult responses with progress >= 10.
incluido <- !is.na(dados$consents) &
  dados$consents == "Yes" &
  !is.na(dados$pp_age) & dados$pp_age >= 18 &
  !is.na(dados$Progress) & dados$Progress >= 10

# Before/after counts make the decision auditable.
n_antes <- nrow(dados)
dados_limpos <- dados[incluido, ]
n_depois <- nrow(dados_limpos)
n_excluidos <- n_antes - n_depois

c(antes = n_antes, depois = n_depois, excluidos = n_excluidos)
    antes    depois excluidos 
        4         2         2 
str(dados_limpos)
'data.frame':   2 obs. of  38 variables:
 $ StartDate            : chr  "2021-11-24 09:55:35" "2021-11-24 09:58:28"
 $ EndDate              : chr  "2021-11-24 09:57:05" "2021-11-24 10:00:02"
 $ Status               : chr  "IP Address" "IP Address"
 $ Progress             : num  100 100
 $ Duration..in.seconds.: chr  "89" "93"
 $ Finished             : chr  "True" "True"
 $ RecordedDate         : chr  "2021-11-24 09:57:05" "2021-11-24 10:00:02"
 $ ResponseId           : chr  "R_OCLWvHc3mmom6lP" "R_2dWJC1L9ystwcv3"
 $ DistributionChannel  : chr  "anonymous" "anonymous"
 $ UserLanguage         : chr  "EN" "EN"
 $ consents             : chr  "Yes" "Yes"
 $ X1_rating_1          : chr  "83" "41"
 $ X2_rating_1          : chr  "69" "25"
 $ X3_rating_1          : chr  "87" "30"
 $ X4_rating_1          : chr  "17" "38"
 $ X5_rating_1          : chr  "80" "53"
 $ X6_rating_1          : chr  "36" "34"
 $ X7_rating_1          : chr  "40" "57"
 $ X8_rating_1          : chr  "87" "65"
 $ X9_rating_1          : chr  "75" "88"
 $ X10_rating_1         : chr  "21" "93"
 $ X11_rating_1         : chr  "27" "70"
 $ X12_rating_1         : chr  "83" "89"
 $ X13_rating_1         : chr  "56" "92"
 $ X14_rating_1         : chr  "75" "31"
 $ X15_rating_1         : chr  "" "75"
 $ X16_rating_1         : chr  "51" "28"
 $ X17_rating_1         : chr  "45" "59"
 $ X18_rating_1         : chr  "92" "78"
 $ X19_rating_1         : chr  "37" "74"
 $ X20_rating_1         : chr  "72" "82"
 $ X21_rating_1         : chr  "31" "81"
 $ X22_rating_1         : chr  "41" "92"
 $ X23_rating_1         : chr  "84" "81"
 $ X24_rating_1         : chr  "89" "85"
 $ X25_rating_1         : chr  "72" "76"
 $ pp_age               : num  26 19
 $ pp_gender            : chr  "Masculino" "Feminino"

A conversão de Progress e pp_age é uma decisão técnica. Só a fazemos depois de retirar as linhas que continham texto de apresentação. Neste ficheiro, a auditoria encontra uma célula vazia em pp_age, que passa a NA; o critério de inclusão trata-a então como idade em falta e exclui essa resposta.

Se em vez disso aparecesse, por exemplo, "vinte", a situação já seria outra. Antes de filtrar, teríamos de perceber se era uma resposta inválida, uma codificação esperada ou um problema de importação. A contagem diz-nos que há algo a investigar; olhar para os valores afectados ajuda a perceber o quê.

# Simple audit of missing values in the columns used for filtering.
colSums(is.na(dados_limpos[, c("Progress", "pp_age")]))
Progress   pp_age 
       0        0 

Neste projecto, a versão limpa tem um destino explícito: data/clean_qualtrics.csv. O bloco seguinte fica sem avaliação durante o render para que construir o site nunca volte a gravar por cima desse ficheiro rastreado.

ficheiro_limpo <- "../data/clean_qualtrics.csv"
write.csv(dados_limpos, ficheiro_limpo, row.names = FALSE)

Num projecto próprio, adopte a mesma regra: escolha um caminho de saída persistente e documentado, diferente do original, e faça da escrita uma etapa explícita do workflow.

Folhas de Cálculo Já Limpas

Uma folha de cálculo limpa manualmente não é proibida nem deixa de poder ser usada. Pode ser o único formato recebido dum colaborador ou representar trabalho legítimo já realizado. Nesse caso, preserve o ficheiro tal como o recebeu, trabalhe numa cópia e documente a origem e as alterações manuais que conseguir reconstruir. A versão limpa passa a ser uma entrada do workflow, não um substituto silencioso do ficheiro de origem.

Para trabalho novo ou repetido, fazer as transformações num script de R costuma ser mais fácil de reproduzir e auditar a longo prazo: os critérios, a ordem e as contagens ficam disponíveis para voltar a executar. Isto não torna o R infalível, nem apaga uma decisão manual que já aconteceu; torna mais claro o rasto que precisamos de conservar.

Um script reproduzível não torna as decisões automaticamente correctas. Pode ter escrito > quando queria >=, escolhido a coluna errada ou aplicado um critério que afinal não estava no protocolo. Por isso vale a pena olhar também para casos perto do limite e anotar no caderno de análise o motivo, a data e a pessoa que aprovou cada exclusão relevante.

Exercícios

  1. Refaça o workflow usando demo_ds.csv. Inclua apenas participantes que consentiram e têm pelo menos 18 anos. Registe quantos casos entram e saem e escreva clean_demo.csv numa pasta de resultados, sem alterar o original.
Solução 1
demo <- read.csv("../data/demo_ds.csv", stringsAsFactors = FALSE)
antes <- nrow(demo)

incluido_demo <- !is.na(demo$consents) &
    demo$consents == "Yes" &
    !is.na(demo$age) & demo$age >= 18
clean_demo <- demo[incluido_demo, , drop = FALSE]
depois <- nrow(clean_demo)

c(antes = antes, depois = depois, excluidos = antes - depois)
    antes    depois excluidos 
      120       118         2 
dir.create("results", showWarnings = FALSE)
write.csv(clean_demo, "results/clean_demo.csv", row.names = FALSE)
O ficheiro original é apenas lido; a saída é escrita separadamente em results/clean_demo.csv.
  1. Escolha um critério do seu próprio estudo. Escreva-o em comentário antes do filtro, mostre uma contagem antes/depois e inspeccione pelo menos dois casos junto ao limite. Explique uma limitação da decisão.
Solução 2 — exemplo aberto
# Example criterion: include participants aged 18 or older.
antes_idade <- nrow(demo)
perto_do_limite <- demo[order(abs(demo$age - 18)), c("pp_number", "age")]
head(perto_do_limite, 2)
pp_number age
8 8 18
9 9 18
incluido_idade <- !is.na(demo$age) & demo$age >= 18
depois_idade <- sum(incluido_idade)
c(antes = antes_idade, depois = depois_idade)
 antes depois 
   120    120 
Este é apenas um exemplo aberto. A idade auto-reportada pode estar incorrecta e o limiar de 18 anos pode não corresponder ao protocolo de outro estudo; substitua ambos pelo critério justificado no seu projecto.
  1. Use valores_originais, valores_convertidos e novos_na para localizar uma conversão que tenha produzido NA. Decida se o valor deve ser corrigido, excluído segundo o protocolo ou investigado, e registe a razão sem alterar raw.
Solução 3
posicoes <- which(novos_na, arr.ind = TRUE)
casos_coercao <- data.frame(
    linha = rownames(valores_originais)[posicoes[, "row"]],
    coluna = colnames(valores_originais)[posicoes[, "col"]],
    valor_original = valores_originais[novos_na],
    row.names = NULL
)
casos_coercao
linha coluna valor_original
5 pp_age
# Example decision: investigate before changing raw.
razao <- "Conversão produziu NA; confirmar o valor na fonte antes de decidir."
razao
[1] "Conversão produziu NA; confirmar o valor na fonte antes de decidir."
A tabela localiza os casos sem alterar raw. A decisão final é aberta: corrija apenas com uma fonte justificável ou aplique o protocolo documentado.
  1. Escreva uma entrada de registo para cada uma destas decisões: retirar uma resposta de teste, aplicar uma exclusão prevista no protocolo e experimentar um limiar novo. Inclua critério, data, versão do ficheiro e estatuto (rotineiro, previsto ou exploratório). Explique qual delas exigiria também comparação com a análise planeada.
Solução 4 — exemplo de registo
registo <- data.frame(
    decisao = c("retirar resposta de teste",
                "aplicar exclusão prevista",
                "experimentar limiar novo"),
    criterio = c("identificada como teste na documentação",
                 "consentimento e elegibilidade do protocolo",
                 "Progress >= 20, além do plano original"),
    data = rep(as.character(Sys.Date()), 3),
    ficheiro = rep("raw_qualtrics.csv", 3),
    estatuto = c("rotineiro", "previsto", "exploratório"),
    stringsAsFactors = FALSE
)
registo
decisao criterio data ficheiro estatuto
retirar resposta de teste identificada como teste na documentação 2026-09-29 raw_qualtrics.csv rotineiro
aplicar exclusão prevista consentimento e elegibilidade do protocolo 2026-09-29 raw_qualtrics.csv previsto
experimentar limiar novo Progress >= 20, além do plano original 2026-09-29 raw_qualtrics.csv exploratório
O limiar novo é exploratório e deve ser comparado com a análise planeada. As outras entradas também devem ser documentadas, mas não representam a mesma alteração exploratória da amostra.
  1. Recebeu uma folha de cálculo que um colaborador já limpou manualmente. Desenhe um pequeno inventário de proveniência: ficheiro recebido, cópia de trabalho, versão limpa, pessoa/data e alterações conhecidas. Indique ainda duas perguntas que faria antes de a usar como entrada da análise.
Solução 5 — exemplo aberto
inventario <- data.frame(
    ficheiro_recebido = "dados_recebidos.xlsx",
    copia_trabalho = "dados_trabalho.xlsx",
    versao_limpa = "dados_limpos.xlsx",
    pessoa = NA_character_,
    data = NA_character_,
    alteracoes_conhecidas = "A confirmar com o colaborador",
    stringsAsFactors = FALSE
)
inventario
ficheiro_recebido copia_trabalho versao_limpa pessoa data alteracoes_conhecidas
dados_recebidos.xlsx dados_trabalho.xlsx dados_limpos.xlsx NA NA A confirmar com o colaborador
Perguntaria: quais foram exactamente as linhas ou valores alterados? E existe uma lista ou regra que permita repetir e verificar essas alterações? Os campos de pessoa e data são preenchidos a partir da informação real, não inventados.