# 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))Aprendizado de Máquina aplicado à Saúde
Problema de Regressão
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 entreguesexcomo 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 parafactor.
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
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
factore nãocharacter? Otidymodels(e a maioria dos algoritmos de ML) espera variáveis categóricas no formatofactor. 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)| 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
ldletcsã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, otidymodelsdivideyem 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:
- A receita é aprendida apenas nos dados de treino
- É 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())
receitaPor 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_dummyapósstep_normalize? Se a dummy desex(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 ostep_zvfinal 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(). Usaremosfit_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_folds10-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 |
| R² | 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_knnMelhores hiperparâmetros da Árvore de Decisão:
best_arvoreMelhores hiperparâmetros do Random Forest:
best_rfMelhores hiperparâmetros do XGBoost:
best_xgb11.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_cvggplot(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_teste12.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
Fluxo estruturado: Todo projeto de ML deve seguir as etapas: EDA → Particionamento → Pré-processamento → Modelagem → Avaliação.
Tipos de variáveis importam:
sexé categórica e deve ser tratada comofactor. O passostep_dummy()cria automaticamente a representação numérica necessária para cada algoritmo.Ordem dos steps importa: normalizar antes de criar dummies evita que variáveis binárias sejam re-escaladas e preserva a interpretabilidade dos coeficientes.
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.
Vazamento de dados (data leakage): o pré-processamento deve ser aprendido apenas nos dados de treino e aplicado no teste. O
tidymodelsgarante isso automaticamente dentro do workflow.Cross-Validation: é a ferramenta correta para escolher hiperparâmetros sem “contaminar” o conjunto de teste.
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
- Kuhn, M. & Silge, J. (2022). Tidy Modeling with R. O’Reilly. Disponível em: https://www.tmwr.org/
- Wickham, H. et al. (2019). Welcome to the Tidyverse. JOSS, 4(43), 1686.
- Efron, B. et al. (2004). Least Angle Regression. Annals of Statistics, 32(2), 407–499.
- Dataset original: https://www4.stat.ncsu.edu/~boos/var.select/diabetes.tab.txt