← Voltar para a página inicial
Este tutorial é parte no material do minicurso ministrado durante o I Congresso do IMECC.
O código fonte deste material está disponível em https://github.com/samarafk/CongressoIMECC2025.
Os dados estão disponíveis no Kaggle: https://www.kaggle.com/fmendes/diabetes-from-dat263x-lab01. Faça o download do arquivo diabetes.csv
e salve na mesma pasta do arquivo .Rmd
.
Vamos carregar o pacote tidyverse
e ler o conjunto de dados diabetes
:
library(tidyverse)
diabetes <- read_csv("diabetes.csv")
Algumas informações rápidas dos dados:
diabetes %>% glimpse()
## Rows: 15,000
## Columns: 10
## $ PatientID <dbl> 1354778, 1147438, 1640031, 1883350, 1424119, 16…
## $ Pregnancies <dbl> 0, 8, 7, 9, 1, 0, 0, 0, 8, 1, 1, 3, 5, 7, 0, 3,…
## $ PlasmaGlucose <dbl> 171, 92, 115, 103, 85, 82, 133, 67, 80, 72, 88,…
## $ DiastolicBloodPressure <dbl> 80, 93, 47, 78, 59, 92, 47, 87, 95, 31, 86, 96,…
## $ TricepsThickness <dbl> 34, 47, 52, 25, 27, 9, 19, 43, 33, 40, 11, 31, …
## $ SerumInsulin <dbl> 23, 36, 35, 304, 35, 253, 227, 36, 24, 42, 58, …
## $ BMI <dbl> 43.50973, 21.24058, 41.51152, 29.58219, 42.6045…
## $ DiabetesPedigree <dbl> 1.21319135, 0.15836498, 0.07901857, 1.28286985,…
## $ Age <dbl> 21, 23, 23, 43, 22, 26, 21, 26, 53, 26, 22, 23,…
## $ Diabetic <dbl> 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 1,…
Temos \(n=15000\) observações.
Temos as seguintes variáveis:
PatientID
- identificador de cada paciente
Pregnancies
- número de gestações
PlasmaGlucose
- concentração de glicose no plasma sanguíneo medida duas horas após a ingestão de glicose
DiastolicBloodPressure
- pressão diastólica
TricepsThickness
- espessura da prega cutânea do tríceps
SerumInsulin
- nível de insulina circulante no soro medida duas horas após a ingestão de glicose.
BMI
- IMC
DiabetesPedigree
- representa uma pontuação de risco genético/familiar para diabetes mellitus (DM), baseada no histórico familiar do paciente.
Age
- idade em anos
Diabetic
- indicador de diabetes (1 se sim e 0 se não)
As 10 primeiras observações:
library(kableExtra)
diabetes %>% head(10) %>% kable(booktabs = TRUE)
PatientID | Pregnancies | PlasmaGlucose | DiastolicBloodPressure | TricepsThickness | SerumInsulin | BMI | DiabetesPedigree | Age | Diabetic |
---|---|---|---|---|---|---|---|---|---|
1354778 | 0 | 171 | 80 | 34 | 23 | 43.50973 | 1.2131914 | 21 | 0 |
1147438 | 8 | 92 | 93 | 47 | 36 | 21.24058 | 0.1583650 | 23 | 0 |
1640031 | 7 | 115 | 47 | 52 | 35 | 41.51152 | 0.0790186 | 23 | 0 |
1883350 | 9 | 103 | 78 | 25 | 304 | 29.58219 | 1.2828698 | 43 | 1 |
1424119 | 1 | 85 | 59 | 27 | 35 | 42.60454 | 0.5495419 | 22 | 0 |
1619297 | 0 | 82 | 92 | 9 | 253 | 19.72416 | 0.1034245 | 26 | 0 |
1660149 | 0 | 133 | 47 | 19 | 227 | 21.94136 | 0.1741598 | 21 | 0 |
1458769 | 0 | 67 | 87 | 43 | 36 | 18.27772 | 0.2361649 | 26 | 0 |
1201647 | 8 | 80 | 95 | 33 | 24 | 26.62493 | 0.4439474 | 53 | 1 |
1403912 | 1 | 72 | 31 | 40 | 42 | 36.88958 | 0.1039436 | 26 | 0 |
PatientID
não será utilizada no modelo, portanto, vamos remover esta variável do conjunto de dados:
diabetes <- diabetes %>% select(-PatientID)
A variável resposta é Diabetic
. Para problemas em que o objetivo é classificação, tidymodels
espera que a variável resposta seja factor
e considera o primeiro nível como o evento de interesse.
diabetes <- diabetes %>%
mutate(Diabetic = factor(Diabetic))
levels(diabetes$Diabetic)
## [1] "0" "1"
Aqui, queremos que o evento de interesse seja 1
, que indica que o paciente é diabético, desta forma, usamos:
diabetes <- diabetes %>%
mutate(Diabetic = relevel(Diabetic, ref = "1"))
levels(diabetes$Diabetic)
## [1] "1" "0"
Algumas estatísticas sumárias:
summary(diabetes)
## Pregnancies PlasmaGlucose DiastolicBloodPressure TricepsThickness
## Min. : 0.000 Min. : 44.0 Min. : 24.00 Min. : 7.00
## 1st Qu.: 0.000 1st Qu.: 84.0 1st Qu.: 58.00 1st Qu.:15.00
## Median : 2.000 Median :104.0 Median : 72.00 Median :31.00
## Mean : 3.225 Mean :107.9 Mean : 71.22 Mean :28.81
## 3rd Qu.: 6.000 3rd Qu.:129.0 3rd Qu.: 85.00 3rd Qu.:41.00
## Max. :14.000 Max. :192.0 Max. :117.00 Max. :93.00
## SerumInsulin BMI DiabetesPedigree Age Diabetic
## Min. : 14.0 Min. :18.20 Min. :0.07804 Min. :21.00 1: 5000
## 1st Qu.: 39.0 1st Qu.:21.26 1st Qu.:0.13774 1st Qu.:22.00 0:10000
## Median : 83.0 Median :31.77 Median :0.20030 Median :24.00
## Mean :137.9 Mean :31.51 Mean :0.39897 Mean :30.14
## 3rd Qu.:195.0 3rd Qu.:39.26 3rd Qu.:0.61629 3rd Qu.:35.00
## Max. :799.0 Max. :56.03 Max. :2.30159 Max. :77.00
Agora vamos carregar o pacote tidymodels
e separar os dados em duas partes.
Para dividir os dados em conjuntos de treinamento e teste usamos a função initial_split()
. Por padrão, a função separa 75% dos dados para o treinamento e 25% para o teste. Aqui, como exemplo, estamos usando 80% para o treinamento e isso é especificado com prop = 0.8
.
A amostragem estratificada é feita através do argumento strata
. A estratificação garante que os dados de teste mantenham uma distribuição próxima a dos dados de treinamento.
Como a separação em treinamento e teste é feita de forma aleatorizada é importante usar set.seed()
para garantirmos sempre a mesma divisão dos dados ao executarmos o código novamente:
library(tidymodels)
tidymodels_prefer()
set.seed(1234)
diabetes_split <- initial_split(diabetes, prop = 0.8, strata = Diabetic)
As funções training()
e testing()
são usadas para extrair os conjuntos de treinamento e teste, respectivamente:
diabetes_train <- training(diabetes_split)
diabetes_test <- testing(diabetes_split)
Mais detalhes sobre divisão do conjunto de dados em treinamento e teste são discutidos nos livros de Hastie, Tibshirani, and Friedman (2001) e James et al. (2013).
Podemos realizar análises exploratórias como de costume no conjunto de dados de treinamento. Por exemplo, qual é a distribuição da variável resposta para o conjunto de treinamento?
diabetes_train %>%
ggplot(aes(x = Diabetic)) +
geom_bar()
A distribuição etária é semelhante entre as diabéticos e não diabéticos?
diabetes_train %>%
ggplot(aes(x = Age, fill = Diabetic, group = Diabetic)) +
geom_density(position = "identity", alpha = .6)
A distribuição de PlasmaGlucose
é semelhante entre as diabéticos e não diabéticos?
diabetes_train %>%
ggplot(aes(x = PlasmaGlucose, fill = Diabetic, group = Diabetic)) +
geom_density(position = "identity", alpha = .6)
Podemos usar também boxplot para avaliar a distribuição de BMI
entre diabéticos e não diabéticos:
diabetes_train %>%
ggplot(aes(x = Diabetic, y = BMI, fill = Diabetic)) +
geom_boxplot(alpha = 0.6) +
labs(x = "Diabetes", y = "Índice de Massa Corporal (BMI)") +
theme_minimal()
No tidymodels
, temos alguns passos para especificar um modelo:
Por exemplo, para especificar um modelo de regressão logística, usamos:
logistic_reg()
## Logistic Regression Model Specification (classification)
##
## Computational engine: glm
Depois que a forma funcional do modelo é especificada, é necessário pensar em um mecanismo/método para ajustar ou treinar o modelo, chamado de engine.
args(logistic_reg)
## function (mode = "classification", engine = "glm", penalty = NULL,
## mixture = NULL)
## NULL
Aqui vemos que o método default para logistic_reg()
é glm
(modelos lineares generalizados), mas outros modelos também estão disponíveis.
Se quisermos ajustar um modelo de regressão logística via modelos lineares generalizados regularizados por lasso e elastic-net, podemos especificar através da engine:
logistic_reg() %>%
set_engine("glmnet")
## Logistic Regression Model Specification (classification)
##
## Computational engine: glmnet
Todos os modelos disponíveis estão listados no site: https://www.tidymodels.org/find/parsnip/
Antes de proceder para o ajuste/treinamento do modelo, faz-se o pré-processamento dos dados, já que:
Alguns modelos exigem que os preditores tenham certas características ou certos formatos.
Algumas variáveis requerem algum tipo de transformação.
Para isso, o tidymodels
usa o que é chamado de receita (recipe
).
Uma primeira receita:
rec_simple <- recipe(Diabetic ~ ., data = diabetes_train)
No exemplo acima, a função recipe()
apenas define a variável resposta e os preditores através da fórmula.
rec_simple %>% summary() %>% kable(booktabs = TRUE)
variable | type | role | source |
---|---|---|---|
Pregnancies | double , numeric | predictor | original |
PlasmaGlucose | double , numeric | predictor | original |
DiastolicBloodPressure | double , numeric | predictor | original |
TricepsThickness | double , numeric | predictor | original |
SerumInsulin | double , numeric | predictor | original |
BMI | double , numeric | predictor | original |
DiabetesPedigree | double , numeric | predictor | original |
Age | double , numeric | predictor | original |
Diabetic | factor , unordered, nominal | outcome | original |
Os passos de pré-processamento de uma receita usam os dados de treinamento para os cálculos. Tipos de cálculos no processamento:
step_zv()
Um outro exemplo de receita:
diabetes_rec <- recipe(Diabetic ~ ., data = diabetes_train) %>%
step_poly(Age, degree = 2)
A tabela a seguir apresenta os dados pré-processamos segundo a receita acima:
Diabetic | Pregnancies | PlasmaGlucose | DiastolicBloodPressure | TricepsThickness | SerumInsulin | BMI | DiabetesPedigree | Age_poly_1 | Age_poly_2 |
---|---|---|---|---|---|---|---|---|---|
0 | 0 | 171 | 80 | 34 | 23 | 43.510 | 1.213 | -0.007 | 0.006 |
0 | 7 | 115 | 47 | 52 | 35 | 41.512 | 0.079 | -0.005 | 0.002 |
0 | 0 | 82 | 92 | 9 | 253 | 19.724 | 0.103 | -0.003 | -0.003 |
0 | 0 | 133 | 47 | 19 | 227 | 21.941 | 0.174 | -0.007 | 0.006 |
0 | 1 | 88 | 86 | 11 | 58 | 43.225 | 0.230 | -0.006 | 0.004 |
0 | 3 | 94 | 96 | 31 | 36 | 21.294 | 0.259 | -0.005 | 0.002 |
0 | 7 | 110 | 82 | 16 | 44 | 36.089 | 0.281 | -0.004 | -0.002 |
0 | 0 | 148 | 58 | 11 | 179 | 39.192 | 0.161 | 0.011 | -0.012 |
0 | 0 | 92 | 84 | 8 | 324 | 21.866 | 0.258 | 0.002 | -0.012 |
0 | 0 | 118 | 95 | 7 | 276 | 42.501 | 0.084 | -0.005 | 0.000 |
Gerenciar um modelo especificado com o parsnip
(model
+ engine
+ mode
) e os passos de pré-processamento usando recipes
, pode ser um desafio.
Para isso, o tidymodels
propõe o uso da função workflow()
, que possui dois argumentos opcionais:
preprocessor
: pode ser uma fórmula ou uma receitaspec
: modelo especificado com parsnip
Vamos especificar um modelo de regressão logística:
logreg_spec <- logistic_reg() %>%
set_engine("glm", family = "binomial") %>%
set_mode("classification")
A receita a ser aplicada está no objeto diabetes_rec
.
Vamos agregar essas duas informações no workflow:
logreg_wf <- workflow(diabetes_rec, logreg_spec)
A função fit()
pode ser usada para ajustar o modelo usando os dados de treinamento:
logreg_fit <- logreg_wf %>% fit(data = diabetes_train)
logreg_fit
## ══ Workflow [trained] ══════════════════════════════════════════════════════════
## Preprocessor: Recipe
## Model: logistic_reg()
##
## ── Preprocessor ────────────────────────────────────────────────────────────────
## 1 Recipe Step
##
## • step_poly()
##
## ── Model ───────────────────────────────────────────────────────────────────────
##
## Call: stats::glm(formula = ..y ~ ., family = ~"binomial", data = data)
##
## Coefficients:
## (Intercept) Pregnancies PlasmaGlucose
## 6.928384 -0.278826 -0.008881
## DiastolicBloodPressure TricepsThickness SerumInsulin
## -0.011326 -0.023160 -0.003905
## BMI DiabetesPedigree Age_poly_1
## -0.052296 -0.987093 -79.422268
## Age_poly_2
## 32.092675
##
## Degrees of Freedom: 11999 Total (i.e. Null); 11990 Residual
## Null Deviance: 15280
## Residual Deviance: 10280 AIC: 10300
A função tidy()
do pacote broom
resume as informações mais importantes de um modelo usando o conceito tidy:
logreg_fit %>% tidy(conf.int = TRUE) %>%
kable(booktabs = TRUE)
term | estimate | std.error | statistic | p.value | conf.low | conf.high |
---|---|---|---|---|---|---|
(Intercept) | 6.9283841 | 0.1918184 | 36.119500 | 0 | 6.5551673 | 7.3071513 |
Pregnancies | -0.2788264 | 0.0075937 | -36.718150 | 0 | -0.2937908 | -0.2640212 |
PlasmaGlucose | -0.0088807 | 0.0007768 | -11.432341 | 0 | -0.0104063 | -0.0073610 |
DiastolicBloodPressure | -0.0113256 | 0.0014974 | -7.563761 | 0 | -0.0142655 | -0.0083954 |
TricepsThickness | -0.0231602 | 0.0017118 | -13.529774 | 0 | -0.0265251 | -0.0198142 |
SerumInsulin | -0.0039049 | 0.0001836 | -21.271690 | 0 | -0.0042664 | -0.0035467 |
BMI | -0.0522958 | 0.0026186 | -19.971214 | 0 | -0.0574471 | -0.0471814 |
DiabetesPedigree | -0.9870933 | 0.0632462 | -15.607145 | 0 | -1.1113740 | -0.8634180 |
Age_poly_1 | -79.4222681 | 2.6000689 | -30.546217 | 0 | -84.5358127 | -74.3425722 |
Age_poly_2 | 32.0926749 | 2.4744368 | 12.969689 | 0 | 27.2599637 | 36.9621785 |
logreg_fit %>% tidy(conf.int = TRUE, exponentiate = TRUE) %>%
kable(booktabs = TRUE)
term | estimate | std.error | statistic | p.value | conf.low | conf.high |
---|---|---|---|---|---|---|
(Intercept) | 1.020843e+03 | 0.1918184 | 36.119500 | 0 | 7.028667e+02 | 1.490924e+03 |
Pregnancies | 7.566713e-01 | 0.0075937 | -36.718150 | 0 | 7.454324e-01 | 7.679573e-01 |
PlasmaGlucose | 9.911586e-01 | 0.0007768 | -11.432341 | 0 | 9.896476e-01 | 9.926660e-01 |
DiastolicBloodPressure | 9.887383e-01 | 0.0014974 | -7.563761 | 0 | 9.858358e-01 | 9.916398e-01 |
TricepsThickness | 9.771059e-01 | 0.0017118 | -13.529774 | 0 | 9.738236e-01 | 9.803808e-01 |
SerumInsulin | 9.961027e-01 | 0.0001836 | -21.271690 | 0 | 9.957427e-01 | 9.964596e-01 |
BMI | 9.490481e-01 | 0.0026186 | -19.971214 | 0 | 9.441719e-01 | 9.539143e-01 |
DiabetesPedigree | 3.726583e-01 | 0.0632462 | -15.607145 | 0 | 3.291065e-01 | 4.217182e-01 |
Age_poly_1 | 0.000000e+00 | 2.6000689 | -30.546217 | 0 | 0.000000e+00 | 0.000000e+00 |
Age_poly_2 | 8.663066e+13 | 2.4744368 | 12.969689 | 0 | 6.900043e+11 | 1.128418e+16 |
A função predict()
calcula os valores preditos para o conjunto especificado:
logreg_fit %>% predict(diabetes_train)
## # A tibble: 12,000 × 1
## .pred_class
## <fct>
## 1 0
## 2 0
## 3 0
## 4 0
## 5 0
## 6 0
## 7 0
## 8 0
## 9 0
## 10 0
## # ℹ 11,990 more rows
A predição com tidymodels
garante que:
Pode-se usar também a função augment()
, que calcula os valores preditos, adicionando-os em uma coluna no conjunto de dados especificado:
logreg_fit %>% augment(diabetes_train)
## # A tibble: 12,000 × 12
## .pred_class .pred_1 .pred_0 Pregnancies PlasmaGlucose DiastolicBloodPressure
## <fct> <dbl> <dbl> <dbl> <dbl> <dbl>
## 1 0 0.289 0.711 0 171 80
## 2 0 0.420 0.580 7 115 47
## 3 0 0.0490 0.951 0 82 92
## 4 0 0.0350 0.965 0 133 47
## 5 0 0.0730 0.927 1 88 86
## 6 0 0.0806 0.919 3 94 96
## 7 0 0.351 0.649 7 110 82
## 8 0 0.372 0.628 0 148 58
## 9 0 0.146 0.854 0 92 84
## 10 0 0.164 0.836 0 118 95
## # ℹ 11,990 more rows
## # ℹ 6 more variables: TricepsThickness <dbl>, SerumInsulin <dbl>, BMI <dbl>,
## # DiabetesPedigree <dbl>, Age <dbl>, Diabetic <fct>
Até aqui, fizemos o pré-processamento, definimos e treinamos o modelo escolhido usando os dados de treinamento.
Como avaliar se o modelo tem bom desempenho (foco em predição)?
Olhar os resultados para cada observação não é produtivo:
logreg_fit %>%
augment(new_data = diabetes_train) %>%
head()
## # A tibble: 6 × 12
## .pred_class .pred_1 .pred_0 Pregnancies PlasmaGlucose DiastolicBloodPressure
## <fct> <dbl> <dbl> <dbl> <dbl> <dbl>
## 1 0 0.289 0.711 0 171 80
## 2 0 0.420 0.580 7 115 47
## 3 0 0.0490 0.951 0 82 92
## 4 0 0.0350 0.965 0 133 47
## 5 0 0.0730 0.927 1 88 86
## 6 0 0.0806 0.919 3 94 96
## # ℹ 6 more variables: TricepsThickness <dbl>, SerumInsulin <dbl>, BMI <dbl>,
## # DiabetesPedigree <dbl>, Age <dbl>, Diabetic <fct>
Temos algumas métricas para comparar os valores preditos com os observados (erro de predição):
Dentro de tidymodels
temos a função metrics()
do pacote yardstick
para avaliar o modelo. Temos que especificar os seguintes argumentos em metrics()
:
truth
: nome da variável com os valores observados da respostaestimate
: nome da variável contendo os valores preditoslogreg_fit %>%
augment(new_data = diabetes_train) %>%
metrics(truth = Diabetic, estimate = .pred_class) %>%
kable(booktabs = TRUE)
.metric | .estimator | .estimate |
---|---|---|
accuracy | binary | 0.7974167 |
kap | binary | 0.5262132 |
Podemos especificar apenas uma métrica:
logreg_fit %>%
augment(new_data = diabetes_train) %>%
accuracy(truth = Diabetic, estimate = .pred_class) %>%
kable(booktabs = TRUE)
.metric | .estimator | .estimate |
---|---|---|
accuracy | binary | 0.7974167 |
Matriz de confusão de treinamento:
logreg_fit %>%
augment(new_data = diabetes_train) %>%
conf_mat(truth = Diabetic, estimate = .pred_class)
## Truth
## Prediction 1 0
## 1 2481 912
## 0 1519 7088
Um gráfico ilustrando os resultados:
logreg_fit %>%
augment(new_data = diabetes_train) %>%
conf_mat(truth = Diabetic, estimate = .pred_class) %>%
autoplot(type = "heatmap")
Também é possível especificar um conjunto de métricas. No exemplo a seguir, diabetes_metrics
é definido como um conjunto de três métricas de interesse:
diabetes_metrics <- metric_set(accuracy, sensitivity, specificity)
E podemos avaliar este conjunto de métricas no modelo ajustado:
augment(logreg_fit, new_data = diabetes_train) %>%
diabetes_metrics(truth = Diabetic, estimate = .pred_class) %>%
kable(booktabs = TRUE)
.metric | .estimator | .estimate |
---|---|---|
accuracy | binary | 0.7974167 |
sensitivity | binary | 0.6202500 |
specificity | binary | 0.8860000 |
Ao calcularmos as métricas tanto no conjunto de treinamento quanto no de teste, podemos avaliar se o modelo está super-ajustando (overfitting):
logreg_fit %>%
augment(diabetes_train) %>%
metrics(truth = Diabetic, estimate = .pred_class)
## # A tibble: 2 × 3
## .metric .estimator .estimate
## <chr> <chr> <dbl>
## 1 accuracy binary 0.797
## 2 kap binary 0.526
logreg_fit %>%
augment(diabetes_test) %>%
metrics(truth = Diabetic, estimate = .pred_class)
## # A tibble: 2 × 3
## .metric .estimator .estimate
## <chr> <chr> <dbl>
## 1 accuracy binary 0.784
## 2 kap binary 0.492
Essa métrica usa como corte a probabilidade 0.5
para declarar um evento (se a probabilidade estimada for acima de 0.5, declaramos Diabético = 1 e 0 caso contrário).
E se decidirmos definir o corte como 0.8
? A variação desse limite afeta as métricas de sensibilidade e especificidade.
Podemos usar a curva ROC (receiver operator characteristic):
portanto, temos um cenário com sensibilidade e especificidade calculadas em todos os limites possíveis.
augment(logreg_fit, new_data = diabetes_train) %>%
roc_curve(truth = Diabetic, .pred_1) %>%
autoplot()
A area sob a curva ROC pode ser usada como métrica:
augment(logreg_fit, new_data = diabetes_train) %>%
roc_auc(truth = Diabetic, .pred_1) %>%
kable(booktabs = TRUE)
.metric | .estimator | .estimate |
---|---|---|
roc_auc | binary | 0.8625254 |
e podemos fixar algumas linhas para visualizar os valores de corte (threshold
) e seus respectivos TPR e FPR:
augment(logreg_fit, new_data = diabetes_train) %>%
roc_curve(truth = Diabetic, .pred_1) %>%
slice(1, 500, 1000, 2000, 4000, 6000, 8000, 10000) %>%
kable(booktabs = TRUE)
.threshold | specificity | sensitivity |
---|---|---|
-Inf | 0.000000 | 1.00000 |
0.0242410 | 0.062000 | 0.99950 |
0.0348151 | 0.124000 | 0.99850 |
0.0561580 | 0.244750 | 0.99000 |
0.1150272 | 0.474625 | 0.94975 |
0.2254710 | 0.678875 | 0.85825 |
0.4256154 | 0.846750 | 0.69400 |
0.6936874 | 0.959375 | 0.41925 |
Usaremos 5 folds como exemplo. Para reamostrar os dados de treino, usaremos o comando vfold_cv
:
set.seed(234)
diabetes_folds <- diabetes_train %>%
vfold_cv(v = 5, strata = Diabetic)
diabetes_folds
## # 5-fold cross-validation using stratification
## # A tibble: 5 × 2
## splits id
## <list> <chr>
## 1 <split [9600/2400]> Fold1
## 2 <split [9600/2400]> Fold2
## 3 <split [9600/2400]> Fold3
## 4 <split [9600/2400]> Fold4
## 5 <split [9600/2400]> Fold5
Ajustando o modelo nas reamostragens:
logreg_cv <- logreg_wf %>% fit_resamples(diabetes_folds)
logreg_cv
## # Resampling results
## # 5-fold cross-validation using stratification
## # A tibble: 5 × 4
## splits id .metrics .notes
## <list> <chr> <list> <list>
## 1 <split [9600/2400]> Fold1 <tibble [3 × 4]> <tibble [0 × 3]>
## 2 <split [9600/2400]> Fold2 <tibble [3 × 4]> <tibble [0 × 3]>
## 3 <split [9600/2400]> Fold3 <tibble [3 × 4]> <tibble [0 × 3]>
## 4 <split [9600/2400]> Fold4 <tibble [3 × 4]> <tibble [0 × 3]>
## 5 <split [9600/2400]> Fold5 <tibble [3 × 4]> <tibble [0 × 3]>
A função collect_metrics()
extrai as métricas obtidas em cada reamostra e calcula a métrica da validação, geralmente através de uma média:
logreg_cv %>%
collect_metrics() %>%
kable(booktabs = TRUE)
.metric | .estimator | mean | n | std_err | .config |
---|---|---|---|---|---|
accuracy | binary | 0.7965833 | 5 | 0.0021707 | Preprocessor1_Model1 |
brier_class | binary | 0.1391407 | 5 | 0.0009101 | Preprocessor1_Model1 |
roc_auc | binary | 0.8618658 | 5 | 0.0015880 | Preprocessor1_Model1 |
Para calcular um conjunto escolhido de métricas, é preciso especificar o conjunto no argumento metrics
dentro de fit_resamples
:
logreg_cv <- fit_resamples(logreg_wf,
diabetes_folds,
metrics = diabetes_metrics)
logreg_cv %>%
collect_metrics() %>%
kable(booktabs = TRUE)
.metric | .estimator | mean | n | std_err | .config |
---|---|---|---|---|---|
accuracy | binary | 0.7965833 | 5 | 0.0021707 | Preprocessor1_Model1 |
sensitivity | binary | 0.6180000 | 5 | 0.0054715 | Preprocessor1_Model1 |
specificity | binary | 0.8858750 | 5 | 0.0025890 | Preprocessor1_Model1 |
Através da validação cruzada, avaliamos o desempenho do modelo apenas com os dados de treinamento, sem usar os dados de teste.
A métrica obtida no conjunto de validação pode ser tomada como uma estimativa da métrica no conjunto de teste.
Caso seja necessário salvar as predições obtidas nas etapas de validação cruzada, para fazer um gráfico, por exemplo, usamos control_resamples
:
ctrl_malaria <- control_resamples(save_pred = TRUE)
logreg_cv <- fit_resamples(logreg_wf,
diabetes_folds,
control = ctrl_malaria)
logreg_preds <- collect_predictions(logreg_cv)
logreg_preds
## # A tibble: 12,000 × 7
## .pred_class .pred_1 .pred_0 id .row Diabetic .config
## <fct> <dbl> <dbl> <chr> <int> <fct> <chr>
## 1 0 0.0735 0.927 Fold1 5 0 Preprocessor1_Model1
## 2 0 0.305 0.695 Fold1 20 0 Preprocessor1_Model1
## 3 0 0.109 0.891 Fold1 24 0 Preprocessor1_Model1
## 4 0 0.362 0.638 Fold1 37 0 Preprocessor1_Model1
## 5 0 0.0716 0.928 Fold1 41 0 Preprocessor1_Model1
## 6 0 0.148 0.852 Fold1 44 0 Preprocessor1_Model1
## 7 0 0.0791 0.921 Fold1 47 0 Preprocessor1_Model1
## 8 0 0.360 0.640 Fold1 49 0 Preprocessor1_Model1
## 9 0 0.0798 0.920 Fold1 50 0 Preprocessor1_Model1
## 10 0 0.280 0.720 Fold1 67 0 Preprocessor1_Model1
## # ℹ 11,990 more rows
Um outro modelo a ser considerado é árvore de decisão. Vamos considerar o seguinte exemplo:
tree_spec <- decision_tree(cost_complexity = 0.005) %>%
set_mode("classification") %>%
set_engine("rpart", model = TRUE)
tree_spec
## Decision Tree Model Specification (classification)
##
## Main Arguments:
## cost_complexity = 0.005
##
## Engine-Specific Arguments:
## model = TRUE
##
## Computational engine: rpart
O modelo de árvore de decisão não requer pré-processamento, de forma que podemos usar um workflow
com a fórmula e o modelo especificado, por exemplo:
tree_wf <- workflow(Diabetic ~ ., tree_spec)
tree_wf
## ══ Workflow ════════════════════════════════════════════════════════════════════
## Preprocessor: Formula
## Model: decision_tree()
##
## ── Preprocessor ────────────────────────────────────────────────────────────────
## Diabetic ~ .
##
## ── Model ───────────────────────────────────────────────────────────────────────
## Decision Tree Model Specification (classification)
##
## Main Arguments:
## cost_complexity = 0.005
##
## Engine-Specific Arguments:
## model = TRUE
##
## Computational engine: rpart
E ajustar com os dados de treinamento:
tree_fit <- tree_wf %>% fit(data = diabetes_train)
Visualização do modelo ajustado:
library(rpart.plot)
tree_fit %>%
extract_fit_engine() %>%
rpart.plot()
Vamos então avaliar o desempenho da árvore de decisão através da validação cruzada:
tree_cv <- tree_wf %>% fit_resamples(diabetes_folds)
tree_cv %>% collect_metrics() %>%
kable(booktabs = TRUE)
.metric | .estimator | mean | n | std_err | .config |
---|---|---|---|---|---|
accuracy | binary | 0.9050833 | 5 | 0.0019913 | Preprocessor1_Model1 |
brier_class | binary | 0.0784368 | 5 | 0.0014673 | Preprocessor1_Model1 |
roc_auc | binary | 0.9335519 | 5 | 0.0022146 | Preprocessor1_Model1 |
Podemos também considerar um modelo de floresta aleatória.
Aqui, usaremos set_engine
e adicionaremos o argumento importance = "impurity"
, para que possamos ter pontuações de importância variáveis, para avaliarmos quais preditores são mais relevantes.
rf_spec <- rand_forest(trees = 1000) %>%
set_mode("classification") %>%
set_engine("ranger", importance = "impurity")
rf_spec
## Random Forest Model Specification (classification)
##
## Main Arguments:
## trees = 1000
##
## Engine-Specific Arguments:
## importance = impurity
##
## Computational engine: ranger
Um workflow
simples:
rf_wf <- workflow(Diabetic ~ ., rf_spec)
rf_wf
## ══ Workflow ════════════════════════════════════════════════════════════════════
## Preprocessor: Formula
## Model: rand_forest()
##
## ── Preprocessor ────────────────────────────────────────────────────────────────
## Diabetic ~ .
##
## ── Model ───────────────────────────────────────────────────────────────────────
## Random Forest Model Specification (classification)
##
## Main Arguments:
## trees = 1000
##
## Engine-Specific Arguments:
## importance = impurity
##
## Computational engine: ranger
Avaliação das métricas usando validação cruzada:
set.seed(2024) #RF uses random numbers, so we need to set the seed
rf_cv <- rf_wf %>%
fit_resamples(diabetes_folds)
rf_cv %>% collect_metrics() %>%
kable(booktabs = TRUE)
.metric | .estimator | mean | n | std_err | .config |
---|---|---|---|---|---|
accuracy | binary | 0.9374167 | 5 | 0.0026333 | Preprocessor1_Model1 |
brier_class | binary | 0.0488073 | 5 | 0.0008318 | Preprocessor1_Model1 |
roc_auc | binary | 0.9824586 | 5 | 0.0007990 | Preprocessor1_Model1 |
Quando queremos comparar vários modelos ao mesmo tempo, é muito trabalhoso fazer um de cada vez, como mostramos anteriormente.
tidymodels
se mostra útil também nesta tarefa. Primeiro, podemos usar a função workflow_set()
para gerar um conjunto de workflows que desejamos avaliar. Os argumentos desta função são:
preproc
: formulas, recipesmodels
: models specified using parsnip
Aqui, definimos um conjunto de workflows de interesse:
wf_set <- workflow_set(preproc = list(rec1 = Diabetic ~ .,
rec2 = diabetes_rec,
rec1 = Diabetic ~ .),
models = list(tree = tree_spec,
logreg = logreg_spec,
rf = rf_spec),
cross = FALSE)
Para processar este conjunto de workflows, usamos a função workflow_map()
. Podemos avaliar os modelos com as métricas desejadas usando a validação cruzada usando fit_resamples
:
wf_set %>%
workflow_map("fit_resamples",
resamples = diabetes_folds,
metrics = diabetes_metrics,
seed = 2024) %>%
rank_results()
## # A tibble: 9 × 9
## wflow_id .config .metric mean std_err n preprocessor model rank
## <chr> <chr> <chr> <dbl> <dbl> <int> <chr> <chr> <int>
## 1 rec1_rf Preprocessor… accura… 0.937 0.00263 5 formula rand… 1
## 2 rec1_rf Preprocessor… sensit… 0.892 0.00170 5 formula rand… 1
## 3 rec1_rf Preprocessor… specif… 0.960 0.00444 5 formula rand… 1
## 4 rec1_tree Preprocessor… accura… 0.905 0.00199 5 formula deci… 2
## 5 rec1_tree Preprocessor… sensit… 0.859 0.00805 5 formula deci… 2
## 6 rec1_tree Preprocessor… specif… 0.928 0.00447 5 formula deci… 2
## 7 rec2_logreg Preprocessor… accura… 0.797 0.00217 5 recipe logi… 3
## 8 rec2_logreg Preprocessor… sensit… 0.618 0.00547 5 recipe logi… 3
## 9 rec2_logreg Preprocessor… specif… 0.886 0.00259 5 recipe logi… 3
Se o argumento cross = TRUE
o workflow_set
faz um produto cruzado das receitas e modelos:
workflow_set(preproc = list(rec1 = Diabetic ~ .,
rec2 = diabetes_rec),
models = list(tree = tree_spec,
logreg = logreg_spec,
rf = rf_spec),
cross = TRUE) %>%
workflow_map("fit_resamples",
resamples = diabetes_folds,
metrics = diabetes_metrics,
seed = 2024) %>%
rank_results()
## # A tibble: 18 × 9
## wflow_id .config .metric mean std_err n preprocessor model rank
## <chr> <chr> <chr> <dbl> <dbl> <int> <chr> <chr> <int>
## 1 rec2_rf Preprocesso… accura… 0.938 0.00257 5 recipe rand… 1
## 2 rec2_rf Preprocesso… sensit… 0.897 0.00310 5 recipe rand… 1
## 3 rec2_rf Preprocesso… specif… 0.958 0.00444 5 recipe rand… 1
## 4 rec1_rf Preprocesso… accura… 0.937 0.00263 5 formula rand… 2
## 5 rec1_rf Preprocesso… sensit… 0.892 0.00170 5 formula rand… 2
## 6 rec1_rf Preprocesso… specif… 0.960 0.00444 5 formula rand… 2
## 7 rec1_tree Preprocesso… accura… 0.905 0.00199 5 formula deci… 3
## 8 rec1_tree Preprocesso… sensit… 0.859 0.00805 5 formula deci… 3
## 9 rec1_tree Preprocesso… specif… 0.928 0.00447 5 formula deci… 3
## 10 rec2_tree Preprocesso… accura… 0.905 0.00199 5 recipe deci… 4
## 11 rec2_tree Preprocesso… sensit… 0.859 0.00805 5 recipe deci… 4
## 12 rec2_tree Preprocesso… specif… 0.928 0.00447 5 recipe deci… 4
## 13 rec2_logreg Preprocesso… accura… 0.797 0.00217 5 recipe logi… 5
## 14 rec2_logreg Preprocesso… sensit… 0.618 0.00547 5 recipe logi… 5
## 15 rec2_logreg Preprocesso… specif… 0.886 0.00259 5 recipe logi… 5
## 16 rec1_logreg Preprocesso… accura… 0.789 0.00109 5 formula logi… 6
## 17 rec1_logreg Preprocesso… sensit… 0.603 0.00617 5 formula logi… 6
## 18 rec1_logreg Preprocesso… specif… 0.883 0.002 5 formula logi… 6
Suponha que o modelo de regressão logística tenha sido o escolhido.
Vamos ajustar o modelo nos dados de treinamento e verificar o desempenho nos dados de teste.
Vimos os comandos fit()
e predict()
/augment()
, mas podemos usar a função final_fit()
, que combina esses passos.
final_fit <- last_fit(logreg_wf,
diabetes_split,
metrics = diabetes_metrics)
Lembre-se que o objeto diabetes_split
tem informações sobre a separação dos dados originais em treino e teste.
final_fit
## # Resampling results
## # Manual resampling
## # A tibble: 1 × 6
## splits id .metrics .notes .predictions .workflow
## <list> <chr> <list> <list> <list> <list>
## 1 <split [12000/3000]> train/test spl… <tibble> <tibble> <tibble> <workflow>
Métricas calculadas para o conjunto de dados teste:
collect_metrics(final_fit)
## # A tibble: 3 × 4
## .metric .estimator .estimate .config
## <chr> <chr> <dbl> <chr>
## 1 accuracy binary 0.784 Preprocessor1_Model1
## 2 sensitivity binary 0.59 Preprocessor1_Model1
## 3 specificity binary 0.880 Preprocessor1_Model1
Predições para o conjunto de dados teste:
collect_predictions(final_fit) %>%
head()
## # A tibble: 6 × 5
## .pred_class id .row Diabetic .config
## <fct> <chr> <int> <fct> <chr>
## 1 0 train/test split 2 0 Preprocessor1_Model1
## 2 0 train/test split 5 0 Preprocessor1_Model1
## 3 0 train/test split 8 0 Preprocessor1_Model1
## 4 0 train/test split 10 0 Preprocessor1_Model1
## 5 1 train/test split 13 1 Preprocessor1_Model1
## 6 0 train/test split 19 0 Preprocessor1_Model1
Quais informações temos em final_fit
?
extract_workflow(final_fit)
## ══ Workflow [trained] ══════════════════════════════════════════════════════════
## Preprocessor: Recipe
## Model: logistic_reg()
##
## ── Preprocessor ────────────────────────────────────────────────────────────────
## 1 Recipe Step
##
## • step_poly()
##
## ── Model ───────────────────────────────────────────────────────────────────────
##
## Call: stats::glm(formula = ..y ~ ., family = ~"binomial", data = data)
##
## Coefficients:
## (Intercept) Pregnancies PlasmaGlucose
## 6.928384 -0.278826 -0.008881
## DiastolicBloodPressure TricepsThickness SerumInsulin
## -0.011326 -0.023160 -0.003905
## BMI DiabetesPedigree Age_poly_1
## -0.052296 -0.987093 -79.422268
## Age_poly_2
## 32.092675
##
## Degrees of Freedom: 11999 Total (i.e. Null); 11990 Residual
## Null Deviance: 15280
## Residual Deviance: 10280 AIC: 10300
Quais as estimativas dos parâmetros do modelo final?
final_fit %>%
extract_fit_parsnip() %>%
tidy()
## # A tibble: 10 × 5
## term estimate std.error statistic p.value
## <chr> <dbl> <dbl> <dbl> <dbl>
## 1 (Intercept) 6.93 0.192 36.1 1.12e-285
## 2 Pregnancies -0.279 0.00759 -36.7 3.75e-295
## 3 PlasmaGlucose -0.00888 0.000777 -11.4 2.88e- 30
## 4 DiastolicBloodPressure -0.0113 0.00150 -7.56 3.92e- 14
## 5 TricepsThickness -0.0232 0.00171 -13.5 1.04e- 41
## 6 SerumInsulin -0.00390 0.000184 -21.3 2.08e-100
## 7 BMI -0.0523 0.00262 -20.0 9.80e- 89
## 8 DiabetesPedigree -0.987 0.0632 -15.6 6.51e- 55
## 9 Age_poly_1 -79.4 2.60 -30.5 6.35e-205
## 10 Age_poly_2 32.1 2.47 13.0 1.82e- 38
Algumas características dos modelos não podem ser estimadas diretamente dos dados.
Escolhemos um modelo de regressão logística, por exemplo, e usamos os dados de treinamento para obter os parâmetros do modelo.
No entanto, algumas escolhas são feitas antes do ajuste do modelo: usaremos alguma forma quadrática? interações? quais variáveis vamos considerar?
Algumas decisões devem ser feitas na etapa receita e outras devem ser feitas dentro do modelo.
Para ajuste fino, podemos testar workflows diferentes e avaliar o desempenho com validação cruzada.
Para um modelo de regressão logística em que uma das variáveis, Age
, será considerada através de um polinômio, temos a seguinte receita:
diabetes_rec <-
recipe(Diabetic ~ ., data = diabetes_train) %>%
step_poly(Age, degree = tune())
Repare que, acima, não especificamos diretamente o grau do polinômio. Vamos escolher o melhor hiperparâmetro usando a função tune()
.
Com a receita definida, vamos agregar as informações:
logregpol_wf <- workflow(diabetes_rec, logreg_spec)
logregpol_wf
## ══ Workflow ════════════════════════════════════════════════════════════════════
## Preprocessor: Recipe
## Model: logistic_reg()
##
## ── Preprocessor ────────────────────────────────────────────────────────────────
## 1 Recipe Step
##
## • step_poly()
##
## ── Model ───────────────────────────────────────────────────────────────────────
## Logistic Regression Model Specification (classification)
##
## Engine-Specific Arguments:
## family = binomial
##
## Computational engine: glm
A função tune_grid()
calcula um conjunto de métricas usando validação cruzada para avaliar o desempenho em um conjunto pré-determinado de hiperparâmetros de um modelo ou de uma receita:
logregpol_res <- tune_grid(logregpol_wf,
diabetes_folds,
grid = tibble(degree=1:5))
logregpol_res
## # Tuning results
## # 5-fold cross-validation using stratification
## # A tibble: 5 × 4
## splits id .metrics .notes
## <list> <chr> <list> <list>
## 1 <split [9600/2400]> Fold1 <tibble [15 × 5]> <tibble [1 × 3]>
## 2 <split [9600/2400]> Fold2 <tibble [15 × 5]> <tibble [1 × 3]>
## 3 <split [9600/2400]> Fold3 <tibble [15 × 5]> <tibble [1 × 3]>
## 4 <split [9600/2400]> Fold4 <tibble [15 × 5]> <tibble [1 × 3]>
## 5 <split [9600/2400]> Fold5 <tibble [15 × 5]> <tibble [1 × 3]>
##
## There were issues with some computations:
##
## - Warning(s) x5: glm.fit: fitted probabilities numerically 0 or 1 occurred
##
## Run `show_notes(.Last.tune.result)` for more information.
Apresentando os resultados (média dos 5 folds) para cada grau de polinômio (hiperparâmetro) considerado:
collect_metrics(logregpol_res)
## # A tibble: 15 × 7
## degree .metric .estimator mean n std_err .config
## <int> <chr> <chr> <dbl> <int> <dbl> <chr>
## 1 1 accuracy binary 0.789 5 0.00109 Preprocessor1_Model1
## 2 1 brier_class binary 0.142 5 0.000820 Preprocessor1_Model1
## 3 1 roc_auc binary 0.860 5 0.00144 Preprocessor1_Model1
## 4 2 accuracy binary 0.797 5 0.00217 Preprocessor2_Model1
## 5 2 brier_class binary 0.139 5 0.000910 Preprocessor2_Model1
## 6 2 roc_auc binary 0.862 5 0.00159 Preprocessor2_Model1
## 7 3 accuracy binary 0.814 5 0.00254 Preprocessor3_Model1
## 8 3 brier_class binary 0.127 5 0.00168 Preprocessor3_Model1
## 9 3 roc_auc binary 0.889 5 0.00244 Preprocessor3_Model1
## 10 4 accuracy binary 0.818 5 0.00253 Preprocessor4_Model1
## 11 4 brier_class binary 0.124 5 0.00187 Preprocessor4_Model1
## 12 4 roc_auc binary 0.894 5 0.00273 Preprocessor4_Model1
## 13 5 accuracy binary 0.830 5 0.00285 Preprocessor5_Model1
## 14 5 brier_class binary 0.118 5 0.00138 Preprocessor5_Model1
## 15 5 roc_auc binary 0.902 5 0.00208 Preprocessor5_Model1
Visualização gráfica usando autoplot()
:
autoplot(logregpol_res, metric = "accuracy")
Mostrando os melhores resultados:
show_best(logregpol_res,
metric = "accuracy",
n = 3)
## # A tibble: 3 × 7
## degree .metric .estimator mean n std_err .config
## <int> <chr> <chr> <dbl> <int> <dbl> <chr>
## 1 5 accuracy binary 0.830 5 0.00285 Preprocessor5_Model1
## 2 4 accuracy binary 0.818 5 0.00253 Preprocessor4_Model1
## 3 3 accuracy binary 0.814 5 0.00254 Preprocessor3_Model1
E se ajustarmos um outro modelo, também com algum outro hiperparâmetro? Como comparar?
Podemos ajustar uma regressão logística com regularização LASSO:
logreglasso_spec <- logistic_reg(penalty = tune(), mixture = 1) %>%
set_engine("glmnet", family = "binomial") %>%
set_mode("classification")
mixture = 1
especifica LASSO puro e o hiperparâmetro penalty
será escolhido (ajuste fino) através da validação cruzada.
Não consideraremos o polinômio para Age
neste workflow, então podemos configurá-lo diretamente com a fórmula e o modelo especificado:
logreglasso_wf <- workflow(Diabetic ~ ., logreglasso_spec)
logreglasso_wf
## ══ Workflow ════════════════════════════════════════════════════════════════════
## Preprocessor: Formula
## Model: logistic_reg()
##
## ── Preprocessor ────────────────────────────────────────────────────────────────
## Diabetic ~ .
##
## ── Model ───────────────────────────────────────────────────────────────────────
## Logistic Regression Model Specification (classification)
##
## Main Arguments:
## penalty = tune()
## mixture = 1
##
## Engine-Specific Arguments:
## family = binomial
##
## Computational engine: glmnet
Para avaliar o desempenho de múltiplos valores do hiperparâmetro usando validação cruzada:
set.seed(2024)
logreglasso_res <-
tune_grid(logreglasso_wf,
resamples = diabetes_folds,
grid = 20,
metrics = diabetes_metrics)
Métricas resultantes da validação cruzada considerando os valores do grid:
logreglasso_res %>% collect_metrics()
## # A tibble: 60 × 7
## penalty .metric .estimator mean n std_err .config
## <dbl> <chr> <chr> <dbl> <int> <dbl> <chr>
## 1 2.77e-10 accuracy binary 0.789 5 0.00107 Preprocessor1_Model01
## 2 2.77e-10 sensitivity binary 0.6 5 0.00597 Preprocessor1_Model01
## 3 2.77e-10 specificity binary 0.883 5 0.00189 Preprocessor1_Model01
## 4 3.45e-10 accuracy binary 0.789 5 0.00107 Preprocessor1_Model02
## 5 3.45e-10 sensitivity binary 0.6 5 0.00597 Preprocessor1_Model02
## 6 3.45e-10 specificity binary 0.883 5 0.00189 Preprocessor1_Model02
## 7 1.17e- 9 accuracy binary 0.789 5 0.00107 Preprocessor1_Model03
## 8 1.17e- 9 sensitivity binary 0.6 5 0.00597 Preprocessor1_Model03
## 9 1.17e- 9 specificity binary 0.883 5 0.00189 Preprocessor1_Model03
## 10 5.42e- 9 accuracy binary 0.789 5 0.00107 Preprocessor1_Model04
## # ℹ 50 more rows
autoplot(logreglasso_res)
O melhor resultado da validação cruzada, considerando a métrica de precisão:
show_best(logreglasso_res, metric = "accuracy", n = 1)
## # A tibble: 1 × 7
## penalty .metric .estimator mean n std_err .config
## <dbl> <chr> <chr> <dbl> <int> <dbl> <chr>
## 1 2.77e-10 accuracy binary 0.789 5 0.00107 Preprocessor1_Model01
Abaixo está um workflow para uma árvore de decisão com ajuste de hiperparâmetro (cost_complexity
). Primeiramente, o modelo é especificado incluindo tune()
:
tree_spec <-
decision_tree(
cost_complexity = tune()
) %>%
set_mode("classification") %>%
set_engine("rpart", model = TRUE)
O workflow:
tree_wf <- workflow(Diabetic ~ ., tree_spec)
Vamos usar tune_grid()
para avaliar múltiplos valores para o hiperparâmetro:
tree_res <-
tune_grid(tree_wf,
resamples = diabetes_folds,
grid = 30,
metrics = diabetes_metrics)
É possível fornecer um data.frame
na opção grid
, para ser mais específico.
Métricas obtidas por meio de validação cruzada considerando os valores da grade
:
tree_res %>% collect_metrics()
## # A tibble: 90 × 7
## cost_complexity .metric .estimator mean n std_err .config
## <dbl> <chr> <chr> <dbl> <int> <dbl> <chr>
## 1 1.24e- 9 accuracy binary 0.913 5 0.00159 Preprocessor1_Mod…
## 2 1.24e- 9 sensitivity binary 0.869 5 0.00393 Preprocessor1_Mod…
## 3 1.24e- 9 specificity binary 0.935 5 0.00306 Preprocessor1_Mod…
## 4 1.40e-10 accuracy binary 0.913 5 0.00159 Preprocessor1_Mod…
## 5 1.40e-10 sensitivity binary 0.869 5 0.00393 Preprocessor1_Mod…
## 6 1.40e-10 specificity binary 0.935 5 0.00306 Preprocessor1_Mod…
## 7 2.22e- 4 accuracy binary 0.914 5 0.00143 Preprocessor1_Mod…
## 8 2.22e- 4 sensitivity binary 0.874 5 0.00435 Preprocessor1_Mod…
## 9 2.22e- 4 specificity binary 0.935 5 0.00344 Preprocessor1_Mod…
## 10 5.45e- 9 accuracy binary 0.913 5 0.00159 Preprocessor1_Mod…
## # ℹ 80 more rows
autoplot(tree_res)
O melhor resultado da validação cruzada, considerando a métrica de acurácia:
show_best(tree_res, metric = "accuracy", n = 1)
## # A tibble: 1 × 7
## cost_complexity .metric .estimator mean n std_err .config
## <dbl> <chr> <chr> <dbl> <int> <dbl> <chr>
## 1 0.00126 accuracy binary 0.920 5 0.00146 Preprocessor1_Model16
Um hiperparâmetro comumente escolhido para modelos de floresta aleatória é mtry
, que é o número de preditores que serão amostrados aleatoriamente em cada bifurcação ao criar os modelos de árvore.
Quando usamos o engine
(algoritmo/método) ranger
, o default é floor(sqrt(ncol(x)))
.
Vamos usar a validação cruzada para fazer o “ajuste fino” desse hiperparâmetro.
Começamos especificando o modelo e incluindo tune()
:
rf_spec <- rand_forest(trees = 1000,
mtry = tune()) %>%
set_mode("classification") %>%
set_engine("ranger", importance = "impurity")
rf_spec
## Random Forest Model Specification (classification)
##
## Main Arguments:
## mtry = tune()
## trees = 1000
##
## Engine-Specific Arguments:
## importance = impurity
##
## Computational engine: ranger
O workflow:
rf_wf <- workflow(Diabetic ~ ., rf_spec)
rf_wf
## ══ Workflow ════════════════════════════════════════════════════════════════════
## Preprocessor: Formula
## Model: rand_forest()
##
## ── Preprocessor ────────────────────────────────────────────────────────────────
## Diabetic ~ .
##
## ── Model ───────────────────────────────────────────────────────────────────────
## Random Forest Model Specification (classification)
##
## Main Arguments:
## mtry = tune()
## trees = 1000
##
## Engine-Specific Arguments:
## importance = impurity
##
## Computational engine: ranger
Vamos usar tune_grid()
para avaliar o desempenho de múltiplos valores do hiperparâmetro usando validação cruzada:
set.seed(2024)
rf_res <-
tune_grid(rf_wf,
resamples = diabetes_folds,
grid = 15,
metrics = diabetes_metrics)
Métricas resultantes da validação cruzada considerando os valores no grid
:
rf_res %>% collect_metrics()
## # A tibble: 24 × 7
## mtry .metric .estimator mean n std_err .config
## <int> <chr> <chr> <dbl> <int> <dbl> <chr>
## 1 8 accuracy binary 0.933 5 0.00207 Preprocessor1_Model1
## 2 8 sensitivity binary 0.894 5 0.00390 Preprocessor1_Model1
## 3 8 specificity binary 0.953 5 0.00432 Preprocessor1_Model1
## 4 3 accuracy binary 0.938 5 0.00247 Preprocessor1_Model2
## 5 3 sensitivity binary 0.894 5 0.00258 Preprocessor1_Model2
## 6 3 specificity binary 0.960 5 0.00432 Preprocessor1_Model2
## 7 6 accuracy binary 0.934 5 0.00262 Preprocessor1_Model3
## 8 6 sensitivity binary 0.892 5 0.00470 Preprocessor1_Model3
## 9 6 specificity binary 0.955 5 0.00437 Preprocessor1_Model3
## 10 7 accuracy binary 0.934 5 0.00268 Preprocessor1_Model4
## # ℹ 14 more rows
autoplot(rf_res)
O melhor resultado da validação cruzada, considerando a acurácia como métrica:
show_best(rf_res, metric = "accuracy", n = 1)
## # A tibble: 1 × 7
## mtry .metric .estimator mean n std_err .config
## <int> <chr> <chr> <dbl> <int> <dbl> <chr>
## 1 3 accuracy binary 0.938 5 0.00247 Preprocessor1_Model2
Vamos supor, por exemplo, que entre as quatro opções (regressão logística polinomial, LASSO, árvore de decisão e floresta aleatória), decidimos usar a regressão logística LASSO com melhor desempenho após a escolha do hiperparâmetro (tuning).
Precisamos então selecionar o modelo com o hiperparâmetro de melhor desempenho, usando a função select_best()
:
best_acc <- select_best(logreglasso_res, metric = "accuracy")
best_acc
## # A tibble: 1 × 2
## penalty .config
## <dbl> <chr>
## 1 2.77e-10 Preprocessor1_Model01
Para ajustar o modelo final, pegamos o workflow desejado (logregpol_wf
neste exemplo) e usamos a função finalize_workflow()
para especificar o hiperparâmetro com melhor desempenho. A função last_fit()
ajusta este modelo final com os dados de treinamento e avalia o desempenho nos dados de teste.
final_lasso_fit <- logreglasso_wf %>%
finalize_workflow(best_acc) %>%
last_fit(diabetes_split)
final_lasso_fit
## # Resampling results
## # Manual resampling
## # A tibble: 1 × 6
## splits id .metrics .notes .predictions .workflow
## <list> <chr> <list> <list> <list> <list>
## 1 <split [12000/3000]> train/test spl… <tibble> <tibble> <tibble> <workflow>
Resultados no conjunto de teste:
final_lasso_fit %>% collect_metrics()
## # A tibble: 3 × 4
## .metric .estimator .estimate .config
## <chr> <chr> <dbl> <chr>
## 1 accuracy binary 0.776 Preprocessor1_Model1
## 2 roc_auc binary 0.846 Preprocessor1_Model1
## 3 brier_class binary 0.150 Preprocessor1_Model1
Para o workflow final selecionado, podemos salvar todas as etapas de ajuste final (obtidas usando o conjunto de treinamento):
fitted_wf <- extract_workflow(final_lasso_fit)
fitted_wf
## ══ Workflow [trained] ══════════════════════════════════════════════════════════
## Preprocessor: Formula
## Model: logistic_reg()
##
## ── Preprocessor ────────────────────────────────────────────────────────────────
## Diabetic ~ .
##
## ── Model ───────────────────────────────────────────────────────────────────────
##
## Call: glmnet::glmnet(x = maybe_matrix(x), y = y, family = ~"binomial", alpha = ~1)
##
## Df %Dev Lambda
## 1 0 0.00 0.194600
## 2 1 2.25 0.177300
## 3 1 4.09 0.161500
## 4 2 6.46 0.147200
## 5 2 8.71 0.134100
## 6 2 10.59 0.122200
## 7 2 12.17 0.111300
## 8 3 13.70 0.101400
## 9 3 15.29 0.092430
## 10 4 16.91 0.084220
## 11 4 18.40 0.076730
## 12 4 19.68 0.069920
## 13 5 20.94 0.063710
## 14 5 22.09 0.058050
## 15 6 23.21 0.052890
## 16 6 24.22 0.048190
## 17 7 25.16 0.043910
## 18 7 26.02 0.040010
## 19 7 26.76 0.036460
## 20 7 27.39 0.033220
## 21 7 27.94 0.030270
## 22 8 28.43 0.027580
## 23 8 28.89 0.025130
## 24 8 29.29 0.022890
## 25 8 29.62 0.020860
## 26 8 29.91 0.019010
## 27 8 30.16 0.017320
## 28 8 30.38 0.015780
## 29 8 30.56 0.014380
## 30 8 30.71 0.013100
## 31 8 30.84 0.011940
## 32 8 30.95 0.010880
## 33 8 31.05 0.009911
## 34 8 31.13 0.009030
## 35 8 31.20 0.008228
## 36 8 31.25 0.007497
## 37 8 31.30 0.006831
## 38 8 31.34 0.006224
## 39 8 31.38 0.005671
## 40 8 31.40 0.005167
## 41 8 31.43 0.004708
## 42 8 31.45 0.004290
## 43 8 31.47 0.003909
## 44 8 31.48 0.003562
## 45 8 31.49 0.003245
## 46 8 31.50 0.002957
##
## ...
## and 13 more lines.
Obtenha valores preditos para os dados de teste:
fitted_wf %>% augment(diabetes_test)
## # A tibble: 3,000 × 12
## .pred_class .pred_1 .pred_0 Pregnancies PlasmaGlucose DiastolicBloodPressure
## <fct> <dbl> <dbl> <dbl> <dbl> <dbl>
## 1 0 0.324 0.676 8 92 93
## 2 0 0.107 0.893 1 85 59
## 3 0 0.0389 0.961 0 67 87
## 4 0 0.0624 0.938 1 72 31
## 5 1 0.581 0.419 5 114 101
## 6 0 0.288 0.712 8 117 39
## 7 1 0.631 0.369 2 44 81
## 8 0 0.136 0.864 0 119 50
## 9 1 0.631 0.369 8 152 83
## 10 0 0.170 0.830 2 118 61
## # ℹ 2,990 more rows
## # ℹ 6 more variables: TricepsThickness <dbl>, SerumInsulin <dbl>, BMI <dbl>,
## # DiabetesPedigree <dbl>, Age <dbl>, Diabetic <fct>
Resultados no conjunto de treinamento:
fitted_wf %>%
augment(diabetes_train) %>%
metrics(truth = Diabetic, estimate = .pred_class)
## # A tibble: 2 × 3
## .metric .estimator .estimate
## <chr> <chr> <dbl>
## 1 accuracy binary 0.790
## 2 kap binary 0.507
Para obter as estimativas do modelo:
final_lasso_fit %>%
extract_fit_parsnip() %>%
tidy() %>%
kable(booktabs = TRUE)
term | estimate | penalty |
---|---|---|
(Intercept) | 8.4881010 | 0 |
Pregnancies | -0.2752126 | 0 |
PlasmaGlucose | -0.0084157 | 0 |
DiastolicBloodPressure | -0.0108160 | 0 |
TricepsThickness | -0.0224399 | 0 |
SerumInsulin | -0.0038390 | 0 |
BMI | -0.0514529 | 0 |
DiabetesPedigree | -0.9734048 | 0 |
Age | -0.0578455 | 0 |
Podemos definir um conjunto de workflows e então avaliá-lo usando tune_grid
.
wf_tune_set <- workflow_set(preproc = list(rec1 = Diabetic ~ .,
rec2 = diabetes_rec,
rec1 = Diabetic ~ .,
rec1 = Diabetic ~ .),
models = list(tree = tree_spec,
logreg = logreg_spec,
rf = rf_spec,
lasso_log = logreglasso_spec),
cross = FALSE)
set.seed(2024)
wf_tune_res <- wf_tune_set %>%
workflow_map(resamples = diabetes_folds,
grid = 40,
metrics = diabetes_metrics,
control = control_grid(save_pred = TRUE))
tune_results <- wf_tune_res %>%
collect_metrics()
tune_results
## # A tibble: 273 × 9
## wflow_id .config preproc model .metric .estimator mean n std_err
## <chr> <chr> <chr> <chr> <chr> <chr> <dbl> <int> <dbl>
## 1 rec1_tree Preprocessor1… formula deci… accura… binary 0.913 5 0.00148
## 2 rec1_tree Preprocessor1… formula deci… sensit… binary 0.869 5 0.00381
## 3 rec1_tree Preprocessor1… formula deci… specif… binary 0.935 5 0.00276
## 4 rec1_tree Preprocessor1… formula deci… accura… binary 0.913 5 0.00159
## 5 rec1_tree Preprocessor1… formula deci… sensit… binary 0.869 5 0.00393
## 6 rec1_tree Preprocessor1… formula deci… specif… binary 0.935 5 0.00306
## 7 rec1_tree Preprocessor1… formula deci… accura… binary 0.916 5 0.00202
## 8 rec1_tree Preprocessor1… formula deci… sensit… binary 0.857 5 0.00199
## 9 rec1_tree Preprocessor1… formula deci… specif… binary 0.945 5 0.00317
## 10 rec1_tree Preprocessor1… formula deci… accura… binary 0.887 5 0.00205
## # ℹ 263 more rows
Vamos selecionar o workflow com melhor desempenho, considerando a acurácia:
best_wf_id <- wf_tune_res %>%
rank_results() %>%
filter(.metric == "accuracy") %>%
arrange(desc(mean)) %>%
slice(1) %>% # Select the best one
pull(wflow_id)
best_wf_id
## [1] "rec1_rf"
Então, podemos extrair o melhor workflow da seguinte forma:
best_workflow <- wf_tune_set %>%
extract_workflow(best_wf_id)
Para finalizar o workflow selecionado fixando o hiperparâmetro com melhor desempenho:
best_workflow_final <- best_workflow %>%
finalize_workflow(
wf_tune_res %>%
extract_workflow_set_result(best_wf_id) %>%
select_best(metric = "accuracy")
)
best_workflow_final
## ══ Workflow ════════════════════════════════════════════════════════════════════
## Preprocessor: Formula
## Model: rand_forest()
##
## ── Preprocessor ────────────────────────────────────────────────────────────────
## Diabetic ~ .
##
## ── Model ───────────────────────────────────────────────────────────────────────
## Random Forest Model Specification (classification)
##
## Main Arguments:
## mtry = 2
## trees = 1000
##
## Engine-Specific Arguments:
## importance = impurity
##
## Computational engine: ranger
final_wf_fit <-
best_workflow_final %>%
last_fit(diabetes_split,
metrics = diabetes_metrics)
final_wf_fit
## # Resampling results
## # Manual resampling
## # A tibble: 1 × 6
## splits id .metrics .notes .predictions .workflow
## <list> <chr> <list> <list> <list> <list>
## 1 <split [12000/3000]> train/test spl… <tibble> <tibble> <tibble> <workflow>
Resultados no conjunto teste:
final_wf_fit %>% collect_metrics()
## # A tibble: 3 × 4
## .metric .estimator .estimate .config
## <chr> <chr> <dbl> <chr>
## 1 accuracy binary 0.937 Preprocessor1_Model1
## 2 sensitivity binary 0.894 Preprocessor1_Model1
## 3 specificity binary 0.958 Preprocessor1_Model1
Para o workflow final selecionado, salvamos todas as etapas de ajuste final (obtidas usando o conjunto de treinamento):
fitted_wf <- extract_workflow(final_wf_fit)
fitted_wf
## ══ Workflow [trained] ══════════════════════════════════════════════════════════
## Preprocessor: Formula
## Model: rand_forest()
##
## ── Preprocessor ────────────────────────────────────────────────────────────────
## Diabetic ~ .
##
## ── Model ───────────────────────────────────────────────────────────────────────
## Ranger result
##
## Call:
## ranger::ranger(x = maybe_data_frame(x), y = y, mtry = min_cols(~2L, x), num.trees = ~1000, importance = ~"impurity", num.threads = 1, verbose = FALSE, seed = sample.int(10^5, 1), probability = TRUE)
##
## Type: Probability estimation
## Number of trees: 1000
## Sample size: 12000
## Number of independent variables: 8
## Mtry: 2
## Target node size: 10
## Variable importance mode: impurity
## Splitrule: gini
## OOB prediction error (Brier s.): 0.04772504
saveRDS(fitted_wf, "diabetes_rf_modelo.rds")
Valores preditos para o conjunto de teste:
final_wf_fit %>% augment()
## # A tibble: 3,000 × 10
## .pred_class Pregnancies PlasmaGlucose DiastolicBloodPressure TricepsThickness
## <fct> <dbl> <dbl> <dbl> <dbl>
## 1 0 8 92 93 47
## 2 0 1 85 59 27
## 3 0 0 67 87 43
## 4 0 1 72 31 40
## 5 1 5 114 101 43
## 6 0 8 117 39 32
## 7 1 2 44 81 46
## 8 0 0 119 50 52
## 9 0 8 152 83 42
## 10 1 2 118 61 56
## # ℹ 2,990 more rows
## # ℹ 5 more variables: SerumInsulin <dbl>, BMI <dbl>, DiabetesPedigree <dbl>,
## # Age <dbl>, Diabetic <fct>
Workflow final:
fitted_wf %>%
extract_fit_parsnip()
## parsnip model object
##
## Ranger result
##
## Call:
## ranger::ranger(x = maybe_data_frame(x), y = y, mtry = min_cols(~2L, x), num.trees = ~1000, importance = ~"impurity", num.threads = 1, verbose = FALSE, seed = sample.int(10^5, 1), probability = TRUE)
##
## Type: Probability estimation
## Number of trees: 1000
## Sample size: 12000
## Number of independent variables: 8
## Mtry: 2
## Target node size: 10
## Variable importance mode: impurity
## Splitrule: gini
## OOB prediction error (Brier s.): 0.04772504
Visualização da importância dos preditores:
library(vip)
final_wf_fit %>%
extract_fit_parsnip() %>%
vip(num_features = 10)
Com o modelo final escolhido e salvo, podemos utilizá-lo na predição de Diabetic
quando tivermos acesso às variáveis preditoras de novos pacientes.
Para carregar o modelo salvo:
modelo_final <- readRDS("diabetes_rf_modelo.rds")
Preparando os dados de dois novos pacientes, por exemplo:
novos_pacientes <- tibble(Pregnancies = c(1, 2),
PlasmaGlucose = c(100, 180),
DiastolicBloodPressure = c(30, 80),
TricepsThickness = c( 15, 42),
SerumInsulin = c(39, 180),
BMI = c(25, 39),
DiabetesPedigree = c(0.15, 0.60),
Age = c(30, 50))
novos_pacientes
## # A tibble: 2 × 8
## Pregnancies PlasmaGlucose DiastolicBloodPressure TricepsThickness SerumInsulin
## <dbl> <dbl> <dbl> <dbl> <dbl>
## 1 1 100 30 15 39
## 2 2 180 80 42 180
## # ℹ 3 more variables: BMI <dbl>, DiabetesPedigree <dbl>, Age <dbl>
e acrescentar as colunas com as predições (tanto de classe quanto de probabilidade para cada classe):
bind_cols(
novos_pacientes,
predict(modelo_final, new_data = novos_pacientes, type = "class"),
predict(modelo_final, new_data = novos_pacientes, type = "prob")
)
## # A tibble: 2 × 11
## Pregnancies PlasmaGlucose DiastolicBloodPressure TricepsThickness SerumInsulin
## <dbl> <dbl> <dbl> <dbl> <dbl>
## 1 1 100 30 15 39
## 2 2 180 80 42 180
## # ℹ 6 more variables: BMI <dbl>, DiabetesPedigree <dbl>, Age <dbl>,
## # .pred_class <fct>, .pred_1 <dbl>, .pred_0 <dbl>