Objetivo

Predecir el consumo para los 3 meses siguiente dada una serie de datos del consumo previo junto a variables exogenas. Las variables next_consume, next_2_consume y next_3_consume son las variables dependientes que queremos predecir.

Cargar Bibliotecas

library(readr) # para leeer el dataset
library(ranger) # random forest con esteroides
library(dplyr) # para manipular datos
library(skimr) # para mirar los datos
library(caret) # framework de machine learning

Cargar el dataset

dataset %>% select(Date)
dataset %>% names() %>% as.data.frame()
 skimr::skim(dataset)# %>% knitr::kable() %>% kable_styling(font_size = 9)
── Data Summary ────────────────────────
                           Values 
Name                       dataset
Number of rows             76     
Number of columns          32     
_______________________           
Column type frequency:            
  numeric                  32     
________________________          
Group variables            None   

La Metodologia


dataset <- dataset %>% tidyr::drop_na()
  # mirando los mismos datos, predicen parecido para los proximos 3 meses.
train<-dataset %>% sample_frac(0.8)
test <-setdiff(dataset,train)
train
test

Modelo para predecir next_consume

rf_model1 <- ranger(next_consume ~ . -next_2_consume - next_3_consume - trimestral_consume ,data=train)
rf_model1
Ranger result

Call:
 ranger(next_consume ~ . - next_2_consume - next_3_consume - trimestral_consume,      data = train) 

Type:                             Regression 
Number of trees:                  500 
Sample size:                      54 
Number of independent variables:  28 
Mtry:                             5 
Target node size:                 5 
Variable importance mode:         none 
Splitrule:                        variance 
OOB prediction error (MSE):       48.75851 
R squared (OOB):                  0.2491389 
rf_model1$prediction.error
[1] 48.75851

Entrenamiento

Out Of Box Sampling.

Los errores MSE y R squared se calculan sobre el OOB. El concepto de OOB está relacionado con el proceso de bootstrapping, que es una técnica de muestreo utilizada en la construcción de los árboles de decisión en Random Forest. En bootstrapping, se extrae una muestra aleatoria de los datos de entrenamiento con reemplazo, lo que significa que algunas instancias pueden ser elegidas varias veces, mientras que otras pueden no ser elegidas en absoluto.

Importancia de las variables

rf_model1 <- ranger(next_consume ~ . -next_2_consume - next_3_consume - trimestral_consume ,data=train, importance = "impurity")
rf_model1
Ranger result

Call:
 ranger(next_consume ~ . - next_2_consume - next_3_consume - trimestral_consume,      data = train, importance = "impurity") 

Type:                             Regression 
Number of trees:                  500 
Sample size:                      54 
Number of independent variables:  28 
Mtry:                             5 
Target node size:                 5 
Variable importance mode:         impurity 
Splitrule:                        variance 
OOB prediction error (MSE):       51.84603 
R squared (OOB):                  0.2015925 

impurity: Este es el método predeterminado, que calcula la importancia de una característica basándose en la disminución de la impureza del nodo (por ejemplo, Gini o entropía) cuando una característica se utiliza para dividir en los árboles de decisión. Cuanto mayor sea la disminución de la impureza, más importante se considera la característica.


rf_model1$variable.importance
                       month_consume                      unique_customer                                stock                         prev_consume 
                           167.57974                            195.31883                            264.89529                            262.75743 
                      prev_2_consume                       prev_3_consume                       prev_4_consume                       prev_5_consume 
                           135.26440                             62.02208                            125.07794                             94.85842 
                      prev_6_consume                       prev_customers                 dolar_estadounidense                     produccion_trigo 
                           206.83914                            217.88084                            151.86929                             70.43617 
                    relacion_dolares                          Tasa_Badlar                      produccion_soja           CABA_precio_m2_3_ambientes 
                            98.15683                             52.10746                             14.43870                             76.52745 
                             ICC_GBA           CABA_precio_m2_2_ambientes Generacion_neta_de_energia_electrica                             Tasa_Fed 
                            57.69531                             63.46328                            186.64863                             44.22305 
              expectativas_inflacion       CABA_precio_m2_1_a_5_ambientes            CABA_precio_m2_1_ambiente                   ICC_BienesDurables 
                            40.51086                             69.55379                             63.88734                             64.55486 
                     ICC_NacionalEco                      ICC_NacionalReg                ICC_SituacionPersonal                      produccion_maiz 
                            49.07231                             44.79680                             94.75268                             84.61231 
data.frame(impurity=rf_model1$variable.importance) %>% arrange(desc(impurity))
NA
rf_model1 <- ranger(next_consume ~ prev_6_consume + month_consume + dolar_estadounidense -next_2_consume - next_3_consume - trimestral_consume ,data=train, importance = "impurity")
rf_model1
Ranger result

Call:
 ranger(next_consume ~ prev_6_consume + month_consume + dolar_estadounidense -      next_2_consume - next_3_consume - trimestral_consume, data = train,      importance = "impurity") 

Type:                             Regression 
Number of trees:                  500 
Sample size:                      54 
Number of independent variables:  3 
Mtry:                             1 
Target node size:                 5 
Variable importance mode:         impurity 
Splitrule:                        variance 
OOB prediction error (MSE):       48.50974 
R squared (OOB):                  0.25297 

Prediccion

predictions1 <- predict(rf_model1,data = test, type='response')
predictions1$predictions %>% as.data.frame

Error MSE en test

mse<-function(act,pred) {mean((act- pred)^2)}

data.frame(pred=predictions1$predictions, act=test$next_consume) %>% summarise(mse=mse(act,pred))

Intervalos de prediccion (quantile regression)

En vez de utilizar el promedio se utilzan los cuantiles para tener un intervalo de predicción (Meinshausen, 2006). A la hora de realizar el split, en vez de utilizar MSE o alguna otra metrica de impureza, se utiliza una metrica que tiene en cuenta a los cuantiles . Luego en cada hoja en vez de calcular el promedio, se calculan cuantiles.

rf_model1 <- ranger(next_consume ~ . -next_2_consume - next_3_consume - trimestral_consume ,data=train, importance = "impurity",quantreg = TRUE)
rf_model1
Ranger result

Call:
 ranger(next_consume ~ . - next_2_consume - next_3_consume - trimestral_consume,      data = train, importance = "impurity", quantreg = TRUE) 

Type:                             Regression 
Number of trees:                  500 
Sample size:                      54 
Number of independent variables:  28 
Mtry:                             5 
Target node size:                 5 
Variable importance mode:         impurity 
Splitrule:                        variance 
OOB prediction error (MSE):       50.83065 
R squared (OOB):                  0.2172288 
predictions1 <- predict(rf_model1,data = test, type= "quantiles")
predictions1$predictions %>% as.data.frame()
NA
NA
p1<-data.frame(predictions1$predictions,act=test$next_consume,label="FCT 1M")

p1  %>% ggplot()+
  geom_point(aes(x=act,y=quantile..0.5),color='red')+
  geom_errorbar(aes(x=act,y=quantile..0.5,ymax=quantile..0.9,ymin=quantile..0.1),color='orange')+
  theme_classic()

Intervalo de confianza

Similar al intervalo de prediccion. En las implementaciones de Random Forests, suele confudirse. En terminos generales, uno se aplica sobre una observacion/predicción en general, mientras que el otro trata sobre estadisticos. Un ejemplo seria, la diferencia entre desviacion estandar de una variable y el error estandar sobre un conjunto de muestras.

If we change the training dataset just a little bit, will Random Forest give you the same result for that particular example?

(Wagner et al. 2014) Basado en una tecnica que se llama jacknife.

rf_model1 <- ranger(next_consume ~ stock + month_consume + prev_6_consume + prev_2_consume  ,data=train, importance = "impurity",keep.inbag  = TRUE)
rf_model1
Ranger result

Call:
 ranger(next_consume ~ stock + month_consume + prev_6_consume +      prev_2_consume, data = train, importance = "impurity", keep.inbag = TRUE) 

Type:                             Regression 
Number of trees:                  500 
Sample size:                      54 
Number of independent variables:  4 
Mtry:                             2 
Target node size:                 5 
Variable importance mode:         impurity 
Splitrule:                        variance 
OOB prediction error (MSE):       56.38659 
R squared (OOB):                  0.1316697 
predictions1 <- predict(rf_model1,data = train, type= "se")

predictions1$predictions %>% as.data.frame()
predictions1$se %>% as.data.frame()

data.frame(pred=predictions1$predictions, se= predictions1$se, act=train$next_consume) %>% ggplot()+
  geom_point(aes(x=act,y=pred),color='red')+
  geom_errorbar(aes(x=act,y=pred,ymax=pred+se,ymin=pred-se),color='orange')+
  theme_classic()

LS0tCnRpdGxlOiAiUmFuZG9tIEZvcmVzdHM6IFNpcnZlIHBhcmEgdG9kbz8iCm91dHB1dDogaHRtbF9ub3RlYm9vawpkYXRlOiAnMjAyMy0wNC0yNycKLS0tCgpgYGB7ciBzZXR1cCwgaW5jbHVkZT1GQUxTRX0Ka25pdHI6Om9wdHNfY2h1bmskc2V0KGVjaG8gPSBUUlVFKQpgYGAKCiMgT2JqZXRpdm8KClByZWRlY2lyIGVsIGNvbnN1bW8gcGFyYSBsb3MgMyBtZXNlcyBzaWd1aWVudGUgZGFkYSB1bmEgc2VyaWUgZGUgZGF0b3MgZGVsIGNvbnN1bW8gcHJldmlvIGp1bnRvIGEgdmFyaWFibGVzIGV4b2dlbmFzLiBMYXMgdmFyaWFibGVzIGBuZXh0X2NvbnN1bWVgLCBgbmV4dF8yX2NvbnN1bWVgIHkgYG5leHRfM19jb25zdW1lYCBzb24gbGFzIHZhcmlhYmxlcyBkZXBlbmRpZW50ZXMgcXVlIHF1ZXJlbW9zIHByZWRlY2lyLgoKIyMgQ2FyZ2FyIEJpYmxpb3RlY2FzCgpgYGB7ciBtZXNzYWdlPUZBTFNFLCB3YXJuaW5nPUZBTFNFfQpsaWJyYXJ5KHJlYWRyKSAjIHBhcmEgbGVlZXIgZWwgZGF0YXNldApsaWJyYXJ5KHJhbmdlcikgIyByYW5kb20gZm9yZXN0IGNvbiBlc3Rlcm9pZGVzCmxpYnJhcnkoZHBseXIpICMgcGFyYSBtYW5pcHVsYXIgZGF0b3MKbGlicmFyeShza2ltcikgIyBwYXJhIG1pcmFyIGxvcyBkYXRvcwpsaWJyYXJ5KGNhcmV0KSAjIGZyYW1ld29yayBkZSBtYWNoaW5lIGxlYXJuaW5nCmBgYAoKIyMgQ2FyZ2FyIGVsIGRhdGFzZXQKCmBgYHtyIG1lc3NhZ2U9RkFMU0UsIHdhcm5pbmc9RkFMU0UsIGluY2x1ZGU9RkFMU0V9CmRhdGFzZXQgPC0gcmVhZHI6OnJlYWRfY3N2KCJodHRwczovL3d3dy5kcm9wYm94LmNvbS9zL2sxaGc5cG05NzVvdXdsMC9kYXRhcmF3X2FpLmNzdj9kbD0xIikKZGF0YXNldCA8LSBkYXRhc2V0ICU+JSBtdXRhdGUoRGF0ZT0gbHVicmlkYXRlOjp5bShEYXRlKSkKZGF0YXNldCA8LSBkYXRhc2V0ICU+JSBzZWxlY3QoLUFnci5JdGVtLC1EYXRlLCAtcHJldl9kYXRlLCAtYC4uLjFgKQoKYGBgCgpgYGB7cn0KZGF0YXNldCAlPiUgc2VsZWN0KERhdGUpCmRhdGFzZXQgJT4lIG5hbWVzKCkgJT4lIGFzLmRhdGEuZnJhbWUoKQpgYGAKCmBgYHtyfQogc2tpbXI6OnNraW0oZGF0YXNldCkjICU+JSBrbml0cjo6a2FibGUoKSAlPiUga2FibGVfc3R5bGluZyhmb250X3NpemUgPSA5KQpgYGAKCiMjIExhIE1ldG9kb2xvZ2lhCgohW10oaHR0cHM6Ly9oYXJwb21heHguZ2l0aHViLmlvL3Bvc3QvMjAyMC0wOS0wOS1leHBlcmltZW50YWwtZGVzaWduLmVuX2ZpbGVzL21sLWV4cGVyaW1lbnRhLWRlc2lnbkEucG5nKXtzdHlsZT0iY29sb3I6d2hpdGUifQoKYGBge3J9CgpkYXRhc2V0IDwtIGRhdGFzZXQgJT4lIHRpZHlyOjpkcm9wX25hKCkKICAjIG1pcmFuZG8gbG9zIG1pc21vcyBkYXRvcywgcHJlZGljZW4gcGFyZWNpZG8gcGFyYSBsb3MgcHJveGltb3MgMyBtZXNlcy4KdHJhaW48LWRhdGFzZXQgJT4lIHNhbXBsZV9mcmFjKDAuOCkKdGVzdCA8LXNldGRpZmYoZGF0YXNldCx0cmFpbikKdHJhaW4KdGVzdApgYGAKCiMjIE1vZGVsbyBwYXJhIHByZWRlY2lyIG5leHRfY29uc3VtZQoKYGBge3J9CnJmX21vZGVsMSA8LSByYW5nZXIobmV4dF9jb25zdW1lIH4gLiAtbmV4dF8yX2NvbnN1bWUgLSBuZXh0XzNfY29uc3VtZSAtIHRyaW1lc3RyYWxfY29uc3VtZSAsZGF0YT10cmFpbikKcmZfbW9kZWwxCmBgYAoKYGBge3J9CnJmX21vZGVsMSRwcmVkaWN0aW9uLmVycm9yCmBgYAoKIyMjIEVudHJlbmFtaWVudG8KCiMjIyMgT3V0IE9mIEJveCBTYW1wbGluZy4KCkxvcyBlcnJvcmVzIE1TRSB5IFIgc3F1YXJlZCBzZSBjYWxjdWxhbiBzb2JyZSBlbCBPT0IuIEVsIGNvbmNlcHRvIGRlICoqT09CKiogZXN0w6EgcmVsYWNpb25hZG8gY29uIGVsIHByb2Nlc28gZGUgKipib290c3RyYXBwaW5nKiosIHF1ZSBlcyB1bmEgdMOpY25pY2EgZGUgbXVlc3RyZW8gdXRpbGl6YWRhIGVuIGxhIGNvbnN0cnVjY2nDs24gZGUgbG9zIMOhcmJvbGVzIGRlIGRlY2lzacOzbiBlbiBSYW5kb20gRm9yZXN0LiBFbiBib290c3RyYXBwaW5nLCBzZSBleHRyYWUgdW5hIG11ZXN0cmEgYWxlYXRvcmlhIGRlIGxvcyBkYXRvcyBkZSBlbnRyZW5hbWllbnRvIGNvbiByZWVtcGxhem8sIGxvIHF1ZSBzaWduaWZpY2EgcXVlIGFsZ3VuYXMgaW5zdGFuY2lhcyBwdWVkZW4gc2VyIGVsZWdpZGFzIHZhcmlhcyB2ZWNlcywgbWllbnRyYXMgcXVlIG90cmFzIHB1ZWRlbiBubyBzZXIgZWxlZ2lkYXMgZW4gYWJzb2x1dG8uXApcCgojIyMjIEltcG9ydGFuY2lhIGRlIGxhcyB2YXJpYWJsZXMKCmBgYHtyfQpyZl9tb2RlbDEgPC0gcmFuZ2VyKG5leHRfY29uc3VtZSB+IC4gLW5leHRfMl9jb25zdW1lIC0gbmV4dF8zX2NvbnN1bWUgLSB0cmltZXN0cmFsX2NvbnN1bWUgLGRhdGE9dHJhaW4sIGltcG9ydGFuY2UgPSAiaW1wdXJpdHkiKQpyZl9tb2RlbDEKYGBgCgoqKmltcHVyaXR5Kio6IEVzdGUgZXMgZWwgbcOpdG9kbyBwcmVkZXRlcm1pbmFkbywgcXVlIGNhbGN1bGEgbGEgaW1wb3J0YW5jaWEgZGUgdW5hIGNhcmFjdGVyw61zdGljYSBiYXPDoW5kb3NlIGVuIGxhIGRpc21pbnVjacOzbiBkZSBsYSBpbXB1cmV6YSBkZWwgbm9kbyAocG9yIGVqZW1wbG8sIEdpbmkgbyBlbnRyb3DDrWEpIGN1YW5kbyB1bmEgY2FyYWN0ZXLDrXN0aWNhIHNlIHV0aWxpemEgcGFyYSBkaXZpZGlyIGVuIGxvcyDDoXJib2xlcyBkZSBkZWNpc2nDs24uIEN1YW50byAqKm1heW9yIHNlYSBsYSBkaXNtaW51Y2nDs24gZGUgbGEgaW1wdXJlemEsIG3DoXMgaW1wb3J0YW50ZSBzZSBjb25zaWRlcmEgbGEgY2FyYWN0ZXLDrXN0aWNhKiouCgpgYGB7cn0KCnJmX21vZGVsMSR2YXJpYWJsZS5pbXBvcnRhbmNlCmRhdGEuZnJhbWUoaW1wdXJpdHk9cmZfbW9kZWwxJHZhcmlhYmxlLmltcG9ydGFuY2UpICU+JSBhcnJhbmdlKGRlc2MoaW1wdXJpdHkpKQoKYGBgCgpgYGB7cn0KcmZfbW9kZWwxIDwtIHJhbmdlcihuZXh0X2NvbnN1bWUgfiBwcmV2XzZfY29uc3VtZSArIG1vbnRoX2NvbnN1bWUgKyBkb2xhcl9lc3RhZG91bmlkZW5zZSAtbmV4dF8yX2NvbnN1bWUgLSBuZXh0XzNfY29uc3VtZSAtIHRyaW1lc3RyYWxfY29uc3VtZSAsZGF0YT10cmFpbiwgaW1wb3J0YW5jZSA9ICJpbXB1cml0eSIpCnJmX21vZGVsMQpgYGAKCiMjIyBQcmVkaWNjaW9uCgpgYGB7cn0KcHJlZGljdGlvbnMxIDwtIHByZWRpY3QocmZfbW9kZWwxLGRhdGEgPSB0ZXN0LCB0eXBlPSdyZXNwb25zZScpCnByZWRpY3Rpb25zMSRwcmVkaWN0aW9ucyAlPiUgYXMuZGF0YS5mcmFtZQpgYGAKCiMjIyMgRXJyb3IgTVNFIGVuIHRlc3QKCmBgYHtyfQptc2U8LWZ1bmN0aW9uKGFjdCxwcmVkKSB7bWVhbigoYWN0LSBwcmVkKV4yKX0KCmRhdGEuZnJhbWUocHJlZD1wcmVkaWN0aW9uczEkcHJlZGljdGlvbnMsIGFjdD10ZXN0JG5leHRfY29uc3VtZSkgJT4lIHN1bW1hcmlzZShtc2U9bXNlKGFjdCxwcmVkKSkKYGBgCgojIyMjIEludGVydmFsb3MgZGUgcHJlZGljY2lvbiAocXVhbnRpbGUgcmVncmVzc2lvbikKCkVuIHZleiBkZSB1dGlsaXphciBlbCBwcm9tZWRpbyBzZSB1dGlsemFuIGxvcyBjdWFudGlsZXMgcGFyYSB0ZW5lciB1biBpbnRlcnZhbG8gZGUgcHJlZGljY2nDs24gKE1laW5zaGF1c2VuLCAyMDA2KS4gQSBsYSBob3JhIGRlIHJlYWxpemFyIGVsIHNwbGl0LCBlbiB2ZXogZGUgdXRpbGl6YXIgTVNFIG8gYWxndW5hIG90cmEgbWV0cmljYSBkZSBpbXB1cmV6YSwgc2UgdXRpbGl6YSB1bmEgbWV0cmljYSBxdWUgdGllbmUgZW4gY3VlbnRhIGEgbG9zIGN1YW50aWxlcyAuIEx1ZWdvIGVuIGNhZGEgaG9qYSBlbiB2ZXogZGUgY2FsY3VsYXIgZWwgcHJvbWVkaW8sIHNlIGNhbGN1bGFuIGN1YW50aWxlcy4KCmBgYHtyfQpyZl9tb2RlbDEgPC0gcmFuZ2VyKG5leHRfY29uc3VtZSB+IC4gLW5leHRfMl9jb25zdW1lIC0gbmV4dF8zX2NvbnN1bWUgLSB0cmltZXN0cmFsX2NvbnN1bWUgLGRhdGE9dHJhaW4sIGltcG9ydGFuY2UgPSAiaW1wdXJpdHkiLHF1YW50cmVnID0gVFJVRSkKcmZfbW9kZWwxCmBgYAoKYGBge3J9CnByZWRpY3Rpb25zMSA8LSBwcmVkaWN0KHJmX21vZGVsMSxkYXRhID0gdGVzdCwgdHlwZT0gInF1YW50aWxlcyIpCnByZWRpY3Rpb25zMSRwcmVkaWN0aW9ucyAlPiUgYXMuZGF0YS5mcmFtZSgpCgoKYGBgCgpgYGB7cn0KcDE8LWRhdGEuZnJhbWUocHJlZGljdGlvbnMxJHByZWRpY3Rpb25zLGFjdD10ZXN0JG5leHRfY29uc3VtZSxsYWJlbD0iRkNUIDFNIikKCnAxICAlPiUgZ2dwbG90KCkrCiAgZ2VvbV9wb2ludChhZXMoeD1hY3QseT1xdWFudGlsZS4uMC41KSxjb2xvcj0ncmVkJykrCiAgZ2VvbV9lcnJvcmJhcihhZXMoeD1hY3QseT1xdWFudGlsZS4uMC41LHltYXg9cXVhbnRpbGUuLjAuOSx5bWluPXF1YW50aWxlLi4wLjEpLGNvbG9yPSdvcmFuZ2UnKSsKICB0aGVtZV9jbGFzc2ljKCkKYGBgCgojIyMjIEludGVydmFsbyBkZSBjb25maWFuemEKClNpbWlsYXIgYWwgaW50ZXJ2YWxvIGRlIHByZWRpY2Npb24uIEVuIGxhcyBpbXBsZW1lbnRhY2lvbmVzIGRlIFJhbmRvbSBGb3Jlc3RzLCBzdWVsZSBjb25mdWRpcnNlLiBFbiB0ZXJtaW5vcyBnZW5lcmFsZXMsIHVubyBzZSBhcGxpY2Egc29icmUgdW5hIG9ic2VydmFjaW9uL3ByZWRpY2Npw7NuIGVuIGdlbmVyYWwsIG1pZW50cmFzIHF1ZSBlbCBvdHJvIHRyYXRhIHNvYnJlIGVzdGFkaXN0aWNvcy4gVW4gZWplbXBsbyBzZXJpYSwgbGEgZGlmZXJlbmNpYSBlbnRyZSBkZXN2aWFjaW9uIGVzdGFuZGFyIGRlIHVuYSB2YXJpYWJsZSB5IGVsIGVycm9yIGVzdGFuZGFyIHNvYnJlIHVuIGNvbmp1bnRvIGRlIG11ZXN0cmFzLgoKSWYgd2UgY2hhbmdlIHRoZSB0cmFpbmluZyBkYXRhc2V0IGp1c3QgYSBsaXR0bGUgYml0LCB3aWxsIFJhbmRvbSBGb3Jlc3QgZ2l2ZSB5b3UgdGhlIHNhbWUgcmVzdWx0IGZvciB0aGF0IHBhcnRpY3VsYXIgZXhhbXBsZT8KCihXYWduZXIgZXQgYWwuIDIwMTQpIEJhc2FkbyBlbiB1bmEgdGVjbmljYSBxdWUgc2UgbGxhbWEgamFja25pZmUuCgpgYGB7cn0KcmZfbW9kZWwxIDwtIHJhbmdlcihuZXh0X2NvbnN1bWUgfiBzdG9jayArIG1vbnRoX2NvbnN1bWUgKyBwcmV2XzZfY29uc3VtZSArIHByZXZfMl9jb25zdW1lICAsZGF0YT10cmFpbiwgaW1wb3J0YW5jZSA9ICJpbXB1cml0eSIsa2VlcC5pbmJhZyAgPSBUUlVFKQpyZl9tb2RlbDEKYGBgCgpgYGB7cn0KcHJlZGljdGlvbnMxIDwtIHByZWRpY3QocmZfbW9kZWwxLGRhdGEgPSB0cmFpbiwgdHlwZT0gInNlIikKCnByZWRpY3Rpb25zMSRwcmVkaWN0aW9ucyAlPiUgYXMuZGF0YS5mcmFtZSgpCnByZWRpY3Rpb25zMSRzZSAlPiUgYXMuZGF0YS5mcmFtZSgpCgpkYXRhLmZyYW1lKHByZWQ9cHJlZGljdGlvbnMxJHByZWRpY3Rpb25zLCBzZT0gcHJlZGljdGlvbnMxJHNlLCBhY3Q9dHJhaW4kbmV4dF9jb25zdW1lKSAlPiUgZ2dwbG90KCkrCiAgZ2VvbV9wb2ludChhZXMoeD1hY3QseT1wcmVkKSxjb2xvcj0ncmVkJykrCiAgZ2VvbV9lcnJvcmJhcihhZXMoeD1hY3QseT1wcmVkLHltYXg9cHJlZCtzZSx5bWluPXByZWQtc2UpLGNvbG9yPSdvcmFuZ2UnKSsKICB0aGVtZV9jbGFzc2ljKCkKYGBgCgo=