Aprendizado de Máquina aplicado à Saúde

Problema de Regressão

Autor

Ana H. A. Silva, Cristina K. T. T. Mendes, Marcelo R. P. Ferreira

Data de Publicação

21 de maio de 2026

1 Introdução

Neste documento vamos aprender os conceitos fundamentais de aprendizagem de máquina supervisionada para problemas de regressão, ou seja, quando queremos prever um valor numérico contínuo.

1.1 O que é um problema de regressão?

Um problema de regressão ocorre quando a variável que queremos prever (variável resposta ou target) é numérica e contínua. Exemplos na área da saúde:

  • Prever o tempo de internação de um paciente
  • Estimar o nível de glicose em jejum
  • Prever a pressão arterial sistólica
  • Estimar o IMC de indivíduos com base em características clínicas

1.2 Dataset utilizado: Diabetes de Efron et al. (2004)

Vamos utilizar o conjunto de dados Diabetes de Efron, Hastie, Johnstone e Tibshirani (2004), disponível publicamente no repositório de dados de Dennis Boos (NCSU). Ele contém 442 pacientes diabéticos com as seguintes variáveis:

Variável Tipo Descrição
age Numérica Idade do paciente
sex Categórica Sexo (1 = feminino, 2 = masculino)
bmi Numérica Índice de Massa Corporal
map Numérica Pressão arterial média (mean arterial pressure)
tc Numérica Colesterol total (S1)
ldl Numérica LDL colesterol (S2)
hdl Numérica HDL colesterol (S3)
tch Numérica Colesterol total / HDL (S4)
ltg Numérica Log de triglicérides (S5)
glu Numérica Glicose sérica (S6)
y Numérica Progressão da diabetes (variável resposta)

Atenção sobre sex: Embora o dataset original entregue sex como número (1 ou 2), ela é uma variável categórica nominal — não há ordem ou distância numérica entre os sexos. Tratá-la como número seria um erro conceitual: o modelo poderia inferir que “o dobro de sex” tem algum significado. Por isso, converteremos para factor.

1.3 Fluxo de trabalho em Machine Learning

Todo projeto de ML segue um fluxo estruturado:

1. Carregar os dados
2. Verificar qualidade dos dados
3. Análise descritiva (EDA)
4. Particionar em treino/teste
5. Pré-processamento (recipe)
6. Definir modelos
7. Avaliar com cross-validation (e tunar hiperparâmetros quando aplicável)
8. Selecionar melhor modelo
9. Avaliar no conjunto de teste
10. Interpretar resultados

Vamos seguir exatamente esse fluxo!


2 Configuração do Ambiente

2.1 Instalação dos pacotes

Se você ainda não tem os pacotes instalados, execute o bloco abaixo uma única vez:

# Execute apenas se necessário
install.packages(c(
  "tidyverse",    # manipulação e visualização de dados
  "tidymodels",   # framework de ML
  "vip",          # importância de variáveis
  "ggcorrplot",   # matriz de correlação
  "skimr",        # sumário estatístico rico
  "ranger",       # Random Forest (backend)
  "xgboost",      # XGBoost (backend)
  "kknn",         # KNN (backend)
  "rpart"         # Árvore de Decisão (backend)
))

2.2 Carregamento dos pacotes

# Framework principal de dados
library(tidyverse)

# Framework de Machine Learning
library(tidymodels)

# Visualização e exploração
library(ggcorrplot)
library(skimr)
library(vip)

# Backends dos modelos
library(ranger)    # Random Forest
library(xgboost)   # XGBoost
library(kknn)      # KNN

# Reprodutibilidade
set.seed(42)

# Tema visual padrão
theme_set(theme_bw(base_size = 13))

Nota sobre reprodutibilidade: O comando set.seed(42) garante que os resultados aleatórios (como a divisão treino/teste) sejam sempre os mesmos quando você re-executar o código.


3 Carregamento dos Dados

Os dados são carregados diretamente da URL pública do repositório de Dennis Boos (NCSU), sem necessidade de nenhum pacote adicional do R.

# URL pública do dataset (Efron et al., 2004)
url_diabetes <- "https://www4.stat.ncsu.edu/~boos/var.select/diabetes.tab.txt"

# Lê o arquivo tab-delimitado diretamente da URL
diabetes_raw <- read_tsv(url_diabetes)

# Padroniza os nomes das colunas para minúsculo
names(diabetes_raw) <- tolower(names(diabetes_raw))

# Renomeia bp -> map e s1..s6 -> nomes clínicos
# para corresponder à nomenclatura do paper original
diabetes_raw <- diabetes_raw |>
  rename(
    map = bp,
    tc  = s1,
    ldl = s2,
    hdl = s3,
    tch = s4,
    ltg = s5,
    glu = s6
  )

# Visualiza a estrutura bruta
glimpse(diabetes_raw)
Rows: 442
Columns: 11
$ age <dbl> 59, 48, 72, 24, 50, 23, 36, 66, 60, 29, 22, 56, 53, 50, 61, 34, 47…
$ sex <dbl> 2, 1, 2, 1, 1, 1, 2, 2, 2, 1, 1, 2, 1, 2, 1, 2, 1, 2, 1, 1, 1, 2, …
$ bmi <dbl> 32.1, 21.6, 30.5, 25.3, 23.0, 22.6, 22.0, 26.2, 32.1, 30.0, 18.6, …
$ map <dbl> 101.00, 87.00, 93.00, 84.00, 101.00, 89.00, 90.00, 114.00, 83.00, …
$ tc  <dbl> 157, 183, 156, 198, 192, 139, 160, 255, 179, 180, 114, 184, 186, 1…
$ ldl <dbl> 93.2, 103.2, 93.6, 131.4, 125.4, 64.8, 99.6, 185.0, 119.4, 93.4, 5…
$ hdl <dbl> 38, 70, 41, 40, 52, 61, 50, 56, 42, 43, 46, 32, 62, 49, 72, 39, 70…
$ tch <dbl> 4.00, 3.00, 4.00, 5.00, 4.00, 2.00, 3.00, 4.55, 4.00, 4.00, 2.00, …
$ ltg <dbl> 4.8598, 3.8918, 4.6728, 4.8903, 4.2905, 4.1897, 3.9512, 4.2485, 4.…
$ glu <dbl> 87, 69, 85, 89, 80, 68, 82, 92, 94, 88, 83, 77, 81, 88, 73, 81, 98…
$ y   <dbl> 151, 75, 141, 206, 135, 97, 138, 63, 110, 310, 101, 69, 179, 185, …

glimpse() é uma função do tidyverse que mostra as primeiras observações de cada coluna, o tipo de dado e as dimensões do dataset. É sempre o primeiro passo para conhecer seus dados.

3.1 Conversão de tipos

Após carregar, precisamos converter sex para fator. No arquivo original os valores são 1 (feminino) e 2 (masculino).

diabetes_df <- diabetes_raw |>
  mutate(
    sex = factor(sex,
                 levels = c(1, 2),
                 labels = c("feminino", "masculino"))
  )

# Confirma a conversão
glimpse(diabetes_df)
Rows: 442
Columns: 11
$ age <dbl> 59, 48, 72, 24, 50, 23, 36, 66, 60, 29, 22, 56, 53, 50, 61, 34, 47…
$ sex <fct> masculino, feminino, masculino, feminino, feminino, feminino, masc…
$ bmi <dbl> 32.1, 21.6, 30.5, 25.3, 23.0, 22.6, 22.0, 26.2, 32.1, 30.0, 18.6, …
$ map <dbl> 101.00, 87.00, 93.00, 84.00, 101.00, 89.00, 90.00, 114.00, 83.00, …
$ tc  <dbl> 157, 183, 156, 198, 192, 139, 160, 255, 179, 180, 114, 184, 186, 1…
$ ldl <dbl> 93.2, 103.2, 93.6, 131.4, 125.4, 64.8, 99.6, 185.0, 119.4, 93.4, 5…
$ hdl <dbl> 38, 70, 41, 40, 52, 61, 50, 56, 42, 43, 46, 32, 62, 49, 72, 39, 70…
$ tch <dbl> 4.00, 3.00, 4.00, 5.00, 4.00, 2.00, 3.00, 4.55, 4.00, 4.00, 2.00, …
$ ltg <dbl> 4.8598, 3.8918, 4.6728, 4.8903, 4.2905, 4.1897, 3.9512, 4.2485, 4.…
$ glu <dbl> 87, 69, 85, 89, 80, 68, 82, 92, 94, 88, 83, 77, 81, 88, 73, 81, 98…
$ y   <dbl> 151, 75, 141, 206, 135, 97, 138, 63, 110, 310, 101, 69, 179, 185, …

Por que factor e não character? O tidymodels (e a maioria dos algoritmos de ML) espera variáveis categóricas no formato factor. Isso permite que o pré-processamento (ex: step_dummy()) crie automaticamente as variáveis dummy necessárias para cada algoritmo.


4 Verificação da Qualidade dos Dados

Esta etapa é fundamental em qualquer projeto de ML. Dados de má qualidade levam a modelos ruins — “garbage in, garbage out”.

4.1 Resumo estatístico completo

skim(diabetes_df)
Data summary
Name diabetes_df
Number of rows 442
Number of columns 11
_______________________
Column type frequency:
factor 1
numeric 10
________________________
Group variables None

Variable type: factor

skim_variable n_missing complete_rate ordered n_unique top_counts
sex 0 1 FALSE 2 fem: 235, mas: 207

Variable type: numeric

skim_variable n_missing complete_rate mean sd p0 p25 p50 p75 p100 hist
age 0 1 48.52 13.11 19.00 38.25 50.00 59.00 79.00 ▃▆▇▆▂
bmi 0 1 26.38 4.42 18.00 23.20 25.70 29.28 42.20 ▅▇▅▂▁
map 0 1 94.65 13.83 62.00 84.00 93.00 105.00 133.00 ▂▇▇▅▁
tc 0 1 189.14 34.61 97.00 164.25 186.00 209.75 301.00 ▁▆▇▃▁
ldl 0 1 115.44 30.41 41.60 96.05 113.00 134.50 242.40 ▂▇▆▁▁
hdl 0 1 49.79 12.93 22.00 40.25 48.00 57.75 99.00 ▂▇▅▁▁
tch 0 1 4.07 1.29 2.00 3.00 4.00 5.00 9.09 ▇▆▆▁▁
ltg 0 1 4.64 0.52 3.26 4.28 4.62 5.00 6.11 ▁▇▇▅▁
glu 0 1 91.26 11.50 58.00 83.25 91.00 98.00 124.00 ▁▅▇▃▁
y 0 1 152.13 77.09 25.00 87.00 140.50 211.50 346.00 ▇▇▆▅▂

O skim() nos mostra de uma vez:

  • Número de valores ausentes (n_missing) — separado por tipo de variável
  • Percentual completo (complete_rate)
  • Para numéricas: média, desvio padrão, mínimo, máximo e histograma inline
  • Para fatores: frequência das categorias

4.2 Verificação de valores ausentes

diabetes_df |>
  summarise(across(everything(), ~ sum(is.na(.)))) |>
  pivot_longer(everything(), names_to = "variavel", values_to = "n_missing") |>
  arrange(desc(n_missing))
diabetes_df |>
  summarise(across(everything(), ~ mean(is.na(.)) * 100)) |>
  pivot_longer(everything(), names_to = "variavel", values_to = "pct_missing") |>
  ggplot(aes(x = reorder(variavel, pct_missing), y = pct_missing)) +
  geom_col(fill = "steelblue") +
  coord_flip() +
  labs(
    title = "Percentual de Valores Ausentes por Variável",
    x     = "Variável",
    y     = "% de Valores Ausentes"
  )

4.3 Verificação de duplicatas

tibble(
  descricao = "Linhas duplicadas",
  valor     = sum(duplicated(diabetes_df))
)

4.4 Distribuição de sex

diabetes_df |>
  count(sex) |>
  mutate(pct = round(n / sum(n) * 100, 1))

5 Análise Descritiva (EDA)

A Análise Exploratória de Dados (EDA — Exploratory Data Analysis) é a etapa em que “conversamos” com os dados antes de construir qualquer modelo.

5.1 Distribuição da variável resposta

ggplot(diabetes_df, aes(x = y)) +
  geom_histogram(bins = 30, fill = "steelblue", color = "white") +
  geom_vline(xintercept = mean(diabetes_df$y), color = "red",
             linetype = "dashed", linewidth = 1) +
  labs(
    title    = "Distribuição da Progressão da Diabetes (variável resposta)",
    subtitle = paste("Média:", round(mean(diabetes_df$y), 1),
                     "| Desvio padrão:", round(sd(diabetes_df$y), 1)),
    x = "Progressão da Diabetes (y)",
    y = "Frequência"
  )

O que observar: A distribuição da variável resposta é aproximadamente normal, o que é favorável para modelos lineares. Se houvesse forte assimetria, poderíamos considerar uma transformação logarítmica.

5.2 Distribuição das variáveis numéricas preditoras

diabetes_df |>
  select(where(is.numeric), -y) |>
  pivot_longer(everything(), names_to = "variavel", values_to = "valor") |>
  ggplot(aes(x = valor)) +
  geom_histogram(bins = 25, fill = "steelblue", color = "white") +
  facet_wrap(~variavel, scales = "free") +
  labs(
    title = "Distribuição das Variáveis Numéricas Preditoras",
    x     = "Valor",
    y     = "Frequência"
  )

5.3 Variável resposta por sexo

ggplot(diabetes_df, aes(x = sex, y = y, fill = sex)) +
  geom_boxplot(alpha = 0.7, outlier.alpha = 0.4) +
  scale_fill_manual(values = c("feminino" = "steelblue", "masculino" = "tomato")) +
  labs(
    title = "Progressão da Diabetes por Sexo",
    x     = "Sexo",
    y     = "Progressão (y)",
    fill  = "Sexo"
  )

5.4 Correlação entre variáveis numéricas

cor_matrix <- diabetes_df |>
  select(where(is.numeric)) |>
  cor(use = "complete.obs")

ggcorrplot(
  cor_matrix,
  method   = "circle",
  type     = "lower",
  lab      = TRUE,
  lab_size = 3,
  colors   = c("#d73027", "white", "#1a9850"),
  title    = "Matriz de Correlação (variáveis numéricas)"
)

Como interpretar: Valores próximos de +1 (verde escuro) indicam forte correlação positiva; próximos de -1 (vermelho) indicam forte correlação negativa. Note que ldl e tc são altamente correlacionados — isso pode causar multicolinearidade em modelos lineares.

5.5 Relação entre preditores numéricos e variável resposta

diabetes_df |>
  select(where(is.numeric)) |>
  pivot_longer(-y, names_to = "variavel", values_to = "valor") |>
  ggplot(aes(x = valor, y = y)) +
  geom_point(alpha = 0.3, color = "steelblue", size = 0.8) +
  geom_smooth(method = "lm", color = "red", se = TRUE) +
  facet_wrap(~variavel, scales = "free_x") +
  labs(
    title = "Relação entre Preditores Numéricos e Variável Resposta",
    x     = "Valor do preditor",
    y     = "Progressão da Diabetes (y)"
  )


6 Particionamento: Treino e Teste

6.1 Por que separar treino e teste?

Imagine que você está estudando para uma prova. Se você memoriza as respostas do gabarito, vai bem na “prova” que já estudou, mas não aprendeu de verdade. O mesmo acontece com modelos de ML: se avaliamos o modelo nos mesmos dados com que treinamos, não sabemos se ele generaliza para dados novos.

Solução: Separamos os dados em:

  • Treino (80%): o modelo “aprende” com estes dados
  • Teste (20%): guardamos estes dados como se fossem “dados futuros” — só usamos ao final para avaliação definitiva
split  <- initial_split(diabetes_df, prop = 0.80, strata = y)
treino <- training(split)
teste  <- testing(split)

tibble(
  conjunto    = c("Treino", "Teste", "Total"),
  observacoes = c(nrow(treino), nrow(teste), nrow(diabetes_df))
)

strata = y: a estratificação garante que a distribuição da variável resposta seja similar nos dois conjuntos. Para regressão, o tidymodels divide y em quartis e amostra proporcionalmente de cada quartil.


7 Pré-Processamento com recipes

7.1 O que é um recipe?

No tidymodels, o pré-processamento é feito através de receitas (recipes). Uma receita é uma sequência de passos de transformação que serão aplicados aos dados. A grande vantagem é que:

  1. A receita é aprendida apenas nos dados de treino
  2. É aplicada automaticamente nos dados de teste (sem vazamento de informação)

7.2 Criando a receita

A ordem dos passos é deliberada e importante:

Passo Por quê aqui
step_impute_median Primeiro: os passos seguintes não toleram NA
step_corr Opera nas numéricas originais, antes de qualquer transformação de escala
step_normalize Normaliza as numéricas antes de criar dummies, para que a coluna 0/1 não seja afetada
step_dummy Cria a variável dummy de sex — após a normalização
step_zv Por último: remove qualquer preditor com variância zero, inclusive dummies que possam ter ficado constantes em algum fold da CV
receita <- recipe(y ~ ., data = treino) |>

  # 1. Imputa NAs com a mediana das variáveis numéricas
  step_impute_median(all_numeric_predictors()) |>

  # 2. Remove variáveis numéricas altamente correlacionadas (r > 0.90)
  step_corr(all_numeric_predictors(), threshold = 0.90) |>

  # 3. Normaliza as numéricas (média 0, dp 1) — antes de criar dummies
  step_normalize(all_numeric_predictors()) |>

  # 4. Cria dummy para 'sex' — após normalização, para não ser normalizada
  step_dummy(all_factor_predictors()) |>

  # 5. Remove preditores com variância zero (cobre numéricos e dummies)
  step_zv(all_predictors())

receita

Por que normalizar? Algoritmos como KNN são sensíveis à escala das variáveis. A normalização coloca todas na mesma escala (média 0, desvio 1), evitando que variáveis com maior amplitude dominem as distâncias.

Por que step_dummy após step_normalize? Se a dummy de sex (0/1) fosse normalizada, perderia sua interpretabilidade direta e, em fold de CV em que só há um sexo, geraria uma coluna constante. Criando-a por último, evitamos ambos os problemas — e o step_zv final ainda a remove caso isso ocorra.

7.3 Verificando a receita aplicada

receita_prep      <- prep(receita, training = treino)
treino_processado <- bake(receita_prep, new_data = NULL)

glimpse(treino_processado)
Rows: 352
Columns: 11
$ age           <dbl> 1.3131098, 0.5472197, -1.0611494, -1.8270395, -1.8270395…
$ bmi           <dbl> -0.03624299, 0.38665866, -1.23446434, -0.48263918, -0.08…
$ map           <dbl> 1.358450594, -0.708785888, -0.922637938, 0.004054278, -0…
$ tc            <dbl> 1.96999596, -0.12659047, -0.95341328, -0.77623696, -0.03…
$ ldl           <dbl> 2.33607398, 0.99245519, -0.91267593, -0.55170372, 0.1769…
$ hdl           <dbl> 0.50594979, -1.36013351, 0.03942896, 0.35044284, 0.50594…
$ tch           <dbl> 0.36104774, 1.47116079, -0.82562483, -0.82562483, -0.825…
$ ltg           <dbl> -0.74680816, -2.03395337, -0.23891808, -1.51793395, -1.2…
$ glu           <dbl> 0.04118490, -1.26094848, 0.30161158, -0.39285956, -0.306…
$ y             <dbl> 63, 69, 68, 49, 68, 59, 87, 65, 61, 53, 59, 52, 37, 61, …
$ sex_masculino <dbl> 1, 1, 0, 1, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 1, 1, 0, 1, 1,…

Note que sex foi convertida para sex_masculino (0 = feminino, 1 = masculino) e todas as variáveis numéricas foram normalizadas, enquanto a dummy permanece em sua escala original.


8 Definição dos Modelos

Vamos comparar 5 algoritmos diferentes. Uma das grandes vantagens do tidymodels é que a interface é uniforme para todos eles.

8.1 Regressão Linear Clássica

A regressão linear estima os coeficientes que minimizam a soma dos erros quadráticos. É o ponto de partida natural para qualquer problema de regressão: interpretável, rápida e sem hiperparâmetros para tunar.

modelo_lm <- linear_reg() |>
  set_engine("lm") |>
  set_mode("regression")

Sem hiperparâmetros: ao contrário dos demais modelos, a regressão linear não requer tune_grid(). Usaremos fit_resamples() para obter as métricas de CV e compará-la com os outros modelos em igualdade de condições.

8.2 K-Nearest Neighbors (KNN)

Para prever um novo paciente, o KNN encontra os k pacientes mais semelhantes no treino e faz a média dos seus valores de y.

modelo_knn <- nearest_neighbor(
  neighbors = tune()
) |>
  set_engine("kknn") |>
  set_mode("regression")

8.3 Árvore de Decisão

Uma árvore divide os dados em regiões através de perguntas do tipo “bmi > 30?”. É muito interpretável, mas pode sofrer de overfitting.

modelo_arvore <- decision_tree(
  cost_complexity = tune(),
  tree_depth      = tune(),
  min_n           = tune()
) |>
  set_engine("rpart") |>
  set_mode("regression")

8.4 Random Forest

Constrói muitas árvores com amostras e variáveis aleatórias, e faz a média das previsões. Robusto e raramente precisa de muito ajuste.

modelo_rf <- rand_forest(
  mtry  = tune(),
  trees = 500,
  min_n = tune()
) |>
  set_engine("ranger", importance = "impurity") |>
  set_mode("regression")

8.5 XGBoost (Gradient Boosting)

Constrói árvores sequencialmente, onde cada nova árvore corrige os erros da anterior. Um dos algoritmos mais poderosos em competições de ML.

modelo_xgb <- boost_tree(
  trees          = 500,
  tree_depth     = tune(),
  learn_rate     = tune(),
  loss_reduction = tune(),
  min_n          = tune()
) |>
  set_engine("xgboost") |>
  set_mode("regression")

9 Workflows

No tidymodels, um workflow combina a receita de pré-processamento com um modelo, garantindo que as mesmas transformações sejam aplicadas de forma consistente em treino, validação e teste.

wf_lm     <- workflow() |> add_recipe(receita) |> add_model(modelo_lm)
wf_knn    <- workflow() |> add_recipe(receita) |> add_model(modelo_knn)
wf_arvore <- workflow() |> add_recipe(receita) |> add_model(modelo_arvore)
wf_rf     <- workflow() |> add_recipe(receita) |> add_model(modelo_rf)
wf_xgb    <- workflow() |> add_recipe(receita) |> add_model(modelo_xgb)

10 Validação Cruzada e Otimização de Hiperparâmetros

10.1 Configuração dos folds

cv_folds <- vfold_cv(
  treino,
  v       = 10,
  repeats = 3,
  strata  = y
)

cv_folds

10-fold com 3 repetições: o dataset é dividido em 10 partes; cada parte é usada uma vez como validação. Repetindo 3 vezes com diferentes divisões aleatórias, obtemos estimativas mais estáveis das métricas.

10.2 Métricas de avaliação

metricas_regressao <- metric_set(
  rmse,  # Root Mean Squared Error
  mae,   # Mean Absolute Error
  rsq    # R²
)
Métrica Interpretação
RMSE Menor é melhor; penaliza erros grandes (eleva ao quadrado)
MAE Menor é melhor; erro médio em unidades originais da variável
Maior é melhor; 1.0 = perfeito, 0.0 = pior que prever a média

10.3 Grades de hiperparâmetros

grid_knn <- grid_regular(
  neighbors(range = c(3, 30)),
  levels = 15
)

grid_arvore <- grid_regular(
  cost_complexity(range = c(-4, -1)),
  tree_depth(range = c(2, 10)),
  min_n(range = c(5, 30)),
  levels = 4
)

grid_rf <- grid_regular(
  mtry(range = c(2, 8)),
  min_n(range = c(2, 20)),
  levels = 5
)

# Latin Hypercube: mais eficiente para muitos hiperparâmetros simultâneos
grid_xgb <- grid_latin_hypercube(
  tree_depth(range = c(2, 8)),
  learn_rate(range = c(-3, -1)),
  loss_reduction(range = c(-5, 0)),
  min_n(range = c(5, 30)),
  size = 30
)

Grid Regular vs Latin Hypercube: para poucos hiperparâmetros usamos o grid regular, que testa todas as combinações. Para muitos hiperparâmetros (como no XGBoost), o Latin Hypercube escolhe pontos que cobrem bem o espaço sem explodir o número de combinações.

10.4 Executando a avaliação e o tuning

# Regressão Linear: sem hiperparâmetros, apenas avalia na CV
rs_lm <- fit_resamples(
  wf_lm,
  resamples = cv_folds,
  metrics   = metricas_regressao
)

# KNN
tune_knn <- tune_grid(
  wf_knn,
  resamples = cv_folds,
  grid      = grid_knn,
  metrics   = metricas_regressao
)

# Árvore de Decisão
tune_arvore <- tune_grid(
  wf_arvore,
  resamples = cv_folds,
  grid      = grid_arvore,
  metrics   = metricas_regressao
)

# Random Forest
tune_rf <- tune_grid(
  wf_rf,
  resamples = cv_folds,
  grid      = grid_rf,
  metrics   = metricas_regressao
)

# XGBoost
tune_xgb <- tune_grid(
  wf_xgb,
  resamples = cv_folds,
  grid      = grid_xgb,
  metrics   = metricas_regressao
)

11 Análise dos Resultados do Tuning

11.1 Visualização das curvas de tuning

Para a regressão linear não há curvas de tuning (sem hiperparâmetros). Para os demais:

autoplot(tune_knn) +
  labs(title = "Tuning do KNN",
       subtitle = "Efeito do número de vizinhos nas métricas de CV")

autoplot(tune_arvore) +
  labs(title = "Tuning da Árvore de Decisão")

autoplot(tune_rf) +
  labs(title = "Tuning do Random Forest")

autoplot(tune_xgb) +
  labs(title = "Tuning do XGBoost")

11.2 Melhores hiperparâmetros

best_knn    <- select_best(tune_knn,    metric = "rmse")
best_arvore <- select_best(tune_arvore, metric = "rmse")
best_rf     <- select_best(tune_rf,     metric = "rmse")
best_xgb    <- select_best(tune_xgb,   metric = "rmse")

Melhores hiperparâmetros do KNN:

best_knn

Melhores hiperparâmetros da Árvore de Decisão:

best_arvore

Melhores hiperparâmetros do Random Forest:

best_rf

Melhores hiperparâmetros do XGBoost:

best_xgb

11.3 Comparação entre modelos na Cross-Validation

resumo_cv <- bind_rows(
  collect_metrics(rs_lm) |>
    filter(.metric == "rmse") |>
    mutate(modelo = "Regressão Linear"),
  collect_metrics(tune_knn) |>
    filter(.metric == "rmse") |>
    slice_min(mean, n = 1) |>
    mutate(modelo = "KNN"),
  collect_metrics(tune_arvore) |>
    filter(.metric == "rmse") |>
    slice_min(mean, n = 1) |>
    mutate(modelo = "Árvore de Decisão"),
  collect_metrics(tune_rf) |>
    filter(.metric == "rmse") |>
    slice_min(mean, n = 1) |>
    mutate(modelo = "Random Forest"),
  collect_metrics(tune_xgb) |>
    filter(.metric == "rmse") |>
    slice_min(mean, n = 1) |>
    mutate(modelo = "XGBoost")
) |>
  select(modelo, rmse_cv = mean, std_err) |>
  arrange(rmse_cv)

resumo_cv
ggplot(resumo_cv, aes(x = reorder(modelo, rmse_cv), y = rmse_cv)) +
  geom_col(fill = "steelblue") +
  geom_errorbar(aes(ymin = rmse_cv - std_err, ymax = rmse_cv + std_err),
                width = 0.3, color = "darkred") +
  coord_flip() +
  labs(
    title    = "Comparação de Modelos — RMSE na Cross-Validation",
    subtitle = "Barras de erro = ± 1 erro padrão | Menor é melhor",
    x        = "Modelo",
    y        = "RMSE"
  )


12 Seleção e Avaliação Final no Conjunto de Teste

12.1 Finalizando os workflows com os melhores hiperparâmetros

A regressão linear não precisa de finalização (não há hiperparâmetros). Para os demais:

wf_final_knn    <- finalize_workflow(wf_knn,    best_knn)
wf_final_arvore <- finalize_workflow(wf_arvore, best_arvore)
wf_final_rf     <- finalize_workflow(wf_rf,     best_rf)
wf_final_xgb    <- finalize_workflow(wf_xgb,    best_xgb)

12.2 Avaliação no conjunto de teste

Regra de ouro: o conjunto de teste só deve ser usado uma única vez, no final. Usá-lo múltiplas vezes durante o desenvolvimento daria uma estimativa artificialmente otimista do desempenho real.

resultado_lm     <- last_fit(wf_lm,          split, metrics = metricas_regressao)
resultado_knn    <- last_fit(wf_final_knn,    split, metrics = metricas_regressao)
resultado_arvore <- last_fit(wf_final_arvore, split, metrics = metricas_regressao)
resultado_rf     <- last_fit(wf_final_rf,     split, metrics = metricas_regressao)
resultado_xgb    <- last_fit(wf_final_xgb,    split, metrics = metricas_regressao)

12.3 Tabela de métricas no conjunto de teste

metricas_teste <- bind_rows(
  collect_metrics(resultado_lm)     |> mutate(modelo = "Regressão Linear"),
  collect_metrics(resultado_knn)    |> mutate(modelo = "KNN"),
  collect_metrics(resultado_arvore) |> mutate(modelo = "Árvore de Decisão"),
  collect_metrics(resultado_rf)     |> mutate(modelo = "Random Forest"),
  collect_metrics(resultado_xgb)    |> mutate(modelo = "XGBoost")
) |>
  select(modelo, metrica = .metric, valor = .estimate) |>
  pivot_wider(names_from = metrica, values_from = valor) |>
  arrange(rmse)

metricas_teste

12.4 Gráfico comparativo

metricas_teste |>
  pivot_longer(-modelo, names_to = "metrica", values_to = "valor") |>
  mutate(
    metrica = case_when(
      metrica == "rmse" ~ "RMSE (↓ melhor)",
      metrica == "mae"  ~ "MAE (↓ melhor)",
      metrica == "rsq"  ~ "R² (↑ melhor)"
    )
  ) |>
  ggplot(aes(x = reorder(modelo, valor), y = valor, fill = modelo)) +
  geom_col(show.legend = FALSE) +
  facet_wrap(~metrica, scales = "free_x") +
  coord_flip() +
  scale_fill_brewer(palette = "Set2") +
  labs(
    title = "Desempenho Final dos Modelos no Conjunto de Teste",
    x     = "Modelo",
    y     = "Valor da Métrica"
  )


13 Análise Aprofundada do Melhor Modelo

melhor_nome_teste <- metricas_teste |> slice_min(rmse) |> pull(modelo)

melhor_resultado <- switch(melhor_nome_teste,
  "Regressão Linear"  = resultado_lm,
  "KNN"               = resultado_knn,
  "Árvore de Decisão" = resultado_arvore,
  "Random Forest"     = resultado_rf,
  "XGBoost"           = resultado_xgb
)

O melhor modelo no conjunto de teste foi: Regressão Linear.

13.1 Predições vs. valores reais

collect_predictions(melhor_resultado) |>
  ggplot(aes(x = y, y = .pred)) +
  geom_point(alpha = 0.5, color = "steelblue") +
  geom_abline(color = "red", linetype = "dashed", linewidth = 1) +
  geom_smooth(method = "lm", color = "darkgreen", se = FALSE) +
  labs(
    title    = paste("Predições vs. Valores Reais —", melhor_nome_teste),
    subtitle = "Linha vermelha = predição perfeita | Verde = tendência observada",
    x = "Valor Real (y)",
    y = "Valor Predito (ŷ)"
  )

Como interpretar: Pontos próximos da linha diagonal vermelha indicam boas previsões. Padrões sistemáticos (ex: subestimar valores altos) indicam limitações do modelo.

13.2 Distribuição dos resíduos

collect_predictions(melhor_resultado) |>
  mutate(residuo = y - .pred) |>
  ggplot(aes(x = residuo)) +
  geom_histogram(bins = 30, fill = "steelblue", color = "white") +
  geom_vline(xintercept = 0, color = "red", linetype = "dashed") +
  labs(
    title    = "Distribuição dos Resíduos",
    subtitle = "Ideal: distribuição simétrica centrada em zero",
    x = "Resíduo (real − predito)",
    y = "Frequência"
  )

13.3 Importância das variáveis — Random Forest

Mesmo que o Random Forest não seja o melhor modelo, sua importância de variáveis é uma leitura valiosa sobre quais preditores mais contribuem para explicar a progressão da diabetes.

modelo_rf_final <- extract_fit_parsnip(resultado_rf)

vip(modelo_rf_final,
    num_features = 10,
    aesthetics   = list(fill = "steelblue")) +
  labs(
    title    = "Importância das Variáveis — Random Forest",
    subtitle = "Baseada na redução de impureza (Gini)"
  )

13.4 Coeficientes da Regressão Linear

A regressão linear tem a vantagem de ser completamente interpretável: cada coeficiente indica o efeito direto da variável na progressão da diabetes, mantendo as demais constantes.

extract_fit_parsnip(resultado_lm) |>
  tidy() |>
  filter(term != "(Intercept)") |>
  mutate(significativo = if_else(p.value < 0.05, "Sim", "Não")) |>
  arrange(p.value)
extract_fit_parsnip(resultado_lm) |>
  tidy() |>
  filter(term != "(Intercept)") |>
  ggplot(aes(x = reorder(term, estimate),
             y = estimate,
             fill = estimate > 0)) +
  geom_col(show.legend = FALSE) +
  geom_errorbar(aes(ymin = estimate - std.error,
                    ymax = estimate + std.error),
                width = 0.3) +
  coord_flip() +
  scale_fill_manual(values = c("FALSE" = "tomato", "TRUE" = "steelblue")) +
  labs(
    title    = "Coeficientes da Regressão Linear",
    subtitle = "Azul = efeito positivo | Vermelho = efeito negativo",
    x        = "Preditor",
    y        = "Coeficiente estimado"
  )


14 Conclusões

14.1 Resumo dos resultados

metricas_teste |>
  mutate(across(where(is.numeric), ~ round(., 3))) |>
  arrange(rmse)

O melhor modelo foi Regressão Linear, com RMSE de 54.3 e R² de 0.482.

14.2 Pontos chave aprendidos

  1. Fluxo estruturado: Todo projeto de ML deve seguir as etapas: EDA → Particionamento → Pré-processamento → Modelagem → Avaliação.

  2. Tipos de variáveis importam: sex é categórica e deve ser tratada como factor. O passo step_dummy() cria automaticamente a representação numérica necessária para cada algoritmo.

  3. Ordem dos steps importa: normalizar antes de criar dummies evita que variáveis binárias sejam re-escaladas e preserva a interpretabilidade dos coeficientes.

  4. Regressão Linear como baseline: é sempre bom incluir um modelo simples e interpretável como ponto de comparação. Se os modelos complexos não superarem a regressão linear, a complexidade não se justifica.

  5. Vazamento de dados (data leakage): o pré-processamento deve ser aprendido apenas nos dados de treino e aplicado no teste. O tidymodels garante isso automaticamente dentro do workflow.

  6. Cross-Validation: é a ferramenta correta para escolher hiperparâmetros sem “contaminar” o conjunto de teste.

  7. Conjunto de teste: use uma única vez. Qualquer decisão tomada com base no teste invalida a estimativa de desempenho real.

14.3 Próximos passos

  • Feature engineering: criar novas variáveis a partir das existentes
  • Stacking/Ensemble: combinar previsões de múltiplos modelos
  • Explicabilidade: usar SHAP values para entender previsões individuais
  • Deploy: colocar o modelo em produção com o pacote vetiver

15 Referências