Conexión de sparklyr en local.

Cargamos sparklyr y dplyr

library(sparklyr)
library(dplyr)
library(ggplot2)

Inicializamos la conexión

sc <- spark_connect(master = "local", version = "2.0.0")

Lectura de datos.

Sparklyr puede leer datos de hive utilizando hive_context(sc) o de ficheros en local o hdfs. Vamos a leer un fichero en formato parquet que está en local. Se trata de los datos del censo de 2011 para Andalucía, que había bajado previamente utilizando el paquete MicroDatosEs de Carlos Gil

Utilizamos la función spark_read_parquet que lee los datos y crea un DataFrame de spark. Al asignarlo a un objeto se crea un tbl_spark que permite utilizar funciones de dplyr sobre un dataframe de Spark.

censo1_tbl <- spark_read_parquet(sc, "censo1", path = "/home/jose/spark-warehouse/censo2/")
class(censo1_tbl)
[1] "tbl_spark" "tbl_sql"   "tbl_lazy"  "tbl"      

censo1_tbl no es un dataframe de R sino una conexión al dataframe de spark

censo1_tbl
Source:   query [?? x 9]
Database: spark connection master=local[4] app=sparklyr local=TRUE

    edad   sexo   cpro     ecivil    factor
   <dbl>  <chr>  <chr>      <chr>     <dbl>
1     77  Mujer Lleida      Viudo  1.413253
2     47  Mujer Lleida     Casado  1.351084
3     50 Hombre Lleida     Casado  1.351084
4     21 Hombre Lleida    Soltero  1.351084
5     15  Mujer Lleida    Soltero  1.351084
6     47  Mujer Lleida    Soltero 28.615887
7     33 Hombre Lleida Divorciado 28.615887
8     34 Hombre Lleida     Casado 28.615887
9     46  Mujer Lleida     Casado  6.735608
10    61 Hombre Lleida     Casado  6.735608
# ... with more rows, and 4 more variables: esreal <chr>, rela <chr>,
#   nhijos <dbl>, nocu <dbl>

Modelo MLlib

Creamos dataset para el modelo. Es importante que el dataset no tenga perdidos, porque puede dar error. Se supone que en la versión de desarrollo de sparklyr está solucionado, pero a mi no me ha funcionado

mod_dataset <- censo1_tbl %>%
   filter(edad > 20, edad < 70, !is.na(edad),!is.na(rela), !is.na(esreal), !is.na(nhijos), !is.na(nocu)) %>%
   mutate(respuesta= as.character(ifelse( ecivil == "Divorciado", 1,0))) %>%
   sdf_register("mod_dataset")
summarise(mod_dataset, n())
Source:   query [?? x 1]
Database: spark connection master=local[4] app=sparklyr local=TRUE

  `count(1)`
       <dbl>
1     921239

Con sdf_partion creamos datos para training y para test

partitions <- mod_dataset %>% sdf_partition(training = 0.7, test = 0.3, seed = 42)
partitions$training %>% group_by(cpro) %>% summarise(n(),prop_divorc = mean(respuesta))
Source:   query [?? x 3]
Database: spark connection master=local[4] app=sparklyr local=TRUE

                     cpro `count(1)` prop_divorc
                    <chr>      <dbl>       <dbl>
1                  Madrid      75864  0.06858325
2                  Huelva       7749  0.05187766
3               Barcelona      66326  0.07633507
4                  Zamora       6154  0.02941176
5                  Burgos       7647  0.03295410
6               Cantabria       8704  0.05641085
7             Ciudad Real       7849  0.03822143
8                  Cuenca       5801  0.02982244
9             Guadalajara       5379  0.04982339
10 Santa Cruz de Tenerife      10034  0.09358182
# ... with more rows
partitions$test %>% group_by(cpro) %>% summarise(n(),prop_divorc = mean(respuesta))
Source:   query [?? x 3]
Database: spark connection master=local[4] app=sparklyr local=TRUE

                     cpro `count(1)` prop_divorc
                    <chr>      <dbl>       <dbl>
1                  Madrid      32607  0.06780753
2                  Huelva       3330  0.05165165
3               Barcelona      28271  0.07742917
4                  Zamora       2571  0.03072734
5                  Burgos       3202  0.04184884
6               Cantabria       3779  0.05398254
7             Ciudad Real       3409  0.02992080
8                  Cuenca       2469  0.03280680
9  Santa Cruz de Tenerife       4211  0.09380195
10                  Ávila       2050  0.02878049
# ... with more rows

Aunque incorpora interfaz fórmula, da problemas con variables categóricas según la versión de spark y sparklyr instalada. Lo siguiente no funciona con la versión de desarrollo de sparklyr ni con algunas versiones de spark

mlformula <- formula(respuesta ~ edad + esreal+ rela+ nhijos + nocu)
mlogis <- partitions$training %>% ml_logistic_regression(mlformula)
summary(mlogis)
Call: ml_logistic_regression(., mlformula)

Coefficients:
                                                                                                                                  (Intercept) 
                                                                                                                                 -1.555407668 
                                                                                                                                         edad 
                                                                                                                                  0.006485887 
                                                     esreal_Diplomatura universitaria, Arquitectura Técnica, Ingeniería Técnica o equivalente 
                                                                                                                                 -0.309846995 
                                                                                                                             esreal_Doctorado 
                                                                                                                                 -0.190056222 
esreal_FP grado medio, FP I, Oficialía industrial o equivalente, Grado Medio de Música y Danza, Certificados de Escuelas Oficiales de Idiomas 
                                                                                                                                 -0.018245016 
                                                                           esreal_FP grado superior, FP II, Maestría industrial o equivalente 
                                                                                                                                 -0.221566091 
                                         esreal_Fue a la escuela 5 o más años pero no llegó al último curso de ESO, EGB o Bachiller Elemental 
                                                                                                                                 -0.699136112 
                                                                                                     esreal_Grado Universitario o equivalente 
                                                                                                                                 -0.025673079 
                                                                                  esreal_Licenciatura, Arquitectura, Ingeniería o equivalente 
                                                                                                                                 -0.302889805 
                 esreal_Llegó al último curso de ESO, EGB o Bachiller Elemental o tiene el Certificado de Escolaridad o de Estudios Primarios 
                                                                                                                                 -0.382232763 
                                                    esreal_Máster oficial universitario (a partir de 2006), Especialidades Médicas o análogos 
                                                                                                                                 -0.055593503 
                                                                                                               esreal_No sabe leer o escribir 
                                                                                                                                 -1.092034826 
                                                                            esreal_Sabe leer y escribir pero fue menos de 5 años a la escuela 
                                                                                                                                 -1.106870226 
                                                                                                                                 rela_Ocupado 
                                                                                                                                  1.998901013 
                                                                                                                          rela_Otra situación 
                                                                                                                                 -0.475906522 
                                                                                                        rela_Parado buscando su primer empleo 
                                                                                                                                  0.310737238 
                                                                                                           rela_Parado que ha trabajado antes 
                                                                                                                                  0.755599099 
                                                                                                rela_Persona con invalidez laboral permanente 
                                                                                                                                  1.119436453 
                                                                                                                                       nhijos 
                                                                                                                                 -0.055198650 
                                                                                                                                         nocu 
                                                                                                                                 -1.111598930 

Con sdf_predict hacemos predicción sobre el conjunto de test. Hay un pequeño bug cuando el modelo es ml_logistic, no se muestran bien las probabilidades, con ml_generalized si funciona bien, pero ml_generalized no permite parámetros de regularización.

pred <- sdf_predict(mlogis, partitions$test)

Para ver las probabilidade hay que llevarlas a R. Esto es un problema con grandes datos, hay abierta un issue en github, supongo que en próximas versiones lo arreglarán

prueba <- select(pred, probability) %>% head(10) %>% collect
prueba
prueba$probability[1]
[[1]]
<jobj[388]>
  class org.apache.spark.ml.linalg.DenseVector
  [0.9514041354980373,0.04859586450196277]

Evaluamos el modelo con ml_binary_classification_eval . Por defecto calcula el AUC

ml_binary_classification_eval(pred, "respuesta", "probability")
[1] 0.716193

Modelo de gradient boosting

Hay diferentes modelos accesibles, ver ayuda del paquete

mgbm <- partitions$training %>% ml_gradient_boosted_trees(mlformula, type="classification")
summary(mgbm)
                            Length Class      Mode       
features                    19     -none-     character  
response                     1     -none-     character  
max.bins                     1     -none-     numeric    
max.depth                    1     -none-     numeric    
trees                       20     -none-     list       
data                         2     spark_jobj environment
ml.options                   5     ml_options list       
categorical.transformations  2     -none-     environment
model.parameters             5     -none-     list       
.call                        4     -none-     call       
.model                       2     spark_jobj environment

Importancia de las variables

importancia <- ml_tree_feature_importance(sc,mgbm)
importancia$feature <- iconv(importancia$feature, to="ASCII//TRANSLIT")
importancia$importance <- as.numeric(as.character(importancia$importance))
importancia$featurebis <- stringr::str_trunc(importancia$feature,50, side="right")
ggplot(importancia, aes(x=reorder(featurebis, importance), y= importance)) + geom_bar(stat="identity") + coord_flip()

El modelo de gbm no devuelve las probabilidades sino la clase predicha, esto es un problema.

predgbm <- mgbm %>% sdf_predict(partitions$test)
predgbm %>% group_by(cpro) %>% summarise(mean(respuesta), mean(prediction))
Source:   query [?? x 3]
Database: spark connection master=local[4] app=sparklyr local=TRUE

                     cpro `avg(CAST(respuesta AS DOUBLE))` `avg(prediction)`
                    <chr>                            <dbl>             <dbl>
1                  Madrid                       0.06780753                 0
2                  Huelva                       0.05165165                 0
3               Barcelona                       0.07742917                 0
4                  Zamora                       0.03072734                 0
5                  Burgos                       0.04184884                 0
6               Cantabria                       0.05398254                 0
7             Ciudad Real                       0.02992080                 0
8                  Cuenca                       0.03280680                 0
9  Santa Cruz de Tenerife                       0.09380195                 0
10                  Ávila                       0.02878049                 0
# ... with more rows

Random forest

mrf <- partitions$training %>% ml_random_forest(mlformula, type="classification")
summary(mrf)
                            Length Class      Mode       
features                    19     -none-     character  
response                     1     -none-     character  
max.bins                     1     -none-     numeric    
max.depth                    1     -none-     numeric    
num.trees                    1     -none-     numeric    
feature.importances         19     -none-     numeric    
trees                       20     -none-     list       
data                         2     spark_jobj environment
ml.options                   5     ml_options list       
categorical.transformations  2     -none-     environment
model.parameters             5     -none-     list       
.call                        4     -none-     call       
.model                       2     spark_jobj environment
importancia <- ml_tree_feature_importance(sc,mrf)
importancia$feature <- iconv(importancia$feature, to="ASCII//TRANSLIT")
importancia$importance <- as.numeric(as.character(importancia$importance))
importancia$featurebis <- stringr::str_trunc(importancia$feature,50, side="right")
ggplot(importancia, aes(x=reorder(featurebis, importance), y= importance)) + geom_bar(stat="identity") + coord_flip()

predrf <- mrf %>% sdf_predict(partitions$test)

ml_random_forest si calcula las probabilidades, pero tenemos el mismo problema que antes, no lo muestra bien en R. Aunque se puede extraer.

predrf %>% select(respuesta, rawPrediction, probability, prediction)
Source:   query [?? x 4]
Database: spark connection master=local[4] app=sparklyr local=TRUE

   respuesta    rawPrediction      probability prediction
       <chr>           <list>           <list>      <dbl>
1        0.0 <S3: spark_jobj> <S3: spark_jobj>          0
2        0.0 <S3: spark_jobj> <S3: spark_jobj>          0
3        0.0 <S3: spark_jobj> <S3: spark_jobj>          0
4        0.0 <S3: spark_jobj> <S3: spark_jobj>          0
5        0.0 <S3: spark_jobj> <S3: spark_jobj>          0
6        0.0 <S3: spark_jobj> <S3: spark_jobj>          0
7        0.0 <S3: spark_jobj> <S3: spark_jobj>          0
8        0.0 <S3: spark_jobj> <S3: spark_jobj>          0
9        0.0 <S3: spark_jobj> <S3: spark_jobj>          0
10       0.0 <S3: spark_jobj> <S3: spark_jobj>          0
# ... with more rows

Guardo en R la columna de probabilidades

probs <- predrf %>% select(probability) %>% collect 

Veo como es y utilizo invoke para llamar al método “toArray” en scala.

head(probs)
invoke(probs$probability[[1]], "toArray")[2]
[1] 0.0668815
# Es ineficiente extraerlas usando sapply por ejemplo

La función ml_binary_classification_eval nos da el AUC

ml_binary_classification_eval(predrf, "respuesta", "probability")
[1] 0.7247844

Casi todos los modelos de MLlib están implementados en sparklyr, (naiveBayes, pca, kmeans, lda,survival regression, glm, decission trees, randomforest, gradient boosting machine) y si no hay alguno se puede implementar mediante extensiones en sparklyr

En mi opinión sparklyr es un gran paquete que quiere ser el pyspark para R. Lo mejor es la posibilidad de utilizar dplyr con Spark DataFrames. Se puede utilizar sparklyr junto con SparkR creando dos conexiones distintas. Aún le falta bastante para que sea cómodo trabajar con los algoritmos de MLlib. La mejor opción para realizar análisis utilizando Spark es H2O, afortunadamente, la gente de Rstudio ha sacado el paquete rsparkling que habilita una conexión entre sparklyr y H2o sparkling water.

LS0tCnRpdGxlOiAiRWplbXBsbyBhbsOhbGlzaXMgTUxsaWIiCm91dHB1dDoKICBodG1sX25vdGVib29rOiBkZWZhdWx0CiAgaHRtbF9kb2N1bWVudDogZGVmYXVsdAotLS0KCiMjIENvbmV4acOzbiBkZSBzcGFya2x5ciBlbiBsb2NhbC4KCkNhcmdhbW9zIHNwYXJrbHlyIHkgZHBseXIKCmBgYHtyIHNldHVwLCBpbmNsdWRlID0gVFJVRX0KbGlicmFyeShzcGFya2x5cikKbGlicmFyeShkcGx5cikKbGlicmFyeShnZ3Bsb3QyKQoKYGBgCgoqKkluaWNpYWxpemFtb3MgbGEgY29uZXhpw7NuKioKCmBgYHtyfQpzYyA8LSBzcGFya19jb25uZWN0KG1hc3RlciA9ICJsb2NhbCIsIHZlcnNpb24gPSAiMi4wLjAiKQpgYGAKCgojIyBMZWN0dXJhIGRlIGRhdG9zLiAKClNwYXJrbHlyIHB1ZWRlIGxlZXIgZGF0b3MgZGUgaGl2ZSAgdXRpbGl6YW5kbyBgaGl2ZV9jb250ZXh0KHNjKWAgbyBkZSBmaWNoZXJvcyBlbiBsb2NhbCBvIGhkZnMuIApWYW1vcyBhIGxlZXIgdW4gZmljaGVybyBlbiBmb3JtYXRvIGBwYXJxdWV0YCAgcXVlIGVzdMOhIGVuIGxvY2FsLiBTZSB0cmF0YSBkZSBsb3MgZGF0b3MgZGVsIGNlbnNvIGRlIDIwMTEgcGFyYSBBbmRhbHVjw61hLCBxdWUgaGFiw61hIGJhamFkbyBwcmV2aWFtZW50ZSB1dGlsaXphbmRvIGVsIHBhcXVldGUgYE1pY3JvRGF0b3NFc2AgZGUgQ2FybG9zIEdpbAoKVXRpbGl6YW1vcyBsYSBmdW5jacOzbiBgc3BhcmtfcmVhZF9wYXJxdWV0YCBxdWUgbGVlIGxvcyBkYXRvcyB5IGNyZWEgdW4gRGF0YUZyYW1lIGRlIHNwYXJrLiBBbCBhc2lnbmFybG8gYSB1biBvYmpldG8gc2UgY3JlYSB1biBgdGJsX3NwYXJrYCBxdWUgcGVybWl0ZSB1dGlsaXphciBmdW5jaW9uZXMgZGUgYGRwbHlyYCBzb2JyZSB1biBkYXRhZnJhbWUgZGUgU3BhcmsuIAoKYGBge3IsY2FjaGU9VFJVRX0KY2Vuc28xX3RibCA8LSBzcGFya19yZWFkX3BhcnF1ZXQoc2MsICJjZW5zbzEiLCBwYXRoID0gIi9ob21lL2pvc2Uvc3Bhcmstd2FyZWhvdXNlL2NlbnNvMi8iKQpjbGFzcyhjZW5zbzFfdGJsKQoKCmBgYAoKY2Vuc28xX3RibCBubyBlcyB1biBkYXRhZnJhbWUgZGUgUiBzaW5vIHVuYSBjb25leGnDs24gYWwgZGF0YWZyYW1lIGRlIHNwYXJrCgpgYGB7ciwgZWNobz1UUlVFfQpjZW5zbzFfdGJsCmBgYAoKIyMgTW9kZWxvIE1MbGliCgoKQ3JlYW1vcyBkYXRhc2V0IHBhcmEgZWwgbW9kZWxvLiBFcyBpbXBvcnRhbnRlIHF1ZSBlbCBkYXRhc2V0IG5vIHRlbmdhIHBlcmRpZG9zLCBwb3JxdWUgcHVlZGUgZGFyIGVycm9yLiBTZSBzdXBvbmUgcXVlIGVuIGxhIHZlcnNpw7NuIGRlIGRlc2Fycm9sbG8gZGUgc3BhcmtseXIgZXN0w6Egc29sdWNpb25hZG8sIHBlcm8gYSBtaSBubyBtZSBoYSBmdW5jaW9uYWRvCgpgYGB7cn0KbW9kX2RhdGFzZXQgPC0gY2Vuc28xX3RibCAlPiUKICAgZmlsdGVyKGVkYWQgPiAyMCwgZWRhZCA8IDcwLCAhaXMubmEoZWRhZCksIWlzLm5hKHJlbGEpLCAhaXMubmEoZXNyZWFsKSwgIWlzLm5hKG5oaWpvcyksICFpcy5uYShub2N1KSkgJT4lCiAgIG11dGF0ZShyZXNwdWVzdGE9IGFzLmNoYXJhY3RlcihpZmVsc2UoIGVjaXZpbCA9PSAiRGl2b3JjaWFkbyIsIDEsMCkpKSAlPiUKICAgc2RmX3JlZ2lzdGVyKCJtb2RfZGF0YXNldCIpCgpzdW1tYXJpc2UobW9kX2RhdGFzZXQsIG4oKSkKYGBgCgpDb24gYHNkZl9wYXJ0aW9uYCBjcmVhbW9zIGRhdG9zIHBhcmEgdHJhaW5pbmcgeSBwYXJhIHRlc3QKCmBgYHtyfQpwYXJ0aXRpb25zIDwtIG1vZF9kYXRhc2V0ICU+JSBzZGZfcGFydGl0aW9uKHRyYWluaW5nID0gMC43LCB0ZXN0ID0gMC4zLCBzZWVkID0gNDIpCnBhcnRpdGlvbnMkdHJhaW5pbmcgJT4lIGdyb3VwX2J5KGNwcm8pICU+JSBzdW1tYXJpc2UobigpLHByb3BfZGl2b3JjID0gbWVhbihyZXNwdWVzdGEpKQpwYXJ0aXRpb25zJHRlc3QgJT4lIGdyb3VwX2J5KGNwcm8pICU+JSBzdW1tYXJpc2UobigpLHByb3BfZGl2b3JjID0gbWVhbihyZXNwdWVzdGEpKQpgYGAKCkF1bnF1ZSBpbmNvcnBvcmEgaW50ZXJmYXogZsOzcm11bGEsIGRhIHByb2JsZW1hcyBjb24gdmFyaWFibGVzIGNhdGVnw7NyaWNhcyBzZWfDum4gbGEgdmVyc2nDs24gZGUgc3BhcmsgeSBzcGFya2x5ciAgaW5zdGFsYWRhLiBMbyBzaWd1aWVudGUgbm8gZnVuY2lvbmEgY29uIGxhIHZlcnNpw7NuIGRlIGRlc2Fycm9sbG8gZGUgc3BhcmtseXIgbmkgY29uIGFsZ3VuYXMgdmVyc2lvbmVzIGRlIHNwYXJrCgpgYGB7cn0KbWxmb3JtdWxhIDwtIGZvcm11bGEocmVzcHVlc3RhIH4gZWRhZCArIGVzcmVhbCsgcmVsYSsgbmhpam9zICsgbm9jdSkKbWxvZ2lzIDwtIHBhcnRpdGlvbnMkdHJhaW5pbmcgJT4lIG1sX2xvZ2lzdGljX3JlZ3Jlc3Npb24obWxmb3JtdWxhKQpzdW1tYXJ5KG1sb2dpcykKCmBgYAoKQ29uIGBzZGZfcHJlZGljdGAgaGFjZW1vcyBwcmVkaWNjacOzbiBzb2JyZSBlbCBjb25qdW50byBkZSB0ZXN0LiBIYXkgdW4gcGVxdWXDsW8gYnVnIGN1YW5kbyBlbCBtb2RlbG8gZXMgbWxfbG9naXN0aWMsIG5vIHNlIG11ZXN0cmFuIGJpZW4gbGFzIHByb2JhYmlsaWRhZGVzLCBjb24gbWxfZ2VuZXJhbGl6ZWQgc2kgZnVuY2lvbmEgYmllbiwgcGVybyBtbF9nZW5lcmFsaXplZCBubyBwZXJtaXRlIHBhcsOhbWV0cm9zIGRlIHJlZ3VsYXJpemFjacOzbi4KCgpgYGB7cn0KcHJlZCA8LSBzZGZfcHJlZGljdChtbG9naXMsIHBhcnRpdGlvbnMkdGVzdCkKaGVhZChwcmVkKQpwcmVkICU+JSBzZWxlY3QocHJlZGljdGlvbiwgcHJvYmFiaWxpdHkgICkKYGBgCgpQYXJhIHZlciBsYXMgcHJvYmFiaWxpZGFkZSBoYXkgcXVlIGxsZXZhcmxhcyBhIFIuIEVzdG8gZXMgdW4gcHJvYmxlbWEgY29uIGdyYW5kZXMgZGF0b3MsIGhheSBhYmllcnRhIHVuIGlzc3VlIGVuIGdpdGh1Yiwgc3Vwb25nbyBxdWUgZW4gcHLDs3hpbWFzIHZlcnNpb25lcyBsbyBhcnJlZ2xhcsOhbgoKYGBge3J9CnBydWViYSA8LSBzZWxlY3QocHJlZCwgcHJvYmFiaWxpdHkpICU+JSBoZWFkKDEwKSAlPiUgY29sbGVjdApwcnVlYmEKcHJ1ZWJhJHByb2JhYmlsaXR5WzFdCmBgYAoKRXZhbHVhbW9zIGVsIG1vZGVsbyBjb24gYG1sX2JpbmFyeV9jbGFzc2lmaWNhdGlvbl9ldmFsYCAuIFBvciBkZWZlY3RvIGNhbGN1bGEgZWwgQVVDCgpgYGB7cn0KbWxfYmluYXJ5X2NsYXNzaWZpY2F0aW9uX2V2YWwocHJlZCwgInJlc3B1ZXN0YSIsICJwcm9iYWJpbGl0eSIpCmBgYAoKCgoKIyMjIE1vZGVsbyBkZSBncmFkaWVudCBib29zdGluZyAKCkhheSBkaWZlcmVudGVzIG1vZGVsb3MgYWNjZXNpYmxlcywgdmVyIGF5dWRhIGRlbCBwYXF1ZXRlCgpgYGB7cn0KbWdibSA8LSBwYXJ0aXRpb25zJHRyYWluaW5nICU+JSBtbF9ncmFkaWVudF9ib29zdGVkX3RyZWVzKG1sZm9ybXVsYSwgdHlwZT0iY2xhc3NpZmljYXRpb24iKQpzdW1tYXJ5KG1nYm0pCgoKYGBgCgoqKkltcG9ydGFuY2lhIGRlIGxhcyB2YXJpYWJsZXMqKgoKYGBge3J9CmltcG9ydGFuY2lhIDwtIG1sX3RyZWVfZmVhdHVyZV9pbXBvcnRhbmNlKHNjLG1nYm0pCmltcG9ydGFuY2lhJGZlYXR1cmUgPC0gaWNvbnYoaW1wb3J0YW5jaWEkZmVhdHVyZSwgdG89IkFTQ0lJLy9UUkFOU0xJVCIpCmltcG9ydGFuY2lhJGltcG9ydGFuY2UgPC0gYXMubnVtZXJpYyhhcy5jaGFyYWN0ZXIoaW1wb3J0YW5jaWEkaW1wb3J0YW5jZSkpCmltcG9ydGFuY2lhJGZlYXR1cmViaXMgPC0gc3RyaW5ncjo6c3RyX3RydW5jKGltcG9ydGFuY2lhJGZlYXR1cmUsNTAsIHNpZGU9InJpZ2h0IikKZ2dwbG90KGltcG9ydGFuY2lhLCBhZXMoeD1yZW9yZGVyKGZlYXR1cmViaXMsIGltcG9ydGFuY2UpLCB5PSBpbXBvcnRhbmNlKSkgKyBnZW9tX2JhcihzdGF0PSJpZGVudGl0eSIpICsgY29vcmRfZmxpcCgpCgpgYGAKCkVsIG1vZGVsbyBkZSBnYm0gbm8gZGV2dWVsdmUgbGFzIHByb2JhYmlsaWRhZGVzIHNpbm8gbGEgY2xhc2UgcHJlZGljaGEsIGVzdG8gZXMgdW4gcHJvYmxlbWEuIAoKYGBge3J9CnByZWRnYm0gPC0gbWdibSAlPiUgc2RmX3ByZWRpY3QocGFydGl0aW9ucyR0ZXN0KQpwcmVkZ2JtICU+JSBncm91cF9ieShjcHJvKSAlPiUgc3VtbWFyaXNlKG1lYW4ocmVzcHVlc3RhKSwgbWVhbihwcmVkaWN0aW9uKSkKCmBgYAoKIyMjIFJhbmRvbSBmb3Jlc3QKCmBgYHtyfQptcmYgPC0gcGFydGl0aW9ucyR0cmFpbmluZyAlPiUgbWxfcmFuZG9tX2ZvcmVzdChtbGZvcm11bGEsIHR5cGU9ImNsYXNzaWZpY2F0aW9uIikKc3VtbWFyeShtcmYpCmBgYAoKYGBge3J9CmltcG9ydGFuY2lhIDwtIG1sX3RyZWVfZmVhdHVyZV9pbXBvcnRhbmNlKHNjLG1yZikKaW1wb3J0YW5jaWEkZmVhdHVyZSA8LSBpY29udihpbXBvcnRhbmNpYSRmZWF0dXJlLCB0bz0iQVNDSUkvL1RSQU5TTElUIikKaW1wb3J0YW5jaWEkaW1wb3J0YW5jZSA8LSBhcy5udW1lcmljKGFzLmNoYXJhY3RlcihpbXBvcnRhbmNpYSRpbXBvcnRhbmNlKSkKaW1wb3J0YW5jaWEkZmVhdHVyZWJpcyA8LSBzdHJpbmdyOjpzdHJfdHJ1bmMoaW1wb3J0YW5jaWEkZmVhdHVyZSw1MCwgc2lkZT0icmlnaHQiKQpnZ3Bsb3QoaW1wb3J0YW5jaWEsIGFlcyh4PXJlb3JkZXIoZmVhdHVyZWJpcywgaW1wb3J0YW5jZSksIHk9IGltcG9ydGFuY2UpKSArIGdlb21fYmFyKHN0YXQ9ImlkZW50aXR5IikgKyBjb29yZF9mbGlwKCkKYGBgCgoKYGBge3J9CnByZWRyZiA8LSBtcmYgJT4lIHNkZl9wcmVkaWN0KHBhcnRpdGlvbnMkdGVzdCkKCmBgYAoKYG1sX3JhbmRvbV9mb3Jlc3RgIHNpIGNhbGN1bGEgbGFzIHByb2JhYmlsaWRhZGVzLCBwZXJvIHRlbmVtb3MgZWwgbWlzbW8gcHJvYmxlbWEgcXVlIGFudGVzLCBubyBsbyBtdWVzdHJhIGJpZW4gZW4gUi4gQXVucXVlIHNlIHB1ZWRlIGV4dHJhZXIuIAoKYGBge3J9CnByZWRyZiAlPiUgc2VsZWN0KHJlc3B1ZXN0YSwgcmF3UHJlZGljdGlvbiwgcHJvYmFiaWxpdHksIHByZWRpY3Rpb24pCmBgYAoKR3VhcmRvIGVuIFIgbGEgY29sdW1uYSBkZSBwcm9iYWJpbGlkYWRlcwpgYGB7cn0KcHJvYnMgPC0gcHJlZHJmICU+JSBzZWxlY3QocHJvYmFiaWxpdHkpICU+JSBjb2xsZWN0IApgYGAKClZlbyBjb21vIGVzIHkgdXRpbGl6byBpbnZva2UgcGFyYSBsbGFtYXIgYWwgbcOpdG9kbyAidG9BcnJheSIgZW4gc2NhbGEuIApgYGB7cn0KaGVhZChwcm9icykKaW52b2tlKHByb2JzJHByb2JhYmlsaXR5W1sxXV0sICJ0b0FycmF5IilbMl0KCiMgRXMgaW5lZmljaWVudGUgZXh0cmFlcmxhcyB1c2FuZG8gc2FwcGx5IHBvciBlamVtcGxvCgoKCmBgYAoKCkxhIGZ1bmNpw7NuIGBtbF9iaW5hcnlfY2xhc3NpZmljYXRpb25fZXZhbGAgbm9zIGRhIGVsIEFVQwpgYGB7cn0KbWxfYmluYXJ5X2NsYXNzaWZpY2F0aW9uX2V2YWwocHJlZHJmLCAicmVzcHVlc3RhIiwgInByb2JhYmlsaXR5IikKYGBgCgpDYXNpIHRvZG9zIGxvcyBtb2RlbG9zIGRlIE1MbGliIGVzdMOhbiBpbXBsZW1lbnRhZG9zIGVuIHNwYXJrbHlyLCAobmFpdmVCYXllcywgcGNhLCBrbWVhbnMsIGxkYSxzdXJ2aXZhbCByZWdyZXNzaW9uLCBnbG0sIGRlY2lzc2lvbiB0cmVlcywgcmFuZG9tZm9yZXN0LCBncmFkaWVudCBib29zdGluZyBtYWNoaW5lKSB5IHNpIG5vIGhheSBhbGd1bm8gc2UgcHVlZGUgaW1wbGVtZW50YXIgbWVkaWFudGUgZXh0ZW5zaW9uZXMgZW4gc3BhcmtseXIKCkVuIG1pIG9waW5pw7NuIHNwYXJrbHlyIGVzIHVuIGdyYW4gcGFxdWV0ZSBxdWUgcXVpZXJlIHNlciBlbCBweXNwYXJrIHBhcmEgUi4gTG8gbWVqb3IgZXMgbGEgcG9zaWJpbGlkYWQgZGUgdXRpbGl6YXIgZHBseXIgY29uIFNwYXJrIERhdGFGcmFtZXMuIFNlIHB1ZWRlIHV0aWxpemFyIHNwYXJrbHlyIGp1bnRvIGNvbiAgU3BhcmtSIGNyZWFuZG8gZG9zIGNvbmV4aW9uZXMgZGlzdGludGFzLiBBw7puIGxlIGZhbHRhIGJhc3RhbnRlIHBhcmEgcXVlIHNlYSBjw7Ntb2RvIHRyYWJhamFyIGNvbiBsb3MgYWxnb3JpdG1vcyBkZSBNTGxpYi4gIExhIG1lam9yIG9wY2nDs24gcGFyYSByZWFsaXphciBhbsOhbGlzaXMgdXRpbGl6YW5kbyBTcGFyayBlcyBIMk8sIGFmb3J0dW5hZGFtZW50ZSwgbGEgZ2VudGUgZGUgUnN0dWRpbyBoYSBzYWNhZG8gZWwgcGFxdWV0ZSAgYHJzcGFya2xpbmdgIHF1ZSBoYWJpbGl0YSB1bmEgY29uZXhpw7NuIGVudHJlIHNwYXJrbHlyIHkgSDJvIHNwYXJrbGluZyB3YXRlci4KCgoKCgo=