library(dplyr)
library(tidymodels)
library(GGally)
library(purrr)
library(kableExtra)
library(modelr)
library(pROC)
library(caret)

Preparación de datos

Se debe armar un modelo de regresión logística para clasificar si una persona que viajaba en el Titanic sobrevivió o no.

Leer archivo

Se leen los datos y se muestra su estructura.

#leer archivo y mostrar estructura
titanic_train <- read.csv(file = "titanic_complete_train.csv")
glimpse(titanic_train)
Observations: 891
Variables: 12
$ PassengerId <int> 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25...
$ Survived    <int> 0, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 1, 0, 1, 0, 1, 0, 1, 1, 1, 0, 1, 0, 0, 1, 0,...
$ Pclass      <int> 3, 1, 3, 1, 3, 3, 1, 3, 3, 2, 3, 1, 3, 3, 3, 2, 3, 2, 3, 3, 2, 2, 3, 1, 3, 3, 3, 1, 3, 3,...
$ Name        <fct> "Braund, Mr. Owen Harris", "Cumings, Mrs. John Bradley (Florence Briggs Thayer)", "Heikki...
$ Sex         <fct> male, female, female, female, male, male, male, male, female, female, female, female, mal...
$ Age         <dbl> 22.00000, 38.00000, 26.00000, 35.00000, 35.00000, 26.50759, 54.00000, 2.00000, 27.00000, ...
$ SibSp       <int> 1, 1, 0, 1, 0, 0, 0, 3, 0, 1, 1, 0, 0, 1, 0, 0, 4, 0, 1, 0, 0, 0, 0, 0, 3, 1, 0, 3, 0, 0,...
$ Parch       <int> 0, 0, 0, 0, 0, 0, 0, 1, 2, 0, 1, 0, 0, 5, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 5, 0, 2, 0, 0,...
$ Ticket      <fct> A/5 21171, PC 17599, STON/O2. 3101282, 113803, 373450, 330877, 17463, 349909, 347742, 237...
$ Fare        <dbl> 7.2500, 71.2833, 7.9250, 53.1000, 8.0500, 8.4583, 51.8625, 21.0750, 11.1333, 30.0708, 16....
$ Cabin       <fct> NA, C85, NA, C123, NA, NA, E46, NA, NA, NA, G6, C103, NA, NA, NA, NA, NA, NA, NA, NA, NA,...
$ Embarked    <fct> S, C, S, S, S, Q, S, S, S, C, S, S, S, S, S, S, Q, S, S, C, S, S, Q, S, S, S, C, S, Q, S,...

Seleccionar Variables

Se seleccionan las variables solicitadas y se transforman a factor las features de Survived, Pclass y Embarked.

titanic_train <-
  titanic_train %>% select(
    'PassengerId',
    'Survived',
    'Pclass',
    'Sex',
    'Age',
    'SibSp',
    'Parch',
    'Fare' ,
    'Embarked'
  )
#transformar las variables a factor
titanic_train[, c('Survived', 'Pclass', 'Embarked')] <-
  map(titanic_train[, c('Survived', 'Pclass', 'Embarked')], as.factor)

Gráfico de GGPairs

Se realiza gráfico de ggpairs y se observa que los que viajaban en 3rd clase sobrevivieron en menor proporción que los de 2da y 1era clase. Sobrevivieron más las mujeres que los hombres. No se observan diferencias significativas por Edad y precio del boleto.

#Grafico GGpairs
titanic_train %>% select(Survived, Pclass, Sex, Age, Fare) %>% ggpairs(., mapping = aes(colour = Survived)) + theme(axis.text.x = element_text(angle = 90, hjust = 1)) + theme_bw()

Distribución de la clase

Se puede ver que se tiene una distribucion desbalancea con 62% de negativos y 38% de positvos.

titanic_train %>% group_by(Survived) %>% summarise(numero_casos=n()/nrow(titanic_train))

ggplot(data=titanic_train, aes(titanic_train$Survived)) + geom_bar() + theme_bw() +
  labs(title="Titanic Train", x="Survived")

#Distribucion de la clase de titanic train.
titanic_train %>% group_by(Survived) %>% summarise(numero_casos=n()/nrow(titanic_train)*100)

Dividir el conjunto de Train

Se observa que luego deñ resample_partition, los datos tienen la misma distribución de clase que la orginal.

#Separo en train y validacion
train_test <- titanic_train %>% resample_partition(c(train=0.7,test=0.3))

titanic_train2 <- train_test$train %>% as_tibble()
titanic_validation <- train_test$test %>% as_tibble()
ggplot(data=titanic_train2, aes(titanic_train2$Survived)) + geom_bar() + theme_bw() +
  labs(title="Titanic Train 70%", x="Survived")

ggplot(data=titanic_validation, aes(titanic_validation$Survived)) + geom_bar() + theme_bw() +
  labs(title="Titanic Validation 30%", x="Survived")

#Distribucion de la clase de titanic train2.
titanic_train2 %>% group_by(Survived) %>% summarise(numero_casos=n()/nrow(titanic_train2)*100)
#Distribucion de la clase de titanic validation
titanic_validation %>% group_by(Survived) %>% summarise(numero_casos=n()/nrow(titanic_validation)*100)
NA

Predicciones

Modelo Pclass+Sex+Age

Se generan varios modelos incluyendo el que se solicita en este punto para mayor practicidad.

logit_formulas <- formulas(.response = ~Survived, 
                         pc_sex_age=~Pclass+Sex+Age, 
                         pc_fare_emb=~Pclass+Fare+ Embarked , 
                         sex_fare= ~ Sex + Fare,
                         pclass= ~Pclass
                         )
models <- data_frame(logit_formulas) %>% # dataframe a partir del objeto formulas
  mutate(models = names(logit_formulas), # columna con los nombres de las formulas
         expression = paste(logit_formulas), # columna con las expresiones de las formulas
         mod = map(logit_formulas, ~glm(.,family = 'binomial', data = titanic_train2)))
`data_frame()` is deprecated, use `tibble()`.
This warning is displayed once per session.

Interpretar coeficientes

Todos los coeficientes resultaron significativos en este modelo. Las variables tienen coeficientes negativos lo cual indica que disminuye la probabilidad esperada de sobrevivir a medida que éstas aumentan.

models %>% 
  filter(models == 'pc_sex_age') %>%
  mutate(tidy = map(mod,tidy)) %>%  
  unnest(tidy, .drop = TRUE) %>% 
  select(term, estimate, std.error, statistic, p.value)
The `.drop` argument of `unnest()` is deprecated as of tidyr 1.0.0.
All list-columns are now preserved.
This warning is displayed once per session.
Call `lifecycle::last_warnings()` to see where this warning was generated.

Quién tiene mayor probabilidad de Supervivencia

Se observa que Jack tenia muy bajas chances de sobrevivir aunque el trozo de madera que utlizo Rose podria haber salvado a ambos.

newdata <- tibble(Age = c(17,20), Pclass = c('1','3'), Sex = c('female','male'))

models %>% 
  filter(models == 'pc_sex_age') %>%
  mutate(pred= map(mod,augment, newdata= newdata,type.predict = "response")) %>%  
  unnest(pred, .drop = TRUE) %>%
  select(Age, Pclass, Sex, .fitted)
NA

Generación de Modelos

Generar 3 modelos

Se generaron tres modelos:

  • Pclass+Fare+ Embarked
  • Sex + Fare
  • Pclass

Se observa que el modelo ~Pclass+Fare+ Embarked, EmbarkedQ y Pclass2 no son significativos.

models %>% 
  filter(models != 'pc_sex_age') %>%
  mutate(tidy = map(mod,tidy)) %>%  
  unnest(tidy, .drop = TRUE) %>% 
  select(term, estimate, std.error, statistic, p.value)

Ordernar por deviance

Elijo el modelo que maximice el porcentaje de la desviance explicada. En este caso, el mejor modelo es ~ Pclass + Sex + Age.

models <- models %>% 
  mutate(glance = map(mod,glance))

models %>% 
  unnest(glance, .drop = TRUE) %>%
  mutate(perc_explained_dev = 1-deviance/null.deviance) %>% 
  select(expression,null.deviance, deviance, perc_explained_dev) %>% 
  arrange(deviance)

Evaluacion del Modelo

Curva ROC y AUC

Se obtuvo un auc de 0.8488 que es mayor al azar 0.5; aunque este valor se obtiene sobre el conjunto de entrenamiento. La curva ROC visualiza la sensibilidad de la predicción versus la especificidad de la misma. La sensiblidad son las predicciones positivas sobre la cantidad total de positivos y la especificidad son las predicciones negativas sobre la cantidad total de negativos.

models <- models %>% 
  mutate(pred= map(mod,augment, type.predict = "response"))


# Calculamos curvas ROC
prediction_pc_sex_age <- models %>% 
  filter(models=="pc_sex_age") %>% 
  unnest(pred, .drop=TRUE)

roc_pc_sex_age <- roc(response=prediction_pc_sex_age$Survived, predictor=prediction_pc_sex_age$.fitted)
Setting levels: control = 0, case = 1
Setting direction: controls < cases
roc_pc_sex_age 

Call:
roc.default(response = prediction_pc_sex_age$Survived, predictor = prediction_pc_sex_age$.fitted)

Data: prediction_pc_sex_age$.fitted in 389 controls (prediction_pc_sex_age$Survived 0) < 234 cases (prediction_pc_sex_age$Survived 1).
Area under the curve: 0.8485
ggroc(roc_pc_sex_age, size=1) + geom_abline(slope = 1, intercept = 1, linetype='dashed') + theme_bw() + labs(title='Curvas ROC', color='Modelo')

Violin Plot

Se puede observar a través del gráfico que las probabilidades de las observaciones de la clase no sobreviviente tiene un concentracion entre 0.25 y 0. Mientras que las probabilidades de las observaciones de los sobrevivientes tienen mayor concentracion en 1. Definir un punto de corte que prediga bien ambas clase de forma tal de cometer el mejor error posible, pareciera no dislumbrase clarametne en el grafico.

ggplot(prediction_pc_sex_age, aes(x=Survived, y=.fitted, group=Survived,fill=factor(Survived))) + 
  geom_violin() +
  theme_bw() +
  guides(fill=FALSE) +
  labs(title='Violin plot', subtitle='Modelo completo', y='Predicted probability')

Eleccion Punto de Corte

Gráfico en funcion del punto de corte

pred_val<-models %>% 
  filter(models == 'pc_sex_age') %>%
  mutate(pred= map(mod,augment, newdata= titanic_validation,type.predict = "response")) %>% 
  unnest(pred, .drop=TRUE)

prediction_metrics <- function(cutoff=0.95, predictions=pred_val){
  table <- predictions %>% 
    mutate(predicted_class=if_else(.fitted>cutoff, 1, 0) %>% as.factor(),
           Survived= factor(Survived))
  
  confusionMatrix(table$predicted_class, table$Survived, positive = "1") %>%
    tidy() %>%
    select(term, estimate) %>%
    filter(term %in% c('accuracy', 'sensitivity', 'specificity', 'precision','recall')) %>%
    mutate(cutoff=cutoff)
  
}
cutoffs = seq(0.01,0.95,0.01)

logit_pred= map_dfr(cutoffs, prediction_metrics)%>% mutate(term=as.factor(term))
Levels are not in the same order for reference and data. Refactoring data to match.Levels are not in the same order for reference and data. Refactoring data to match.Levels are not in the same order for reference and data. Refactoring data to match.
ggplot(logit_pred, aes(cutoff,estimate, group=term, color=term)) + geom_line(size=1) +
  theme_bw() +
  labs(title= 'Accuracy, Sensitivity, Specificity, Recall y Precision', subtitle= 'Modelo Pclass+Sex+Age', color="") +
  scale_x_continuous(breaks =seq(0.05,0.95,0.1))

Elegir el Punto de Corte

La elección se basa en la métrica que queremos maximizar. En este caso buscamos la intersección de sensivivity y specifity de forma tal de obtener un balance entre los positivos y negativos. Elegimos como punto de corte 0.4.

Matriz de Confusión

La tabla nos muestra que se clasifico correctamente 117 personas como no sobrevivientes y 84 personas como sobrevivientes. 24 personas las clasifico como no sobrevientes cuando enrealidad si sobrevivieron y 43 personas como sobrevivientes cuando no sobrevivieron.

table <- pred_val %>%
  mutate(
    predicted_class = if_else(.fitted > 0.4, 1, 0) %>% as.factor(),
    Survived = factor(Survived)
  )

confusionMatrix(table$predicted_class, table$Survived, positive = "1")
Confusion Matrix and Statistics

          Reference
Prediction   0   1
         0 117  24
         1  43  84
                                          
               Accuracy : 0.75            
                 95% CI : (0.6937, 0.8007)
    No Information Rate : 0.597           
    P-Value [Acc > NIR] : 1.033e-07       
                                          
                  Kappa : 0.4949          
                                          
 Mcnemar's Test P-Value : 0.02787         
                                          
            Sensitivity : 0.7778          
            Specificity : 0.7312          
         Pos Pred Value : 0.6614          
         Neg Pred Value : 0.8298          
             Prevalence : 0.4030          
         Detection Rate : 0.3134          
   Detection Prevalence : 0.4739          
      Balanced Accuracy : 0.7545          
                                          
       'Positive' Class : 1               
                                          
  

Testeo

Leeer archivo de Testeo

titanic_test <- read.csv(file = "titanic_complete_test.csv")
titanic_test[, c('Survived', 'Pclass', 'Embarked')] <-
  map(titanic_test[, c('Survived', 'Pclass', 'Embarked')], as.factor)

Clasificar a las personas de Testing

pred_test<-models %>% 
  filter(models == 'pc_sex_age') %>%
  mutate(pred= map(mod,augment, newdata= titanic_test,type.predict = "response")) %>% 
  unnest(pred, .drop=TRUE)

table <- pred_test %>%
  mutate(
    predicted_class = if_else(.fitted > 0.4, 1, 0) %>% as.factor(),
    Survived = factor(Survived)
  )

table %>%
  select(PassengerId, Pclass, Sex, Age, prob = .fitted, predicted_class) %>%
  arrange(-prob)

Matriz de Confusión

En el conjunto de test el modelo clasificó correctamente 194 personas como no sobrevivientes y 115 personas como sobrevivientes. 42 personas las clasifico como no sobrevientes cuando enrealidad si sobrevivieron y 67 personas como sobrevivientes cuando no sobrevivieron. En comparacion con las métricas obtenidas sobre el conjunto de validacion, la sensibilidad, especificidad y el acurrancy son similares.

  • Test: Acurrancy: 0.7392 , Sensitivity: 0.7325 y Specificity: 0.7433
  • Validacion: Acurrancy: 0.75, Sensitivity: 0.7778 y Specificity: 0.7312
confusionMatrix(table$predicted_class, table$Survived, positive = "1")
Confusion Matrix and Statistics

          Reference
Prediction   0   1
         0 194  42
         1  67 115
                                          
               Accuracy : 0.7392          
                 95% CI : (0.6943, 0.7807)
    No Information Rate : 0.6244          
    P-Value [Acc > NIR] : 4.341e-07       
                                          
                  Kappa : 0.4611          
                                          
 Mcnemar's Test P-Value : 0.02152         
                                          
            Sensitivity : 0.7325          
            Specificity : 0.7433          
         Pos Pred Value : 0.6319          
         Neg Pred Value : 0.8220          
             Prevalence : 0.3756          
         Detection Rate : 0.2751          
   Detection Prevalence : 0.4354          
      Balanced Accuracy : 0.7379          
                                          
       'Positive' Class : 1               
                                          
LS0tDQp0aXRsZTogIlRQMDMgLSBFc3RlZmFuaWEgRGUgTWFyemlvIg0KYXV0aG9yOiAiQ29taXNpw7NuIERpZWdvIEtvemxvd3NraSINCm91dHB1dDogDQogIGh0bWxfbm90ZWJvb2s6IA0KICAgIHRvYzogdHJ1ZQ0KICAgIHRvY19mbG9hdDogdHJ1ZQ0KICAgIGRlcHRoOiAyDQotLS0NCg0KYGBge3IgbGlicmVyaWFzLCBtZXNzYWdlPUZBTFNFfQ0KbGlicmFyeShkcGx5cikNCmxpYnJhcnkodGlkeW1vZGVscykNCmxpYnJhcnkoR0dhbGx5KQ0KbGlicmFyeShwdXJycikNCmxpYnJhcnkoa2FibGVFeHRyYSkNCmxpYnJhcnkobW9kZWxyKQ0KbGlicmFyeShwUk9DKQ0KbGlicmFyeShjYXJldCkNCmBgYA0KIyBQcmVwYXJhY2nDs24gZGUgZGF0b3MNClNlIGRlYmUgYXJtYXIgdW4gbW9kZWxvIGRlIHJlZ3Jlc2nDs24gbG9nw61zdGljYSBwYXJhIGNsYXNpZmljYXIgc2kgdW5hIHBlcnNvbmEgcXVlIHZpYWphYmEgZW4gZWwgVGl0YW5pYyBzb2JyZXZpdmnDsyBvIG5vLiANCg0KIyMgTGVlciBhcmNoaXZvDQpTZSBsZWVuIGxvcyBkYXRvcyB5IHNlIG11ZXN0cmEgc3UgZXN0cnVjdHVyYS4NCmBgYHtyIHByZXBhcmFjaW9uRGF0b3MxfQ0KI2xlZXIgYXJjaGl2byB5IG1vc3RyYXIgZXN0cnVjdHVyYQ0KdGl0YW5pY190cmFpbiA8LSByZWFkLmNzdihmaWxlID0gInRpdGFuaWNfY29tcGxldGVfdHJhaW4uY3N2IikNCmdsaW1wc2UodGl0YW5pY190cmFpbikNCmBgYA0KIyMgU2VsZWNjaW9uYXIgVmFyaWFibGVzDQpTZSBzZWxlY2Npb25hbiBsYXMgdmFyaWFibGVzIHNvbGljaXRhZGFzIHkgc2UgdHJhbnNmb3JtYW4gYSBmYWN0b3IgbGFzIGZlYXR1cmVzIGRlIFN1cnZpdmVkLCBQY2xhc3MgeSBFbWJhcmtlZC4NCmBgYHtyIHByZXBhcmFjaW9uRGF0b3MyfQ0KdGl0YW5pY190cmFpbiA8LQ0KICB0aXRhbmljX3RyYWluICU+JSBzZWxlY3QoDQogICAgJ1Bhc3NlbmdlcklkJywNCiAgICAnU3Vydml2ZWQnLA0KICAgICdQY2xhc3MnLA0KICAgICdTZXgnLA0KICAgICdBZ2UnLA0KICAgICdTaWJTcCcsDQogICAgJ1BhcmNoJywNCiAgICAnRmFyZScgLA0KICAgICdFbWJhcmtlZCcNCiAgKQ0KI3RyYW5zZm9ybWFyIGxhcyB2YXJpYWJsZXMgYSBmYWN0b3INCnRpdGFuaWNfdHJhaW5bLCBjKCdTdXJ2aXZlZCcsICdQY2xhc3MnLCAnRW1iYXJrZWQnKV0gPC0NCiAgbWFwKHRpdGFuaWNfdHJhaW5bLCBjKCdTdXJ2aXZlZCcsICdQY2xhc3MnLCAnRW1iYXJrZWQnKV0sIGFzLmZhY3RvcikNCmBgYA0KIyMgR3LDoWZpY28gZGUgR0dQYWlycw0KU2UgcmVhbGl6YSBncsOhZmljbyBkZSBnZ3BhaXJzIHkgc2Ugb2JzZXJ2YSBxdWUgbG9zIHF1ZSB2aWFqYWJhbiBlbiAzcmQgY2xhc2Ugc29icmV2aXZpZXJvbiBlbiBtZW5vciBwcm9wb3JjacOzbiBxdWUgbG9zIGRlIDJkYSB5IDFlcmEgY2xhc2UuIFNvYnJldml2aWVyb24gbcOhcyBsYXMgbXVqZXJlcyBxdWUgbG9zIGhvbWJyZXMuIE5vIHNlIG9ic2VydmFuIGRpZmVyZW5jaWFzIHNpZ25pZmljYXRpdmFzIHBvciBFZGFkIHkgcHJlY2lvIGRlbCBib2xldG8uDQoNCg0KYGBge3IgcHJlcGFyYWNpb25EYXRvczN9DQojR3JhZmljbyBHR3BhaXJzDQp0aXRhbmljX3RyYWluICU+JSBzZWxlY3QoU3Vydml2ZWQsIFBjbGFzcywgU2V4LCBBZ2UsIEZhcmUpICU+JSBnZ3BhaXJzKC4sIG1hcHBpbmcgPSBhZXMoY29sb3VyID0gU3Vydml2ZWQpKSArIHRoZW1lKGF4aXMudGV4dC54ID0gZWxlbWVudF90ZXh0KGFuZ2xlID0gOTAsIGhqdXN0ID0gMSkpICsgdGhlbWVfYncoKQ0KYGBgDQojIyBEaXN0cmlidWNpw7NuIGRlIGxhIGNsYXNlIA0KU2UgcHVlZGUgdmVyIHF1ZSBzZSB0aWVuZSB1bmEgZGlzdHJpYnVjaW9uIGRlc2JhbGFuY2VhIGNvbiA2MiUgZGUgbmVnYXRpdm9zIHkgMzglIGRlIHBvc2l0dm9zLg0KDQoNCmBgYHtyIHByZXBhcmFjaW9uRGF0b3M0fQ0KdGl0YW5pY190cmFpbiAlPiUgZ3JvdXBfYnkoU3Vydml2ZWQpICU+JSBzdW1tYXJpc2UobnVtZXJvX2Nhc29zPW4oKS9ucm93KHRpdGFuaWNfdHJhaW4pKQ0KDQpnZ3Bsb3QoZGF0YT10aXRhbmljX3RyYWluLCBhZXModGl0YW5pY190cmFpbiRTdXJ2aXZlZCkpICsgZ2VvbV9iYXIoKSArIHRoZW1lX2J3KCkgKw0KICBsYWJzKHRpdGxlPSJUaXRhbmljIFRyYWluIiwgeD0iU3Vydml2ZWQiKQ0KI0Rpc3RyaWJ1Y2lvbiBkZSBsYSBjbGFzZSBkZSB0aXRhbmljIHRyYWluLg0KdGl0YW5pY190cmFpbiAlPiUgZ3JvdXBfYnkoU3Vydml2ZWQpICU+JSBzdW1tYXJpc2UobnVtZXJvX2Nhc29zPW4oKS9ucm93KHRpdGFuaWNfdHJhaW4pKjEwMCkNCmBgYA0KIyMgRGl2aWRpciBlbCBjb25qdW50byBkZSBUcmFpbg0KU2Ugb2JzZXJ2YSBxdWUgbHVlZ28gZGXDsSByZXNhbXBsZV9wYXJ0aXRpb24sIGxvcyBkYXRvcyB0aWVuZW4gbGEgbWlzbWEgZGlzdHJpYnVjacOzbiBkZSBjbGFzZSBxdWUgbGEgb3JnaW5hbC4NCmBgYHtyIHByZXBhcmFjaW9uRGF0b3M1fQ0KI1NlcGFybyBlbiB0cmFpbiB5IHZhbGlkYWNpb24NCnRyYWluX3Rlc3QgPC0gdGl0YW5pY190cmFpbiAlPiUgcmVzYW1wbGVfcGFydGl0aW9uKGModHJhaW49MC43LHRlc3Q9MC4zKSkNCg0KdGl0YW5pY190cmFpbjIgPC0gdHJhaW5fdGVzdCR0cmFpbiAlPiUgYXNfdGliYmxlKCkNCnRpdGFuaWNfdmFsaWRhdGlvbiA8LSB0cmFpbl90ZXN0JHRlc3QgJT4lIGFzX3RpYmJsZSgpDQpnZ3Bsb3QoZGF0YT10aXRhbmljX3RyYWluMiwgYWVzKHRpdGFuaWNfdHJhaW4yJFN1cnZpdmVkKSkgKyBnZW9tX2JhcigpICsgdGhlbWVfYncoKSArDQogIGxhYnModGl0bGU9IlRpdGFuaWMgVHJhaW4gNzAlIiwgeD0iU3Vydml2ZWQiKQ0KZ2dwbG90KGRhdGE9dGl0YW5pY192YWxpZGF0aW9uLCBhZXModGl0YW5pY192YWxpZGF0aW9uJFN1cnZpdmVkKSkgKyBnZW9tX2JhcigpICsgdGhlbWVfYncoKSArDQogIGxhYnModGl0bGU9IlRpdGFuaWMgVmFsaWRhdGlvbiAzMCUiLCB4PSJTdXJ2aXZlZCIpDQojRGlzdHJpYnVjaW9uIGRlIGxhIGNsYXNlIGRlIHRpdGFuaWMgdHJhaW4yLg0KdGl0YW5pY190cmFpbjIgJT4lIGdyb3VwX2J5KFN1cnZpdmVkKSAlPiUgc3VtbWFyaXNlKG51bWVyb19jYXNvcz1uKCkvbnJvdyh0aXRhbmljX3RyYWluMikqMTAwKQ0KI0Rpc3RyaWJ1Y2lvbiBkZSBsYSBjbGFzZSBkZSB0aXRhbmljIHZhbGlkYXRpb24NCnRpdGFuaWNfdmFsaWRhdGlvbiAlPiUgZ3JvdXBfYnkoU3Vydml2ZWQpICU+JSBzdW1tYXJpc2UobnVtZXJvX2Nhc29zPW4oKS9ucm93KHRpdGFuaWNfdmFsaWRhdGlvbikqMTAwKQ0KDQpgYGANCiMgUHJlZGljY2lvbmVzDQojIyBNb2RlbG8gUGNsYXNzK1NleCtBZ2UNClNlIGdlbmVyYW4gdmFyaW9zIG1vZGVsb3MgaW5jbHV5ZW5kbyBlbCBxdWUgc2Ugc29saWNpdGEgZW4gZXN0ZSBwdW50byBwYXJhIG1heW9yIHByYWN0aWNpZGFkLg0KYGBge3IgUHJlZGljY2lvbmVzMX0NCmxvZ2l0X2Zvcm11bGFzIDwtIGZvcm11bGFzKC5yZXNwb25zZSA9IH5TdXJ2aXZlZCwgDQogICAgICAgICAgICAgICAgICAgICAgICAgcGNfc2V4X2FnZT1+UGNsYXNzK1NleCtBZ2UsIA0KICAgICAgICAgICAgICAgICAgICAgICAgIHBjX2ZhcmVfZW1iPX5QY2xhc3MrRmFyZSsgRW1iYXJrZWQgLCANCiAgICAgICAgICAgICAgICAgICAgICAgICBzZXhfZmFyZT0gfiBTZXggKyBGYXJlLA0KICAgICAgICAgICAgICAgICAgICAgICAgIHBjbGFzcz0gflBjbGFzcw0KICAgICAgICAgICAgICAgICAgICAgICAgICkNCm1vZGVscyA8LSBkYXRhX2ZyYW1lKGxvZ2l0X2Zvcm11bGFzKSAlPiUgIyBkYXRhZnJhbWUgYSBwYXJ0aXIgZGVsIG9iamV0byBmb3JtdWxhcw0KICBtdXRhdGUobW9kZWxzID0gbmFtZXMobG9naXRfZm9ybXVsYXMpLCAjIGNvbHVtbmEgY29uIGxvcyBub21icmVzIGRlIGxhcyBmb3JtdWxhcw0KICAgICAgICAgZXhwcmVzc2lvbiA9IHBhc3RlKGxvZ2l0X2Zvcm11bGFzKSwgIyBjb2x1bW5hIGNvbiBsYXMgZXhwcmVzaW9uZXMgZGUgbGFzIGZvcm11bGFzDQogICAgICAgICBtb2QgPSBtYXAobG9naXRfZm9ybXVsYXMsIH5nbG0oLixmYW1pbHkgPSAnYmlub21pYWwnLCBkYXRhID0gdGl0YW5pY190cmFpbjIpKSkNCmBgYA0KIyMgSW50ZXJwcmV0YXIgY29lZmljaWVudGVzDQpUb2RvcyBsb3MgY29lZmljaWVudGVzIHJlc3VsdGFyb24gc2lnbmlmaWNhdGl2b3MgZW4gZXN0ZSBtb2RlbG8uIExhcyB2YXJpYWJsZXMgdGllbmVuIGNvZWZpY2llbnRlcyBuZWdhdGl2b3MgbG8gY3VhbCBpbmRpY2EgcXVlIGRpc21pbnV5ZSBsYSBwcm9iYWJpbGlkYWQgZXNwZXJhZGEgZGUgc29icmV2aXZpciBhIG1lZGlkYSBxdWUgw6lzdGFzIGF1bWVudGFuLg0KYGBge3IgUHJlZGljY2lvbmVzMn0NCm1vZGVscyAlPiUgDQogIGZpbHRlcihtb2RlbHMgPT0gJ3BjX3NleF9hZ2UnKSAlPiUNCiAgbXV0YXRlKHRpZHkgPSBtYXAobW9kLHRpZHkpKSAlPiUgIA0KICB1bm5lc3QodGlkeSwgLmRyb3AgPSBUUlVFKSAlPiUgDQogIHNlbGVjdCh0ZXJtLCBlc3RpbWF0ZSwgc3RkLmVycm9yLCBzdGF0aXN0aWMsIHAudmFsdWUpDQpgYGANCiMjIFF1acOpbiB0aWVuZSBtYXlvciBwcm9iYWJpbGlkYWQgZGUgU3VwZXJ2aXZlbmNpYQ0KU2Ugb2JzZXJ2YSBxdWUgSmFjayB0ZW5pYSBtdXkgYmFqYXMgY2hhbmNlcyBkZSBzb2JyZXZpdmlyIGF1bnF1ZSBlbCB0cm96byBkZSBtYWRlcmEgcXVlIHV0bGl6byBSb3NlIHBvZHJpYSBoYWJlciBzYWx2YWRvIGEgYW1ib3MuDQpgYGB7ciBQcmVkaWNjaW9uZXMzfQ0KbmV3ZGF0YSA8LSB0aWJibGUoQWdlID0gYygxNywyMCksIFBjbGFzcyA9IGMoJzEnLCczJyksIFNleCA9IGMoJ2ZlbWFsZScsJ21hbGUnKSkNCg0KbW9kZWxzICU+JSANCiAgZmlsdGVyKG1vZGVscyA9PSAncGNfc2V4X2FnZScpICU+JQ0KICBtdXRhdGUocHJlZD0gbWFwKG1vZCxhdWdtZW50LCBuZXdkYXRhPSBuZXdkYXRhLHR5cGUucHJlZGljdCA9ICJyZXNwb25zZSIpKSAlPiUgIA0KICB1bm5lc3QocHJlZCwgLmRyb3AgPSBUUlVFKSAlPiUNCiAgc2VsZWN0KEFnZSwgUGNsYXNzLCBTZXgsIC5maXR0ZWQpDQoNCmBgYA0KIyBHZW5lcmFjacOzbiBkZSBNb2RlbG9zDQojIyBHZW5lcmFyIDMgbW9kZWxvcw0KU2UgZ2VuZXJhcm9uIHRyZXMgbW9kZWxvczoNCg0KKiBQY2xhc3MrRmFyZSsgRW1iYXJrZWQNCiogU2V4ICsgRmFyZQ0KKiBQY2xhc3MNCg0KU2Ugb2JzZXJ2YSBxdWUgZWwgbW9kZWxvIH5QY2xhc3MrRmFyZSsgRW1iYXJrZWQsIEVtYmFya2VkUSB5IFBjbGFzczIgbm8gc29uIHNpZ25pZmljYXRpdm9zLg0KYGBge3IgR2VuZXJhY2lvbk1vZGVsb3MxfQ0KbW9kZWxzICU+JSANCiAgZmlsdGVyKG1vZGVscyAhPSAncGNfc2V4X2FnZScpICU+JQ0KICBtdXRhdGUodGlkeSA9IG1hcChtb2QsdGlkeSkpICU+JSAgDQogIHVubmVzdCh0aWR5LCAuZHJvcCA9IFRSVUUpICU+JSANCiAgc2VsZWN0KHRlcm0sIGVzdGltYXRlLCBzdGQuZXJyb3IsIHN0YXRpc3RpYywgcC52YWx1ZSkNCmBgYA0KIyMgT3JkZXJuYXIgcG9yIGRldmlhbmNlDQpFbGlqbyBlbCBtb2RlbG8gcXVlIG1heGltaWNlIGVsIHBvcmNlbnRhamUgZGUgbGEgZGVzdmlhbmNlIGV4cGxpY2FkYS4gRW4gZXN0ZSBjYXNvLCBlbCBtZWpvciBtb2RlbG8gZXMgIH4gUGNsYXNzICsgU2V4ICsgQWdlLg0KYGBge3IgR2VuZXJhY2lvbk1vZGVsb3MyfQ0KbW9kZWxzIDwtIG1vZGVscyAlPiUgDQogIG11dGF0ZShnbGFuY2UgPSBtYXAobW9kLGdsYW5jZSkpDQoNCm1vZGVscyAlPiUgDQogIHVubmVzdChnbGFuY2UsIC5kcm9wID0gVFJVRSkgJT4lDQogIG11dGF0ZShwZXJjX2V4cGxhaW5lZF9kZXYgPSAxLWRldmlhbmNlL251bGwuZGV2aWFuY2UpICU+JSANCiAgc2VsZWN0KGV4cHJlc3Npb24sbnVsbC5kZXZpYW5jZSwgZGV2aWFuY2UsIHBlcmNfZXhwbGFpbmVkX2RldikgJT4lIA0KICBhcnJhbmdlKGRldmlhbmNlKQ0KYGBgDQojIEV2YWx1YWNpb24gZGVsIE1vZGVsbw0KIyMgQ3VydmEgUk9DIHkgQVVDDQpTZSBvYnR1dm8gdW4gYXVjIGRlIDAuODQ4OCBxdWUgZXMgbWF5b3IgYWwgYXphciAwLjU7IGF1bnF1ZSBlc3RlIHZhbG9yIHNlIG9idGllbmUgc29icmUgZWwgY29uanVudG8gZGUgZW50cmVuYW1pZW50by4gDQpMYSBjdXJ2YSBST0MgdmlzdWFsaXphIGxhIHNlbnNpYmlsaWRhZCBkZSBsYSBwcmVkaWNjacOzbiB2ZXJzdXMgbGEgZXNwZWNpZmljaWRhZCBkZSBsYSBtaXNtYS4gTGEgc2Vuc2libGlkYWQgc29uIGxhcyBwcmVkaWNjaW9uZXMgcG9zaXRpdmFzIHNvYnJlIGxhIGNhbnRpZGFkIHRvdGFsIGRlIHBvc2l0aXZvcyB5IGxhIGVzcGVjaWZpY2lkYWQgc29uIGxhcyBwcmVkaWNjaW9uZXMgbmVnYXRpdmFzIHNvYnJlIGxhIGNhbnRpZGFkIHRvdGFsIGRlIG5lZ2F0aXZvcy4NCmBgYHtyIEV2YWx1YWNpb25Nb2RlbG9zMX0NCm1vZGVscyA8LSBtb2RlbHMgJT4lIA0KICBtdXRhdGUocHJlZD0gbWFwKG1vZCxhdWdtZW50LCB0eXBlLnByZWRpY3QgPSAicmVzcG9uc2UiKSkNCg0KDQojIENhbGN1bGFtb3MgY3VydmFzIFJPQw0KcHJlZGljdGlvbl9wY19zZXhfYWdlIDwtIG1vZGVscyAlPiUgDQogIGZpbHRlcihtb2RlbHM9PSJwY19zZXhfYWdlIikgJT4lIA0KICB1bm5lc3QocHJlZCwgLmRyb3A9VFJVRSkNCg0Kcm9jX3BjX3NleF9hZ2UgPC0gcm9jKHJlc3BvbnNlPXByZWRpY3Rpb25fcGNfc2V4X2FnZSRTdXJ2aXZlZCwgcHJlZGljdG9yPXByZWRpY3Rpb25fcGNfc2V4X2FnZSQuZml0dGVkKQ0KDQpyb2NfcGNfc2V4X2FnZSANCg0KZ2dyb2Mocm9jX3BjX3NleF9hZ2UsIHNpemU9MSkgKyBnZW9tX2FibGluZShzbG9wZSA9IDEsIGludGVyY2VwdCA9IDEsIGxpbmV0eXBlPSdkYXNoZWQnKSArIHRoZW1lX2J3KCkgKyBsYWJzKHRpdGxlPSdDdXJ2YXMgUk9DJywgY29sb3I9J01vZGVsbycpDQpgYGANCg0KIyMgVmlvbGluIFBsb3QNClNlIHB1ZWRlIG9ic2VydmFyIGEgdHJhdsOpcyBkZWwgZ3LDoWZpY28gcXVlIGxhcyBwcm9iYWJpbGlkYWRlcyBkZSBsYXMgb2JzZXJ2YWNpb25lcyBkZSBsYSBjbGFzZSBubyBzb2JyZXZpdmllbnRlIHRpZW5lIHVuIGNvbmNlbnRyYWNpb24gZW50cmUgMC4yNSB5IDAuIE1pZW50cmFzIHF1ZSBsYXMgcHJvYmFiaWxpZGFkZXMgZGUgbGFzIG9ic2VydmFjaW9uZXMgZGUgbG9zIHNvYnJldml2aWVudGVzIHRpZW5lbiBtYXlvciBjb25jZW50cmFjaW9uIGVuIDEuIERlZmluaXIgdW4gcHVudG8gZGUgY29ydGUgcXVlIHByZWRpZ2EgYmllbiBhbWJhcyBjbGFzZSBkZSBmb3JtYSB0YWwgZGUgY29tZXRlciBlbCBtZWpvciBlcnJvciBwb3NpYmxlLCBwYXJlY2llcmEgbm8gZGlzbHVtYnJhc2UgY2xhcmFtZXRuZSBlbiBlbCBncmFmaWNvLiANCmBgYHtyIEV2YWx1YWNpb25Nb2RlbG9zMn0NCmdncGxvdChwcmVkaWN0aW9uX3BjX3NleF9hZ2UsIGFlcyh4PVN1cnZpdmVkLCB5PS5maXR0ZWQsIGdyb3VwPVN1cnZpdmVkLGZpbGw9ZmFjdG9yKFN1cnZpdmVkKSkpICsgDQogIGdlb21fdmlvbGluKCkgKw0KICB0aGVtZV9idygpICsNCiAgZ3VpZGVzKGZpbGw9RkFMU0UpICsNCiAgbGFicyh0aXRsZT0nVmlvbGluIHBsb3QnLCBzdWJ0aXRsZT0nTW9kZWxvIGNvbXBsZXRvJywgeT0nUHJlZGljdGVkIHByb2JhYmlsaXR5JykNCg0KYGBgDQojIEVsZWNjaW9uIFB1bnRvIGRlIENvcnRlDQojIyBHcsOhZmljbyBlbiBmdW5jaW9uIGRlbCBwdW50byBkZSBjb3J0ZQ0KYGBge3IgUHRvQ29ydGUxfQ0KcHJlZF92YWw8LW1vZGVscyAlPiUgDQogIGZpbHRlcihtb2RlbHMgPT0gJ3BjX3NleF9hZ2UnKSAlPiUNCiAgbXV0YXRlKHByZWQ9IG1hcChtb2QsYXVnbWVudCwgbmV3ZGF0YT0gdGl0YW5pY192YWxpZGF0aW9uLHR5cGUucHJlZGljdCA9ICJyZXNwb25zZSIpKSAlPiUgDQogIHVubmVzdChwcmVkLCAuZHJvcD1UUlVFKQ0KDQpwcmVkaWN0aW9uX21ldHJpY3MgPC0gZnVuY3Rpb24oY3V0b2ZmPTAuOTUsIHByZWRpY3Rpb25zPXByZWRfdmFsKXsNCiAgdGFibGUgPC0gcHJlZGljdGlvbnMgJT4lIA0KICAgIG11dGF0ZShwcmVkaWN0ZWRfY2xhc3M9aWZfZWxzZSguZml0dGVkPmN1dG9mZiwgMSwgMCkgJT4lIGFzLmZhY3RvcigpLA0KICAgICAgICAgICBTdXJ2aXZlZD0gZmFjdG9yKFN1cnZpdmVkKSkNCiAgDQogIGNvbmZ1c2lvbk1hdHJpeCh0YWJsZSRwcmVkaWN0ZWRfY2xhc3MsIHRhYmxlJFN1cnZpdmVkLCBwb3NpdGl2ZSA9ICIxIikgJT4lDQogICAgdGlkeSgpICU+JQ0KICAgIHNlbGVjdCh0ZXJtLCBlc3RpbWF0ZSkgJT4lDQogICAgZmlsdGVyKHRlcm0gJWluJSBjKCdhY2N1cmFjeScsICdzZW5zaXRpdml0eScsICdzcGVjaWZpY2l0eScsICdwcmVjaXNpb24nLCdyZWNhbGwnKSkgJT4lDQogICAgbXV0YXRlKGN1dG9mZj1jdXRvZmYpDQogIA0KfQ0KY3V0b2ZmcyA9IHNlcSgwLjAxLDAuOTUsMC4wMSkNCg0KbG9naXRfcHJlZD0gbWFwX2RmcihjdXRvZmZzLCBwcmVkaWN0aW9uX21ldHJpY3MpJT4lIG11dGF0ZSh0ZXJtPWFzLmZhY3Rvcih0ZXJtKSkNCg0KZ2dwbG90KGxvZ2l0X3ByZWQsIGFlcyhjdXRvZmYsZXN0aW1hdGUsIGdyb3VwPXRlcm0sIGNvbG9yPXRlcm0pKSArIGdlb21fbGluZShzaXplPTEpICsNCiAgdGhlbWVfYncoKSArDQogIGxhYnModGl0bGU9ICdBY2N1cmFjeSwgU2Vuc2l0aXZpdHksIFNwZWNpZmljaXR5LCBSZWNhbGwgeSBQcmVjaXNpb24nLCBzdWJ0aXRsZT0gJ01vZGVsbyBQY2xhc3MrU2V4K0FnZScsIGNvbG9yPSIiKSArDQogIHNjYWxlX3hfY29udGludW91cyhicmVha3MgPXNlcSgwLjA1LDAuOTUsMC4xKSkNCmBgYA0KIyMgRWxlZ2lyIGVsIFB1bnRvIGRlIENvcnRlDQpMYSBlbGVjY2nDs24gc2UgYmFzYSBlbiBsYSBtw6l0cmljYSBxdWUgcXVlcmVtb3MgbWF4aW1pemFyLiBFbiBlc3RlIGNhc28gYnVzY2Ftb3MgbGEgaW50ZXJzZWNjacOzbiBkZSBzZW5zaXZpdml0eSB5IHNwZWNpZml0eSBkZSBmb3JtYSB0YWwgZGUgb2J0ZW5lciB1biBiYWxhbmNlIGVudHJlIGxvcyBwb3NpdGl2b3MgeSBuZWdhdGl2b3MuIEVsZWdpbW9zIGNvbW8gcHVudG8gZGUgY29ydGUgMC40LiANCg0KIyMgTWF0cml6IGRlIENvbmZ1c2nDs24NCkxhIHRhYmxhIG5vcyBtdWVzdHJhIHF1ZSBzZSBjbGFzaWZpY28gY29ycmVjdGFtZW50ZSAxMTcgcGVyc29uYXMgY29tbyBubyBzb2JyZXZpdmllbnRlcyB5IDg0IHBlcnNvbmFzIGNvbW8gc29icmV2aXZpZW50ZXMuIDI0IHBlcnNvbmFzIGxhcyBjbGFzaWZpY28gY29tbyBubyBzb2JyZXZpZW50ZXMgY3VhbmRvIGVucmVhbGlkYWQgc2kgc29icmV2aXZpZXJvbiB5IDQzIHBlcnNvbmFzIGNvbW8gc29icmV2aXZpZW50ZXMgY3VhbmRvIG5vIHNvYnJldml2aWVyb24uDQpgYGB7ciBQdG9Db3J0ZTJ9DQp0YWJsZSA8LSBwcmVkX3ZhbCAlPiUNCiAgbXV0YXRlKA0KICAgIHByZWRpY3RlZF9jbGFzcyA9IGlmX2Vsc2UoLmZpdHRlZCA+IDAuNCwgMSwgMCkgJT4lIGFzLmZhY3RvcigpLA0KICAgIFN1cnZpdmVkID0gZmFjdG9yKFN1cnZpdmVkKQ0KICApDQoNCmNvbmZ1c2lvbk1hdHJpeCh0YWJsZSRwcmVkaWN0ZWRfY2xhc3MsIHRhYmxlJFN1cnZpdmVkLCBwb3NpdGl2ZSA9ICIxIikNCiAgDQpgYGANCg0KIyBUZXN0ZW8NCiMjIExlZWVyIGFyY2hpdm8gZGUgVGVzdGVvDQpgYGB7ciBUZXN0ZW8xfQ0KdGl0YW5pY190ZXN0IDwtIHJlYWQuY3N2KGZpbGUgPSAidGl0YW5pY19jb21wbGV0ZV90ZXN0LmNzdiIpDQp0aXRhbmljX3Rlc3RbLCBjKCdTdXJ2aXZlZCcsICdQY2xhc3MnLCAnRW1iYXJrZWQnKV0gPC0NCiAgbWFwKHRpdGFuaWNfdGVzdFssIGMoJ1N1cnZpdmVkJywgJ1BjbGFzcycsICdFbWJhcmtlZCcpXSwgYXMuZmFjdG9yKQ0KYGBgDQoNCiMjIENsYXNpZmljYXIgYSBsYXMgcGVyc29uYXMgZGUgVGVzdGluZw0KYGBge3IgVGVzdGVvMn0NCnByZWRfdGVzdDwtbW9kZWxzICU+JSANCiAgZmlsdGVyKG1vZGVscyA9PSAncGNfc2V4X2FnZScpICU+JQ0KICBtdXRhdGUocHJlZD0gbWFwKG1vZCxhdWdtZW50LCBuZXdkYXRhPSB0aXRhbmljX3Rlc3QsdHlwZS5wcmVkaWN0ID0gInJlc3BvbnNlIikpICU+JSANCiAgdW5uZXN0KHByZWQsIC5kcm9wPVRSVUUpDQoNCnRhYmxlIDwtIHByZWRfdGVzdCAlPiUNCiAgbXV0YXRlKA0KICAgIHByZWRpY3RlZF9jbGFzcyA9IGlmX2Vsc2UoLmZpdHRlZCA+IDAuNCwgMSwgMCkgJT4lIGFzLmZhY3RvcigpLA0KICAgIFN1cnZpdmVkID0gZmFjdG9yKFN1cnZpdmVkKQ0KICApDQoNCnRhYmxlICU+JQ0KICBzZWxlY3QoUGFzc2VuZ2VySWQsIFBjbGFzcywgU2V4LCBBZ2UsIHByb2IgPSAuZml0dGVkLCBwcmVkaWN0ZWRfY2xhc3MpICU+JQ0KICBhcnJhbmdlKC1wcm9iKQ0KYGBgDQoNCiMjIE1hdHJpeiBkZSBDb25mdXNpw7NuDQpFbiBlbCBjb25qdW50byBkZSB0ZXN0IGVsIG1vZGVsbyBjbGFzaWZpY8OzIGNvcnJlY3RhbWVudGUgMTk0IHBlcnNvbmFzIGNvbW8gbm8gc29icmV2aXZpZW50ZXMgeSAxMTUgcGVyc29uYXMgY29tbyBzb2JyZXZpdmllbnRlcy4gNDIgcGVyc29uYXMgbGFzIGNsYXNpZmljbyBjb21vIG5vIHNvYnJldmllbnRlcyBjdWFuZG8gZW5yZWFsaWRhZCBzaSBzb2JyZXZpdmllcm9uIHkgNjcgcGVyc29uYXMgY29tbyBzb2JyZXZpdmllbnRlcyBjdWFuZG8gbm8gc29icmV2aXZpZXJvbi4gDQpFbiBjb21wYXJhY2lvbiBjb24gbGFzIG3DqXRyaWNhcyBvYnRlbmlkYXMgc29icmUgZWwgY29uanVudG8gZGUgdmFsaWRhY2lvbiwgbGEgc2Vuc2liaWxpZGFkLCBlc3BlY2lmaWNpZGFkIHkgZWwgYWN1cnJhbmN5IHNvbiBzaW1pbGFyZXMuDQoNCiogVGVzdDogQWN1cnJhbmN5OiAwLjczOTIgLCBTZW5zaXRpdml0eTogMC43MzI1IHkgU3BlY2lmaWNpdHk6IDAuNzQzMw0KKiBWYWxpZGFjaW9uOiBBY3VycmFuY3k6IDAuNzUsIFNlbnNpdGl2aXR5OiAwLjc3NzggeSBTcGVjaWZpY2l0eTogMC43MzEyIA0KYGBge3IgVGVzdGVvM30NCmNvbmZ1c2lvbk1hdHJpeCh0YWJsZSRwcmVkaWN0ZWRfY2xhc3MsIHRhYmxlJFN1cnZpdmVkLCBwb3NpdGl2ZSA9ICIxIikNCg0KYGBgDQoNCg0KDQoNCg0K