PCA before Classification

1 Setup

library(dplyr)
library(rsample)
library(recipes)
library(caret)

2 Business Question

  • Tujuan: Membuat model klasifikasi untuk memprediksi apakah seorang karyawan akan resign atau tidak.
  • Fungsi PCA: Data pre-processing sebelum model klasifikasi

Kita akan menggunakan library recipes untuk menerapkan PCA pada data train, lalu data test akan ditransformasi menggunakan eigen vector yang telah dipelajari dari data train.

3 Read Data

attrition <- read.csv("attrition.csv", stringsAsFactors = TRUE)
glimpse(attrition)
## Rows: 1,470
## Columns: 35
## $ attrition                  <fct> yes, no, yes, no, no, no, no, no, no, no, n~
## $ age                        <int> 41, 49, 37, 33, 27, 32, 59, 30, 38, 36, 35,~
## $ business_travel            <fct> travel_rarely, travel_frequently, travel_ra~
## $ daily_rate                 <int> 1102, 279, 1373, 1392, 591, 1005, 1324, 135~
## $ department                 <fct> sales, research_development, research_devel~
## $ distance_from_home         <int> 1, 8, 2, 3, 2, 2, 3, 24, 23, 27, 16, 15, 26~
## $ education                  <int> 2, 1, 2, 4, 1, 2, 3, 1, 3, 3, 3, 2, 1, 2, 3~
## $ education_field            <fct> life_sciences, life_sciences, other, life_s~
## $ employee_count             <int> 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1~
## $ employee_number            <int> 1, 2, 4, 5, 7, 8, 10, 11, 12, 13, 14, 15, 1~
## $ environment_satisfaction   <int> 2, 3, 4, 4, 1, 4, 3, 4, 4, 3, 1, 4, 1, 2, 3~
## $ gender                     <fct> female, male, male, female, male, male, fem~
## $ hourly_rate                <int> 94, 61, 92, 56, 40, 79, 81, 67, 44, 94, 84,~
## $ job_involvement            <int> 3, 2, 2, 3, 3, 3, 4, 3, 2, 3, 4, 2, 3, 3, 2~
## $ job_level                  <int> 2, 2, 1, 1, 1, 1, 1, 1, 3, 2, 1, 2, 1, 1, 1~
## $ job_role                   <fct> sales_executive, research_scientist, labora~
## $ job_satisfaction           <int> 4, 2, 3, 3, 2, 4, 1, 3, 3, 3, 2, 3, 3, 4, 3~
## $ marital_status             <fct> single, married, single, married, married, ~
## $ monthly_income             <int> 5993, 5130, 2090, 2909, 3468, 3068, 2670, 2~
## $ monthly_rate               <int> 19479, 24907, 2396, 23159, 16632, 11864, 99~
## $ num_companies_worked       <int> 8, 1, 6, 1, 9, 0, 4, 1, 0, 6, 0, 0, 1, 0, 5~
## $ over_18                    <fct> y, y, y, y, y, y, y, y, y, y, y, y, y, y, y~
## $ over_time                  <fct> yes, no, yes, yes, no, no, yes, no, no, no,~
## $ percent_salary_hike        <int> 11, 23, 15, 11, 12, 13, 20, 22, 21, 13, 13,~
## $ performance_rating         <int> 3, 4, 3, 3, 3, 3, 4, 4, 4, 3, 3, 3, 3, 3, 3~
## $ relationship_satisfaction  <int> 1, 4, 2, 3, 4, 3, 1, 2, 2, 2, 3, 4, 4, 3, 2~
## $ standard_hours             <int> 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80,~
## $ stock_option_level         <int> 0, 1, 0, 0, 1, 0, 3, 1, 0, 2, 1, 0, 1, 1, 0~
## $ total_working_years        <int> 8, 10, 7, 8, 6, 8, 12, 1, 10, 17, 6, 10, 5,~
## $ training_times_last_year   <int> 0, 3, 3, 3, 3, 2, 3, 2, 2, 3, 5, 3, 1, 2, 4~
## $ work_life_balance          <int> 1, 3, 3, 3, 3, 2, 2, 3, 3, 2, 3, 3, 2, 3, 3~
## $ years_at_company           <int> 6, 10, 0, 8, 2, 7, 1, 1, 9, 7, 5, 9, 5, 2, ~
## $ years_in_current_role      <int> 4, 7, 0, 7, 2, 7, 0, 0, 7, 7, 4, 5, 2, 2, 2~
## $ years_since_last_promotion <int> 0, 1, 0, 3, 2, 3, 0, 0, 1, 7, 0, 0, 4, 1, 0~
## $ years_with_curr_manager    <int> 5, 7, 0, 0, 2, 6, 0, 0, 8, 7, 3, 8, 3, 2, 3~

4 Data Inspection

4.1 Predictor

Inspeksi banyaknya nilai unique dan tipe data untuk setiap kolom.

attrition %>% 
  summarise_all(n_distinct) %>% 
  t() %>% 
  as.data.frame() %>% 
  rename(n_distinct = V1) %>% 
  cbind(data_type = sapply(attrition, class)) %>% 
  arrange(-n_distinct) 

Kolom yang perlu dihapus:

  • Kolom employee_number tidak cocok untuk menjadi prediktor karena unik untuk setiap baris.
  • Kolom employee_count, over_18, dan standard_hours tidak cocok untuk menjadi prediktor karena hanya memiliki 1 nilai unik.

Ke-empat kolom tersebut akan dihapus di tahap Data Preparation

4.2 Target

Cek proporsi target

prop.table(table(attrition$attrition))
## 
##        no       yes 
## 0.8387755 0.1612245

Proporsi sedikit tidak seimbang. Pada tahap Data Preparation, kita akan coba lakukan upsampling.

5 Cross Validation

Sebelum melakukan PCA terlebih dahulu dilakukan cross validation, yaitu membagi data menjadi training set untuk proses pemodelan dan testing set untuk melakukan evaluasi. Namun, data train dan data test tidak langsung dimasukkan ke dalam sebuah objek melainkan dilakukan PCA terlebih dahulu.

Cross validation akan dilakukan dengan menggunakan fungsi initial_split() dari library rsample. Fungsi tersebut akan melakukan proses sampling untuk cross validation dengan metode stratified random sampling, sehingga proporsi target variabel pada data awal, akan dipertahankan baik pada training set maupun testing set.

set.seed(123)
splitted <- initial_split(data = attrition,
                          prop = 0.75, # persentase data train
                          strata = "attrition") # mempertahankan proporsi target
splitted
## <Analysis/Assess/Total>
## <1103/367/1470>

6 Data Preparation

Melakukan tahapan data preparation yang didalamnya termasuk melakukan PCA. Data preparation yang akan dilakukan adalah menghapus variabel yang dianggap tidak berpengaruh, membuang variabel yang variansinya mendekati 0 (tidak informatif), melakukan scaling, dan melakukan PCA. Proses yang dilakukan pada tahapan data preparation akan dilakukan dengan menggunakan fungsi dari library recipes, yaitu:

  • step_rm() untuk menghapus variabel
  • step_nzv() untuk membuang variabel yang variansinya mendekati 0
  • step_center() dan step_scale() untuk melakukan scaling
  • step_pca() untuk melakukan PCA, threshold = 0.9 maksudnya ingin mempertahankan minimal 90% informasi data awal
  • step_upsample() untuk melakukan upsampling pada kolom target, yaitu attrition. Parameter over_ratio mengatur rasio setelah sampling antara kelas mayoritas dibandingkan minoritas.
rec <- recipe(formula = attrition ~ ., data = training(splitted)) %>% 
  step_rm(employee_number) %>% # dihapus karena semua unique
  step_nzv(all_predictors()) %>% # dihapus karena hanya mengandung 1 nilai
  step_center(all_numeric()) %>% 
  step_scale(all_numeric()) %>% 
  step_pca(all_numeric(), threshold = 0.9) %>% 
  step_upsample(attrition) %>% 
  prep()

rec
## Data Recipe
## 
## Inputs:
## 
##       role #variables
##    outcome          1
##  predictor         34
## 
## Training data contained 1103 data points and no missing data.
## 
## Operations:
## 
## Variables removed employee_number [trained]
## Sparse, unbalanced variable filter removed employee_count, over_18, standard_hours [trained]
## Centering for age, daily_rate, distance_from_home, ... [trained]
## Scaling for age, daily_rate, distance_from_home, ... [trained]
## PCA extraction with age, daily_rate, distance_from_home, ... [trained]
## Up-sampling based on attrition [trained]

Setelah mendefinisikan proses data preparation pada objek rec, selanjutnya proses tersebut diterapkan ke data train menggunakan fungsi juice() dan ke data test menggunakan fungsi bake() dari library recipes.

# data train
attrition_train <- juice(rec)
head(attrition_train, 5)
# data test
attrition_test <- bake(rec, new_data = testing(splitted))
head(attrition_test, 5)

Dari output di atas diketahui bahwa variabel numerik sudah berbentuk sebuah PC, yang merangkum minimal 90% variansi data awal. Selanjutnya, data train dan test sudah siap untuk dilanjutkan ke tahap modeling.

Recap:

  • Dimensi data awal attrition: 26 numerik dan 9 kategorik (total: 35)
  • Dimensi data sebelum PCA (menghapus 4 kolom): 23 numerik dan 8 kategorik (total: 31)
  • Dimensi data setelah PCA: 16 numerik dan 8 kategorik (total: 24)
  • PCA mereduksi 23 kolom numerik menjadi 16 kolom dengan merangkum minimal 90% variansi data awal

7 Modeling

Kita coba menggunakan model Random Forest karena ingin mendapatkan hasil prediksi yang setepat mungkin, saat ini belum membutuhkan interpretasi model.

set.seed(123)
attrition_rf <- train(attrition ~ .,
                      data = attrition_train,
                      method = "rf")
saveRDS(attrition_rf, "attrition_rf.RDS")
attrition_rf <- readRDS("attrition_rf.RDS")
attrition_rf
## Random Forest 
## 
## 1850 samples
##   23 predictor
##    2 classes: 'no', 'yes' 
## 
## No pre-processing
## Resampling: Bootstrapped (25 reps) 
## Summary of sample sizes: 1850, 1850, 1850, 1850, 1850, 1850, ... 
## Resampling results across tuning parameters:
## 
##   mtry  Accuracy   Kappa    
##    2    0.9764871  0.9529251
##   19    0.9535222  0.9070345
##   37    0.9405830  0.8812178
## 
## Accuracy was used to select the optimal model using the largest value.
## The final value used for the model was mtry = 2.

8 Model Evaluation

  • Kelas Positive: attrition = "yes"
  • Kasus False Positive: Karyawan yang sebenarnya tidak resign, diprediksi akan resign
  • Kasus False Negative: Karyawan yang sebenarnya resign, diprediksi akan tidak resign

Misalnya untuk semua karyawan yang diprediksi resign akan dievaluasi kembali kinerjanya dan ada kemungkinan untuk meningkatkan benefit karyawan tersebut. Sedangkan untuk yang diprediksi tidak resign maka tidak akan dilakukan treatment apapun. Dalam kasus ini, kita ingin meminimalisir kasus False Negative. Maka dari itu, kita ingin recall setinggi mungkin.

Evaluasi model di data training

confusionMatrix(data = predict(attrition_rf, attrition_train),
                reference = attrition_train$attrition,
                positive = "yes",
                mode = "prec_recall")
## Confusion Matrix and Statistics
## 
##           Reference
## Prediction  no yes
##        no  925   0
##        yes   0 925
##                                                
##                Accuracy : 1                    
##                  95% CI : (0.998, 1)           
##     No Information Rate : 0.5                  
##     P-Value [Acc > NIR] : < 0.00000000000000022
##                                                
##                   Kappa : 1                    
##                                                
##  Mcnemar's Test P-Value : NA                   
##                                                
##               Precision : 1.0                  
##                  Recall : 1.0                  
##                      F1 : 1.0                  
##              Prevalence : 0.5                  
##          Detection Rate : 0.5                  
##    Detection Prevalence : 0.5                  
##       Balanced Accuracy : 1.0                  
##                                                
##        'Positive' Class : yes                  
## 

Evaluasi model di data testing

confusionMatrix(data = predict(attrition_rf, attrition_test),
                reference = attrition_test$attrition,
                positive = "yes",
                mode = "prec_recall")
## Confusion Matrix and Statistics
## 
##           Reference
## Prediction  no yes
##        no  305  46
##        yes   3  13
##                                           
##                Accuracy : 0.8665          
##                  95% CI : (0.8274, 0.8996)
##     No Information Rate : 0.8392          
##     P-Value [Acc > NIR] : 0.08614         
##                                           
##                   Kappa : 0.2986          
##                                           
##  Mcnemar's Test P-Value : 0.000000001973  
##                                           
##               Precision : 0.81250         
##                  Recall : 0.22034         
##                      F1 : 0.34667         
##              Prevalence : 0.16076         
##          Detection Rate : 0.03542         
##    Detection Prevalence : 0.04360         
##       Balanced Accuracy : 0.60530         
##                                           
##        'Positive' Class : yes             
## 

9 Conclusion

Model Random Forest yang dihasilkan masih overfit, karena performa di data train perfect namun di data test masih belum memuaskan. Fokus pada artikel ini adalah untuk memberikan contoh bagaimana PCA dapat diimplementasi sebelum dilakukan model supervised learning, dalam kasus ini adalah klasifikasi.

LS0tDQp0aXRsZTogIlBDQSBiZWZvcmUgQ2xhc3NpZmljYXRpb24iDQphdXRob3I6ICJUb215IFRqYW5kcmEiDQpkYXRlOiAiSnVuZSA2LCAyMDIyIg0Kb3V0cHV0Og0KICBybWRmb3JtYXRzOjpyZWFkdGhlZG93bjoNCiAgICBudW1iZXJfc2VjdGlvbnM6IHRydWUNCiAgICBoaWdobGlnaHQ6IHRhbmdvDQogICAgdG9jX2Zsb2F0Og0KICAgICAgY29sbGFwc2VkOiBmYWxzZQ0KICAgIGNvZGVfZG93bmxvYWQ6IHRydWUNCiAgICBkZl9wcmludDogcGFnZWQNCi0tLQ0KDQo8c3R5bGU+DQpib2R5IHsNCnRleHQtYWxpZ246IGp1c3RpZnl9DQo8L3N0eWxlPg0KDQojIFNldHVwDQoNCmBgYHtyIHNldHVwLCBlY2hvPUZBTFNFfQ0KIyBjbGVhci11cCB0aGUgZW52aXJvbm1lbnQNCnJtKGxpc3QgPSBscygpKQ0KDQojIGNodW5rIG9wdGlvbnMNCmtuaXRyOjpvcHRzX2NodW5rJHNldCgNCiAgbWVzc2FnZSA9IEZBTFNFLA0KICB3YXJuaW5nID0gRkFMU0UsDQogIGZpZy5hbGlnbiA9ICJjZW50ZXIiDQopDQoNCm9wdGlvbnMoc2NpcGVuID0gOTk5KQ0KYGBgDQoNCmBgYHtyfQ0KbGlicmFyeShkcGx5cikNCmxpYnJhcnkocnNhbXBsZSkNCmxpYnJhcnkocmVjaXBlcykNCmxpYnJhcnkoY2FyZXQpDQpgYGANCg0KIyBCdXNpbmVzcyBRdWVzdGlvbg0KDQotIFR1anVhbjogTWVtYnVhdCBtb2RlbCBrbGFzaWZpa2FzaSB1bnR1ayBtZW1wcmVkaWtzaSBhcGFrYWggc2VvcmFuZyBrYXJ5YXdhbiBha2FuIHJlc2lnbiBhdGF1IHRpZGFrLg0KLSBGdW5nc2kgUENBOiBEYXRhIHByZS1wcm9jZXNzaW5nIHNlYmVsdW0gbW9kZWwga2xhc2lmaWthc2kNCg0KPiBLaXRhIGFrYW4gbWVuZ2d1bmFrYW4gbGlicmFyeSBgcmVjaXBlc2AgdW50dWsgbWVuZXJhcGthbiBQQ0EgcGFkYSBkYXRhIHRyYWluLCBsYWx1IGRhdGEgdGVzdCBha2FuIGRpdHJhbnNmb3JtYXNpIG1lbmdndW5ha2FuIGVpZ2VuIHZlY3RvciB5YW5nIHRlbGFoIGRpcGVsYWphcmkgZGFyaSBkYXRhIHRyYWluLg0KDQojIFJlYWQgRGF0YQ0KDQpgYGB7ciwgZWNobz1GQUxTRX0NCmxpYnJhcnkoZG93bmxvYWR0aGlzKQ0KDQpyZWFkLmNzdigiYXR0cml0aW9uLmNzdiIpICU+JQ0KICBkb3dubG9hZF90aGlzKA0KICAgIG91dHB1dF9uYW1lID0gImF0dHJpdGlvbiIsDQogICAgb3V0cHV0X2V4dGVuc2lvbiA9ICIuY3N2IiwNCiAgICBidXR0b25fbGFiZWwgPSAiQ2xpY2sgdG8gZG93bmxvYWQgYXR0cml0aW9uIGRhdGEiLA0KICAgIGJ1dHRvbl90eXBlID0gImRlZmF1bHQiLA0KICAgIGljb24gPSAiZmEgZmEtc2F2ZSIsDQogICAgc2VsZl9jb250YWluZWQgPSBUUlVFDQogICkNCmBgYA0KDQpgYGB7cn0NCmF0dHJpdGlvbiA8LSByZWFkLmNzdigiYXR0cml0aW9uLmNzdiIsIHN0cmluZ3NBc0ZhY3RvcnMgPSBUUlVFKQ0KZ2xpbXBzZShhdHRyaXRpb24pDQpgYGANCg0KIyBEYXRhIEluc3BlY3Rpb24NCg0KIyMgUHJlZGljdG9yDQoNCkluc3Bla3NpIGJhbnlha255YSBuaWxhaSB1bmlxdWUgZGFuIHRpcGUgZGF0YSB1bnR1ayBzZXRpYXAga29sb20uDQoNCmBgYHtyfQ0KYXR0cml0aW9uICU+JSANCiAgc3VtbWFyaXNlX2FsbChuX2Rpc3RpbmN0KSAlPiUgDQogIHQoKSAlPiUgDQogIGFzLmRhdGEuZnJhbWUoKSAlPiUgDQogIHJlbmFtZShuX2Rpc3RpbmN0ID0gVjEpICU+JSANCiAgY2JpbmQoZGF0YV90eXBlID0gc2FwcGx5KGF0dHJpdGlvbiwgY2xhc3MpKSAlPiUgDQogIGFycmFuZ2UoLW5fZGlzdGluY3QpIA0KYGBgDQoNCioqS29sb20geWFuZyBwZXJsdSBkaWhhcHVzOioqDQoNCi0gS29sb20gYGVtcGxveWVlX251bWJlcmAgdGlkYWsgY29jb2sgdW50dWsgbWVuamFkaSBwcmVkaWt0b3Iga2FyZW5hIHVuaWsgdW50dWsgc2V0aWFwIGJhcmlzLg0KLSBLb2xvbSBgZW1wbG95ZWVfY291bnRgLCBgb3Zlcl8xOGAsIGRhbiBgc3RhbmRhcmRfaG91cnNgIHRpZGFrIGNvY29rIHVudHVrIG1lbmphZGkgcHJlZGlrdG9yIGthcmVuYSBoYW55YSBtZW1pbGlraSAxIG5pbGFpIHVuaWsuDQoNCj4gS2UtZW1wYXQga29sb20gdGVyc2VidXQgYWthbiBkaWhhcHVzIGRpIHRhaGFwIERhdGEgUHJlcGFyYXRpb24NCg0KIyMgVGFyZ2V0DQoNCkNlayBwcm9wb3JzaSB0YXJnZXQNCg0KYGBge3J9DQpwcm9wLnRhYmxlKHRhYmxlKGF0dHJpdGlvbiRhdHRyaXRpb24pKQ0KYGBgDQoNCj4gUHJvcG9yc2kgc2VkaWtpdCB0aWRhayBzZWltYmFuZy4gUGFkYSB0YWhhcCBEYXRhIFByZXBhcmF0aW9uLCBraXRhIGFrYW4gY29iYSBsYWt1a2FuIHVwc2FtcGxpbmcuDQoNCiMgQ3Jvc3MgVmFsaWRhdGlvbg0KDQpTZWJlbHVtIG1lbGFrdWthbiBQQ0EgdGVybGViaWggZGFodWx1IGRpbGFrdWthbiBjcm9zcyB2YWxpZGF0aW9uLCB5YWl0dSBtZW1iYWdpIGRhdGEgbWVuamFkaSAqKnRyYWluaW5nIHNldCoqIHVudHVrIHByb3NlcyBwZW1vZGVsYW4gZGFuICoqdGVzdGluZyBzZXQqKiB1bnR1ayBtZWxha3VrYW4gZXZhbHVhc2kuIE5hbXVuLCBkYXRhIHRyYWluIGRhbiBkYXRhIHRlc3QgdGlkYWsgbGFuZ3N1bmcgZGltYXN1a2thbiBrZSBkYWxhbSBzZWJ1YWggb2JqZWsgbWVsYWlua2FuIGRpbGFrdWthbiBQQ0EgdGVybGViaWggZGFodWx1Lg0KDQpDcm9zcyB2YWxpZGF0aW9uIGFrYW4gZGlsYWt1a2FuIGRlbmdhbiBtZW5nZ3VuYWthbiBmdW5nc2kgYGluaXRpYWxfc3BsaXQoKWAgZGFyaSBsaWJyYXJ5IGByc2FtcGxlYC4gRnVuZ3NpIHRlcnNlYnV0IGFrYW4gbWVsYWt1a2FuIHByb3NlcyBzYW1wbGluZyB1bnR1ayBjcm9zcyB2YWxpZGF0aW9uIGRlbmdhbiBtZXRvZGUgKipzdHJhdGlmaWVkIHJhbmRvbSBzYW1wbGluZyoqLCBzZWhpbmdnYSBwcm9wb3JzaSB0YXJnZXQgdmFyaWFiZWwgcGFkYSBkYXRhIGF3YWwsIGFrYW4gZGlwZXJ0YWhhbmthbiBiYWlrIHBhZGEgdHJhaW5pbmcgc2V0IG1hdXB1biB0ZXN0aW5nIHNldC4NCg0KYGBge3J9DQpzZXQuc2VlZCgxMjMpDQpzcGxpdHRlZCA8LSBpbml0aWFsX3NwbGl0KGRhdGEgPSBhdHRyaXRpb24sDQogICAgICAgICAgICAgICAgICAgICAgICAgIHByb3AgPSAwLjc1LCAjIHBlcnNlbnRhc2UgZGF0YSB0cmFpbg0KICAgICAgICAgICAgICAgICAgICAgICAgICBzdHJhdGEgPSAiYXR0cml0aW9uIikgIyBtZW1wZXJ0YWhhbmthbiBwcm9wb3JzaSB0YXJnZXQNCnNwbGl0dGVkDQpgYGANCg0KIyBEYXRhIFByZXBhcmF0aW9uDQoNCk1lbGFrdWthbiB0YWhhcGFuIGRhdGEgcHJlcGFyYXRpb24geWFuZyBkaWRhbGFtbnlhIHRlcm1hc3VrIG1lbGFrdWthbiBQQ0EuIERhdGEgcHJlcGFyYXRpb24geWFuZyBha2FuIGRpbGFrdWthbiBhZGFsYWggbWVuZ2hhcHVzIHZhcmlhYmVsIHlhbmcgZGlhbmdnYXAgdGlkYWsgYmVycGVuZ2FydWgsIG1lbWJ1YW5nIHZhcmlhYmVsIHlhbmcgdmFyaWFuc2lueWEgbWVuZGVrYXRpIDAgKHRpZGFrIGluZm9ybWF0aWYpLCBtZWxha3VrYW4gc2NhbGluZywgZGFuIG1lbGFrdWthbiBQQ0EuIFByb3NlcyB5YW5nIGRpbGFrdWthbiBwYWRhIHRhaGFwYW4gZGF0YSBwcmVwYXJhdGlvbiBha2FuIGRpbGFrdWthbiBkZW5nYW4gbWVuZ2d1bmFrYW4gZnVuZ3NpIGRhcmkgbGlicmFyeSBgcmVjaXBlc2AsIHlhaXR1Og0KDQotIGBzdGVwX3JtKClgIHVudHVrIG1lbmdoYXB1cyB2YXJpYWJlbA0KLSBgc3RlcF9uenYoKWAgdW50dWsgbWVtYnVhbmcgdmFyaWFiZWwgeWFuZyB2YXJpYW5zaW55YSBtZW5kZWthdGkgMA0KLSBgc3RlcF9jZW50ZXIoKWAgZGFuIGBzdGVwX3NjYWxlKClgIHVudHVrIG1lbGFrdWthbiBzY2FsaW5nDQotIGBzdGVwX3BjYSgpYCB1bnR1ayBtZWxha3VrYW4gUENBLCBgdGhyZXNob2xkID0gMC45YCBtYWtzdWRueWEgaW5naW4gbWVtcGVydGFoYW5rYW4gbWluaW1hbCA5MCUgaW5mb3JtYXNpIGRhdGEgYXdhbA0KLSBgc3RlcF91cHNhbXBsZSgpYCB1bnR1ayBtZWxha3VrYW4gdXBzYW1wbGluZyBwYWRhIGtvbG9tIHRhcmdldCwgeWFpdHUgYGF0dHJpdGlvbmAuIFBhcmFtZXRlciBgb3Zlcl9yYXRpb2AgbWVuZ2F0dXIgcmFzaW8gc2V0ZWxhaCBzYW1wbGluZyBhbnRhcmEga2VsYXMgbWF5b3JpdGFzIGRpYmFuZGluZ2thbiBtaW5vcml0YXMuDQoNCmBgYHtyfQ0KcmVjIDwtIHJlY2lwZShmb3JtdWxhID0gYXR0cml0aW9uIH4gLiwgZGF0YSA9IHRyYWluaW5nKHNwbGl0dGVkKSkgJT4lIA0KICBzdGVwX3JtKGVtcGxveWVlX251bWJlcikgJT4lICMgZGloYXB1cyBrYXJlbmEgc2VtdWEgdW5pcXVlDQogIHN0ZXBfbnp2KGFsbF9wcmVkaWN0b3JzKCkpICU+JSAjIGRpaGFwdXMga2FyZW5hIGhhbnlhIG1lbmdhbmR1bmcgMSBuaWxhaQ0KICBzdGVwX2NlbnRlcihhbGxfbnVtZXJpYygpKSAlPiUgDQogIHN0ZXBfc2NhbGUoYWxsX251bWVyaWMoKSkgJT4lIA0KICBzdGVwX3BjYShhbGxfbnVtZXJpYygpLCB0aHJlc2hvbGQgPSAwLjkpICU+JSANCiAgc3RlcF91cHNhbXBsZShhdHRyaXRpb24pICU+JSANCiAgcHJlcCgpDQoNCnJlYw0KYGBgDQoNClNldGVsYWggbWVuZGVmaW5pc2lrYW4gcHJvc2VzIGRhdGEgcHJlcGFyYXRpb24gcGFkYSBvYmplayBgcmVjYCwgc2VsYW5qdXRueWEgcHJvc2VzIHRlcnNlYnV0IGRpdGVyYXBrYW4ga2UgZGF0YSB0cmFpbiBtZW5nZ3VuYWthbiBmdW5nc2kgYGp1aWNlKClgIGRhbiBrZSBkYXRhIHRlc3QgbWVuZ2d1bmFrYW4gZnVuZ3NpIGBiYWtlKClgIGRhcmkgbGlicmFyeSBgcmVjaXBlc2AuDQoNCmBgYHtyfQ0KIyBkYXRhIHRyYWluDQphdHRyaXRpb25fdHJhaW4gPC0ganVpY2UocmVjKQ0KaGVhZChhdHRyaXRpb25fdHJhaW4sIDUpDQpgYGANCg0KYGBge3J9DQojIGRhdGEgdGVzdA0KYXR0cml0aW9uX3Rlc3QgPC0gYmFrZShyZWMsIG5ld19kYXRhID0gdGVzdGluZyhzcGxpdHRlZCkpDQpoZWFkKGF0dHJpdGlvbl90ZXN0LCA1KQ0KYGBgDQoNCkRhcmkgb3V0cHV0IGRpIGF0YXMgZGlrZXRhaHVpIGJhaHdhIHZhcmlhYmVsIG51bWVyaWsgc3VkYWggYmVyYmVudHVrIHNlYnVhaCBQQywgeWFuZyBtZXJhbmdrdW0gbWluaW1hbCA5MCUgdmFyaWFuc2kgZGF0YSBhd2FsLiBTZWxhbmp1dG55YSwgZGF0YSBgdHJhaW5gIGRhbiBgdGVzdGAgc3VkYWggc2lhcCB1bnR1ayBkaWxhbmp1dGthbiBrZSB0YWhhcCBtb2RlbGluZy4NCg0KKipSZWNhcDoqKg0KDQotIERpbWVuc2kgZGF0YSBhd2FsIGBhdHRyaXRpb25gOiAyNiBudW1lcmlrIGRhbiA5IGthdGVnb3JpayAodG90YWw6IDM1KQ0KLSBEaW1lbnNpIGRhdGEgc2ViZWx1bSBQQ0EgKG1lbmdoYXB1cyA0IGtvbG9tKTogMjMgbnVtZXJpayBkYW4gOCBrYXRlZ29yaWsgKHRvdGFsOiAzMSkNCi0gRGltZW5zaSBkYXRhIHNldGVsYWggUENBOiAxNiBudW1lcmlrIGRhbiA4IGthdGVnb3JpayAodG90YWw6IDI0KQ0KLSBQQ0EgbWVyZWR1a3NpIDIzIGtvbG9tIG51bWVyaWsgbWVuamFkaSAxNiBrb2xvbSBkZW5nYW4gbWVyYW5na3VtIG1pbmltYWwgOTAlIHZhcmlhbnNpIGRhdGEgYXdhbA0KDQojIE1vZGVsaW5nDQoNCktpdGEgY29iYSBtZW5nZ3VuYWthbiBtb2RlbCBSYW5kb20gRm9yZXN0IGthcmVuYSBpbmdpbiBtZW5kYXBhdGthbiBoYXNpbCBwcmVkaWtzaSB5YW5nIHNldGVwYXQgbXVuZ2tpbiwgc2FhdCBpbmkgYmVsdW0gbWVtYnV0dWhrYW4gaW50ZXJwcmV0YXNpIG1vZGVsLg0KDQpgYGB7ciwgZXZhbD1GQUxTRX0NCnNldC5zZWVkKDEyMykNCmF0dHJpdGlvbl9yZiA8LSB0cmFpbihhdHRyaXRpb24gfiAuLA0KICAgICAgICAgICAgICAgICAgICAgIGRhdGEgPSBhdHRyaXRpb25fdHJhaW4sDQogICAgICAgICAgICAgICAgICAgICAgbWV0aG9kID0gInJmIikNCnNhdmVSRFMoYXR0cml0aW9uX3JmLCAiYXR0cml0aW9uX3JmLlJEUyIpDQpgYGANCg0KYGBge3J9DQphdHRyaXRpb25fcmYgPC0gcmVhZFJEUygiYXR0cml0aW9uX3JmLlJEUyIpDQphdHRyaXRpb25fcmYNCmBgYA0KDQojIE1vZGVsIEV2YWx1YXRpb24NCg0KLSBLZWxhcyBQb3NpdGl2ZTogYGF0dHJpdGlvbiA9ICJ5ZXMiYA0KLSBLYXN1cyBGYWxzZSBQb3NpdGl2ZTogS2FyeWF3YW4geWFuZyBzZWJlbmFybnlhIHRpZGFrIHJlc2lnbiwgZGlwcmVkaWtzaSBha2FuIHJlc2lnbg0KLSBLYXN1cyBGYWxzZSBOZWdhdGl2ZTogS2FyeWF3YW4geWFuZyBzZWJlbmFybnlhIHJlc2lnbiwgZGlwcmVkaWtzaSBha2FuIHRpZGFrIHJlc2lnbg0KDQo+IE1pc2FsbnlhIHVudHVrIHNlbXVhIGthcnlhd2FuIHlhbmcgZGlwcmVkaWtzaSByZXNpZ24gYWthbiBkaWV2YWx1YXNpIGtlbWJhbGkga2luZXJqYW55YSBkYW4gYWRhIGtlbXVuZ2tpbmFuIHVudHVrIG1lbmluZ2thdGthbiBiZW5lZml0IGthcnlhd2FuIHRlcnNlYnV0LiBTZWRhbmdrYW4gdW50dWsgeWFuZyBkaXByZWRpa3NpIHRpZGFrIHJlc2lnbiBtYWthIHRpZGFrIGFrYW4gZGlsYWt1a2FuIHRyZWF0bWVudCBhcGFwdW4uIERhbGFtIGthc3VzIGluaSwga2l0YSBpbmdpbiBtZW1pbmltYWxpc2lyIGthc3VzIEZhbHNlIE5lZ2F0aXZlLiBNYWthIGRhcmkgaXR1LCBraXRhIGluZ2luIHJlY2FsbCBzZXRpbmdnaSBtdW5na2luLg0KDQoqKkV2YWx1YXNpIG1vZGVsIGRpIGRhdGEgdHJhaW5pbmcqKg0KDQpgYGB7cn0NCmNvbmZ1c2lvbk1hdHJpeChkYXRhID0gcHJlZGljdChhdHRyaXRpb25fcmYsIGF0dHJpdGlvbl90cmFpbiksDQogICAgICAgICAgICAgICAgcmVmZXJlbmNlID0gYXR0cml0aW9uX3RyYWluJGF0dHJpdGlvbiwNCiAgICAgICAgICAgICAgICBwb3NpdGl2ZSA9ICJ5ZXMiLA0KICAgICAgICAgICAgICAgIG1vZGUgPSAicHJlY19yZWNhbGwiKQ0KYGBgDQoNCioqRXZhbHVhc2kgbW9kZWwgZGkgZGF0YSB0ZXN0aW5nKioNCg0KYGBge3J9DQpjb25mdXNpb25NYXRyaXgoZGF0YSA9IHByZWRpY3QoYXR0cml0aW9uX3JmLCBhdHRyaXRpb25fdGVzdCksDQogICAgICAgICAgICAgICAgcmVmZXJlbmNlID0gYXR0cml0aW9uX3Rlc3QkYXR0cml0aW9uLA0KICAgICAgICAgICAgICAgIHBvc2l0aXZlID0gInllcyIsDQogICAgICAgICAgICAgICAgbW9kZSA9ICJwcmVjX3JlY2FsbCIpDQpgYGANCg0KIyBDb25jbHVzaW9uDQoNCk1vZGVsIFJhbmRvbSBGb3Jlc3QgeWFuZyBkaWhhc2lsa2FuIG1hc2loIG92ZXJmaXQsIGthcmVuYSBwZXJmb3JtYSBkaSBkYXRhIHRyYWluIHBlcmZlY3QgbmFtdW4gZGkgZGF0YSB0ZXN0IG1hc2loIGJlbHVtIG1lbXVhc2thbi4gRm9rdXMgcGFkYSBhcnRpa2VsIGluaSBhZGFsYWggdW50dWsgbWVtYmVyaWthbiBjb250b2ggYmFnYWltYW5hIFBDQSBkYXBhdCBkaWltcGxlbWVudGFzaSBzZWJlbHVtIGRpbGFrdWthbiBtb2RlbCBzdXBlcnZpc2VkIGxlYXJuaW5nLCBkYWxhbSBrYXN1cyBpbmkgYWRhbGFoIGtsYXNpZmlrYXNpLg0K