Autor

Jair Malta e Wesley Pinheiro

Data de Publicação

07/09/2026, 15:10

1 📌 Introdução

O controle tecnológico do concreto constitui etapa fundamental para a garantia da qualidade e da segurança das estruturas de concreto armado. Conforme estabelecido pela Associação Brasileira de Normas Técnicas (2015) esse controle envolve a verificação sistemática das propriedades do concreto desde a dosagem até o endurecimento, buscando assegurar que o material atenda aos requisitos definidos em projeto.

No contexto da Engenharia Civil, a análise estatística dos dados de controle tecnológico permite identificar padrões, avaliar a conformidade dos lotes produzidos e apoiar decisões sobre aceitação, investigação ou rejeição de materiais. Entretanto, a confiabilidade dessas análises depende diretamente da qualidade dos dados utilizados, conforme destacado por Neville (2016)

O presente trabalho utiliza uma base contendo 100 observações de corpos de prova de concreto, com informações sobre resistência à compressão, consumo de cimento, relação água/cimento, abatimento, densidade e absorção do agregado. A base apresenta problemas de qualidade que impedem sua utilização direta, incluindo:

  • substituição do número zero pela letra O;

  • uso inconsistente de vírgula e ponto como separadores decimais;

  • categorias textuais não padronizadas;

  • valores ausentes;

  • variáveis quantitativas representadas como texto;

  • valores potencialmente incompatíveis com o contexto físico.

2 🎯 Objetivos

2.1 Objetivo geral

Realizar o diagnóstico, a limpeza e a análise exploratória de uma base de controle tecnológico do concreto utilizando prioritariamente ferramentas do ecossistema Tidyverse, Wickham, Hadley et al. (2026), gerando informações úteis para a tomada de decisão na Engenharia Civil.

2.2 Objetivos específicos

  • importar a base sem alterar os dados brutos;

  • inspecionar sua estrutura e representação;

  • identificar valores ausentes, erros de digitação e inconsistências categóricas;

  • produzir uma versão limpa e rastreável dos dados;

  • gerar estatísticas descritivas gerais e por grupo;

  • criar variáveis derivadas relacionadas ao desempenho do concreto;

  • visualizar distribuições e relações relevantes com ggplot2;

  • produzir informações que auxiliem o engenheiro responsável;

  • comparar a lógica do Base R, R Core Team (2026), com o paradigma do Tidyverse, Wickham, Hadley et al. (2026).

3 ⚙️ Metodologia

3.1 Base de dados

A base utilizada é o arquivo base_processamento_dados_engenharia_civil.csv, composto por 100 observações e 10 variáveis. Para preservar os registros exatamente como foram fornecidos, a importação inicial será feita com todas as colunas como texto. A conversão para tipos numéricos ocorrerá somente na etapa de limpeza.

Código
tabela_variaveis <- tribble(
  ~Variavel, ~Descricao, ~Tipo_esperado,
  "id_corpo_prova", "Identificador único do corpo de prova", "Qualitativa",
  "obra", "Obra de origem do concreto", "Qualitativa",
  "tipo_concreto", "Classe de resistência do concreto", "Qualitativa",
  "idade_dias", "Idade do corpo de prova no ensaio, em dias", "Quantitativa",
  "resistencia_mpa", "Resistência à compressão, em MPa", "Quantitativa",
  "cimento_kg_m3", "Consumo de cimento, em kg/m³", "Quantitativa",
  "relacao_a_c", "Relação água/cimento", "Quantitativa",
  "abatimento_mm", "Abatimento do concreto, em mm", "Quantitativa",
  "densidade_kg_m3", "Densidade do concreto, em kg/m³", "Quantitativa",
  "absorcao_agregado_pct", "Absorção do agregado, em %", "Quantitativa"
)

kable(tabela_variaveis, booktabs = TRUE, align = c("l", "l", "c")) |>
  kable_styling(
    bootstrap_options = c("striped", "hover", "condensed"),
    full_width = FALSE,
    position = "center"
  ) |>
  scroll_box(width = "100%", height = "380px")
Descrição das variáveis utilizadas no estudo
Variavel Descricao Tipo_esperado
id_corpo_prova Identificador único do corpo de prova Qualitativa
obra Obra de origem do concreto Qualitativa
tipo_concreto Classe de resistência do concreto Qualitativa
idade_dias Idade do corpo de prova no ensaio, em dias Quantitativa
resistencia_mpa Resistência à compressão, em MPa Quantitativa
cimento_kg_m3 Consumo de cimento, em kg/m³ Quantitativa
relacao_a_c Relação água/cimento Quantitativa
abatimento_mm Abatimento do concreto, em mm Quantitativa
densidade_kg_m3 Densidade do concreto, em kg/m³ Quantitativa
absorcao_agregado_pct Absorção do agregado, em % Quantitativa

3.2 Ferramentas utilizadas

Todas as etapas de processamento são realizadas prioritariamente com a família Tidyverse:

  • readr: read_csv2(), parse_double() e parse_integer();

  • dplyr: select(), filter(), mutate(), summarise(), group_by(), count(), distinct(), across(), case_when() e if_else();

  • stringr: str_trim(), str_to_upper(), str_replace_all() e str_detect();

  • tidyr: pivot_longer(), drop_na() e replace_na();

  • purrr: map_dbl();

  • ggplot2: ggplot(), geom_histogram(), geom_boxplot(), geom_col() e geom_point().

No script anterior, funções como tapply(), aggregate(), apply(), sapply(), ifelse(), ave() e subset() resolviam tarefas isoladas. No Tidyverse, a análise é organizada como um fluxo: o objeto entra pelo operador |>, passa por verbos de transformação e produz uma saída tabular ou gráfica. Essa estrutura tende a tornar o código mais legível, modular e fácil de auditar.

4 🔍 Resultados e discussão

4.1 Importação e preservação da base original

A função read_csv2() é adequada para arquivos que usam ponto e vírgula como separador de campos e vírgula como separador decimal. O argumento show_col_types = FALSE suprime a exibição dos tipos de coluna.

Código
arquivo_dados <- "base_processamento_dados_engenharia_civil.csv"

if (!file.exists(arquivo_dados)) {
  stop("Arquivo não encontrado: ", arquivo_dados)
}

# Importar TUDO como texto para preservar os dados originais
dados <- read_csv2(
  arquivo_dados,
  col_types = cols(.default = col_character()),
  na = c("", " ", "NA", "N/A", "-", "null", "NULL"),
  show_col_types = FALSE
)

Diferentemente de read.csv2(), que pertence ao Base R, read_csv2() integra o pacote readr, retorna um tibble e fornece uma sintaxe consistente com as etapas posteriores do Tidyverse.

4.2 Inspeção inicial

Código
glimpse(dados)
Rows: 100
Columns: 10
$ id_corpo_prova        <chr> "CP-001", "CP-002", "CP-003", "CP-004", "CP-005"…
$ obra                  <chr> "Bloco A", "Bloco A", "Bloco C", "Bloco A", "Blo…
$ tipo_concreto         <chr> "C35", "C30", "C30", "C40", "C25", "C40", "C25",…
$ idade_dias            <chr> "28", "7", "56", "28", "28", "28", "56", "28", "…
$ resistencia_mpa       <chr> "36.3", "28.4", "36.1", "47.2", "27.9", "41.5", …
$ cimento_kg_m3         <chr> "395", "340", "338", "407", "316", "398", "351",…
$ relacao_a_c           <chr> "0.5", "0.54", "0.56", "0.47", "0.61", "0.43", "…
$ abatimento_mm         <chr> "111.0", "114.0", "106.0", "142.0", "133.0", "98…
$ densidade_kg_m3       <chr> "2332.0", "2389.0", "2427.0", "2345.0", "2448.0"…
$ absorção_agregado_pct <chr> "2.3", "1.85", "1.89", "1.97", "2.31", "0.8", "1…

A função glimpse() desempenha papel semelhante a str(), mas apresenta as colunas de maneira compacta e adequada a tibbles.

Código
dimensoes <- dados |>
  summarise(
    Observacoes = n(),
    Variaveis = ncol(dados)
  )

kable(dimensoes, align = "c") |>
  kable_styling(full_width = FALSE, position = "center")
Dimensões da base importada
Observacoes Variaveis
100 10
Código
dados |>
  slice_head(n = 6) |>
  kable(align = "c") |>
  kable_styling(full_width = FALSE, position = "center") |>
  scroll_box(width = "100%")
Primeiras observações da base bruta
id_corpo_prova obra tipo_concreto idade_dias resistencia_mpa cimento_kg_m3 relacao_a_c abatimento_mm densidade_kg_m3 absorção_agregado_pct
CP-001 Bloco A C35 28 36.3 395 0.5 111.0 2332.0 2.3
CP-002 Bloco A C30 7 28.4 340 0.54 114.0 2389.0 1.85
CP-003 Bloco C C30 56 36.1 338 0.56 106.0 2427.0 1.89
CP-004 Bloco A C40 28 47.2 407 0.47 142.0 2345.0 1.97
CP-005 Bloco B C25 28 27.9 316 0.61 133.0 2448.0 2.31
CP-006 Bloco B C40 28 41.5 398 0.43 98.0 2324.0 0.8

Na Base R, seriam usadas funções separadas como head(), str() e dim(). No Tidyverse, slice_head(), glimpse() e summarise() mantêm a inspeção alinhada à mesma lógica de verbos aplicada em todo o relatório.

4.3 Diagnóstico da qualidade dos dados

4.3.1 Valores ausentes

Código
resumo_na <- dados |>
  summarise(
    across(
      everything(),
      list(
        quantidade = ~ sum(is.na(.x)),
        percentual = ~ mean(is.na(.x)) * 100
      ),
      .names = "{.col}_{.fn}"
    )
  ) |>
  pivot_longer(
    cols = everything(),
    names_to = c("Variavel", ".value"),
    names_pattern = "(.*)_(quantidade|percentual)$"
  ) |>
  filter(quantidade > 0) |>
  arrange(desc(quantidade), Variavel)

kable(resumo_na, digits = 2, align = c("l", "c", "c")) |>
  kable_styling(full_width = FALSE, position = "center")
Quantidade e percentual de valores ausentes por variável
Variavel quantidade percentual
abatimento_mm 1 1
absorção_agregado_pct 1 1
densidade_kg_m3 1 1
relacao_a_c 1 1
resistencia_mpa 1 1

No script em Base R, a contagem era realizada com sapply(). No Tidyverse, summarise() com across() aplica a mesma função a todas as colunas, enquanto pivot_longer() organiza o resultado em formato longo, mais adequado para leitura e visualização.

Código
localizacao_na <- dados |>
  pivot_longer(
    cols = -id_corpo_prova,
    names_to = "item_ausente",
    values_to = "valor"
  ) |>
  filter(is.na(valor)) |>
  select(id_corpo_prova, item_ausente)

kable(localizacao_na, align = "c") |>
  kable_styling(full_width = FALSE, position = "center")
Localização dos valores ausentes
id_corpo_prova item_ausente
CP-015 abatimento_mm
CP-038 relacao_a_c
CP-077 densidade_kg_m3
CP-092 resistencia_mpa
CP-097 absorção_agregado_pct

4.3.2 Inconsistências categóricas

Código
categorias_brutas <- bind_rows(
  dados |> count(categoria = obra, name = "frequencia") |> mutate(variavel = "obra"),
  dados |> count(categoria = tipo_concreto, name = "frequencia") |> mutate(variavel = "tipo_concreto")
) |>
  select(variavel, categoria, frequencia)

kable(categorias_brutas, align = c("l", "l", "c")) |>
  kable_styling(full_width = FALSE, position = "center")
Categorias observadas antes da padronização
variavel categoria frequencia
obra Bloco A 28
obra Bloco B 29
obra Bloco C 28
obra Bloco D 15
tipo_concreto C25 29
tipo_concreto C30 31
tipo_concreto C35 23
tipo_concreto C40 16
tipo_concreto c30 1

A função count() substitui a combinação table() e conversões posteriores para data frame. Sua vantagem é retornar diretamente um tibble que pode continuar no fluxo de transformação.

4.3.3 Problemas de representação numérica

Código
colunas_numericas_brutas <- c(
  "idade_dias", "resistencia_mpa", "cimento_kg_m3", "relacao_a_c",
  "abatimento_mm", "densidade_kg_m3", "absorção_agregado_pct"
)

erros_representacao <- dados |>
  pivot_longer(
    cols = all_of(colunas_numericas_brutas),
    names_to = "variavel",
    values_to = "valor_original"
  ) |>
  filter(str_detect(valor_original, "[Oo,]")) |>
  select(id_corpo_prova, variavel, valor_original)

kable(erros_representacao, align = "c") |>
  kable_styling(full_width = FALSE, position = "center")
Registros com letras ou vírgulas em campos numéricos
id_corpo_prova variavel valor_original
CP-008 resistencia_mpa 38,7
CP-019 resistencia_mpa 3O,4
CP-045 relacao_a_c 0,55
CP-064 cimento_kg_m3 390O

No Base R, essas buscas poderiam exigir gsub(), indexação lógica e chamadas repetidas. Com pivot_longer() e str_detect(), todas as colunas numéricas são investigadas com uma lógica única e explícita.

4.4 Limpeza e conversão dos dados

A base original de dados não será alterada. A versão processada será armazenada em dados_limpos, preservando a rastreabilidade entre dados brutos e dados tratados.

Código
# Função auxiliar baseada em stringr + readr.
# Ela corrige O/o por zero, troca vírgula por ponto e converte para double.
converter_numero <- function(x) {
  x |>
    str_trim() |>
    str_replace_all("[Oo]", "0") |>
    str_replace_all(",", ".") |>
    parse_double(
      locale = locale(decimal_mark = "."),
      na = c("", "NA", "N/A", "-", "null", "NULL")
    )
}

dados_limpos <- dados |>
  rename(absorcao_agregado_pct = `absorção_agregado_pct`) |>
  mutate(
    id_corpo_prova = str_trim(id_corpo_prova),
    obra = str_trim(obra),
    tipo_concreto = str_to_upper(str_trim(tipo_concreto)),
    idade_dias = parse_integer(str_trim(idade_dias)),
    across(
      c(
        resistencia_mpa,
        cimento_kg_m3,
        relacao_a_c,
        abatimento_mm,
        densidade_kg_m3,
        absorcao_agregado_pct
      ),
      converter_numero
    )
  )

Na solução anterior, cada coluna era tratada por uma sequência própria de gsub() e as.numeric(). Na solução Tidyverse, uma função de conversão reaproveitável é aplicada às colunas por meio de across(). Isso reduz repetição e garante que todas recebam o mesmo tratamento.

4.4.1 Auditoria das alterações

Código
auditoria_limpeza <- dados |>
  transmute(
    id_corpo_prova,
    obra_original = obra,
    obra_padronizada = str_trim(obra),
    tipo_original = tipo_concreto,
    tipo_padronizado = str_to_upper(str_trim(tipo_concreto))
  ) |>
  filter(
    obra_original != obra_padronizada |
      tipo_original != tipo_padronizado
  )

kable(auditoria_limpeza, align = "c") |>
  kable_styling(full_width = FALSE, position = "center")
Auditoria dos registros textuais corrigidos
id_corpo_prova obra_original obra_padronizada tipo_original tipo_padronizado
CP-059 Bloco B Bloco B c30 C30
Código
estrutura_limpa <- dados_limpos |>
  summarise(across(everything(), ~ class(.x)[1])) |>
  pivot_longer(
    cols = everything(),
    names_to = "Variavel",
    values_to = "Classe"
  )

kable(estrutura_limpa, align = "c") |>
  kable_styling(full_width = FALSE, position = "center")
Classes das variáveis após a limpeza
Variavel Classe
id_corpo_prova character
obra character
tipo_concreto character
idade_dias integer
resistencia_mpa numeric
cimento_kg_m3 numeric
relacao_a_c numeric
abatimento_mm numeric
densidade_kg_m3 numeric
absorcao_agregado_pct numeric

4.5 Investigação de valores potencialmente incompatíveis

Os limites abaixo são utilizados apenas como regras de triagem. Um registro sinalizado não é automaticamente excluído ou alterado. A decisão técnica deve considerar ficha de ensaio, método, tipo de concreto, materiais e condições de produção.

Código
valores_suspeitos <- dados_limpos |>
  filter(
    abatimento_mm > 300 |
      densidade_kg_m3 < 1000 |
      absorcao_agregado_pct > 4 |
      idade_dias > 100
  ) |>
  mutate(
    motivo_investigacao = case_when(
      abatimento_mm > 300 ~ "Abatimento acima do limite de triagem",
      densidade_kg_m3 < 1000 ~ "Densidade muito baixa para concreto convencional",
      absorcao_agregado_pct > 4 ~ "Absorção elevada para agregado convencional",
      idade_dias > 100 ~ "Idade de ensaio muito superior às idades usuais",
      TRUE ~ "Verificar"
    )
  ) |>
  select(
    id_corpo_prova, obra, tipo_concreto, idade_dias,
    abatimento_mm, densidade_kg_m3, absorcao_agregado_pct,
    motivo_investigacao
  )

kable(valores_suspeitos, align = "c") |>
  kable_styling(full_width = FALSE, position = "center") |>
  scroll_box(width = "100%")
Valores que exigem investigação técnica
id_corpo_prova obra tipo_concreto idade_dias abatimento_mm densidade_kg_m3 absorcao_agregado_pct motivo_investigacao
CP-032 Bloco D C35 14 1250 2383 2.13 Abatimento acima do limite de triagem
CP-053 Bloco D C30 28 97 238 1.79 Densidade muito baixa para concreto convencional
CP-072 Bloco B C40 14 98 2414 18.40 Absorção elevada para agregado convencional
CP-085 Bloco A C30 280 141 2377 1.86 Idade de ensaio muito superior às idades usuais

A função filter() substitui subset(), enquanto case_when() substitui cadeias de ifelse(). A vantagem de case_when() é apresentar múltiplas condições de forma vertical, facilitando sua leitura e revisão.

4.5.1 Decisão adotada

Neste relatório, os valores suspeitos são mantidos sem correção automática. Os registros de abatimento igual a 1250 mm e densidade igual a 238 kg/m³ são plausíveis como erros de digitação, mas sua alteração para 125 e 2380 exige confirmação na fonte original. O mesmo cuidado se aplica à absorção de 18,4% e à idade de 280 dias.

Caso a conferência documental confirme os erros, uma versão corrigida poderá ser criada sem modificar dados_limpos:

Código
# Executar somente após confirmação na ficha original do ensaio.
dados_corrigidos <- dados_limpos |>
  mutate(
    abatimento_mm = if_else(
      id_corpo_prova == "CP-032" & abatimento_mm == 1250,
      125,
      abatimento_mm
    ),
    densidade_kg_m3 = case_when(
      id_corpo_prova == "CP-053" & densidade_kg_m3 == 238 ~ 2380,
      TRUE ~ densidade_kg_m3
    )
  )

4.6 Tratamento dos valores ausentes

4.6.1 Decisão adotada para valores ausentes

Optou-se por manter todos os valores ausentes (NA) sem imputação ou exclusão, pelas seguintes razões:

  • CP-015 (abatimento): manter NA. É uma variável secundária; a imputação poderia mascarar problemas reais de medição.

  • CP-038 (relacao_a_c): manter NA. Embora possa ser estimada pela relação com cimento, a imputação poderia criar uma correlação artificial.

  • CP-077 (densidade): manter NA. Variável secundária com apenas 1 valor ausente em 100 observações.

  • CP-092 (resistencia): manter NA. Esta é a principal decisão. A resistência é a variável resposta central. Imputar seu valor poderia introduzir viés significativo nas análises estatísticas subsequentes.

  • CP-097 (absorcao): manter NA. Variável secundária com apenas 1 valor ausente.

Justificativa geral: A imputação de valores, especialmente para a variável resposta (resistência), pode distorcer médias, desvios padrão e correlações. A exclusão de observações com NAs reduziria o tamanho amostral e poderia eliminar outras informações válidas. Portanto, manter os NAs e utilizar na.rm = TRUE nos cálculos é a abordagem mais conservadora e transparente.

Código
dados_limpos |>
  summarise(across(everything(), ~ sum(is.na(.x)))) |>
  pivot_longer(
    everything(),
    names_to = "Variavel",
    values_to = "Quantidade_NA"
  ) |>
  filter(Quantidade_NA > 0) |>
  kable(align = "c") |>
  kable_styling(full_width = FALSE, position = "center")
Valores ausentes após a conversão
Variavel Quantidade_NA
resistencia_mpa 1
relacao_a_c 1
abatimento_mm 1
densidade_kg_m3 1
absorcao_agregado_pct 1

4.7 Estatísticas descritivas

4.7.1 Resumo geral da resistência

Código
resumo_resistencia <- dados_limpos |>
  summarise(
    n_validos = sum(!is.na(resistencia_mpa)),
    media_mpa = mean(resistencia_mpa, na.rm = TRUE),
    mediana_mpa = median(resistencia_mpa, na.rm = TRUE),
    desvio_padrao_mpa = sd(resistencia_mpa, na.rm = TRUE),
    coeficiente_variacao_pct = 100 * desvio_padrao_mpa / media_mpa,
    minimo_mpa = min(resistencia_mpa, na.rm = TRUE),
    q1_mpa = quantile(resistencia_mpa, 0.25, na.rm = TRUE),
    q3_mpa = quantile(resistencia_mpa, 0.75, na.rm = TRUE),
    maximo_mpa = max(resistencia_mpa, na.rm = TRUE)
  )

kable(resumo_resistencia, digits = 2, align = "c") |>
  kable_styling(full_width = FALSE, position = "center") |>
  scroll_box(width = "100%")
Estatísticas descritivas da resistência à compressão
n_validos media_mpa mediana_mpa desvio_padrao_mpa coeficiente_variacao_pct minimo_mpa q1_mpa q3_mpa maximo_mpa
99 33.55 32.3 6.53 19.47 19 28.75 38.35 47.5

4.7.2 Médias das variáveis quantitativas

Código
medias_quantitativas <- dados_limpos |>
  select(where(is.numeric)) |>
  summarise(across(everything(), ~ mean(.x, na.rm = TRUE))) |>
  pivot_longer(
    everything(),
    names_to = "Variavel",
    values_to = "Media"
  )

kable(medias_quantitativas, digits = 2, align = "c") |>
  kable_styling(full_width = FALSE, position = "center")
Médias das variáveis quantitativas
Variavel Media
idade_dias 28.00
resistencia_mpa 33.55
cimento_kg_m3 392.72
relacao_a_c 0.55
abatimento_mm 121.70
densidade_kg_m3 2351.46
absorcao_agregado_pct 1.95

Essa operação substitui apply(…, MARGIN = 2, mean). Com across(where(is.numeric), …), a seleção das colunas e a função aplicada ficam explícitas, sem converter o tibble em matriz.

4.7.3 Uso de purrr

Código
medias_com_purrr <- dados_limpos |>
  select(where(is.numeric)) |>
  map_dbl(~ mean(.x, na.rm = TRUE))

medias_com_purrr
           idade_dias       resistencia_mpa         cimento_kg_m3 
           28.0000000            33.5474747           392.7200000 
          relacao_a_c         abatimento_mm       densidade_kg_m3 
            0.5537374           121.6969697          2351.4646465 
absorcao_agregado_pct 
            1.9534343 

A função map_dbl() é uma alternativa a sapply() quando se deseja garantir um vetor numérico como saída. O sufixo _dbl documenta o tipo esperado e torna o comportamento mais previsível.

4.8 Estatísticas por agrupamento

4.8.1 Resistência por obra

Código
resumo_por_obra <- dados_limpos |>
  group_by(obra) |>
  summarise(
    n = sum(!is.na(resistencia_mpa)),
    media_mpa = mean(resistencia_mpa, na.rm = TRUE),
    mediana_mpa = median(resistencia_mpa, na.rm = TRUE),
    desvio_padrao_mpa = sd(resistencia_mpa, na.rm = TRUE),
    cv_pct = 100 * desvio_padrao_mpa / media_mpa,
    .groups = "drop"
  ) |>
  arrange(desc(media_mpa))

kable(resumo_por_obra, digits = 2, align = "c") |>
  kable_styling(full_width = FALSE, position = "center")
Resistência à compressão por obra
obra n media_mpa mediana_mpa desvio_padrao_mpa cv_pct
Bloco B 28 35.70 36.25 5.28 14.78
Bloco A 28 33.19 30.50 6.42 19.35
Bloco D 15 32.39 31.80 7.92 24.45
Bloco C 28 32.37 31.55 6.79 20.97

A função group_by() define os grupos e summarise() calcula as estatísticas. Esse conjunto substitui tapply() e também parte das aplicações de aggregate(), com a vantagem de retornar um tibble nomeado e pronto para novos filtros, ordenações e gráficos.

Código
ggplot(dados_limpos, aes(x = obra, y = resistencia_mpa, fill = obra)) +
  geom_boxplot(alpha = 0.8, na.rm = TRUE, show.legend = FALSE) +
  labs(
    title = "Resistência à compressão por obra",
    x = "Obra",
    y = "Resistência (MPa)"
  ) +
  tema_relatorio
Figura 1: Distribuição da resistência à compressão por obra

4.8.2 Resistência por tipo de concreto

Código
resumo_por_tipo <- dados_limpos |>
  group_by(tipo_concreto) |>
  summarise(
    n = sum(!is.na(resistencia_mpa)),
    media_mpa = mean(resistencia_mpa, na.rm = TRUE),
    mediana_mpa = median(resistencia_mpa, na.rm = TRUE),
    desvio_padrao_mpa = sd(resistencia_mpa, na.rm = TRUE),
    cv_pct = 100 * desvio_padrao_mpa / media_mpa,
    minimo_mpa = min(resistencia_mpa, na.rm = TRUE),
    maximo_mpa = max(resistencia_mpa, na.rm = TRUE),
    .groups = "drop"
  ) |>
  arrange(tipo_concreto)

kable(resumo_por_tipo, digits = 2, align = "c") |>
  kable_styling(full_width = FALSE, position = "center") |>
  scroll_box(width = "100%")
Resumo da resistência por classe de concreto
tipo_concreto n media_mpa mediana_mpa desvio_padrao_mpa cv_pct minimo_mpa maximo_mpa
C25 29 27.07 27.80 3.07 11.33 19.0 31.6
C30 32 32.08 31.80 3.57 11.12 24.5 39.4
C35 22 37.10 36.75 3.68 9.93 29.9 44.6
C40 16 43.33 43.15 3.08 7.11 37.3 47.5

A resistência média aumenta consistentemente com a classe do concreto, como esperado pela especificação técnica da Associação Brasileira de Normas Técnicas (2014). A classe C40 apresentou a maior resistência média (43,33 MPa), enquanto a C25 apresentou a menor (27,07 MPa). É importante notar que a resistência média de cada classe supera o valor característico de referência, o que indica bom desempenho geral.

Código
ggplot(resumo_por_tipo, aes(x = tipo_concreto, y = media_mpa, fill = tipo_concreto)) +
  geom_col(width = 0.7, show.legend = FALSE) +
  geom_text(aes(label = round(media_mpa, 2)), vjust = -0.4) +
  scale_y_continuous(expand = expansion(mult = c(0, 0.12))) +
  labs(
    title = "Resistência média por classe de concreto",
    x = "Classe de concreto",
    y = "Resistência média (MPa)"
  ) +
  tema_relatorio
Figura 2: Resistência média por classe de concreto

4.8.3 Resistência por idade

Código
resumo_por_idade <- dados_limpos |>
  group_by(idade_dias) |>
  summarise(
    n = sum(!is.na(resistencia_mpa)),
    media_mpa = mean(resistencia_mpa, na.rm = TRUE),
    desvio_padrao_mpa = sd(resistencia_mpa, na.rm = TRUE),
    .groups = "drop"
  ) |>
  arrange(idade_dias)

kable(resumo_por_idade, digits = 2, align = "c") |>
  kable_styling(full_width = FALSE, position = "center")
Resistência média por idade de ensaio
idade_dias n media_mpa desvio_padrao_mpa
7 16 28.78 4.94
14 15 34.74 6.34
28 56 34.59 6.60
56 11 33.12 6.21
280 1 38.30 NA
Código
ggplot(resumo_por_idade, aes(x = factor(idade_dias), y = media_mpa)) +
  geom_col(fill = "#2E86C1", width = 0.7) +
  geom_text(aes(label = round(media_mpa, 2)), vjust = -0.4) +
  scale_y_continuous(expand = expansion(mult = c(0, 0.12))) +
  labs(
    title = "Resistência média por idade",
    x = "Idade (dias)",
    y = "Resistência média (MPa)"
  ) +
  tema_relatorio
Figura 3: Resistência média por idade do corpo de prova

As médias por idade devem ser interpretadas com cuidado, pois os grupos podem conter diferentes classes de concreto e tamanhos amostrais distintos. Uma diferença entre idades não representa necessariamente apenas o efeito do tempo de cura.

4.8.4 Médias de múltiplas variáveis por tipo

Código
medias_multiplas_tipo <- dados_limpos |>
  group_by(tipo_concreto) |>
  summarise(
    across(
      c(
        resistencia_mpa,
        cimento_kg_m3,
        relacao_a_c,
        abatimento_mm,
        densidade_kg_m3,
        absorcao_agregado_pct
      ),
      ~ mean(.x, na.rm = TRUE)
    ),
    .groups = "drop"
  )

kable(medias_multiplas_tipo, digits = 2, align = "c") |>
  kable_styling(full_width = FALSE, position = "center") |>
  scroll_box(width = "100%")
Médias das propriedades por classe de concreto
tipo_concreto resistencia_mpa cimento_kg_m3 relacao_a_c abatimento_mm densidade_kg_m3 absorcao_agregado_pct
C25 27.07 322.52 0.62 115.17 2374.93 1.91
C30 32.08 461.12 0.57 110.72 2316.53 1.63
C35 37.10 378.13 0.51 158.45 2363.83 1.88
C40 43.33 404.12 0.48 104.94 2362.50 2.79

Essa solução substitui aggregate(cbind(…) ~ grupo, …). O uso de across() facilita incluir ou remover variáveis e aplicar mais de uma estatística, mantendo o agrupamento explícito.

4.9 Criação de variáveis derivadas

Serão criadas quatro variáveis derivadas:

  • fck_referencia: resistência característica indicada pela classe;

  • resistencia_relativa: razão entre resistência observada e referência;

  • classificacao: categoria de desempenho relativo;

  • acima_media_classe: comparação de cada ensaio com a média de sua classe.

Código
dados_limpos <- dados_limpos |>
  mutate(
    fck_referencia = case_when(
      tipo_concreto == "C25" ~ 25,
      tipo_concreto == "C30" ~ 30,
      tipo_concreto == "C35" ~ 35,
      tipo_concreto == "C40" ~ 40,
      TRUE ~ NA_real_
    ),
    resistencia_relativa = resistencia_mpa / fck_referencia,
    classificacao = case_when(
      is.na(resistencia_relativa) ~ NA_character_,
      resistencia_relativa >= 1.10 ~ "Excelente",
      resistencia_relativa >= 0.95 ~ "Bom",
      resistencia_relativa >= 0.85 ~ "Regular",
      TRUE ~ "Insatisfatório"
    )
  ) |>
  group_by(tipo_concreto) |>
  mutate(
    media_classe = mean(resistencia_mpa, na.rm = TRUE),
    acima_media_classe = case_when(
      is.na(resistencia_mpa) ~ NA,
      TRUE ~ resistencia_mpa > media_classe
    )
  ) |>
  ungroup()

Na Base R, a resistência relativa era criada com atribuição por $, a classificação com ifelse() aninhado e a média da classe com ave(). No Tidyverse, mutate() reúne a criação das variáveis, case_when() organiza as regras e group_by() permite calcular valores dentro de cada classe.

4.9.1 Distribuição da classificação

Código
resumo_classificacao <- dados_limpos |>
  count(classificacao, name = "frequencia") |>
  mutate(
    classificacao = replace_na(classificacao, "Ausente"),
    percentual = 100 * frequencia / sum(frequencia)
  ) |>
  arrange(desc(frequencia))

kable(resumo_classificacao, digits = 1, align = "c") |>
  kable_styling(full_width = FALSE, position = "center")
Distribuição da classificação de desempenho
classificacao frequencia percentual
Bom 44 44
Excelente 42 42
Regular 11 11
Insatisfatório 2 2
Ausente 1 1
Código
ggplot(
  resumo_classificacao |> filter(classificacao != "Ausente"),
  aes(x = reorder(classificacao, -frequencia), y = frequencia, fill = classificacao)
) +
  geom_col(show.legend = FALSE) +
  labs(
    title = "Distribuição da classificação de desempenho",
    x = "Classificação",
    y = "Frequência"
  ) +
  tema_relatorio
Figura 4: Classificação do desempenho relativo dos corpos de prova

4.9.2 Proporção acima da média da classe

Código
resumo_acima_media <- dados_limpos |>
  count(acima_media_classe, name = "frequencia") |>
  mutate(
    situacao = case_when(
      is.na(acima_media_classe) ~ "Resistência ausente",
      acima_media_classe ~ "Acima da média",
      TRUE ~ "Na média ou abaixo"
    ),
    percentual = 100 * frequencia / sum(frequencia)
  ) |>
  select(situacao, frequencia, percentual)

kable(resumo_acima_media, digits = 1, align = "c") |>
  kable_styling(full_width = FALSE, position = "center")
Corpos de prova acima da média de sua classe
situacao frequencia percentual
Na média ou abaixo 50 50
Acima da média 49 49
Resistência ausente 1 1

4.10 Visualização exploratória

4.10.1 Histograma da resistência

Código
ggplot(dados_limpos, aes(x = resistencia_mpa)) +
  geom_histogram(bins = 15, fill = "#2E86C1", color = "white", na.rm = TRUE) +
  labs(
    title = "Distribuição da resistência à compressão",
    x = "Resistência (MPa)",
    y = "Frequência"
  ) +
  tema_relatorio
Figura 5: Distribuição da resistência à compressão

4.10.2 Boxplot por classe

Código
ggplot(dados_limpos, aes(x = tipo_concreto, y = resistencia_mpa, fill = tipo_concreto)) +
  geom_boxplot(alpha = 0.8, na.rm = TRUE, show.legend = FALSE) +
  labs(
    title = "Resistência por classe de concreto",
    x = "Classe de concreto",
    y = "Resistência (MPa)"
  ) +
  tema_relatorio
Figura 6: Distribuição da resistência por classe de concreto

4.11 Relações entre variáveis

4.11.1 Consumo de cimento e resistência

Código
cor_cimento <- dados_limpos |>
  drop_na(cimento_kg_m3, resistencia_mpa) |>
  summarise(
    n = n(),
    correlacao_pearson = cor(cimento_kg_m3, resistencia_mpa)
  )

kable(cor_cimento, digits = 4, align = "c") |>
  kable_styling(full_width = FALSE, position = "center")
Correlação entre consumo de cimento e resistência
n correlacao_pearson
99 -0.0533
Código
ggplot(dados_limpos, aes(x = cimento_kg_m3, y = resistencia_mpa, color = tipo_concreto)) +
  geom_point(size = 2.4, alpha = 0.8, na.rm = TRUE) +
  geom_smooth(method = "lm", se = TRUE, color = "black", na.rm = TRUE) +
  labs(
    title = "Consumo de cimento versus resistência",
    x = "Consumo de cimento (kg/m³)",
    y = "Resistência (MPa)",
    color = "Classe"
  ) +
  tema_relatorio
Figura 7: Relação entre consumo de cimento e resistência

A correlação simples deve ser interpretada com cautela. O consumo de cimento não atua isoladamente, pois resistência, relação água/cimento, idade, cura, adensamento e materiais estão relacionados.

4.11.2 Relação água/cimento e resistência

Código
cor_ac <- dados_limpos |>
  drop_na(relacao_a_c, resistencia_mpa) |>
  summarise(
    n = n(),
    correlacao_pearson = cor(relacao_a_c, resistencia_mpa)
  )

kable(cor_ac, digits = 4, align = "c") |>
  kable_styling(full_width = FALSE, position = "center")
Correlação entre relação água/cimento e resistência
n correlacao_pearson
98 -0.7639
Código
ggplot(dados_limpos, aes(x = relacao_a_c, y = resistencia_mpa, color = tipo_concreto)) +
  geom_point(size = 2.4, alpha = 0.8, na.rm = TRUE) +
  geom_smooth(method = "lm", se = TRUE, color = "black", na.rm = TRUE) +
  labs(
    title = "Relação água/cimento versus resistência",
    x = "Relação água/cimento",
    y = "Resistência (MPa)",
    color = "Classe"
  ) +
  tema_relatorio
Figura 8: Relação água/cimento versus resistência

Uma correlação negativa indica que, na amostra, maiores relações água/cimento tendem a estar associadas a menores resistências. Entretanto, correlação não demonstra causalidade isolada e pode refletir simultaneamente as classes de concreto presentes na base.

4.11.3 Matriz de correlação em formato longo

Código
variaveis_correlacao <- dados_limpos |>
  select(
    resistencia_mpa,
    cimento_kg_m3,
    relacao_a_c,
    idade_dias,
    abatimento_mm,
    densidade_kg_m3,
    absorcao_agregado_pct
  )

matriz_correlacao <- variaveis_correlacao |>
  cor(use = "pairwise.complete.obs") |>
  as_tibble(rownames = "Variavel")

kable(matriz_correlacao, digits = 3, align = "c") |>
  kable_styling(full_width = FALSE, position = "center") |>
  scroll_box(width = "100%")
Matriz de correlação das variáveis selecionadas
Variavel resistencia_mpa cimento_kg_m3 relacao_a_c idade_dias abatimento_mm densidade_kg_m3 absorcao_agregado_pct
resistencia_mpa 1.000 -0.053 -0.764 0.130 0.021 0.077 0.055
cimento_kg_m3 -0.053 1.000 -0.007 -0.077 -0.006 0.036 -0.029
relacao_a_c -0.764 -0.007 1.000 0.127 -0.027 -0.066 -0.131
idade_dias 0.130 -0.077 0.127 1.000 -0.022 0.010 -0.045
abatimento_mm 0.021 -0.006 -0.027 -0.022 1.000 0.028 0.008
densidade_kg_m3 0.077 0.036 -0.066 0.010 0.028 1.000 0.032
absorcao_agregado_pct 0.055 -0.029 -0.131 -0.045 0.008 0.032 1.000

A seleção das variáveis é realizada com select(). A função estatística cor() continua necessária, pois o Tidyverse organiza e prepara os dados, mas não substitui os fundamentos estatísticos do R.

4.12 Avaliação da afirmação sobre o Bloco C

A afirmação de que o Bloco C apresenta desempenho superior deve ser comparada às médias observadas, mas também exige considerar a composição das classes e idades em cada obra.

Código
comparacao_blocos <- resumo_por_obra |>
  mutate(
    posicao = row_number(),
    bloco_c = if_else(obra == "Bloco C", "Bloco analisado", "Demais blocos")
  )

kable(comparacao_blocos, digits = 2, align = "c") |>
  kable_styling(full_width = FALSE, position = "center")
Comparação da resistência média entre as obras
obra n media_mpa mediana_mpa desvio_padrao_mpa cv_pct posicao bloco_c
Bloco B 28 35.70 36.25 5.28 14.78 1 Demais blocos
Bloco A 28 33.19 30.50 6.42 19.35 2 Demais blocos
Bloco D 15 32.39 31.80 7.92 24.45 3 Demais blocos
Bloco C 28 32.37 31.55 6.79 20.97 4 Bloco analisado

A conclusão deve ser extraída da tabela calculada no momento da renderização. Uma média bruta inferior para o Bloco C não sustenta a afirmação de superioridade. Contudo, uma comparação mais rigorosa deveria controlar classe de concreto, idade e demais condições de produção.

4.13 Informações estratégicas para o engenheiro

4.13.1 Conformidade descritiva por classe

A comparação abaixo é exploratória e individual. A aceitação normativa de lotes não deve ser baseada apenas na proporção de resultados individuais acima do fck.

Código
conformidade_classe <- dados_limpos |>
  group_by(tipo_concreto, fck_referencia) |>
  summarise(
    n_validos = sum(!is.na(resistencia_mpa)),
    resistencia_media_mpa = mean(resistencia_mpa, na.rm = TRUE),
    desvio_padrao_mpa = sd(resistencia_mpa, na.rm = TRUE),
    cv_pct = 100 * desvio_padrao_mpa / resistencia_media_mpa,
    n_abaixo_referencia = sum(resistencia_mpa < fck_referencia, na.rm = TRUE),
    pct_abaixo_referencia = 100 * n_abaixo_referencia / n_validos,
    .groups = "drop"
  ) |>
  arrange(tipo_concreto)

kable(conformidade_classe, digits = 2, align = "c") |>
  kable_styling(full_width = FALSE, position = "center") |>
  scroll_box(width = "100%")
Desempenho descritivo em relação ao valor de referência da classe
tipo_concreto fck_referencia n_validos resistencia_media_mpa desvio_padrao_mpa cv_pct n_abaixo_referencia pct_abaixo_referencia
C25 25 29 27.07 3.07 11.33 7 24.14
C30 30 32 32.08 3.57 11.12 9 28.12
C35 35 22 37.10 3.68 9.93 5 22.73
C40 40 16 43.33 3.08 7.11 3 18.75

4.13.2 Corpos de prova prioritários para investigação

Código
prioridades <- dados_limpos |>
  mutate(
    prioridade = case_when(
      is.na(resistencia_mpa) ~ "Resistência ausente",
      resistencia_mpa < fck_referencia ~ "Resistência abaixo da referência",
      abatimento_mm > 300 ~ "Abatimento suspeito",
      densidade_kg_m3 < 1000 ~ "Densidade suspeita",
      absorcao_agregado_pct > 4 ~ "Absorção suspeita",
      idade_dias > 100 ~ "Idade atípica",
      TRUE ~ NA_character_
    )
  ) |>
  filter(!is.na(prioridade)) |>
  select(
    id_corpo_prova, obra, tipo_concreto, idade_dias,
    resistencia_mpa, fck_referencia, prioridade
  ) |>
  arrange(prioridade, id_corpo_prova)

kable(prioridades, align = "c") |>
  kable_styling(full_width = FALSE, position = "center") |>
  scroll_box(width = "100%", height = "400px")
Corpos de prova prioritários para investigação
id_corpo_prova obra tipo_concreto idade_dias resistencia_mpa fck_referencia prioridade
CP-032 Bloco D C35 14 36.6 35 Abatimento suspeito
CP-085 Bloco A C30 280 38.3 30 Idade atípica
CP-002 Bloco A C30 7 28.4 30 Resistência abaixo da referência
CP-007 Bloco C C25 56 23.7 25 Resistência abaixo da referência
CP-012 Bloco A C40 28 39.4 40 Resistência abaixo da referência
CP-021 Bloco A C30 28 28.8 30 Resistência abaixo da referência
CP-026 Bloco C C25 7 24.4 25 Resistência abaixo da referência
CP-029 Bloco D C25 7 23.7 25 Resistência abaixo da referência
CP-034 Bloco C C30 7 28.7 30 Resistência abaixo da referência
CP-036 Bloco C C30 7 24.5 30 Resistência abaixo da referência
CP-043 Bloco A C35 14 33.9 35 Resistência abaixo da referência
CP-044 Bloco C C25 28 23.6 25 Resistência abaixo da referência
CP-049 Bloco A C40 28 38.8 40 Resistência abaixo da referência
CP-053 Bloco D C30 28 27.4 30 Resistência abaixo da referência
CP-058 Bloco D C35 14 33.7 35 Resistência abaixo da referência
CP-059 Bloco B C30 7 29.9 30 Resistência abaixo da referência
CP-064 Bloco A C30 7 25.6 30 Resistência abaixo da referência
CP-067 Bloco C C30 14 29.6 30 Resistência abaixo da referência
CP-069 Bloco C C35 28 33.6 35 Resistência abaixo da referência
CP-072 Bloco B C40 14 37.3 40 Resistência abaixo da referência
CP-077 Bloco D C25 14 19.0 25 Resistência abaixo da referência
CP-079 Bloco D C30 28 29.9 30 Resistência abaixo da referência
CP-084 Bloco C C25 28 23.3 25 Resistência abaixo da referência
CP-086 Bloco C C35 7 30.1 35 Resistência abaixo da referência
CP-094 Bloco A C35 7 29.9 35 Resistência abaixo da referência
CP-096 Bloco D C25 7 21.3 25 Resistência abaixo da referência
CP-092 Bloco B C35 14 NA 35 Resistência ausente

4.13.3 Resumo executivo calculado automaticamente

Código
indicadores_gerais <- dados_limpos |>
  summarise(
    resistencia_media_geral = mean(resistencia_mpa, na.rm = TRUE),
    percentual_excelente = 100 * mean(classificacao == "Excelente", na.rm = TRUE),
    percentual_bom_ou_excelente = 100 * mean(
      classificacao %in% c("Excelente", "Bom"),
      na.rm = TRUE
    )
  )

melhor_bloco <- resumo_por_obra |>
  slice_max(media_mpa, n = 1, with_ties = FALSE)

maior_classe <- resumo_por_tipo |>
  slice_max(media_mpa, n = 1, with_ties = FALSE)

resumo_executivo <- tibble(
  Indicador = c(
    "Resistência média geral",
    "Obra com maior resistência média",
    "Classe com maior resistência média",
    "Corpos de prova classificados como Excelente",
    "Corpos de prova classificados como Bom ou Excelente",
    "Correlação entre a/c e resistência"
  ),
  Resultado = c(
    paste0(round(indicadores_gerais$resistencia_media_geral, 2), " MPa"),
    paste0(melhor_bloco$obra, " (", round(melhor_bloco$media_mpa, 2), " MPa)"),
    paste0(maior_classe$tipo_concreto, " (", round(maior_classe$media_mpa, 2), " MPa)"),
    paste0(round(indicadores_gerais$percentual_excelente, 1), "%"),
    paste0(round(indicadores_gerais$percentual_bom_ou_excelente, 1), "%"),
    round(cor_ac$correlacao_pearson, 3)
  )
)

kable(resumo_executivo, align = c("l", "c")) |>
  kable_styling(
    bootstrap_options = c("striped", "hover"),
    full_width = FALSE,
    position = "center"
  )
Indicador Resultado
Resistência média geral 33.55 MPa
Obra com maior resistência média Bloco B (35.7 MPa)
Classe com maior resistência média C40 (43.33 MPa)
Corpos de prova classificados como Excelente 42.4%
Corpos de prova classificados como Bom ou Excelente 86%
Correlação entre a/c e resistência -0.764

4.14 Verificação final

Código
glimpse(dados_limpos)
Rows: 100
Columns: 15
$ id_corpo_prova        <chr> "CP-001", "CP-002", "CP-003", "CP-004", "CP-005"…
$ obra                  <chr> "Bloco A", "Bloco A", "Bloco C", "Bloco A", "Blo…
$ tipo_concreto         <chr> "C35", "C30", "C30", "C40", "C25", "C40", "C25",…
$ idade_dias            <int> 28, 7, 56, 28, 28, 28, 56, 28, 14, 7, 7, 28, 28,…
$ resistencia_mpa       <dbl> 36.3, 28.4, 36.1, 47.2, 27.9, 41.5, 23.7, 38.7, …
$ cimento_kg_m3         <dbl> 395, 340, 338, 407, 316, 398, 351, 347, 346, 315…
$ relacao_a_c           <dbl> 0.50, 0.54, 0.56, 0.47, 0.61, 0.43, 0.65, 0.54, …
$ abatimento_mm         <dbl> 111, 114, 106, 142, 133, 98, 99, 116, 125, 127, …
$ densidade_kg_m3       <dbl> 2332, 2389, 2427, 2345, 2448, 2324, 2385, 2387, …
$ absorcao_agregado_pct <dbl> 2.30, 1.85, 1.89, 1.97, 2.31, 0.80, 1.18, 1.33, …
$ fck_referencia        <dbl> 35, 30, 30, 40, 25, 40, 25, 30, 30, 25, 30, 40, …
$ resistencia_relativa  <dbl> 1.0371429, 0.9466667, 1.2033333, 1.1800000, 1.11…
$ classificacao         <chr> "Bom", "Regular", "Excelente", "Excelente", "Exc…
$ media_classe          <dbl> 37.10455, 32.07812, 32.07812, 43.33125, 27.07241…
$ acima_media_classe    <lgl> FALSE, FALSE, TRUE, TRUE, TRUE, FALSE, FALSE, TR…
Código
verificacao_final <- tibble(
  Verificacao = c(
    "Número de observações",
    "Número de variáveis após derivação",
    "Categorias de obra",
    "Categorias de tipo de concreto",
    "Valores ausentes totais"
  ),
  Resultado = c(
    as.character(nrow(dados_limpos)),
    as.character(ncol(dados_limpos)),
    as.character(n_distinct(dados_limpos$obra)),
    as.character(n_distinct(dados_limpos$tipo_concreto)),
    as.character(sum(is.na(dados_limpos)))
  )
)

kable(verificacao_final, align = c("l", "c")) |>
  kable_styling(full_width = FALSE, position = "center")
Verificação final da base processada
Verificacao Resultado
Número de observações 100
Número de variáveis após derivação 15
Categorias de obra 4
Categorias de tipo de concreto 4
Valores ausentes totais 8

5 🧩 Script consolidado do desafio

O bloco abaixo reúne a lógica essencial em um único fluxo reproduzível. Ele pode ser utilizado como versão resumida da solução.

#| label: script-consolidado
#| eval: false

library(tidyverse)

converter_numero <- function(x) {
  x |>
    str_trim() |>
    str_replace_all("[Oo]", "0") |>
    str_replace_all(",", ".") |>
    parse_double(locale = locale(decimal_mark = "."))
}

dados <- read_delim(
  "base_processamento_dados_engenharia_civil.csv",
  delim = ";",
  na = c("", " ", "NA", "N/A", "-", "null", "NULL"),
  col_types = cols(.default = col_character()),
  trim_ws = FALSE,
  show_col_types = FALSE
)

# Diagnóstico inicial
dados |> glimpse()

dados |>
  summarise(across(everything(), ~ sum(is.na(.x))))

dados |> count(obra)
dados |> count(tipo_concreto)

dados |>
  filter(obra != str_trim(obra))

# Limpeza sem alterar o objeto original
dados_limpos <- dados |>
  rename(absorcao_agregado_pct = `absorção_agregado_pct`) |>
  mutate(
    id_corpo_prova = str_trim(id_corpo_prova),
    obra = str_trim(obra),
    tipo_concreto = str_to_upper(str_trim(tipo_concreto)),
    idade_dias = parse_integer(str_trim(idade_dias)),
    across(
      c(
        resistencia_mpa, cimento_kg_m3, relacao_a_c,
        abatimento_mm, densidade_kg_m3, absorcao_agregado_pct
      ),
      converter_numero
    ),
    fck_referencia = case_when(
      tipo_concreto == "C25" ~ 25,
      tipo_concreto == "C30" ~ 30,
      tipo_concreto == "C35" ~ 35,
      tipo_concreto == "C40" ~ 40,
      TRUE ~ NA_real_
    ),
    resistencia_relativa = resistencia_mpa / fck_referencia,
    classificacao = case_when(
      is.na(resistencia_relativa) ~ NA_character_,
      resistencia_relativa >= 1.10 ~ "Excelente",
      resistencia_relativa >= 0.95 ~ "Bom",
      resistencia_relativa >= 0.85 ~ "Regular",
      TRUE ~ "Insatisfatório"
    )
  ) |>
  group_by(tipo_concreto) |>
  mutate(
    media_classe = mean(resistencia_mpa, na.rm = TRUE),
    acima_media_classe = resistencia_mpa > media_classe
  ) |>
  ungroup()

# Estatísticas gerais
dados_limpos |>
  summarise(
    n = sum(!is.na(resistencia_mpa)),
    media = mean(resistencia_mpa, na.rm = TRUE),
    mediana = median(resistencia_mpa, na.rm = TRUE),
    desvio_padrao = sd(resistencia_mpa, na.rm = TRUE),
    minimo = min(resistencia_mpa, na.rm = TRUE),
    maximo = max(resistencia_mpa, na.rm = TRUE)
  )

# Estatísticas por classe
dados_limpos |>
  group_by(tipo_concreto) |>
  summarise(
    n = sum(!is.na(resistencia_mpa)),
    media = mean(resistencia_mpa, na.rm = TRUE),
    mediana = median(resistencia_mpa, na.rm = TRUE),
    desvio_padrao = sd(resistencia_mpa, na.rm = TRUE),
    .groups = "drop"
  )

# Estatísticas por obra
dados_limpos |>
  group_by(obra) |>
  summarise(
    resistencia_media = mean(resistencia_mpa, na.rm = TRUE),
    n = sum(!is.na(resistencia_mpa)),
    .groups = "drop"
  ) |>
  arrange(desc(resistencia_media))

# Correlações relevantes
dados_limpos |>
  select(resistencia_mpa, cimento_kg_m3, relacao_a_c, idade_dias, abatimento_mm) |>
  cor(use = "pairwise.complete.obs")

# Gráfico principal
ggplot(dados_limpos, aes(relacao_a_c, resistencia_mpa, color = tipo_concreto)) +
  geom_point() +
  geom_smooth(method = "lm", se = TRUE, color = "black") +
  theme_minimal()

6 🧠 Considerações finais

A aplicação do Tidyverse permitiu organizar o processamento em uma sequência clara: importação, diagnóstico, padronização, conversão, investigação, criação de variáveis, agrupamento, resumo e visualização. Os dados brutos foram preservados, e a base limpa foi criada em um objeto independente.

A principal mudança em relação ao Base R não está apenas nos nomes das funções, mas na lógica de organização. Em vez de executar operações isoladas e armazenar diversos objetos intermediários, o Tidyverse favorece fluxos encadeados, nos quais cada etapa recebe o resultado da etapa anterior.

O diagnóstico confirmou a necessidade de tratar representações numéricas inconsistentes, padronizar categorias e registrar valores ausentes. Também foram identificados registros que exigem investigação técnica.

A análise demonstra que uma ferramenta de programação pode facilitar o tratamento, mas não decide sozinha se um valor é erro, condição especial do material ou observação válida. Essa decisão depende da integração entre estatística, rastreabilidade e conhecimento de Engenharia Civil.

7 📝 Reflexão Final

7.1 Qual é a principal diferença entre resolver um problema de processamento de dados utilizando Base R e utilizando as ferramentas da família tidyverse?

A principal diferença está na filosofia de organização do código. No Base R, as operações são geralmente executadas de forma isolada, com funções como tapply(), aggregate() e apply() que resolvem tarefas específicas, mas frequentemente exigem a criação de objetos intermediários e o aninhamento de funções. O código tende a ser mais fragmentado e menos intuitivo para quem não está familiarizado com a sintaxe.

No Tidyverse, a análise é organizada como um fluxo contínuo utilizando o operador |>. Os verbos do dplyr (filter(), mutate(), summarise(), group_by()) descrevem explicitamente cada ação realizada, e o encadeamento permite ler o código de cima para baixo, seguindo a ordem lógica do processamento. Isso torna o código mais legível, modular e fácil de auditar.

7.2 Por que o processamento e a limpeza dos dados são etapas fundamentais antes da aplicação de métodos estatísticos na Engenharia Civil?

O processamento e a limpeza de dados são fundamentais por diversas razões:

  • Garantia da confiabilidade dos resultados: Erros de digitação como “3O,4” em vez de “30,4” ou “0,55” em vez de “0,55” podem levar a conclusões equivocadas sobre o desempenho do concreto. Uma estrutura projetada com base em dados incorretos pode estar subdimensionada, comprometendo a segurança estrutural.

  • Permissão de cálculos estatísticos válidos: Variáveis lidas como texto não permitem o cálculo de médias, desvios, correlações ou testes de hipótese. Sem a conversão adequada, análises essenciais para o controle tecnológico se tornam impossíveis.

  • Garantia de comparabilidade e consistência: Categorias não padronizadas (“c30” vs “C30”, “Bloco B” vs “Bloco B”) criam grupos artificiais que distorcem a análise por grupos, levando a conclusões incorretas sobre o desempenho de diferentes obras ou classes de concreto.

  • Prevenção de decisões técnicas erradas: Um valor de resistência incorreto pode levar a aceitar um lote que deveria ser rejeitado (comprometendo a segurança estrutural) ou rejeitar um lote aceitável (gerando desperdício financeiro e atrasos na obra).

  • Base para decisões de projeto e normativas: Em Engenharia Civil, as decisões são frequentemente baseadas em valores característicos e estatísticas de amostras, conforme estabelecido por normas como a ABNT NBR 12655 e NBR 6118. A confiabilidade dessas estimativas depende diretamente da qualidade dos dados de entrada.

7.3 Descreva brevemente: qual foi o principal problema encontrado na base; qual decisão você tomou para tratá-lo; quais funções do tidyverse foram utilizadas; por que você considera que sua solução é adequada.

Principal problema encontrado:

A inconsistência generalizada de tipos de dados, onde variáveis quantitativas essenciais para o controle tecnológico do concreto foram importadas como texto (character) devido a erros de digitação sistemáticos:

  • Substituição do número zero (0) pela letra “O” maiúscula em valores numéricos (ex: “3O,4” no CP-019, “390O” no CP-064);

  • Uso de vírgula como separador decimal em um sistema configurado para ponto decimal (ex: “38,7” no CP-008, “0,55” no CP-045);

  • Espaços extras em categorias textuais (“Bloco B” em vez de “Bloco B”);

  • Diferenças de maiúsculas/minúsculas em variáveis categóricas (“c30” em vez de “C30”).

Decisão adotada:

A limpeza sistemática com substituição de caracteres problemáticos e conversão de tipos, preservando a base original em um objeto separado (dados_limpos). Para os valores ausentes, optou-se por mantê-los, utilizando na.rm = TRUE nos cálculos, pois a imputação poderia introduzir viés, especialmente na variável resposta (resistência). Os valores suspeitos (abatimento 1250 mm, densidade 238 kg/m³) foram sinalizados para investigação, mas não corrigidos automaticamente.

Funções do Tidyverse utilizadas:

Código
library(tibble)
library(knitr)
library(kableExtra)

# Criando a tabela com as funções
tabela_funcoes <- tribble(
  ~Etapa, ~Funções,
  "Importação", "`read_csv2()`",
  "Padronização textual", "`str_trim()`, `str_to_upper()`, `str_replace_all()`",
  "Conversão numérica", "`parse_double()`, `parse_integer()`",
  "Limpeza e transformação", "`mutate()`, `across()`, `rename()`",
  "Diagnóstico", "`glimpse()`, `count()`, `distinct()`, `filter()`",
  "Agrupamento e resumo", "`group_by()`, `summarise()`",
  "Criação de variáveis", "`case_when()`, `if_else()`",
  "Visualização", "`ggplot()`, `geom_boxplot()`, `geom_point()`, `geom_col()`"
)

# Número de colunas (para aplicar centralização em todas)
n_col <- ncol(tabela_funcoes)

kable(tabela_funcoes, 
      booktabs = TRUE,
      align = c("l", "l")) |>  # alinhamento à esquerda para melhor leitura
  kable_styling(bootstrap_options = c("striped", "hover", "condensed"),
                full_width = FALSE,
                position = "center") |>
  column_spec(1, bold = TRUE, color = "#0056b3") |>  # destaca a primeira coluna
  column_spec(2, color = "#6C757D") |>  # funções em cinza
  row_spec(0, bold = TRUE, background = "#F8F9FA") |>  # cabeçalho
  scroll_box(width = "100%", height = "400px")
Funções do Tidyverse utilizadas no processamento
Etapa Funções
Importação `read_csv2()`
Padronização textual `str_trim()`, `str_to_upper()`, `str_replace_all()`
Conversão numérica `parse_double()`, `parse_integer()`
Limpeza e transformação `mutate()`, `across()`, `rename()`
Diagnóstico `glimpse()`, `count()`, `distinct()`, `filter()`
Agrupamento e resumo `group_by()`, `summarise()`
Criação de variáveis `case_when()`, `if_else()`
Visualização `ggplot()`, `geom_boxplot()`, `geom_point()`, `geom_col()`

Por que a solução é adequada:

A solução é adequada porque:

  • Preserva a rastreabilidade: A base original não é alterada, permitindo consultar os dados brutos quando necessário e auditando todas as transformações realizadas.

  • É sistemática e reproduzível: A função converter_numero() é aplicada a todas as colunas numéricas com across(), garantindo tratamento consistente e evitando repetição de código.

  • É documentada e transparente: Cada etapa é explicada e justificada, permitindo que outro engenheiro compreenda o processo e valide as decisões tomadas.

  • Mantém a integridade dos dados: Os valores suspeitos são sinalizados, mas não corrigidos automaticamente, pois a confirmação exige consulta às fichas originais de ensaio. Isso evita decisões baseadas em suposições.

  • Utiliza ferramentas adequadas ao problema: O Tidyverse facilita a leitura e manutenção do código, com verbos que descrevem claramente cada ação, tornando o fluxo de transformação compreensível mesmo para quem não é especialista em programação.

  • Alinha-se às boas práticas de Engenharia: Decisões conservadoras sobre valores ausentes e suspeitos refletem a responsabilidade profissional, onde a segurança estrutural não pode ser comprometida por suposições estatísticas não verificadas.

Referências

ASSOCIAÇÃO BRASILEIRA DE NORMAS TÉCNICAS. ABNT NBR 6118: Projeto de estruturas de concreto – Procedimento. Rio de Janeiro: Associação Brasileira de Normas Técnicas, 2014. p. 238
___. ABNT NBR 12655: Concreto de cimento Portland – Preparo, controle, recebimento e aceitação – Procedimento. Rio de Janeiro: Associação Brasileira de Normas Técnicas, 2015.
NEVILLE, A. M. Propriedades do Concreto. 5. ed. Porto Alegre: Bookman, 2016.
R CORE TEAM. The R Project for Statistical Computing. [s.l: s.n.].
WICKHAM, HADLEY ET AL. Tidyverse: R packages for data science, 2026. Disponível em: <https://tidyverse.org/>