Las librerías de Tidymodels fueron realizadas con el objetivo de incorporarse al flujo de trabajo del tidyverse. A diferencia de versiones previas, como Caret, son modulares, y cada librería tiene un objetivo acotado.

Dentro del paquete tidymodels se encuentran otros paquetes, cómo:

library(tidymodels)

Veamos un ejemplo con iris

Preprocesamiento

Remuestreo de datos

con rsample::initial_split() creamos una división entre train y test

iris_split <- initial_split(iris, prop = 0.6)
iris_split
<90/60/150>

cuando llamamos al objeto iris_split nos muestra la cantidad de elementos en train/test/total

Si queremos recuprar el dataset de entrenamiento, utilizamos el comando training()

iris_split %>%
  training()  

Receta de transformaciones

Con recipes::recipe() indicamos que comenzamos un pipeline de preprocesamiento, indicando cual es la variable a predecir, con Species ~..

El objetivo de realizar el preprocesamiento de esta manera es que para muchas etapas del preprocesamiento, como puede ser el cálculo de la media para la imputación, sólo podemos utilizar la información del set de entrenamiento para evitar el information leaking. Por ello, si construimos un pipeline donde se calcula todo sobre entrenamiento, después es más sencillo reutilizarlo en test, sin cometer errores de este tipo.

utilizando recipes entrenamos el preprocesamiento.

  • recipe() Empieza un nuevo set de transformaciones para ser aplicadas.

  • prep() Es el último paso, que indica que hay que ejecutar toods los pasos anteriores sobre los datos.

Cada tranformación es un step(paso). Las funciones son tipos específicos de pasos. En este caso utilizamos:

  • step_corr() Elimina las variables que tienen una correlación alta con otras variables
  • step_center() Centra los datos para que tengan media cero
  • step_scale() Normaliza los datos para que tengan desvío estandar de 1.

También hay una serie de funciones para seleccionar las variables sobre las que se va a operar:

  • all_predictors y all_outcomes ayudan a espcificar sobre las variables predictivas y predictoras.
  • Se puede definir otros roles con update_role y luego seleccionarlas vía has_role
  • all_numeric, all_nominal y has_type permite elegir las variables según su tipo.
iris_recipe <- training(iris_split) %>%
  recipe(Species ~.) %>%
  step_corr(all_predictors()) %>%
  step_center(all_predictors(), -all_outcomes()) %>%
  step_scale(all_predictors(), -all_outcomes()) %>%
  prep()

iris_recipe
Data Recipe

Inputs:

Training data contained 90 data points and no missing data.

Operations:

Correlation filter removed Petal.Length [trained]
Centering for Sepal.Length, Sepal.Width, Petal.Width [trained]
Scaling for Sepal.Length, Sepal.Width, Petal.Width [trained]

en iris_recipe tenemos la receta del preprocesamiento, pero aún no se aplicó sobre ningún set de datos.

Aplicamos la receta en los datos de train y test

Dado que la receta fue construida con los datos de entrenamiento, este dataset ya se encuentra implícito en iris_recipe. Para extraerlo, ya preprocesado, utilizamos la función juice directamente sobre la receta.

iris_training <- juice(iris_recipe)

 iris_training

Con la función bake() podemos aplicar la receta que preparamos antes para los datos de test. Para ello el primer objeto que pasamos es la receta y como parámetro de bake pasamos el dataset de test (recuperado con la función testing de rsample)

iris_testing <- iris_recipe %>%
  bake(testing(iris_split)) 

iris_testing

Entrenamiento de modelos

En R hay muchos paquetes que implementan el mismo tipo de modelo. Normalmente cada implementación define los parámetros del modelo de forma distinta. Por ejemplo, para el modelo Random Forest, la librería ranger define el número de árboles como num.trees, mientras que la librería randomForest lo nombra ntree.

Al igual que la vieja librería caret, tidymodels remplaza la interfaz, pero no el paquete. Es decir, no es una nueva implementación de los modelos, sino una interfaz que unifica las diferentes sintaxis.

iris_ranger <- rand_forest(trees = 100, mode = "classification") %>%
  set_engine("ranger") %>%
  fit(Species ~ ., data = iris_training)

iris_ranger
parsnip model object

Ranger result

Call:
 ranger::ranger(formula = formula, data = data, num.trees = ~100,      num.threads = 1, verbose = FALSE, seed = sample.int(10^5,          1), probability = TRUE) 

Type:                             Probability estimation 
Number of trees:                  100 
Sample size:                      90 
Number of independent variables:  3 
Mtry:                             1 
Target node size:                 10 
Variable importance mode:         none 
Splitrule:                        gini 
OOB prediction error (Brier s.):  0.06436353 
iris_rf <-  rand_forest(trees = 100, mode = "classification") %>%
  set_engine("randomForest") %>%
  fit(Species ~ ., data = iris_training)

iris_rf
parsnip model object


Call:
 randomForest(x = as.data.frame(x), y = y, ntree = ~100) 
               Type of random forest: classification
                     Number of trees: 100
No. of variables tried at each split: 1

        OOB estimate of  error rate: 7.78%
Confusion matrix:
           setosa versicolor virginica class.error
setosa         34          0         0   0.0000000
versicolor      0         23         4   0.1481481
virginica       0          3        26   0.1034483
iris_noengine <- rand_forest(trees = 100, mode = "classification") %>%
  fit(Species ~ ., data = iris_training)
Engine set to `ranger`

Predicciones

Para predecir los datos de test utilizamos la conocida función predict. Sin embargo, cuando utilizamos esta función sobre un objeto de la librería parsnip devuelve un tibble

predict(iris_ranger, iris_testing)

Para agregar las predicciones a los datos de test podemos utilizar la función bind_cols

iris_ranger %>%
  predict(iris_testing) %>%
  bind_cols(iris_testing)  

Validación

Con la función yardstick::metrics() podemos medir la performance del modelo. Elige automáticamente las métricas apropiadas dado el tipo de modelo. Necesitmaos aclarar cuales son los datos predichos,estimate y reales, truth.

iris_ranger %>%
  predict(iris_testing) %>%
  bind_cols(iris_testing) %>%
  metrics(truth = Species, estimate = .pred_class)
iris_rf %>%
  predict(iris_testing) %>%
  bind_cols(iris_testing) %>%
  metrics(truth = Species, estimate = .pred_class)

Metricas por clasificador

Si en el predict elegimos type="prob"

iris_ranger %>%
  predict(iris_testing, type = "prob")  
iris_probs <- iris_ranger %>%
  predict(iris_testing, type = "prob") %>%
  bind_cols(iris_testing)

iris_probs

Con el tibble iris_probs es fácil calcular métodos de curva, como la ROC, utilizando la función roc_curve

iris_probs%>%
  roc_curve(Species, .pred_setosa:.pred_virginica)

finalmente, con la función autoplot podemos graficar las curvas. Dado que el resultado es un objeto ggplot, podemos hacer otras modificaciones posteriores.

iris_probs%>%
  roc_curve(Species, .pred_setosa:.pred_virginica) %>%
  autoplot()+
  ggthemes::theme_fivethirtyeight()+
  labs(title = 'Curvas ROC')

Si queremos un tibble con la clase predicha y la probabilidad de cada clase, podemos utilizar bind_cols para juntar los resultados de ambos tipos

predict(iris_ranger, iris_testing, type = "prob") %>%
  bind_cols(predict(iris_ranger, iris_testing)) %>%
  bind_cols(select(iris_testing, Species))

si utilizamos metrics sobre estos resultados, obtenemos además de accuracy y kap, el log loss y el area debajo de la curva ROC.

predict(iris_ranger, iris_testing, type = "prob") %>%
  bind_cols(predict(iris_ranger, iris_testing)) %>%
  bind_cols(select(iris_testing, Species)) %>%
  metrics(truth=Species, .pred_setosa:.pred_virginica, estimate = .pred_class)

  1. basado en las notas de Edgar Ruiz https://rviews.rstudio.com/2019/06/19/a-gentle-intro-to-tidymodels/

LS0tCnRpdGxlOiAiVGlkeSBNb2RlbHMsIHVuYSBpbnRyb2R1Y2Npw7NuXltiYXNhZG8gZW4gbGFzIG5vdGFzIGRlIEVkZ2FyIFJ1aXogaHR0cHM6Ly9ydmlld3MucnN0dWRpby5jb20vMjAxOS8wNi8xOS9hLWdlbnRsZS1pbnRyby10by10aWR5bW9kZWxzL10iCmF1dGhvcjogIkRpZWdvIEtvemxvd3NraSB5SnVhbiBNYW51ZWwgQmFycmlvbGEiCm91dHB1dDogaHRtbF9ub3RlYm9vawotLS0KCgpMYXMgbGlicmVyw61hcyBkZSBfVGlkeW1vZGVsc18gZnVlcm9uIHJlYWxpemFkYXMgY29uIGVsIG9iamV0aXZvIGRlIGluY29ycG9yYXJzZSBhbCBmbHVqbyBkZSB0cmFiYWpvIGRlbCBfdGlkeXZlcnNlXy4gQSBkaWZlcmVuY2lhIGRlIHZlcnNpb25lcyBwcmV2aWFzLCBjb21vIENhcmV0LCBzb24gbW9kdWxhcmVzLCB5IGNhZGEgbGlicmVyw61hIHRpZW5lIHVuIG9iamV0aXZvIGFjb3RhZG8uIAoKIVtdKGltZy9kcy5wbmcpCgpEZW50cm8gZGVsIHBhcXVldGUgX3RpZHltb2RlbHNfIHNlIGVuY3VlbnRyYW4gb3Ryb3MgcGFxdWV0ZXMsIGPDs21vOiAKCiFbXShpbWcvdGlkeW1vZGVscy5wbmcpCgoKLSBgcnNhbXBsZWAgLSBEaWZlcmVudGVzIHRpcG9zIGRlIHJlbXVlc3RyZW8KLSBgcmVjaXBlc2AgLSBPcmdhbml6YWNpw7NuIGRlIGxhcyB0cmFuc2Zvcm1hY2lvbmVzIHBhcmEgZWwgcHJlcHJvY2VzYW1pZW50byBkZSBsYSBpbmZvcm1hY2nDs24gZGUgZm9ybWEgdGFsIHF1ZSBzZWEgbG8gbcOhcyByZXByb2R1Y2libGUgcG9zaWJsZS4gRW4gZXN0ZSBzZW50aWRvLCBzZSBhY2VyY2EgYSBsb3MgX3BpcGVsaW5lc18gZGUgc2stbGVhcm4KLSBgcGFybmlwYCAtIFVuIGludGVyZmF6IGNvbcO6biBwYXJhIGNyZWFyIG1vZGVsb3MsIGluZGVwZW5kaWVudGVtZW50ZSBkZSBsYSBpbXBsZW1lbnRhY2nDs24gKHBhcXVldGUpCi0gYHlhcmRzdGlja2AgLSBNZXRyaWNhcyBkZSBwZXJmb3JtYW5jZSBkZWwgbW9kZWxvLgoKCmBgYHtyIGVjaG89VFJVRSwgbWVzc2FnZT1GQUxTRSwgd2FybmluZz1GQUxTRX0KbGlicmFyeSh0aWR5bW9kZWxzKQpgYGAKCiFbXShpbWcvbGlicmFyeV90aWR5bW9kZWxzLnBuZykKClZlYW1vcyB1biBlamVtcGxvIGNvbiBpcmlzCgoKIyMgUHJlcHJvY2VzYW1pZW50bwoKIyMjIFJlbXVlc3RyZW8gZGUgZGF0b3MKCiFbXShpbWcvcnNhbXBsZV9sb2dvLnBuZyl7d2lkdGg9MTAwfQoKY29uIGByc2FtcGxlOjppbml0aWFsX3NwbGl0KClgIGNyZWFtb3MgdW5hIGRpdmlzacOzbiBlbnRyZSB0cmFpbiB5IHRlc3QKCmBgYHtyfQppcmlzX3NwbGl0IDwtIGluaXRpYWxfc3BsaXQoaXJpcywgcHJvcCA9IDAuNikKaXJpc19zcGxpdApgYGAKY3VhbmRvIGxsYW1hbW9zIGFsIG9iamV0byBgaXJpc19zcGxpdGAgbm9zIG11ZXN0cmEgbGEgY2FudGlkYWQgZGUgZWxlbWVudG9zIGVuIHRyYWluL3Rlc3QvdG90YWwKClNpIHF1ZXJlbW9zIHJlY3VwcmFyIGVsIGRhdGFzZXQgZGUgZW50cmVuYW1pZW50bywgdXRpbGl6YW1vcyBlbCBjb21hbmRvIGB0cmFpbmluZygpYAoKYGBge3J9CmlyaXNfc3BsaXQgJT4lCiAgdHJhaW5pbmcoKSAgCmBgYAoKIyMjIFJlY2V0YSBkZSB0cmFuc2Zvcm1hY2lvbmVzCgohW10oaW1nL3JlY2lwZXNfbG9nby5wbmcpe3dpZHRoPTEwMH0KCkNvbiBgcmVjaXBlczo6cmVjaXBlKClgIGluZGljYW1vcyBxdWUgY29tZW56YW1vcyB1biBwaXBlbGluZSBkZSBwcmVwcm9jZXNhbWllbnRvLCBpbmRpY2FuZG8gY3VhbCBlcyBsYSB2YXJpYWJsZSBhIHByZWRlY2lyLCBjb24gYFNwZWNpZXMgfi5gLgoKRWwgb2JqZXRpdm8gZGUgcmVhbGl6YXIgZWwgcHJlcHJvY2VzYW1pZW50byBkZSBlc3RhIG1hbmVyYSBlcyBxdWUgcGFyYSBtdWNoYXMgZXRhcGFzIGRlbCBwcmVwcm9jZXNhbWllbnRvLCBjb21vIHB1ZWRlIHNlciBlbCBjw6FsY3VsbyBkZSBsYSBtZWRpYSBwYXJhIGxhIGltcHV0YWNpw7NuLCBzw7NsbyBwb2RlbW9zIHV0aWxpemFyIGxhIGluZm9ybWFjacOzbiBkZWwgc2V0IGRlIGVudHJlbmFtaWVudG8gcGFyYSBldml0YXIgZWwgX2luZm9ybWF0aW9uIGxlYWtpbmdfLiBQb3IgZWxsbywgc2kgY29uc3RydWltb3MgdW4gcGlwZWxpbmUgZG9uZGUgc2UgY2FsY3VsYSB0b2RvIHNvYnJlIGVudHJlbmFtaWVudG8sIGRlc3B1w6lzIGVzIG3DoXMgc2VuY2lsbG8gcmV1dGlsaXphcmxvIGVuIHRlc3QsIHNpbiBjb21ldGVyIGVycm9yZXMgZGUgZXN0ZSB0aXBvLgoKdXRpbGl6YW5kbyBgcmVjaXBlc2AgX19lbnRyZW5hbW9zX18gZWwgcHJlcHJvY2VzYW1pZW50by4KCi0gYHJlY2lwZSgpYCBFbXBpZXphIHVuIG51ZXZvIHNldCBkZSB0cmFuc2Zvcm1hY2lvbmVzIHBhcmEgc2VyIGFwbGljYWRhcy4gCgotIGBwcmVwKClgIEVzIGVsIMO6bHRpbW8gcGFzbywgcXVlIGluZGljYSBxdWUgaGF5IHF1ZSBlamVjdXRhciB0b29kcyBsb3MgcGFzb3MgYW50ZXJpb3JlcyBzb2JyZSBsb3MgZGF0b3MuCgpDYWRhIHRyYW5mb3JtYWNpw7NuIGVzIHVuIF9zdGVwXyhwYXNvKS4gTGFzIGZ1bmNpb25lcyBzb24gdGlwb3MgZXNwZWPDrWZpY29zIGRlIHBhc29zLiBFbiBlc3RlIGNhc28gdXRpbGl6YW1vczoKCi0gYHN0ZXBfY29ycigpYCBFbGltaW5hIGxhcyB2YXJpYWJsZXMgcXVlIHRpZW5lbiB1bmEgY29ycmVsYWNpw7NuIGFsdGEgY29uIG90cmFzIHZhcmlhYmxlcwotIGBzdGVwX2NlbnRlcigpYCBDZW50cmEgbG9zIGRhdG9zIHBhcmEgcXVlIHRlbmdhbiBtZWRpYSBjZXJvCi0gYHN0ZXBfc2NhbGUoKWAgTm9ybWFsaXphIGxvcyBkYXRvcyBwYXJhIHF1ZSB0ZW5nYW4gZGVzdsOtbyBlc3RhbmRhciBkZSAxLgoKVGFtYmnDqW4gaGF5IHVuYSBzZXJpZSBkZSBmdW5jaW9uZXMgcGFyYSBzZWxlY2Npb25hciBsYXMgdmFyaWFibGVzIHNvYnJlIGxhcyBxdWUgc2UgdmEgYSBvcGVyYXI6CgotIGBhbGxfcHJlZGljdG9yc2AgeSBgYWxsX291dGNvbWVzYCBheXVkYW4gYSBlc3BjaWZpY2FyIHNvYnJlIGxhcyB2YXJpYWJsZXMgcHJlZGljdGl2YXMgeSBwcmVkaWN0b3Jhcy4KLSBTZSBwdWVkZSBkZWZpbmlyIG90cm9zIHJvbGVzIGNvbiBgdXBkYXRlX3JvbGVgIHkgbHVlZ28gc2VsZWNjaW9uYXJsYXMgdsOtYSBgaGFzX3JvbGVgCi0gYGFsbF9udW1lcmljYCwgYGFsbF9ub21pbmFsYCB5IGBoYXNfdHlwZWAgcGVybWl0ZSBlbGVnaXIgbGFzIHZhcmlhYmxlcyBzZWfDum4gc3UgdGlwby4KCmBgYHtyIHdhcm5pbmc9RkFMU0V9CmlyaXNfcmVjaXBlIDwtIHRyYWluaW5nKGlyaXNfc3BsaXQpICU+JQogIHJlY2lwZShTcGVjaWVzIH4uKSAlPiUKICBzdGVwX2NvcnIoYWxsX3ByZWRpY3RvcnMoKSkgJT4lCiAgc3RlcF9jZW50ZXIoYWxsX3ByZWRpY3RvcnMoKSwgLWFsbF9vdXRjb21lcygpKSAlPiUKICBzdGVwX3NjYWxlKGFsbF9wcmVkaWN0b3JzKCksIC1hbGxfb3V0Y29tZXMoKSkgJT4lCiAgcHJlcCgpCgppcmlzX3JlY2lwZQpgYGAKCmVuIGBpcmlzX3JlY2lwZWAgdGVuZW1vcyBsYSBfX3JlY2V0YV9fIGRlbCBwcmVwcm9jZXNhbWllbnRvLCBwZXJvIGHDum4gbm8gc2UgYXBsaWPDsyBzb2JyZSBuaW5nw7puIHNldCBkZSBkYXRvcy4KCiMjIyBBcGxpY2Ftb3MgbGEgcmVjZXRhIGVuIGxvcyBkYXRvcyBkZSB0cmFpbiB5IHRlc3QKCgpEYWRvIHF1ZSBsYSByZWNldGEgZnVlIGNvbnN0cnVpZGEgY29uIGxvcyBkYXRvcyBkZSBlbnRyZW5hbWllbnRvLCBlc3RlIGRhdGFzZXQgeWEgc2UgZW5jdWVudHJhIGltcGzDrWNpdG8gZW4gYGlyaXNfcmVjaXBlYC4gUGFyYSBleHRyYWVybG8sIHlhIHByZXByb2Nlc2FkbywgdXRpbGl6YW1vcyBsYSBmdW5jacOzbiBganVpY2VgIGRpcmVjdGFtZW50ZSBzb2JyZSBsYSByZWNldGEuCgoKYGBge3Igd2FybmluZz1GQUxTRX0KaXJpc190cmFpbmluZyA8LSBqdWljZShpcmlzX3JlY2lwZSkKCiBpcmlzX3RyYWluaW5nCmBgYAoKCkNvbiBsYSBmdW5jacOzbiBgYmFrZSgpYCBwb2RlbW9zIGFwbGljYXIgbGEgX3JlY2V0YV8gcXVlIHByZXBhcmFtb3MgYW50ZXMgcGFyYSBsb3MgZGF0b3MgZGUgdGVzdC4gUGFyYSBlbGxvIGVsIHByaW1lciBvYmpldG8gcXVlIHBhc2Ftb3MgZXMgbGEgX3JlY2V0YV8geSBjb21vIHBhcsOhbWV0cm8gZGUgX2Jha2VfIHBhc2Ftb3MgZWwgZGF0YXNldCBkZSB0ZXN0IChyZWN1cGVyYWRvIGNvbiBsYSBmdW5jacOzbiBgdGVzdGluZ2AgZGUgcnNhbXBsZSkKCmBgYHtyIHdhcm5pbmc9RkFMU0V9CmlyaXNfdGVzdGluZyA8LSBpcmlzX3JlY2lwZSAlPiUKICBiYWtlKHRlc3RpbmcoaXJpc19zcGxpdCkpIAoKaXJpc190ZXN0aW5nCmBgYAoKCiMjIEVudHJlbmFtaWVudG8gZGUgbW9kZWxvcwoKIVtdKGltZy9wYXJzbmlwX2xvZ28ucG5nKXt3aWR0aD0xMDB9CgpFbiBSIGhheSBtdWNob3MgcGFxdWV0ZXMgcXVlIGltcGxlbWVudGFuIGVsIG1pc21vIHRpcG8gZGUgbW9kZWxvLiBOb3JtYWxtZW50ZSBjYWRhIGltcGxlbWVudGFjacOzbiBkZWZpbmUgbG9zIHBhcsOhbWV0cm9zIGRlbCBtb2RlbG8gZGUgZm9ybWEgZGlzdGludGEuIFBvciBlamVtcGxvLCBwYXJhIGVsIG1vZGVsbyBSYW5kb20gRm9yZXN0LCBsYSBsaWJyZXLDrWEgYHJhbmdlcmAgZGVmaW5lIGVsIG7Dum1lcm8gZGUgw6FyYm9sZXMgY29tbyBudW0udHJlZXMsIG1pZW50cmFzIHF1ZSBsYSBsaWJyZXLDrWEgYHJhbmRvbUZvcmVzdGAgbG8gbm9tYnJhIG50cmVlLiAKCkFsIGlndWFsIHF1ZSBsYSB2aWVqYSBsaWJyZXLDrWEgY2FyZXQsIHRpZHltb2RlbHMgcmVtcGxhemEgbGEgaW50ZXJmYXosIHBlcm8gbm8gZWwgcGFxdWV0ZS4gRXMgZGVjaXIsIG5vIGVzIHVuYSBudWV2YSBpbXBsZW1lbnRhY2nDs24gZGUgbG9zIG1vZGVsb3MsIHNpbm8gdW5hIGludGVyZmF6IHF1ZSB1bmlmaWNhIGxhcyBkaWZlcmVudGVzIHNpbnRheGlzLgoKLSBFbiBlc3RlIGNhc28sIGxhIGZ1bmNpw7NuIGByYW5kX2ZvcmVzdGAgaW5jaWFsaXphIGVsIG1vZGVsbyBkZSBSYW5kb20gRm9yZXN0LCBkZWZpbmllbmRvIHN1cyBwYXLDoW1ldHJvcy4KLSBMYSBmdW5jacOzbiBgc2V0X2VuZ2luZWAgc2lydmUgcGFyYSBlc3BlY2lmaWNhciBxdcOpIGltcGxlbWVudGFjacOzbiB1dGlsaXphci4KLSBQYXJhIGVudHJlbmFyIGVsIG1vZGVsbywgc2UgdXRpbGl6YSBsYSBmdW5jacOzbiBgZml0YAoKYGBge3J9CmlyaXNfcmFuZ2VyIDwtIHJhbmRfZm9yZXN0KHRyZWVzID0gMTAwLCBtb2RlID0gImNsYXNzaWZpY2F0aW9uIikgJT4lCiAgc2V0X2VuZ2luZSgicmFuZ2VyIikgJT4lCiAgZml0KFNwZWNpZXMgfiAuLCBkYXRhID0gaXJpc190cmFpbmluZykKCmlyaXNfcmFuZ2VyCmBgYAoKCmBgYHtyfQppcmlzX3JmIDwtICByYW5kX2ZvcmVzdCh0cmVlcyA9IDEwMCwgbW9kZSA9ICJjbGFzc2lmaWNhdGlvbiIpICU+JQogIHNldF9lbmdpbmUoInJhbmRvbUZvcmVzdCIpICU+JQogIGZpdChTcGVjaWVzIH4gLiwgZGF0YSA9IGlyaXNfdHJhaW5pbmcpCgppcmlzX3JmCmBgYAoKCmBgYHtyfQppcmlzX25vZW5naW5lIDwtIHJhbmRfZm9yZXN0KHRyZWVzID0gMTAwLCBtb2RlID0gImNsYXNzaWZpY2F0aW9uIikgJT4lCiAgZml0KFNwZWNpZXMgfiAuLCBkYXRhID0gaXJpc190cmFpbmluZykKYGBgCgoKIyMgUHJlZGljY2lvbmVzCgpQYXJhIHByZWRlY2lyIGxvcyBkYXRvcyBkZSB0ZXN0IHV0aWxpemFtb3MgbGEgY29ub2NpZGEgZnVuY2nDs24gYHByZWRpY3RgLiBTaW4gZW1iYXJnbywgY3VhbmRvIHV0aWxpemFtb3MgZXN0YSBmdW5jacOzbiBzb2JyZSB1biBvYmpldG8gZGUgbGEgbGlicmVyw61hIGBwYXJzbmlwYCBkZXZ1ZWx2ZSB1biBfdGliYmxlXwoKYGBge3J9CnByZWRpY3QoaXJpc19yYW5nZXIsIGlyaXNfdGVzdGluZykKYGBgCgpQYXJhIGFncmVnYXIgbGFzIHByZWRpY2Npb25lcyBhIGxvcyBkYXRvcyBkZSB0ZXN0IHBvZGVtb3MgdXRpbGl6YXIgbGEgZnVuY2nDs24gYGJpbmRfY29sc2AKCmBgYHtyfQppcmlzX3JhbmdlciAlPiUKICBwcmVkaWN0KGlyaXNfdGVzdGluZykgJT4lCiAgYmluZF9jb2xzKGlyaXNfdGVzdGluZykgIApgYGAKCgojIyBWYWxpZGFjacOzbgoKIVtdKGltZy95YXJkc3RpY2tfbG9nby5wbmcpe3dpZHRoPTEwMH0KCkNvbiBsYSBmdW5jacOzbiBgeWFyZHN0aWNrOjptZXRyaWNzKClgIHBvZGVtb3MgbWVkaXIgbGEgcGVyZm9ybWFuY2UgZGVsIG1vZGVsby4gRWxpZ2UgYXV0b23DoXRpY2FtZW50ZSBsYXMgbcOpdHJpY2FzIGFwcm9waWFkYXMgZGFkbyBlbCB0aXBvIGRlIG1vZGVsby4gTmVjZXNpdG1hb3MgYWNsYXJhciBjdWFsZXMgc29uIGxvcyBkYXRvcyBwcmVkaWNob3MsX2VzdGltYXRlXyB5IHJlYWxlcywgX3RydXRoXy4KCmBgYHtyfQppcmlzX3JhbmdlciAlPiUKICBwcmVkaWN0KGlyaXNfdGVzdGluZykgJT4lCiAgYmluZF9jb2xzKGlyaXNfdGVzdGluZykgJT4lCiAgbWV0cmljcyh0cnV0aCA9IFNwZWNpZXMsIGVzdGltYXRlID0gLnByZWRfY2xhc3MpCmBgYAoKYGBge3J9CmlyaXNfcmYgJT4lCiAgcHJlZGljdChpcmlzX3Rlc3RpbmcpICU+JQogIGJpbmRfY29scyhpcmlzX3Rlc3RpbmcpICU+JQogIG1ldHJpY3ModHJ1dGggPSBTcGVjaWVzLCBlc3RpbWF0ZSA9IC5wcmVkX2NsYXNzKQpgYGAKCiMjIE1ldHJpY2FzIHBvciBjbGFzaWZpY2Fkb3IKClNpIGVuIGVsIHByZWRpY3QgZWxlZ2ltb3MgYHR5cGU9InByb2IiYCAKCmBgYHtyfQppcmlzX3JhbmdlciAlPiUKICBwcmVkaWN0KGlyaXNfdGVzdGluZywgdHlwZSA9ICJwcm9iIikgIApgYGAKCgoKYGBge3J9CmlyaXNfcHJvYnMgPC0gaXJpc19yYW5nZXIgJT4lCiAgcHJlZGljdChpcmlzX3Rlc3RpbmcsIHR5cGUgPSAicHJvYiIpICU+JQogIGJpbmRfY29scyhpcmlzX3Rlc3RpbmcpCgppcmlzX3Byb2JzCmBgYAoKQ29uIGVsIHRpYmJsZSBpcmlzX3Byb2JzIGVzIGbDoWNpbCBjYWxjdWxhciBtw6l0b2RvcyBkZSBjdXJ2YSwgY29tbyBsYSBST0MsIHV0aWxpemFuZG8gbGEgZnVuY2nDs24gYHJvY19jdXJ2ZWAKCmBgYHtyfQppcmlzX3Byb2JzJT4lCiAgcm9jX2N1cnZlKFNwZWNpZXMsIC5wcmVkX3NldG9zYToucHJlZF92aXJnaW5pY2EpCmBgYAoKZmluYWxtZW50ZSwgY29uIGxhIGZ1bmNpw7NuIGBhdXRvcGxvdGAgcG9kZW1vcyBncmFmaWNhciBsYXMgY3VydmFzLiBEYWRvIHF1ZSBlbCByZXN1bHRhZG8gZXMgdW4gb2JqZXRvIGdncGxvdCwgcG9kZW1vcyBoYWNlciBvdHJhcyBtb2RpZmljYWNpb25lcyBwb3N0ZXJpb3Jlcy4gCgpgYGB7cn0KaXJpc19wcm9icyU+JQogIHJvY19jdXJ2ZShTcGVjaWVzLCAucHJlZF9zZXRvc2E6LnByZWRfdmlyZ2luaWNhKSAlPiUKICBhdXRvcGxvdCgpKwogIGdndGhlbWVzOjp0aGVtZV9maXZldGhpcnR5ZWlnaHQoKSsKICBsYWJzKHRpdGxlID0gJ0N1cnZhcyBST0MnKQpgYGAKClNpIHF1ZXJlbW9zIHVuIHRpYmJsZSBjb24gbGEgY2xhc2UgcHJlZGljaGEgeSBsYSBwcm9iYWJpbGlkYWQgZGUgY2FkYSBjbGFzZSwgcG9kZW1vcyB1dGlsaXphciBgYmluZF9jb2xzYCBwYXJhIGp1bnRhciBsb3MgcmVzdWx0YWRvcyBkZSBhbWJvcyB0aXBvcwoKYGBge3J9CnByZWRpY3QoaXJpc19yYW5nZXIsIGlyaXNfdGVzdGluZywgdHlwZSA9ICJwcm9iIikgJT4lCiAgYmluZF9jb2xzKHByZWRpY3QoaXJpc19yYW5nZXIsIGlyaXNfdGVzdGluZykpICU+JQogIGJpbmRfY29scyhzZWxlY3QoaXJpc190ZXN0aW5nLCBTcGVjaWVzKSkKYGBgCgoKCnNpIHV0aWxpemFtb3MgYG1ldHJpY3NgIHNvYnJlIGVzdG9zIHJlc3VsdGFkb3MsIG9idGVuZW1vcyBhZGVtw6FzIGRlIGFjY3VyYWN5IHkga2FwLCBlbCBsb2cgbG9zcyB5IGVsIGFyZWEgZGViYWpvIGRlIGxhIGN1cnZhIFJPQy4KCmBgYHtyfQpwcmVkaWN0KGlyaXNfcmFuZ2VyLCBpcmlzX3Rlc3RpbmcsIHR5cGUgPSAicHJvYiIpICU+JQogIGJpbmRfY29scyhwcmVkaWN0KGlyaXNfcmFuZ2VyLCBpcmlzX3Rlc3RpbmcpKSAlPiUKICBiaW5kX2NvbHMoc2VsZWN0KGlyaXNfdGVzdGluZywgU3BlY2llcykpICU+JQogIG1ldHJpY3ModHJ1dGg9U3BlY2llcywgLnByZWRfc2V0b3NhOi5wcmVkX3ZpcmdpbmljYSwgZXN0aW1hdGUgPSAucHJlZF9jbGFzcykKYGBgCgoK