← 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.
Vamos carregar o pacote tidyverse
e ler o conjunto de dados abalone
que usaremos neste tutorial:
library(tidyverse)
abalone <- read_csv("abalone.csv")
Algumas informações rápidas dos dados:
abalone %>% glimpse()
## Rows: 4,177
## Columns: 9
## $ sex <chr> "male", "male", "female", "male", "infant", "infant", "…
## $ length <dbl> 0.455, 0.350, 0.530, 0.440, 0.330, 0.425, 0.530, 0.545,…
## $ diameter <dbl> 0.365, 0.265, 0.420, 0.365, 0.255, 0.300, 0.415, 0.425,…
## $ height <dbl> 0.095, 0.090, 0.135, 0.125, 0.080, 0.095, 0.150, 0.125,…
## $ whole_weight <dbl> 0.5140, 0.2255, 0.6770, 0.5160, 0.2050, 0.3515, 0.7775,…
## $ shucked_weight <dbl> 0.2245, 0.0995, 0.2565, 0.2155, 0.0895, 0.1410, 0.2370,…
## $ viscera_weight <dbl> 0.1010, 0.0485, 0.1415, 0.1140, 0.0395, 0.0775, 0.1415,…
## $ shell_weight <dbl> 0.150, 0.070, 0.210, 0.155, 0.055, 0.120, 0.330, 0.260,…
## $ rings <dbl> 15, 7, 9, 10, 7, 8, 20, 16, 9, 19, 14, 10, 11, 10, 10, …
Temos \(n=4177\) observações. A variável resposta é rings
.
As primeiras 10 observações:
abalone %>% head(10)
## # A tibble: 10 × 9
## sex length diameter height whole_weight shucked_weight viscera_weight
## <chr> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl>
## 1 male 0.455 0.365 0.095 0.514 0.224 0.101
## 2 male 0.35 0.265 0.09 0.226 0.0995 0.0485
## 3 female 0.53 0.42 0.135 0.677 0.256 0.142
## 4 male 0.44 0.365 0.125 0.516 0.216 0.114
## 5 infant 0.33 0.255 0.08 0.205 0.0895 0.0395
## 6 infant 0.425 0.3 0.095 0.352 0.141 0.0775
## 7 female 0.53 0.415 0.15 0.778 0.237 0.142
## 8 female 0.545 0.425 0.125 0.768 0.294 0.150
## 9 male 0.475 0.37 0.125 0.509 0.216 0.112
## 10 female 0.55 0.44 0.15 0.894 0.314 0.151
## # ℹ 2 more variables: shell_weight <dbl>, rings <dbl>
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)
set.seed(1234)
ring_split <- initial_split(abalone, prop = 0.8, strata = rings)
As funções training()
e testing()
são usadas para extrair os conjuntos de treinamento e teste, respectivamente:
ring_train <- training(ring_split)
ring_test <- testing(ring_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).
No tidymodels
, temos alguns passos para especificar um modelo:
Por exemplo, se quisermos especificar um modelo de regressão linear:
linear_reg()
## Linear Regression Model Specification (regression)
##
## Computational engine: lm
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(linear_reg)
## function (mode = "regression", engine = "lm", penalty = NULL,
## mixture = NULL)
## NULL
Veja que o método default para linear_reg()
é lm
, que ajusta o modelo por mínimos quadrados ordinários, mas é possível escolher outros métodos.
Por exemplo, um modelo de regressão linear via mínimos quadrados generalizados:
linear_reg() %>%
set_engine("gls")
## Linear Regression Model Specification (regression)
##
## Computational engine: gls
Todos os modelos disponíveis sã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:
ring_rec <- recipe(rings ~ ., data = ring_train)
No exemplo acima, a função recipe()
apenas define a variável resposta e os preditores através da fórmula.
ring_rec %>% summary()
## # A tibble: 9 × 4
## variable type role source
## <chr> <list> <chr> <chr>
## 1 sex <chr [3]> predictor original
## 2 length <chr [2]> predictor original
## 3 diameter <chr [2]> predictor original
## 4 height <chr [2]> predictor original
## 5 whole_weight <chr [2]> predictor original
## 6 shucked_weight <chr [2]> predictor original
## 7 viscera_weight <chr [2]> predictor original
## 8 shell_weight <chr [2]> predictor original
## 9 rings <chr [2]> 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:
* Níveis de uma variável categórica
* Avaliar se uma coluna tem variância nula (constante) - step_zv()
* Calcular média e desvio-padrão para a normalização
* Projetar os novos dados nas componentes principais obtidas pelos dados de treinamento.
Um outro exemplo de receita:
ring_rec <- recipe(rings ~ ., data = ring_train) %>%
step_normalize(all_numeric_predictors()) %>%
step_corr(all_numeric_predictors(), threshold = 0.9) %>%
step_dummy(all_nominal_predictors()) %>%
step_poly(shucked_weight, degree = 2)
A tabela a seguir apresenta os dados pré-processamos segundo a receita acima:
rings | height | shell_weight | sex_infant | sex_male | shucked_weight_poly_1 | shucked_weight_poly_2 |
---|---|---|---|---|---|---|
8 | -1.044 | -0.855 | 1 | 0 | -0.017 | 0.008 |
7 | -1.278 | -0.890 | 1 | 0 | -0.021 | 0.015 |
8 | -0.810 | -0.819 | 0 | 1 | -0.010 | -0.003 |
5 | -2.214 | -1.573 | 1 | 0 | -0.026 | 0.027 |
5 | -1.980 | -1.630 | 1 | 0 | -0.026 | 0.028 |
4 | -2.097 | -1.608 | 1 | 0 | -0.027 | 0.030 |
7 | -1.044 | -1.178 | 1 | 0 | -0.021 | 0.017 |
6 | -1.629 | -1.393 | 1 | 0 | -0.022 | 0.019 |
7 | -0.927 | -0.926 | 0 | 1 | -0.015 | 0.004 |
8 | 0.009 | -0.136 | 0 | 0 | -0.008 | -0.006 |
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 linear:
reg_spec <- linear_reg() %>%
set_engine("lm") %>%
set_mode("regression")
A receita a ser aplicada está no objeto ring_rec
.
Vamos agregar essas duas informações no workflow:
reg_wf <- workflow(ring_rec, reg_spec)
A função fit()
pode ser usada para ajustar o modelo usando os dados de treinamento:
reg_fit <- reg_wf %>% fit(data = ring_train)
reg_fit
## ══ Workflow [trained] ══════════════════════════════════════════════════════════
## Preprocessor: Recipe
## Model: linear_reg()
##
## ── Preprocessor ────────────────────────────────────────────────────────────────
## 4 Recipe Steps
##
## • step_normalize()
## • step_corr()
## • step_dummy()
## • step_poly()
##
## ── Model ───────────────────────────────────────────────────────────────────────
##
## Call:
## stats::lm(formula = ..y ~ ., data = data)
##
## Coefficients:
## (Intercept) height shell_weight
## 10.27917 0.59969 3.20871
## sex_infant sex_male shucked_weight_poly_1
## -1.02274 -0.02678 -127.72989
## shucked_weight_poly_2
## -7.59663
A função tidy()
do pacote broom
resume as informações mais importantes de um modelo usando o conceito tidy:
reg_fit %>% tidy(conf.int = TRUE)
## # A tibble: 7 × 7
## term estimate std.error statistic p.value conf.low conf.high
## <chr> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl>
## 1 (Intercept) 1.03e+1 0.0746 138. 0 10.1 10.4
## 2 height 6.00e-1 0.0704 8.52 2.35e- 17 0.462 0.738
## 3 shell_weight 3.21e+0 0.0953 33.7 2.47e-214 3.02 3.40
## 4 sex_infant -1.02e+0 0.118 -8.65 7.83e- 18 -1.25 -0.791
## 5 sex_male -2.68e-2 0.0964 -0.278 7.81e- 1 -0.216 0.162
## 6 shucked_weight_poly… -1.28e+2 5.29 -24.2 6.75e-119 -138. -117.
## 7 shucked_weight_poly… -7.60e+0 2.50 -3.04 2.40e- 3 -12.5 -2.69
A função predict()
calcula os valores preditos para o conjunto especificado:
reg_fit %>% predict(ring_train)
## # A tibble: 3,340 × 1
## .pred
## <dbl>
## 1 8.00
## 2 8.15
## 3 8.48
## 4 5.94
## 5 5.94
## 6 6.01
## 7 7.43
## 8 6.49
## 9 8.55
## 10 10.9
## # ℹ 3,330 more rows
A predição com tidymodels
garante que:
Pode-se usar também a função augment()
, que calcula os valores preditos e resíduos, adicionando-os em uma coluna no conjunto de dados especificado:
reg_fit %>% augment(ring_train)
## # A tibble: 3,340 × 11
## .pred .resid sex length diameter height whole_weight shucked_weight
## <dbl> <dbl> <chr> <dbl> <dbl> <dbl> <dbl> <dbl>
## 1 8.00 -0.000960 infant 0.425 0.3 0.095 0.352 0.141
## 2 8.15 -1.15 infant 0.355 0.28 0.085 0.290 0.095
## 3 8.48 -0.482 male 0.465 0.355 0.105 0.480 0.227
## 4 5.94 -0.936 infant 0.24 0.175 0.045 0.07 0.0315
## 5 5.94 -0.942 infant 0.205 0.15 0.055 0.042 0.0255
## 6 6.01 -2.01 infant 0.21 0.15 0.05 0.042 0.0175
## 7 7.43 -0.429 infant 0.39 0.295 0.095 0.203 0.0875
## 8 6.49 -0.490 infant 0.325 0.245 0.07 0.161 0.0755
## 9 8.55 -1.55 male 0.405 0.31 0.1 0.385 0.173
## 10 10.9 -2.92 female 0.5 0.4 0.14 0.662 0.256
## # ℹ 3,330 more rows
## # ℹ 3 more variables: viscera_weight <dbl>, shell_weight <dbl>, rings <dbl>
Para visualizar as estimativas dos parâmetros do modelo ajustado:
reg_fit %>% tidy()
## # A tibble: 7 × 5
## term estimate std.error statistic p.value
## <chr> <dbl> <dbl> <dbl> <dbl>
## 1 (Intercept) 10.3 0.0746 138. 0
## 2 height 0.600 0.0704 8.52 2.35e- 17
## 3 shell_weight 3.21 0.0953 33.7 2.47e-214
## 4 sex_infant -1.02 0.118 -8.65 7.83e- 18
## 5 sex_male -0.0268 0.0964 -0.278 7.81e- 1
## 6 shucked_weight_poly_1 -128. 5.29 -24.2 6.75e-119
## 7 shucked_weight_poly_2 -7.60 2.50 -3.04 2.40e- 3
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:
reg_fit %>%
augment(ring_train) %>%
head()
## # A tibble: 6 × 11
## .pred .resid sex length diameter height whole_weight shucked_weight
## <dbl> <dbl> <chr> <dbl> <dbl> <dbl> <dbl> <dbl>
## 1 8.00 -0.000960 infant 0.425 0.3 0.095 0.352 0.141
## 2 8.15 -1.15 infant 0.355 0.28 0.085 0.290 0.095
## 3 8.48 -0.482 male 0.465 0.355 0.105 0.480 0.227
## 4 5.94 -0.936 infant 0.24 0.175 0.045 0.07 0.0315
## 5 5.94 -0.942 infant 0.205 0.15 0.055 0.042 0.0255
## 6 6.01 -2.01 infant 0.21 0.15 0.05 0.042 0.0175
## # ℹ 3 more variables: viscera_weight <dbl>, shell_weight <dbl>, rings <dbl>
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 preditosreg_fit %>%
augment(new_data = ring_train) %>%
metrics(rings, .pred)
## # A tibble: 3 × 3
## .metric .estimator .estimate
## <chr> <chr> <dbl>
## 1 rmse standard 2.26
## 2 rsq standard 0.510
## 3 mae standard 1.62
Podemos especificar apenas uma métrica:
reg_fit %>%
augment(new_data = ring_train) %>%
rmse(rings, .pred)
## # A tibble: 1 × 3
## .metric .estimator .estimate
## <chr> <chr> <dbl>
## 1 rmse standard 2.26
Também é possível especificar um conjunto de métricas. No exemplo a seguir, abalone_metrics
é definido como um conjunto de quatro métricas: RMSE, MAE, MAPE e \(R^2\):
abalone_metrics <- metric_set(rmse, mae, mape, rsq)
E podemos avaliar este conjunto de métricas no modelo ajustado:
augment(reg_fit, new_data = ring_train) %>%
abalone_metrics(rings, .pred)
## # A tibble: 4 × 3
## .metric .estimator .estimate
## <chr> <chr> <dbl>
## 1 rmse standard 2.26
## 2 mae standard 1.62
## 3 mape standard 16.5
## 4 rsq standard 0.510
Ao calcularmos as métricas tanto no conjunto de treinamento quanto no de teste, podemos avaliar se o modelo está super-ajustando (overfitting):
reg_fit %>%
augment(ring_train) %>%
metrics(rings, .pred)
## # A tibble: 3 × 3
## .metric .estimator .estimate
## <chr> <chr> <dbl>
## 1 rmse standard 2.26
## 2 rsq standard 0.510
## 3 mae standard 1.62
reg_fit %>%
augment(ring_test) %>%
metrics(rings, .pred)
## # A tibble: 3 × 3
## .metric .estimator .estimate
## <chr> <chr> <dbl>
## 1 rmse standard 2.24
## 2 rsq standard 0.505
## 3 mae standard 1.63
Usaremos como exemplo 5 folds. Para fazer a reamostragem nos dados de treinamento, usaremos o comando vfold_cv
:
set.seed(234)
ring_folds <- ring_train %>%
vfold_cv(v = 5, strata = rings)
ring_folds
## # 5-fold cross-validation using stratification
## # A tibble: 5 × 2
## splits id
## <list> <chr>
## 1 <split [2670/670]> Fold1
## 2 <split [2672/668]> Fold2
## 3 <split [2672/668]> Fold3
## 4 <split [2673/667]> Fold4
## 5 <split [2673/667]> Fold5
Ajustando o modelo nas reamostras:
reg_cv <- reg_wf %>% fit_resamples(ring_folds)
reg_cv
## # Resampling results
## # 5-fold cross-validation using stratification
## # A tibble: 5 × 4
## splits id .metrics .notes
## <list> <chr> <list> <list>
## 1 <split [2670/670]> Fold1 <tibble [2 × 4]> <tibble [0 × 3]>
## 2 <split [2672/668]> Fold2 <tibble [2 × 4]> <tibble [0 × 3]>
## 3 <split [2672/668]> Fold3 <tibble [2 × 4]> <tibble [0 × 3]>
## 4 <split [2673/667]> Fold4 <tibble [2 × 4]> <tibble [0 × 3]>
## 5 <split [2673/667]> Fold5 <tibble [2 × 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:
reg_cv %>%
collect_metrics()
## # A tibble: 2 × 6
## .metric .estimator mean n std_err .config
## <chr> <chr> <dbl> <int> <dbl> <chr>
## 1 rmse standard 2.33 5 0.126 Preprocessor1_Model1
## 2 rsq standard 0.487 5 0.0323 Preprocessor1_Model1
Para calcular um conjunto escolhido de métricas, é preciso especificar o conjunto no argumento metrics
dentro de fit_resamples
:
reg_cv <- fit_resamples(reg_wf,
ring_folds,
metrics = abalone_metrics)
reg_cv %>%
collect_metrics()
## # A tibble: 4 × 6
## .metric .estimator mean n std_err .config
## <chr> <chr> <dbl> <int> <dbl> <chr>
## 1 mae standard 1.64 5 0.0377 Preprocessor1_Model1
## 2 mape standard 16.7 5 0.295 Preprocessor1_Model1
## 3 rmse standard 2.33 5 0.126 Preprocessor1_Model1
## 4 rsq standard 0.487 5 0.0323 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_abalone <- control_resamples(save_pred = TRUE)
reg_cv <- fit_resamples(reg_wf,
ring_folds,
control = ctrl_abalone)
reg_preds <- collect_predictions(reg_cv)
reg_preds
## # A tibble: 3,340 × 5
## .pred id .row rings .config
## <dbl> <chr> <int> <dbl> <chr>
## 1 8.03 Fold1 1 8 Preprocessor1_Model1
## 2 8.45 Fold1 3 8 Preprocessor1_Model1
## 3 10.9 Fold1 10 8 Preprocessor1_Model1
## 4 7.98 Fold1 23 7 Preprocessor1_Model1
## 5 7.26 Fold1 24 7 Preprocessor1_Model1
## 6 6.31 Fold1 25 7 Preprocessor1_Model1
## 7 5.86 Fold1 30 5 Preprocessor1_Model1
## 8 8.98 Fold1 34 8 Preprocessor1_Model1
## 9 9.51 Fold1 38 8 Preprocessor1_Model1
## 10 8.24 Fold1 39 7 Preprocessor1_Model1
## # ℹ 3,330 more rows
Podemos fazer um gráfico de preditos versus observados, separados por cada fold da validação cruzada:
reg_preds %>%
ggplot(aes(rings, .pred, color = id)) +
geom_abline(lty = 2, col = "gray", size = 1.5) +
geom_point(alpha = 0.5) +
coord_obs_pred()
Um outro modelo a ser considerado é árvore de decisão. Vamos considerar o seguinte exemplo:
tree_spec <- decision_tree(cost_complexity = 0.02) %>%
set_mode("regression") %>%
set_engine("rpart")
O modelo de árvore de regressã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(rings ~ ., tree_spec)
tree_wf
## ══ Workflow ════════════════════════════════════════════════════════════════════
## Preprocessor: Formula
## Model: decision_tree()
##
## ── Preprocessor ────────────────────────────────────────────────────────────────
## rings ~ .
##
## ── Model ───────────────────────────────────────────────────────────────────────
## Decision Tree Model Specification (regression)
##
## Main Arguments:
## cost_complexity = 0.02
##
## Computational engine: rpart
E o ajuste é com os dados de treinamento:
tree_fit <- tree_wf %>% fit(data = ring_train)
Vamos então avaliar o desempenho da árvore de decisão usando validação cruzada:
tree_cv <- tree_wf %>% fit_resamples(ring_folds)
tree_cv %>% collect_metrics()
## # A tibble: 2 × 6
## .metric .estimator mean n std_err .config
## <chr> <chr> <dbl> <int> <dbl> <chr>
## 1 rmse standard 2.50 5 0.0752 Preprocessor1_Model1
## 2 rsq standard 0.402 5 0.0114 Preprocessor1_Model1
Quando queremos comparar vários modelos ao mesmo, fica muito trabalhoso fazer um por vez, como mostramos anteriormente.
Para isso, existe a função workflow_set()
que gera um conjunto de workflows. Os argumentos desta função são:
preproc
: formulas, receitasmodels
: modelos especificados usando parsnip
wf_set <- workflow_set(preproc = list(rec1 = rings ~ ., rec2 = ring_rec),
models = list(tree = tree_spec, reg = reg_spec),
cross = FALSE)
Agora podemos avaliar os modelos com as métricas desejadas usando a validação cruzada:
wf_set %>%
workflow_map("fit_resamples", resamples = ring_folds) %>%
rank_results()
## # A tibble: 4 × 9
## wflow_id .config .metric mean std_err n preprocessor model rank
## <chr> <chr> <chr> <dbl> <dbl> <int> <chr> <chr> <int>
## 1 rec2_reg Preprocessor1_… rmse 2.33 0.126 5 recipe line… 1
## 2 rec2_reg Preprocessor1_… rsq 0.487 0.0323 5 recipe line… 1
## 3 rec1_tree Preprocessor1_… rmse 2.50 0.0752 5 formula deci… 2
## 4 rec1_tree Preprocessor1_… rsq 0.402 0.0114 5 formula deci… 2
Se o argumento cross = TRUE
o workflow_set
faz um produto cruzado das receitas e modelos:
workflow_set(preproc = list(rec1 = rings ~ ., rec2 = ring_rec),
models = list(tree = tree_spec, reg = reg_spec),
cross = TRUE) %>%
workflow_map("fit_resamples", resamples = ring_folds) %>%
rank_results()
## # A tibble: 8 × 9
## wflow_id .config .metric mean std_err n preprocessor model rank
## <chr> <chr> <chr> <dbl> <dbl> <int> <chr> <chr> <int>
## 1 rec1_reg Preprocessor1_… rmse 2.24 0.0921 5 formula line… 1
## 2 rec1_reg Preprocessor1_… rsq 0.521 0.0213 5 formula line… 1
## 3 rec2_reg Preprocessor1_… rmse 2.33 0.126 5 recipe line… 2
## 4 rec2_reg Preprocessor1_… rsq 0.487 0.0323 5 recipe line… 2
## 5 rec1_tree Preprocessor1_… rmse 2.50 0.0752 5 formula deci… 3
## 6 rec1_tree Preprocessor1_… rsq 0.402 0.0114 5 formula deci… 3
## 7 rec2_tree Preprocessor1_… rmse 2.50 0.0752 5 recipe deci… 4
## 8 rec2_tree Preprocessor1_… rsq 0.402 0.0114 5 recipe deci… 4
Suponha que o modelo de regressão linear 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(reg_wf, ring_split)
Lembre-se que o objeto ring_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 [3340/837]> train/test split <tibble> <tibble> <tibble> <workflow>
Métricas calculadas para o conjunto de dados teste:
collect_metrics(final_fit)
## # A tibble: 2 × 4
## .metric .estimator .estimate .config
## <chr> <chr> <dbl> <chr>
## 1 rmse standard 2.24 Preprocessor1_Model1
## 2 rsq standard 0.505 Preprocessor1_Model1
Predições para o conjunto de dados teste:
collect_predictions(final_fit) %>%
head()
## # A tibble: 6 × 5
## .pred id .row rings .config
## <dbl> <chr> <int> <dbl> <chr>
## 1 8.14 train/test split 2 7 Preprocessor1_Model1
## 2 10.6 train/test split 3 9 Preprocessor1_Model1
## 3 6.74 train/test split 5 7 Preprocessor1_Model1
## 4 11.2 train/test split 16 12 Preprocessor1_Model1
## 5 8.71 train/test split 19 7 Preprocessor1_Model1
## 6 8.69 train/test split 20 9 Preprocessor1_Model1
collect_predictions(final_fit) %>%
ggplot(aes(rings, .pred)) +
geom_abline(lty = 2, col = "deeppink4", size = 1.5) +
geom_point(alpha = 0.5) +
coord_obs_pred()
Quais informações temos em final_fit
?
extract_workflow(final_fit)
## ══ Workflow [trained] ══════════════════════════════════════════════════════════
## Preprocessor: Recipe
## Model: linear_reg()
##
## ── Preprocessor ────────────────────────────────────────────────────────────────
## 4 Recipe Steps
##
## • step_normalize()
## • step_corr()
## • step_dummy()
## • step_poly()
##
## ── Model ───────────────────────────────────────────────────────────────────────
##
## Call:
## stats::lm(formula = ..y ~ ., data = data)
##
## Coefficients:
## (Intercept) height shell_weight
## 10.27917 0.59969 3.20871
## sex_infant sex_male shucked_weight_poly_1
## -1.02274 -0.02678 -127.72989
## shucked_weight_poly_2
## -7.59663
Quais as estimativas dos parâmetros do modelo final?
final_fit %>%
extract_fit_parsnip() %>%
tidy()
## # A tibble: 7 × 5
## term estimate std.error statistic p.value
## <chr> <dbl> <dbl> <dbl> <dbl>
## 1 (Intercept) 10.3 0.0746 138. 0
## 2 height 0.600 0.0704 8.52 2.35e- 17
## 3 shell_weight 3.21 0.0953 33.7 2.47e-214
## 4 sex_infant -1.02 0.118 -8.65 7.83e- 18
## 5 sex_male -0.0268 0.0964 -0.278 7.81e- 1
## 6 shucked_weight_poly_1 -128. 5.29 -24.2 6.75e-119
## 7 shucked_weight_poly_2 -7.60 2.50 -3.04 2.40e- 3
Algumas características dos modelos não podem ser estimadas diretamente dos dados.
Escolhemos um modelo de regressão linear, 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 linear em que uma das variáveis será considerada através de um polinômio, temos a seguinte receita:
ring_rec <-
recipe(rings ~ ., data = ring_train) %>%
step_dummy(all_nominal_predictors()) %>%
step_poly(shucked_weight, 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:
regpol_wf <- workflow(ring_rec, linear_reg())
regpol_wf
## ══ Workflow ════════════════════════════════════════════════════════════════════
## Preprocessor: Recipe
## Model: linear_reg()
##
## ── Preprocessor ────────────────────────────────────────────────────────────────
## 2 Recipe Steps
##
## • step_dummy()
## • step_poly()
##
## ── Model ───────────────────────────────────────────────────────────────────────
## Linear Regression Model Specification (regression)
##
## Computational engine: lm
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:
set.seed(123)
regpol_res <- tune_grid(regpol_wf, ring_folds, grid = tibble(degree=1:6))
regpol_res
## # Tuning results
## # 5-fold cross-validation using stratification
## # A tibble: 5 × 4
## splits id .metrics .notes
## <list> <chr> <list> <list>
## 1 <split [2670/670]> Fold1 <tibble [12 × 5]> <tibble [0 × 3]>
## 2 <split [2672/668]> Fold2 <tibble [12 × 5]> <tibble [0 × 3]>
## 3 <split [2672/668]> Fold3 <tibble [12 × 5]> <tibble [0 × 3]>
## 4 <split [2673/667]> Fold4 <tibble [12 × 5]> <tibble [0 × 3]>
## 5 <split [2673/667]> Fold5 <tibble [12 × 5]> <tibble [0 × 3]>
Apresentando os resultados (média dos 5 folds) para cada grau de polinômio (hiperparâmetro) considerado:
collect_metrics(regpol_res)
## # A tibble: 12 × 7
## degree .metric .estimator mean n std_err .config
## <int> <chr> <chr> <dbl> <int> <dbl> <chr>
## 1 1 rmse standard 2.24 5 0.0921 Preprocessor1_Model1
## 2 1 rsq standard 0.521 5 0.0213 Preprocessor1_Model1
## 3 2 rmse standard 2.24 5 0.0952 Preprocessor2_Model1
## 4 2 rsq standard 0.521 5 0.0222 Preprocessor2_Model1
## 5 3 rmse standard 2.22 5 0.0866 Preprocessor3_Model1
## 6 3 rsq standard 0.531 5 0.0191 Preprocessor3_Model1
## 7 4 rmse standard 2.21 5 0.0845 Preprocessor4_Model1
## 8 4 rsq standard 0.535 5 0.0187 Preprocessor4_Model1
## 9 5 rmse standard 2.21 5 0.0813 Preprocessor5_Model1
## 10 5 rsq standard 0.537 5 0.0176 Preprocessor5_Model1
## 11 6 rmse standard 2.21 5 0.0811 Preprocessor6_Model1
## 12 6 rsq standard 0.537 5 0.0176 Preprocessor6_Model1
Visualização gráfica usando autoplot()
:
autoplot(regpol_res, metric = "rmse")
Mostrando os melhores resultados:
show_best(regpol_res, metric = "rmse", n = 3)
## # A tibble: 3 × 7
## degree .metric .estimator mean n std_err .config
## <int> <chr> <chr> <dbl> <int> <dbl> <chr>
## 1 5 rmse standard 2.21 5 0.0813 Preprocessor5_Model1
## 2 6 rmse standard 2.21 5 0.0811 Preprocessor6_Model1
## 3 4 rmse standard 2.21 5 0.0845 Preprocessor4_Model1
E se ajustarmos um outro modelo, também com algum outro hiperparâmetro? Como comparar?
A seguir temos um workflow para árvore de decisão com ajuste de hiperparâmetro.
Primeiro, o modelo é especificado incluindo tune()
:
tree_spec <-
decision_tree(
cost_complexity = tune()
) %>%
set_mode("regression") %>%
set_engine("rpart")
A receita é definida:
tree_rec <-
recipe(rings ~ ., data = ring_train) %>%
step_dummy(all_nominal_predictors())
Essas duas informações são agregadaas com workflow:
tree_wf <- workflow(tree_rec, tree_spec)
Vamos usar tune_grid()
para avaliar vários valores para o hiperparâmwtro:
set.seed(9)
tree_res <-
tune_grid(tree_wf,
resamples = ring_folds,
grid = 15,
metrics = abalone_metrics)
É possível fornecer um data.frame
na opção grid
, para ser mais específico.
Métricas obtidas através de validação cruzada considerando os valores de grid
:
tree_res %>% collect_metrics()
## # A tibble: 60 × 7
## cost_complexity .metric .estimator mean n std_err .config
## <dbl> <chr> <chr> <dbl> <int> <dbl> <chr>
## 1 0.00000000423 mae standard 1.73 5 0.0492 Preprocessor1_Model01
## 2 0.00000000423 mape standard 17.0 5 0.426 Preprocessor1_Model01
## 3 0.00000000423 rmse standard 2.43 5 0.0858 Preprocessor1_Model01
## 4 0.00000000423 rsq standard 0.462 5 0.0160 Preprocessor1_Model01
## 5 0.0000000452 mae standard 1.73 5 0.0492 Preprocessor1_Model02
## 6 0.0000000452 mape standard 17.0 5 0.426 Preprocessor1_Model02
## 7 0.0000000452 rmse standard 2.43 5 0.0858 Preprocessor1_Model02
## 8 0.0000000452 rsq standard 0.462 5 0.0160 Preprocessor1_Model02
## 9 0.000998 mae standard 1.65 5 0.0502 Preprocessor1_Model03
## 10 0.000998 mape standard 16.3 5 0.462 Preprocessor1_Model03
## # ℹ 50 more rows
Em resumo, o melhor resultado para regressão polinomial, segundo a validação cruzada:
regpol_res %>%
show_best(metric = "rmse", n = 1) %>%
select(.metric, .estimator, mean, n, std_err, .config)
## # A tibble: 1 × 6
## .metric .estimator mean n std_err .config
## <chr> <chr> <dbl> <int> <dbl> <chr>
## 1 rmse standard 2.21 5 0.0813 Preprocessor5_Model1
E melhor resultado para regressão polinomial, segundo a validação cruzada:
tree_res %>%
show_best(metric = "rmse", n = 1) %>%
select(.metric, .estimator, mean, n, std_err, .config)
## # A tibble: 1 × 6
## .metric .estimator mean n std_err .config
## <chr> <chr> <dbl> <int> <dbl> <chr>
## 1 rmse standard 2.37 5 0.0941 Preprocessor1_Model15
Entre as duas opções (regressão polinomial e árvore), vamos ficar com a regressão polinomial. Vamos selecionar o modelo com hiperparâmtro de melhor desempenho:
best_rmse <- select_best(regpol_res, metric = "rmse")
best_rmse
## # A tibble: 1 × 2
## degree .config
## <int> <chr>
## 1 5 Preprocessor5_Model1
E ajustando o modelo final:
final_res <-
regpol_wf %>%
finalize_workflow(best_rmse) %>%
last_fit(ring_split)
final_res
## # Resampling results
## # Manual resampling
## # A tibble: 1 × 6
## splits id .metrics .notes .predictions .workflow
## <list> <chr> <list> <list> <list> <list>
## 1 <split [3340/837]> train/test split <tibble> <tibble> <tibble> <workflow>
last_fit()
ajusta o modelo final com os dados de treino e avalia o desempenho nos dados de teste.
Resultado no conjunto de teste:
final_res %>% collect_metrics()
## # A tibble: 2 × 4
## .metric .estimator .estimate .config
## <chr> <chr> <dbl> <chr>
## 1 rmse standard 2.18 Preprocessor1_Model1
## 2 rsq standard 0.533 Preprocessor1_Model1
Workflow final
Guardar todos os passos do ajuste final (obtidos usando o conjunto de treinamento):
fitted_wf <- extract_workflow(final_res)
fitted_wf
## ══ Workflow [trained] ══════════════════════════════════════════════════════════
## Preprocessor: Recipe
## Model: linear_reg()
##
## ── Preprocessor ────────────────────────────────────────────────────────────────
## 2 Recipe Steps
##
## • step_dummy()
## • step_poly()
##
## ── Model ───────────────────────────────────────────────────────────────────────
##
## Call:
## stats::lm(formula = ..y ~ ., data = data)
##
## Coefficients:
## (Intercept) length diameter
## -0.05048 -6.66421 9.96106
## height whole_weight viscera_weight
## 7.33184 9.41184 -8.93447
## shell_weight sex_infant sex_male
## 10.52625 -0.96181 -0.01411
## shucked_weight_poly_1 shucked_weight_poly_2 shucked_weight_poly_3
## -241.15746 -8.36663 24.57516
## shucked_weight_poly_4 shucked_weight_poly_5
## -11.06719 8.32636
Obter valores preditos:
predict(fitted_wf, ring_test[1:3,])
## # A tibble: 3 × 1
## .pred
## <dbl>
## 1 8.06
## 2 11.4
## 3 6.85
Workflow final:
final_res %>%
extract_fit_parsnip() %>%
tidy()
## # A tibble: 14 × 5
## term estimate std.error statistic p.value
## <chr> <dbl> <dbl> <dbl> <dbl>
## 1 (Intercept) -0.0505 0.740 -0.0682 9.46e- 1
## 2 length -6.66 2.23 -2.99 2.79e- 3
## 3 diameter 9.96 2.59 3.84 1.24e- 4
## 4 height 7.33 1.64 4.47 8.14e- 6
## 5 whole_weight 9.41 0.790 11.9 4.84e-32
## 6 viscera_weight -8.93 1.42 -6.29 3.57e-10
## 7 shell_weight 10.5 1.25 8.44 4.57e-17
## 8 sex_infant -0.962 0.115 -8.38 7.49e-17
## 9 sex_male -0.0141 0.0926 -0.152 8.79e- 1
## 10 shucked_weight_poly_1 -241. 12.1 -19.9 3.33e-83
## 11 shucked_weight_poly_2 -8.37 3.79 -2.21 2.73e- 2
## 12 shucked_weight_poly_3 24.6 2.71 9.07 1.96e-19
## 13 shucked_weight_poly_4 -11.1 2.34 -4.73 2.34e- 6
## 14 shucked_weight_poly_5 8.33 2.25 3.70 2.23e- 4