Contexto

Telco es una empresa internacional que vende una variedad de servicios de telecomunicaciones. Churn es un indicador de la deserción de clientes, fenómeno en el cual los clientes cancelan sus servicios con la empresa. El objetivo de este trabajo es predecir si un cliente abandonará o se quedará en la empresa.

Instalar paquetes y llamar librerias

library(tidyverse)
## ── Attaching core tidyverse packages ──────────────────────── tidyverse 2.0.0 ──
## ✔ dplyr     1.1.4     ✔ readr     2.1.5
## ✔ forcats   1.0.0     ✔ stringr   1.5.1
## ✔ ggplot2   3.5.1     ✔ tibble    3.2.1
## ✔ lubridate 1.9.3     ✔ tidyr     1.3.1
## ✔ purrr     1.0.4     
## ── Conflicts ────────────────────────────────────────── tidyverse_conflicts() ──
## ✖ dplyr::filter() masks stats::filter()
## ✖ dplyr::lag()    masks stats::lag()
## ℹ Use the conflicted package (<http://conflicted.r-lib.org/>) to force all conflicts to become errors
library(caret)
## Cargando paquete requerido: lattice
## 
## Adjuntando el paquete: 'caret'
## 
## The following object is masked from 'package:purrr':
## 
##     lift
library(e1071) # Confusion matrix

Importar la base de datos

df <- read.csv('C:\\Users\\ACER\\Downloads\\Telco Customer Churn.csv')

Entender la base de datos

summary(df)
##   customerID           gender          SeniorCitizen      Partner         
##  Length:7043        Length:7043        Min.   :0.0000   Length:7043       
##  Class :character   Class :character   1st Qu.:0.0000   Class :character  
##  Mode  :character   Mode  :character   Median :0.0000   Mode  :character  
##                                        Mean   :0.1621                     
##                                        3rd Qu.:0.0000                     
##                                        Max.   :1.0000                     
##                                                                           
##   Dependents            tenure      PhoneService       MultipleLines     
##  Length:7043        Min.   : 0.00   Length:7043        Length:7043       
##  Class :character   1st Qu.: 9.00   Class :character   Class :character  
##  Mode  :character   Median :29.00   Mode  :character   Mode  :character  
##                     Mean   :32.37                                        
##                     3rd Qu.:55.00                                        
##                     Max.   :72.00                                        
##                                                                          
##  InternetService    OnlineSecurity     OnlineBackup       DeviceProtection  
##  Length:7043        Length:7043        Length:7043        Length:7043       
##  Class :character   Class :character   Class :character   Class :character  
##  Mode  :character   Mode  :character   Mode  :character   Mode  :character  
##                                                                             
##                                                                             
##                                                                             
##                                                                             
##  TechSupport        StreamingTV        StreamingMovies      Contract        
##  Length:7043        Length:7043        Length:7043        Length:7043       
##  Class :character   Class :character   Class :character   Class :character  
##  Mode  :character   Mode  :character   Mode  :character   Mode  :character  
##                                                                             
##                                                                             
##                                                                             
##                                                                             
##  PaperlessBilling   PaymentMethod      MonthlyCharges    TotalCharges   
##  Length:7043        Length:7043        Min.   : 18.25   Min.   :  18.8  
##  Class :character   Class :character   1st Qu.: 35.50   1st Qu.: 401.4  
##  Mode  :character   Mode  :character   Median : 70.35   Median :1397.5  
##                                        Mean   : 64.76   Mean   :2283.3  
##                                        3rd Qu.: 89.85   3rd Qu.:3794.7  
##                                        Max.   :118.75   Max.   :8684.8  
##                                                         NA's   :11      
##     Churn          
##  Length:7043       
##  Class :character  
##  Mode  :character  
##                    
##                    
##                    
## 
#head(df)
#str(df)

Regresión Logística

La regresión logística en un modelo que se utiliza para predecir la probabilidad de que ocurra un evento, basado en una o más variables independientes. A diferencia de la regresión logística que predice valores continuos, la regrsión logística predecir resultados binarios o categóricos, como un Si/No, 1/0, verdadero/falso.

Si la probabilidad es mayor que 0.5, el modelo predice que el evento ocurrirá (Ej. el cliente se dará de baja). Si la probabilidad es menor que 0.5, predice que el evento no ocurrirá. En caso de ser 0.5, se conoce como umbral de decisión.

Preparar la base de datos

# Drop unecessary columns
cleaned_df <- df %>% select(-customerID)
# Convert categorical into factor
cleaned_df <- cleaned_df %>% mutate(across(where(is.character), as.factor))
str(cleaned_df)
## 'data.frame':    7043 obs. of  20 variables:
##  $ gender          : Factor w/ 2 levels "Female","Male": 1 2 2 2 1 1 2 1 1 2 ...
##  $ SeniorCitizen   : int  0 0 0 0 0 0 0 0 0 0 ...
##  $ Partner         : Factor w/ 2 levels "No","Yes": 2 1 1 1 1 1 1 1 2 1 ...
##  $ Dependents      : Factor w/ 2 levels "No","Yes": 1 1 1 1 1 1 2 1 1 2 ...
##  $ tenure          : int  1 34 2 45 2 8 22 10 28 62 ...
##  $ PhoneService    : Factor w/ 2 levels "No","Yes": 1 2 2 1 2 2 2 1 2 2 ...
##  $ MultipleLines   : Factor w/ 3 levels "No","No phone service",..: 2 1 1 2 1 3 3 2 3 1 ...
##  $ InternetService : Factor w/ 3 levels "DSL","Fiber optic",..: 1 1 1 1 2 2 2 1 2 1 ...
##  $ OnlineSecurity  : Factor w/ 3 levels "No","No internet service",..: 1 3 3 3 1 1 1 3 1 3 ...
##  $ OnlineBackup    : Factor w/ 3 levels "No","No internet service",..: 3 1 3 1 1 1 3 1 1 3 ...
##  $ DeviceProtection: Factor w/ 3 levels "No","No internet service",..: 1 3 1 3 1 3 1 1 3 1 ...
##  $ TechSupport     : Factor w/ 3 levels "No","No internet service",..: 1 1 1 3 1 1 1 1 3 1 ...
##  $ StreamingTV     : Factor w/ 3 levels "No","No internet service",..: 1 1 1 1 1 3 3 1 3 1 ...
##  $ StreamingMovies : Factor w/ 3 levels "No","No internet service",..: 1 1 1 1 1 3 1 1 3 1 ...
##  $ Contract        : Factor w/ 3 levels "Month-to-month",..: 1 2 1 2 1 1 1 1 1 2 ...
##  $ PaperlessBilling: Factor w/ 2 levels "No","Yes": 2 1 2 1 2 2 2 1 2 1 ...
##  $ PaymentMethod   : Factor w/ 4 levels "Bank transfer (automatic)",..: 3 4 4 1 3 3 2 4 3 1 ...
##  $ MonthlyCharges  : num  29.9 57 53.9 42.3 70.7 ...
##  $ TotalCharges    : num  29.9 1889.5 108.2 1840.8 151.7 ...
##  $ Churn           : Factor w/ 2 levels "No","Yes": 1 1 2 1 2 2 1 1 2 1 ...
# Drop NAs
cleaned_df <- na.omit(cleaned_df)

Entrenar el modelo

set.seed(123)
train_rows <- createDataPartition(cleaned_df$Churn, p = 0.8, list = FALSE)
train <- cleaned_df[train_rows, ]
test <- cleaned_df[-train_rows, ]
model <- glm(
  Churn ~ .,
  data = train,
  family = binomial
)
summary(model)
## 
## Call:
## glm(formula = Churn ~ ., family = binomial, data = train)
## 
## Coefficients: (7 not defined because of singularities)
##                                        Estimate Std. Error z value Pr(>|z|)    
## (Intercept)                           1.820e+00  9.035e-01   2.014  0.04398 *  
## genderMale                           -3.227e-02  7.248e-02  -0.445  0.65620    
## SeniorCitizen                         2.636e-01  9.426e-02   2.797  0.00516 ** 
## PartnerYes                           -3.523e-02  8.694e-02  -0.405  0.68533    
## DependentsYes                        -8.790e-02  1.008e-01  -0.872  0.38300    
## tenure                               -6.307e-02  7.049e-03  -8.947  < 2e-16 ***
## PhoneServiceYes                       6.129e-01  7.190e-01   0.852  0.39398    
## MultipleLinesNo phone service                NA         NA      NA       NA    
## MultipleLinesYes                      5.201e-01  1.965e-01   2.647  0.00811 ** 
## InternetServiceFiber optic            2.320e+00  8.861e-01   2.618  0.00884 ** 
## InternetServiceNo                    -2.354e+00  8.936e-01  -2.634  0.00844 ** 
## OnlineSecurityNo internet service            NA         NA      NA       NA    
## OnlineSecurityYes                    -9.074e-02  1.977e-01  -0.459  0.64625    
## OnlineBackupNo internet service              NA         NA      NA       NA    
## OnlineBackupYes                       1.376e-01  1.949e-01   0.706  0.48017    
## DeviceProtectionNo internet service          NA         NA      NA       NA    
## DeviceProtectionYes                   2.800e-01  1.960e-01   1.428  0.15317    
## TechSupportNo internet service               NA         NA      NA       NA    
## TechSupportYes                       -2.619e-02  2.007e-01  -0.130  0.89619    
## StreamingTVNo internet service               NA         NA      NA       NA    
## StreamingTVYes                        8.437e-01  3.614e-01   2.334  0.01958 *  
## StreamingMoviesNo internet service           NA         NA      NA       NA    
## StreamingMoviesYes                    8.890e-01  3.630e-01   2.449  0.01433 *  
## ContractOne year                     -6.819e-01  1.202e-01  -5.673 1.41e-08 ***
## ContractTwo year                     -1.450e+00  1.981e-01  -7.320 2.49e-13 ***
## PaperlessBillingYes                   3.523e-01  8.316e-02   4.236 2.27e-05 ***
## PaymentMethodCredit card (automatic)  8.669e-04  1.266e-01   0.007  0.99454    
## PaymentMethodElectronic check         2.757e-01  1.052e-01   2.620  0.00878 ** 
## PaymentMethodMailed check            -1.337e-01  1.278e-01  -1.046  0.29562    
## MonthlyCharges                       -6.431e-02  3.521e-02  -1.827  0.06775 .  
## TotalCharges                          3.522e-04  7.966e-05   4.421 9.81e-06 ***
## ---
## Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
## 
## (Dispersion parameter for binomial family taken to be 1)
## 
##     Null deviance: 6517.2  on 5626  degrees of freedom
## Residual deviance: 4660.2  on 5603  degrees of freedom
## AIC: 4708.2
## 
## Number of Fisher Scoring iterations: 6

Evaluar el modelo

# Confusion matrix for training data
train_result <- predict(model, train, type = 'response')
train_result_ <-  ifelse(train_result >= 0.5, 'Yes', 'No')              
train_confusion <- confusionMatrix(factor(train_result_, levels = c('Yes', 'No')), train$Churn)
## Warning in confusionMatrix.default(factor(train_result_, levels = c("Yes", :
## Levels are not in the same order for reference and data. Refactoring data to
## match.
train_confusion
## Confusion Matrix and Statistics
## 
##           Reference
## Prediction   No  Yes
##        No  3707  672
##        Yes  424  824
##                                           
##                Accuracy : 0.8052          
##                  95% CI : (0.7946, 0.8155)
##     No Information Rate : 0.7341          
##     P-Value [Acc > NIR] : < 2.2e-16       
##                                           
##                   Kappa : 0.4732          
##                                           
##  Mcnemar's Test P-Value : 8.593e-14       
##                                           
##             Sensitivity : 0.8974          
##             Specificity : 0.5508          
##          Pos Pred Value : 0.8465          
##          Neg Pred Value : 0.6603          
##              Prevalence : 0.7341          
##          Detection Rate : 0.6588          
##    Detection Prevalence : 0.7782          
##       Balanced Accuracy : 0.7241          
##                                           
##        'Positive' Class : No              
## 
# Confusion matrix for test data
test_result <- predict(model, test, type= 'response')
test_result_ <-  ifelse(test_result >= 0.5, 'Yes', 'No')              
test_confusion <- confusionMatrix(factor(test_result_, levels = c('Yes', 'No')), test$Churn)
## Warning in confusionMatrix.default(factor(test_result_, levels = c("Yes", :
## Levels are not in the same order for reference and data. Refactoring data to
## match.
test_confusion
## Confusion Matrix and Statistics
## 
##           Reference
## Prediction  No Yes
##        No  928 156
##        Yes 104 217
##                                           
##                Accuracy : 0.8149          
##                  95% CI : (0.7936, 0.8349)
##     No Information Rate : 0.7345          
##     P-Value [Acc > NIR] : 8.78e-13        
##                                           
##                   Kappa : 0.5034          
##                                           
##  Mcnemar's Test P-Value : 0.001562        
##                                           
##             Sensitivity : 0.8992          
##             Specificity : 0.5818          
##          Pos Pred Value : 0.8561          
##          Neg Pred Value : 0.6760          
##              Prevalence : 0.7345          
##          Detection Rate : 0.6605          
##    Detection Prevalence : 0.7715          
##       Balanced Accuracy : 0.7405          
##                                           
##        'Positive' Class : No              
## 

Ejemplo de predicción

client_info <- cleaned_df[1, ]
client_info <- client_info %>% select(-Churn)
probabilities <- predict(model, client_info, type = 'response')
probabilities_cat <- ifelse(probabilities >= 0.5, 'Yes', 'No')

probabilities_cat
##     1 
## "Yes"
str(cleaned_df)
## 'data.frame':    7032 obs. of  20 variables:
##  $ gender          : Factor w/ 2 levels "Female","Male": 1 2 2 2 1 1 2 1 1 2 ...
##  $ SeniorCitizen   : int  0 0 0 0 0 0 0 0 0 0 ...
##  $ Partner         : Factor w/ 2 levels "No","Yes": 2 1 1 1 1 1 1 1 2 1 ...
##  $ Dependents      : Factor w/ 2 levels "No","Yes": 1 1 1 1 1 1 2 1 1 2 ...
##  $ tenure          : int  1 34 2 45 2 8 22 10 28 62 ...
##  $ PhoneService    : Factor w/ 2 levels "No","Yes": 1 2 2 1 2 2 2 1 2 2 ...
##  $ MultipleLines   : Factor w/ 3 levels "No","No phone service",..: 2 1 1 2 1 3 3 2 3 1 ...
##  $ InternetService : Factor w/ 3 levels "DSL","Fiber optic",..: 1 1 1 1 2 2 2 1 2 1 ...
##  $ OnlineSecurity  : Factor w/ 3 levels "No","No internet service",..: 1 3 3 3 1 1 1 3 1 3 ...
##  $ OnlineBackup    : Factor w/ 3 levels "No","No internet service",..: 3 1 3 1 1 1 3 1 1 3 ...
##  $ DeviceProtection: Factor w/ 3 levels "No","No internet service",..: 1 3 1 3 1 3 1 1 3 1 ...
##  $ TechSupport     : Factor w/ 3 levels "No","No internet service",..: 1 1 1 3 1 1 1 1 3 1 ...
##  $ StreamingTV     : Factor w/ 3 levels "No","No internet service",..: 1 1 1 1 1 3 3 1 3 1 ...
##  $ StreamingMovies : Factor w/ 3 levels "No","No internet service",..: 1 1 1 1 1 3 1 1 3 1 ...
##  $ Contract        : Factor w/ 3 levels "Month-to-month",..: 1 2 1 2 1 1 1 1 1 2 ...
##  $ PaperlessBilling: Factor w/ 2 levels "No","Yes": 2 1 2 1 2 2 2 1 2 1 ...
##  $ PaymentMethod   : Factor w/ 4 levels "Bank transfer (automatic)",..: 3 4 4 1 3 3 2 4 3 1 ...
##  $ MonthlyCharges  : num  29.9 57 53.9 42.3 70.7 ...
##  $ TotalCharges    : num  29.9 1889.5 108.2 1840.8 151.7 ...
##  $ Churn           : Factor w/ 2 levels "No","Yes": 1 1 2 1 2 2 1 1 2 1 ...
##  - attr(*, "na.action")= 'omit' Named int [1:11] 489 754 937 1083 1341 3332 3827 4381 5219 6671 ...
##   ..- attr(*, "names")= chr [1:11] "489" "754" "937" "1083" ...
LS0tDQp0aXRsZTogIkNodXJuIg0KYXV0aG9yOiAnT3ZlZCBSdWl6IC0gYTAxMTc0NDM1Jw0KZGF0ZTogIjIwMjUtMDItMjUiDQpvdXRwdXQ6IA0KICBodG1sX2RvY3VtZW50Og0KICAgICAgdG9jOiBUUlVFDQogICAgICB0b2NfZmxvYXQ6IFRSVUUNCiAgICAgIGNvZGVfZG93bmxvYWQ6IFRSVUUNCiAgICAgIHRoZW1lOiAnc2ltcGxlJw0KICAgICAgaGlnaGxpZ2h0OiBweWdtZW50DQotLS0NCg0KIVtdKEM6XFxVc2Vyc1xcQUNFUlxcRG93bmxvYWRzXFxPSVAgKDMpLmpwZykNCg0KIyA8c3BhbiBzdHlsZT0iY29sb3I6IExpZ2h0IEJsdWU7Ij5Db250ZXh0bzwvc3Bhbj4NCg0KKipUZWxjbyoqIGVzIHVuYSBlbXByZXNhIGludGVybmFjaW9uYWwgcXVlIHZlbmRlIHVuYSB2YXJpZWRhZCBkZSBzZXJ2aWNpb3MgZGUgdGVsZWNvbXVuaWNhY2lvbmVzLiAqKkNodXJuKiogZXMgdW4gaW5kaWNhZG9yIGRlIGxhIGRlc2VyY2nDs24gZGUgY2xpZW50ZXMsIGZlbsOzbWVubyBlbiBlbCBjdWFsIGxvcyBjbGllbnRlcyBjYW5jZWxhbiBzdXMgc2VydmljaW9zIGNvbiBsYSBlbXByZXNhLiBFbCBvYmpldGl2byBkZSBlc3RlIHRyYWJham8gZXMgcHJlZGVjaXIgc2kgdW4gY2xpZW50ZSBhYmFuZG9uYXLDoSBvIHNlIHF1ZWRhcsOhIGVuIGxhIGVtcHJlc2EuIA0KDQojIDxzcGFuIHN0eWxlPSJjb2xvcjogTGlnaHQgQmx1ZTsiPkluc3RhbGFyIHBhcXVldGVzIHkgbGxhbWFyIGxpYnJlcmlhczwvc3Bhbj4NCg0KYGBge3J9DQpsaWJyYXJ5KHRpZHl2ZXJzZSkNCmxpYnJhcnkoY2FyZXQpDQpsaWJyYXJ5KGUxMDcxKSAjIENvbmZ1c2lvbiBtYXRyaXgNCmBgYA0KDQojIDxzcGFuIHN0eWxlPSJjb2xvcjogTGlnaHQgQmx1ZTsiPkltcG9ydGFyIGxhIGJhc2UgZGUgZGF0b3M8L3NwYW4+DQoNCmBgYHtyfQ0KZGYgPC0gcmVhZC5jc3YoJ0M6XFxVc2Vyc1xcQUNFUlxcRG93bmxvYWRzXFxUZWxjbyBDdXN0b21lciBDaHVybi5jc3YnKQ0KYGBgDQoNCiMgPHNwYW4gc3R5bGU9ImNvbG9yOiBMaWdodCBCbHVlOyI+RW50ZW5kZXIgbGEgYmFzZSBkZSBkYXRvczwvc3Bhbj4NCg0KYGBge3J9DQpzdW1tYXJ5KGRmKQ0KI2hlYWQoZGYpDQojc3RyKGRmKQ0KYGBgDQoNCiMgPHNwYW4gc3R5bGU9ImNvbG9yOiBMaWdodCBCbHVlOyI+UmVncmVzacOzbiBMb2fDrXN0aWNhPC9zcGFuPg0KDQpMYSAqKnJlZ3Jlc2nDs24gbG9nw61zdGljYSoqIGVuIHVuIG1vZGVsbyBxdWUgc2UgdXRpbGl6YSBwYXJhIHByZWRlY2lyIGxhIHByb2JhYmlsaWRhZCBkZSBxdWUgb2N1cnJhIHVuIGV2ZW50bywgYmFzYWRvIGVuIHVuYSBvIG3DoXMgdmFyaWFibGVzIGluZGVwZW5kaWVudGVzLiBBIGRpZmVyZW5jaWEgZGUgbGEgcmVncmVzacOzbiBsb2fDrXN0aWNhIHF1ZSBwcmVkaWNlIHZhbG9yZXMgY29udGludW9zLCBsYSByZWdyc2nDs24gbG9nw61zdGljYSBwcmVkZWNpciByZXN1bHRhZG9zICoqYmluYXJpb3MqKiBvICoqY2F0ZWfDs3JpY29zKiosIGNvbW8gdW4gU2kvTm8sIDEvMCwgdmVyZGFkZXJvL2ZhbHNvLiAgDQoNCg0KU2kgbGEgcHJvYmFiaWxpZGFkIGVzIG1heW9yIHF1ZSAwLjUsIGVsIG1vZGVsbyBwcmVkaWNlIHF1ZSBlbCBldmVudG8gb2N1cnJpcsOhIChFai4gZWwgY2xpZW50ZSBzZSBkYXLDoSBkZSBiYWphKS4gU2kgbGEgcHJvYmFiaWxpZGFkIGVzIG1lbm9yIHF1ZSAwLjUsIHByZWRpY2UgcXVlIGVsIGV2ZW50byBubyBvY3Vycmlyw6EuIEVuIGNhc28gZGUgc2VyIDAuNSwgc2UgY29ub2NlIGNvbW8gdW1icmFsIGRlIGRlY2lzacOzbi4gIA0KDQojIDxzcGFuIHN0eWxlPSJjb2xvcjogTGlnaHQgQmx1ZTsiPlByZXBhcmFyIGxhIGJhc2UgZGUgZGF0b3M8L3NwYW4+DQoNCmBgYHtyfQ0KIyBEcm9wIHVuZWNlc3NhcnkgY29sdW1ucw0KY2xlYW5lZF9kZiA8LSBkZiAlPiUgc2VsZWN0KC1jdXN0b21lcklEKQ0KIyBDb252ZXJ0IGNhdGVnb3JpY2FsIGludG8gZmFjdG9yDQpjbGVhbmVkX2RmIDwtIGNsZWFuZWRfZGYgJT4lIG11dGF0ZShhY3Jvc3Mod2hlcmUoaXMuY2hhcmFjdGVyKSwgYXMuZmFjdG9yKSkNCnN0cihjbGVhbmVkX2RmKQ0KIyBEcm9wIE5Bcw0KY2xlYW5lZF9kZiA8LSBuYS5vbWl0KGNsZWFuZWRfZGYpDQpgYGANCg0KIyA8c3BhbiBzdHlsZT0iY29sb3I6IExpZ2h0IEJsdWU7Ij5FbnRyZW5hciBlbCBtb2RlbG88L3NwYW4+DQoNCmBgYHtyfQ0Kc2V0LnNlZWQoMTIzKQ0KdHJhaW5fcm93cyA8LSBjcmVhdGVEYXRhUGFydGl0aW9uKGNsZWFuZWRfZGYkQ2h1cm4sIHAgPSAwLjgsIGxpc3QgPSBGQUxTRSkNCnRyYWluIDwtIGNsZWFuZWRfZGZbdHJhaW5fcm93cywgXQ0KdGVzdCA8LSBjbGVhbmVkX2RmWy10cmFpbl9yb3dzLCBdDQpgYGANCg0KYGBge3J9DQptb2RlbCA8LSBnbG0oDQogIENodXJuIH4gLiwNCiAgZGF0YSA9IHRyYWluLA0KICBmYW1pbHkgPSBiaW5vbWlhbA0KKQ0Kc3VtbWFyeShtb2RlbCkNCmBgYA0KDQojIDxzcGFuIHN0eWxlPSJjb2xvcjogTGlnaHQgQmx1ZTsiPkV2YWx1YXIgZWwgbW9kZWxvPC9zcGFuPg0KDQpgYGB7cn0NCiMgQ29uZnVzaW9uIG1hdHJpeCBmb3IgdHJhaW5pbmcgZGF0YQ0KdHJhaW5fcmVzdWx0IDwtIHByZWRpY3QobW9kZWwsIHRyYWluLCB0eXBlID0gJ3Jlc3BvbnNlJykNCnRyYWluX3Jlc3VsdF8gPC0gIGlmZWxzZSh0cmFpbl9yZXN1bHQgPj0gMC41LCAnWWVzJywgJ05vJykgICAgICAgICAgICAgIA0KdHJhaW5fY29uZnVzaW9uIDwtIGNvbmZ1c2lvbk1hdHJpeChmYWN0b3IodHJhaW5fcmVzdWx0XywgbGV2ZWxzID0gYygnWWVzJywgJ05vJykpLCB0cmFpbiRDaHVybikNCnRyYWluX2NvbmZ1c2lvbg0KIyBDb25mdXNpb24gbWF0cml4IGZvciB0ZXN0IGRhdGENCnRlc3RfcmVzdWx0IDwtIHByZWRpY3QobW9kZWwsIHRlc3QsIHR5cGU9ICdyZXNwb25zZScpDQp0ZXN0X3Jlc3VsdF8gPC0gIGlmZWxzZSh0ZXN0X3Jlc3VsdCA+PSAwLjUsICdZZXMnLCAnTm8nKSAgICAgICAgICAgICAgDQp0ZXN0X2NvbmZ1c2lvbiA8LSBjb25mdXNpb25NYXRyaXgoZmFjdG9yKHRlc3RfcmVzdWx0XywgbGV2ZWxzID0gYygnWWVzJywgJ05vJykpLCB0ZXN0JENodXJuKQ0KdGVzdF9jb25mdXNpb24NCmBgYA0KDQojIDxzcGFuIHN0eWxlPSJjb2xvcjogTGlnaHQgQmx1ZTsiPkVqZW1wbG8gZGUgcHJlZGljY2nDs248L3NwYW4+DQoNCmBgYHtyfQ0KY2xpZW50X2luZm8gPC0gY2xlYW5lZF9kZlsxLCBdDQpjbGllbnRfaW5mbyA8LSBjbGllbnRfaW5mbyAlPiUgc2VsZWN0KC1DaHVybikNCnByb2JhYmlsaXRpZXMgPC0gcHJlZGljdChtb2RlbCwgY2xpZW50X2luZm8sIHR5cGUgPSAncmVzcG9uc2UnKQ0KcHJvYmFiaWxpdGllc19jYXQgPC0gaWZlbHNlKHByb2JhYmlsaXRpZXMgPj0gMC41LCAnWWVzJywgJ05vJykNCg0KcHJvYmFiaWxpdGllc19jYXQNCmBgYA0KDQpgYGB7cn0NCnN0cihjbGVhbmVkX2RmKQ0KYGBgDQoNCg0KDQoNCg0KDQo=