Motivation

Cuộc thi Zillow’s Home Value Prediction có tổng giải thưởng lên đến $1,200,000. Bộ dữ liệu này ngay cả khi đã nén có dung lượng 1.37 GB. Để test khả năng dự báo của các mô hình khác nhau chúng ta có thể sử dụng bộ dữ liệu bé hơn và đơn giản hơn là Boston Housing Dataset trước khi “chơi” với bộ dữ liệu siêu lớn của cuộc thi.

Trước hết load và xử lí qua data:

# Clear R environment: 

rm(list = ls())

# Load data: 

read.table("https://raw.githubusercontent.com/rupakc/UCI-Data-Analysis/master/Boston%20Housing%20Dataset/Boston%20Housing/housing.data", header = FALSE) -> bostonData


# Load some R packages: 

library(dplyr) # R package for data wrangling. 
library(viridis)
library(ggpmisc)
library(ggplot2)
library(caret) # For training ML models. 
library(caretEnsemble) # For training and comparing ML models. 

# Rename for all features (https://search.r-project.org/CRAN/refmans/A3/html/housing.html): 

names(bostonData) <- c("CRIM", "ZN", "INDUS", "CHAS", "NOX", "RM", "AGE", "DIS", 
                       "RAD", "TAX", "PTRATIO", "B", "LSTAT", "MEDV")

Experimental Design

Để đánh giá và so sánh khả năng dự báo, 12 mô hình Machine Learning (ML) khác nhau được chọn để thử nghiệm. Bộ dữ liệu ban đầu được chia thành tỉ lệ 80 - 20 trong đó 80% dữ liệu là training + validation, 20% còn lại là test data để đánh giá ngược lại khả năng dự báo của 12 mô hình ML khác nhau. Dưới đây là R codes:

# Select some ML models for predicting housing price: 

my_models <- c("lm", 
               "knn", 
               "bam", 
               "gam", 
               "treebag",
               "gbm",
               "rf", 
               "ranger", 
               "ridge", 
               "glmStepAIC", 
               "gamSpline",
               "xgbLinear")
# Split data: 

n_total <- nrow(bostonData)

set.seed(29)

id <- sample(1:n_total, size = 0.8*n_total, replace = FALSE)

dfTrain <- bostonData[id, ] # 80% for training.  

dfTest <- bostonData[-id, ] # 20% for evaluating and comparing ML models.

Sử dụng K-fold Cross-Validation với K = 4, lặp lại 10 lần để đánh giá và so sánh khả năng dự báo của 12 mô hình ML khác nhau căn cứ vào tiêu chuẩn R-Squared. Dưới đây là R codes:

# Define configuration for training and comparing ML models: 

set.seed(29)
number <- 4
repeats <- 10
control <- trainControl(method = "repeatedcv", 
                        number = number , 
                        repeats = repeats, 
                        savePredictions = "final", 
                        index = createResample(dfTrain$MEDV, repeats*number),
                        allowParallel = TRUE)


# Train these ML Models: 

set.seed(1)
system.time(model_list1 <- caretList(MEDV ~., 
                                     data = dfTrain,
                                     trControl = control,
                                     metric = "Rsquared",
                                     methodList = my_models))

# Extract results for comparing among ML models: 

list_of_results <- lapply(my_models, function(x) {model_list1[[x]]$resample})

total_df <- do.call("bind_rows", list_of_results)

total_df %>% mutate(Model = lapply(my_models, function(x) {rep(x, number*repeats)}) %>% unlist()) -> total_df

Empirical Results

Random Forest là mô hình có khả năng dự báo chính xác nhất với trung bình R-Squared (trên 40 mẫu dữ liệu validation) là 85.9% (Table 1). Với mô hình hồi quy tuyến tính cổ điển thì con số này là 70.4%. Dưới đây là R codes:

total_df %>% 
  group_by(Model) %>% 
  summarise(avg.R2 = mean(Rsquared, na.rm = TRUE), SD = sd(Rsquared, na.rm = TRUE), MIN = min(Rsquared, na.rm = TRUE), 
            MAX = max(Rsquared, na.rm = TRUE), MED = median(Rsquared, na.rm = TRUE), 
            N = n()) %>% 
  ungroup() %>% 
  arrange(-avg.R2) %>% 
  mutate_if(is.numeric, function(x) {round(x, 3)}) %>% 
  knitr::kable(caption = "Table 1: Model Performance in decreasing order of R2", 
               col.names = c("Model", "Ave.R2", "StdDev", "Min", "Max", "Med", "N"))
Table 1: Model Performance in decreasing order of R2
Model Ave.R2 StdDev Min Max Med N
ranger 0.859 0.039 0.723 0.917 0.865 40
rf 0.856 0.041 0.714 0.897 0.869 40
gbm 0.832 0.037 0.733 0.891 0.834 40
xgbLinear 0.826 0.055 0.684 0.906 0.845 40
gamSpline 0.813 0.033 0.697 0.865 0.818 40
treebag 0.788 0.055 0.608 0.867 0.799 40
bam 0.783 0.057 0.620 0.856 0.799 40
gam 0.782 0.058 0.627 0.856 0.798 40
ridge 0.705 0.049 0.570 0.785 0.711 40
lm 0.704 0.044 0.588 0.786 0.709 40
glmStepAIC 0.703 0.046 0.579 0.789 0.707 40
knn 0.392 0.090 0.173 0.620 0.388 40

Sử dụng 12 mô hình ML đã có để thực hiện dự báo trên test data. Dưới đây là R codes:

# Funtion predicts house price on test data for a selected ML model: 

pred_fun <- function(x) {
  
  sp_model <- model_list1[[my_models[x]]]
  
  predict(sp_model, dfTest) -> predi
  
  data.frame(predicted = predi, actual = dfTest$MEDV, Model = my_models[x]) -> finalDF
  
  return(finalDF)
  
  
}


# Use the function and extract results: 

lapply(1:length(my_models), pred_fun) -> list_predictions

do.call("bind_rows", list_predictions) -> df_predictions_on_testData

Trên test data, Random Forest có R-squared là 91% và đây là mô hình dự báo chính xác nhất trong số 12 thuật toán ML (Figure 1). Kết quả này là nhất quán với trend đã được chỉ ra trong Table 1 ở trên. Dưới đây là R codes:

# Rank model performance: 

spaceDF <- data.frame()

for (model in my_models) {
  
  df_predictions_on_testData %>% 
    filter(Model == model) %>% 
    select(-Model) %>% 
    cor() %>% 
    as.data.frame() %>% 
    .[1, 2] -> myCorr
  
  data.frame(myRsquared = myCorr^2, Model = model) -> df_i
  
  bind_rows(df_i, spaceDF) -> spaceDF
  
}


spaceDF %>% 
  arrange(-myRsquared) %>% 
  pull(Model) -> modelsOrdered

# Visualize predicted values and actuals: 

df_predictions_on_testData %>% 
  mutate(Model = factor(Model, levels = modelsOrdered)) %>% 
  ggplot(aes(actual, predicted, color = predicted)) + 
  geom_point(alpha = 0.25) + 
  facet_wrap(~ Model) + 
  stat_smooth(method = "lm", formula = y ~ x, geom = "smooth", 
              color = "red", size = 0.4) + 
  theme(legend.position = "top") + 
  stat_poly_eq() + 
  scale_color_viridis(direction = -1, 
                      option = "D", 
                      name = "Price", 
                      guide = guide_colourbar(direction = "horizontal",
                                              barheight = unit(2, units = "mm"),
                                              barwidth = unit(35, units = "mm"),
                                              title.hjust = 0.5,
                                              label.hjust = 0.5, 
                                              title.position = "top")) + 
  labs(x = "Actual", y = "Predicted", 
       title = "Figure 1: Actual and Predicted by ML Models on Test Data", 
       caption = "Source: https://archive.ics.uci.edu/datasets")

Summary

Thử nghiệm 12 thuật toán cho dự báo giá nhà ở với bộ số liệu Boston Housing Dataset cho thấy Random Forest là nhóm mô hình ML có khả năng dự báo chính xác nhất. Việc thử nghiệm 12 thuật toán ML này trên bộ dữ liệu này có thể cung cấp một vài insights và gợi ý để đề ra phương án phù hợp cho cuộc thi Zillow’s Home Value Prediction.

LS0tDQp0aXRsZTogIk1hY2hpbmUgTGVhcm5pbmcgQWxnb3JpdGhtcyBmb3IgSG91c2luZyBQcmljZSBQcmVkaWN0aW9uOiBFbXBpcmljYWwgUmVzdWx0cyBmcm9tIEJvc3RvbiBIb3VzaW5nIERhdGFzZXQiDQphdXRob3I6ICJBdXRob3I6IE5ndXllbiBDaGkgRHVuZyINCnN1YnRpdGxlOiAiUiBNYWNoaW5lIExlYXJuaW5nIFNlcmllcyINCm91dHB1dDoNCiAgaHRtbF9kb2N1bWVudDoNCiAgICBjb2RlX2Rvd25sb2FkOiB5ZXMNCiAgICBoaWdobGlnaHQ6IHplbmJ1cm4NCiAgICB0aGVtZTogZmxhdGx5DQogICAgdG9jOiB5ZXMNCiAgICB0b2NfZmxvYXQ6IHllcw0KICB3b3JkX2RvY3VtZW50Og0KICAgIHRvYzogeWVzDQogIHBkZl9kb2N1bWVudDoNCiAgICB0b2M6IHllcw0KLS0tDQoNCmBgYHtyIHNldHVwLGluY2x1ZGU9RkFMU0V9DQprbml0cjo6b3B0c19jaHVuayRzZXQoZWNobyA9IFRSVUUsIHdhcm5pbmcgPSBGQUxTRSwgbWVzc2FnZSA9IEZBTFNFLCBjYWNoZSA9IFRSVUUpDQoNCmBgYA0KDQoNCg0KIVtdKEM6XFxVc2Vyc1xcQWRtaW5cXERvY3VtZW50c1xcYm9zdG9uSG91c2UyLmpwZykNCg0KIyBNb3RpdmF0aW9uDQoNCkN14buZYyB0aGkgW1ppbGxvd+KAmXMgSG9tZSBWYWx1ZSBQcmVkaWN0aW9uXShodHRwczovL3d3dy5rYWdnbGUuY29tL2NvbXBldGl0aW9ucy96aWxsb3ctcHJpemUtMS9vdmVydmlldykgY8OzIHThu5VuZyBnaeG6o2kgdGjGsOG7n25nIGzDqm4gxJHhur9uICQxLDIwMCwwMDAuIELhu5kgZOG7ryBsaeG7h3UgbsOgeSBuZ2F5IGPhuqMga2hpIMSRw6MgbsOpbiBjw7MgZHVuZyBsxrDhu6NuZyAxLjM3IEdCLiDEkOG7gyB0ZXN0IGto4bqjIG7Eg25nIGThu7EgYsOhbyBj4bunYSBjw6FjIG3DtCBow6xuaCBraMOhYyBuaGF1IGNow7puZyB0YSBjw7MgdGjhu4Mgc+G7rSBk4bulbmcgYuG7mSBk4buvIGxp4buHdSBiw6kgaMahbiB2w6AgxJHGoW4gZ2nhuqNuIGjGoW4gbMOgIFtCb3N0b24gSG91c2luZyBEYXRhc2V0XShodHRwczovL2dpdGh1Yi5jb20vcnVwYWtjL1VDSS1EYXRhLUFuYWx5c2lzL2Jsb2IvbWFzdGVyL0Jvc3RvbiUyMEhvdXNpbmclMjBEYXRhc2V0L0Jvc3RvbiUyMEhvdXNpbmcvVUNJJTIwTWFjaGluZSUyMExlYXJuaW5nJTIwUmVwb3NpdG9yeV8lMjBIb3VzaW5nJTIwRGF0YSUyMFNldC5wZGYpIHRyxrDhu5tjIGtoaSAiY2jGoWkiIHbhu5tpIGLhu5kgZOG7ryBsaeG7h3Ugc2nDqnUgbOG7m24gY+G7p2EgY3Xhu5ljIHRoaS4gDQoNClRyxrDhu5tjIGjhur90IGxvYWQgdsOgIHjhu60gbMOtIHF1YSBkYXRhOiANCg0KYGBge3IsIGV2YWw9RkFMU0V9DQojIENsZWFyIFIgZW52aXJvbm1lbnQ6IA0KDQpybShsaXN0ID0gbHMoKSkNCg0KIyBMb2FkIGRhdGE6IA0KDQpyZWFkLnRhYmxlKCJodHRwczovL3Jhdy5naXRodWJ1c2VyY29udGVudC5jb20vcnVwYWtjL1VDSS1EYXRhLUFuYWx5c2lzL21hc3Rlci9Cb3N0b24lMjBIb3VzaW5nJTIwRGF0YXNldC9Cb3N0b24lMjBIb3VzaW5nL2hvdXNpbmcuZGF0YSIsIGhlYWRlciA9IEZBTFNFKSAtPiBib3N0b25EYXRhDQoNCg0KIyBMb2FkIHNvbWUgUiBwYWNrYWdlczogDQoNCmxpYnJhcnkoZHBseXIpICMgUiBwYWNrYWdlIGZvciBkYXRhIHdyYW5nbGluZy4gDQpsaWJyYXJ5KHZpcmlkaXMpDQpsaWJyYXJ5KGdncG1pc2MpDQpsaWJyYXJ5KGdncGxvdDIpDQpsaWJyYXJ5KGNhcmV0KSAjIEZvciB0cmFpbmluZyBNTCBtb2RlbHMuIA0KbGlicmFyeShjYXJldEVuc2VtYmxlKSAjIEZvciB0cmFpbmluZyBhbmQgY29tcGFyaW5nIE1MIG1vZGVscy4gDQoNCiMgUmVuYW1lIGZvciBhbGwgZmVhdHVyZXMgKGh0dHBzOi8vc2VhcmNoLnItcHJvamVjdC5vcmcvQ1JBTi9yZWZtYW5zL0EzL2h0bWwvaG91c2luZy5odG1sKTogDQoNCm5hbWVzKGJvc3RvbkRhdGEpIDwtIGMoIkNSSU0iLCAiWk4iLCAiSU5EVVMiLCAiQ0hBUyIsICJOT1giLCAiUk0iLCAiQUdFIiwgIkRJUyIsIA0KICAgICAgICAgICAgICAgICAgICAgICAiUkFEIiwgIlRBWCIsICJQVFJBVElPIiwgIkIiLCAiTFNUQVQiLCAiTUVEViIpDQpgYGANCg0KIyBFeHBlcmltZW50YWwgRGVzaWduDQoNCsSQ4buDIMSRw6FuaCBnacOhIHbDoCBzbyBzw6FuaCBraOG6oyBuxINuZyBk4buxIGLDoW8sIDEyIG3DtCBow6xuaCBNYWNoaW5lIExlYXJuaW5nIChNTCkga2jDoWMgbmhhdSDEkcaw4bujYyBjaOG7jW4gxJHhu4MgdGjhu60gbmdoaeG7h20uIELhu5kgZOG7ryBsaeG7h3UgYmFuIMSR4bqndSDEkcaw4bujYyBjaGlhIHRow6BuaCB04buJIGzhu4cgODAgLSAyMCB0cm9uZyDEkcOzIDgwJSBk4buvIGxp4buHdSBsw6AgdHJhaW5pbmcgKyB2YWxpZGF0aW9uLCAyMCUgY8OybiBs4bqhaSBsw6AgdGVzdCBkYXRhIMSR4buDIMSRw6FuaCBnacOhIG5nxrDhu6NjIGzhuqFpIGto4bqjIG7Eg25nIGThu7EgYsOhbyBj4bunYSAxMiBtw7QgaMOsbmggTUwga2jDoWMgbmhhdS4gRMaw4bubaSDEkcOieSBsw6AgUiBjb2RlczogDQoNCmBgYHtyLCBldmFsPVRSVUV9DQojIFNlbGVjdCBzb21lIE1MIG1vZGVscyBmb3IgcHJlZGljdGluZyBob3VzaW5nIHByaWNlOiANCg0KbXlfbW9kZWxzIDwtIGMoImxtIiwgDQogICAgICAgICAgICAgICAia25uIiwgDQogICAgICAgICAgICAgICAiYmFtIiwgDQogICAgICAgICAgICAgICAiZ2FtIiwgDQogICAgICAgICAgICAgICAidHJlZWJhZyIsDQogICAgICAgICAgICAgICAiZ2JtIiwNCiAgICAgICAgICAgICAgICJyZiIsIA0KICAgICAgICAgICAgICAgInJhbmdlciIsIA0KICAgICAgICAgICAgICAgInJpZGdlIiwgDQogICAgICAgICAgICAgICAiZ2xtU3RlcEFJQyIsIA0KICAgICAgICAgICAgICAgImdhbVNwbGluZSIsDQogICAgICAgICAgICAgICAieGdiTGluZWFyIikNCmBgYA0KDQoNCmBgYHtyLCBldmFsPUZBTFNFfQ0KIyBTcGxpdCBkYXRhOiANCg0Kbl90b3RhbCA8LSBucm93KGJvc3RvbkRhdGEpDQoNCnNldC5zZWVkKDI5KQ0KDQppZCA8LSBzYW1wbGUoMTpuX3RvdGFsLCBzaXplID0gMC44Km5fdG90YWwsIHJlcGxhY2UgPSBGQUxTRSkNCg0KZGZUcmFpbiA8LSBib3N0b25EYXRhW2lkLCBdICMgODAlIGZvciB0cmFpbmluZy4gIA0KDQpkZlRlc3QgPC0gYm9zdG9uRGF0YVstaWQsIF0gIyAyMCUgZm9yIGV2YWx1YXRpbmcgYW5kIGNvbXBhcmluZyBNTCBtb2RlbHMuDQpgYGANCg0KU+G7rSBk4bulbmcgW0stZm9sZCBDcm9zcy1WYWxpZGF0aW9uXShodHRwczovL21hY2hpbmVsZWFybmluZ21hc3RlcnkuY29tL2stZm9sZC1jcm9zcy12YWxpZGF0aW9uLykgduG7m2kgSyA9IDQsIGzhurdwIGzhuqFpIDEwIGzhuqduIMSR4buDIMSRw6FuaCBnacOhIHbDoCBzbyBzw6FuaCBraOG6oyBuxINuZyBk4buxIGLDoW8gY+G7p2EgMTIgbcO0IGjDrG5oIE1MIGtow6FjIG5oYXUgY8SDbiBj4bupIHbDoG8gdGnDqnUgY2h14bqpbiBbUi1TcXVhcmVkXShodHRwczovL2VuLndpa2lwZWRpYS5vcmcvd2lraS9Db2VmZmljaWVudF9vZl9kZXRlcm1pbmF0aW9uKS4gRMaw4bubaSDEkcOieSBsw6AgUiBjb2RlczogDQoNCg0KYGBge3IsIGV2YWw9RkFMU0V9DQojIERlZmluZSBjb25maWd1cmF0aW9uIGZvciB0cmFpbmluZyBhbmQgY29tcGFyaW5nIE1MIG1vZGVsczogDQoNCnNldC5zZWVkKDI5KQ0KbnVtYmVyIDwtIDQNCnJlcGVhdHMgPC0gMTANCmNvbnRyb2wgPC0gdHJhaW5Db250cm9sKG1ldGhvZCA9ICJyZXBlYXRlZGN2IiwgDQogICAgICAgICAgICAgICAgICAgICAgICBudW1iZXIgPSBudW1iZXIgLCANCiAgICAgICAgICAgICAgICAgICAgICAgIHJlcGVhdHMgPSByZXBlYXRzLCANCiAgICAgICAgICAgICAgICAgICAgICAgIHNhdmVQcmVkaWN0aW9ucyA9ICJmaW5hbCIsIA0KICAgICAgICAgICAgICAgICAgICAgICAgaW5kZXggPSBjcmVhdGVSZXNhbXBsZShkZlRyYWluJE1FRFYsIHJlcGVhdHMqbnVtYmVyKSwNCiAgICAgICAgICAgICAgICAgICAgICAgIGFsbG93UGFyYWxsZWwgPSBUUlVFKQ0KDQoNCiMgVHJhaW4gdGhlc2UgTUwgTW9kZWxzOiANCg0Kc2V0LnNlZWQoMSkNCnN5c3RlbS50aW1lKG1vZGVsX2xpc3QxIDwtIGNhcmV0TGlzdChNRURWIH4uLCANCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBkYXRhID0gZGZUcmFpbiwNCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICB0ckNvbnRyb2wgPSBjb250cm9sLA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIG1ldHJpYyA9ICJSc3F1YXJlZCIsDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgbWV0aG9kTGlzdCA9IG15X21vZGVscykpDQoNCiMgRXh0cmFjdCByZXN1bHRzIGZvciBjb21wYXJpbmcgYW1vbmcgTUwgbW9kZWxzOiANCg0KbGlzdF9vZl9yZXN1bHRzIDwtIGxhcHBseShteV9tb2RlbHMsIGZ1bmN0aW9uKHgpIHttb2RlbF9saXN0MVtbeF1dJHJlc2FtcGxlfSkNCg0KdG90YWxfZGYgPC0gZG8uY2FsbCgiYmluZF9yb3dzIiwgbGlzdF9vZl9yZXN1bHRzKQ0KDQp0b3RhbF9kZiAlPiUgbXV0YXRlKE1vZGVsID0gbGFwcGx5KG15X21vZGVscywgZnVuY3Rpb24oeCkge3JlcCh4LCBudW1iZXIqcmVwZWF0cyl9KSAlPiUgdW5saXN0KCkpIC0+IHRvdGFsX2RmDQpgYGANCg0KIyBFbXBpcmljYWwgUmVzdWx0cw0KDQpbUmFuZG9tIEZvcmVzdF0oaHR0cHM6Ly9naXRodWIuY29tL2ltYnMtaGwvcmFuZ2VyKSBsw6AgbcO0IGjDrG5oIGPDsyBraOG6oyBuxINuZyBk4buxIGLDoW8gY2jDrW5oIHjDoWMgbmjhuqV0IHbhu5tpIHRydW5nIGLDrG5oIFItU3F1YXJlZCAodHLDqm4gNDAgbeG6q3UgZOG7ryBsaeG7h3UgdmFsaWRhdGlvbikgbMOgIDg1LjklIChUYWJsZSAxKS4gVuG7m2kgbcO0IGjDrG5oIGjhu5NpIHF1eSB0dXnhur9uIHTDrW5oIGPhu5UgxJFp4buDbiB0aMOsIGNvbiBz4buRIG7DoHkgbMOgIDcwLjQlLiBExrDhu5tpIMSRw6J5IGzDoCBSIGNvZGVzOiANCg0KYGBge3IsIGV2YWw9VFJVRSwgZWNobz1GQUxTRX0NCg0KbGlicmFyeShkcGx5cikgDQpsaWJyYXJ5KHZpcmlkaXMpDQpsaWJyYXJ5KGdncG1pc2MpDQpsaWJyYXJ5KGdncGxvdDIpDQpsaWJyYXJ5KHJlYWRyKQ0KDQpyZWFkX2NzdigidG90YWxfZGYuY3N2IikgLT4gdG90YWxfZGYNCmBgYA0KDQoNCg0KYGBge3IsIGV2YWw9VFJVRX0NCnRvdGFsX2RmICU+JSANCiAgZ3JvdXBfYnkoTW9kZWwpICU+JSANCiAgc3VtbWFyaXNlKGF2Zy5SMiA9IG1lYW4oUnNxdWFyZWQsIG5hLnJtID0gVFJVRSksIFNEID0gc2QoUnNxdWFyZWQsIG5hLnJtID0gVFJVRSksIE1JTiA9IG1pbihSc3F1YXJlZCwgbmEucm0gPSBUUlVFKSwgDQogICAgICAgICAgICBNQVggPSBtYXgoUnNxdWFyZWQsIG5hLnJtID0gVFJVRSksIE1FRCA9IG1lZGlhbihSc3F1YXJlZCwgbmEucm0gPSBUUlVFKSwgDQogICAgICAgICAgICBOID0gbigpKSAlPiUgDQogIHVuZ3JvdXAoKSAlPiUgDQogIGFycmFuZ2UoLWF2Zy5SMikgJT4lIA0KICBtdXRhdGVfaWYoaXMubnVtZXJpYywgZnVuY3Rpb24oeCkge3JvdW5kKHgsIDMpfSkgJT4lIA0KICBrbml0cjo6a2FibGUoY2FwdGlvbiA9ICJUYWJsZSAxOiBNb2RlbCBQZXJmb3JtYW5jZSBpbiBkZWNyZWFzaW5nIG9yZGVyIG9mIFIyIiwgDQogICAgICAgICAgICAgICBjb2wubmFtZXMgPSBjKCJNb2RlbCIsICJBdmUuUjIiLCAiU3RkRGV2IiwgIk1pbiIsICJNYXgiLCAiTWVkIiwgIk4iKSkNCmBgYA0KDQpT4butIGThu6VuZyAxMiBtw7QgaMOsbmggTUwgxJHDoyBjw7MgxJHhu4MgdGjhu7FjIGhp4buHbiBk4buxIGLDoW8gdHLDqm4gdGVzdCBkYXRhLiBExrDhu5tpIMSRw6J5IGzDoCBSIGNvZGVzOiANCg0KYGBge3IsIGV2YWw9RkFMU0V9DQojIEZ1bnRpb24gcHJlZGljdHMgaG91c2UgcHJpY2Ugb24gdGVzdCBkYXRhIGZvciBhIHNlbGVjdGVkIE1MIG1vZGVsOiANCg0KcHJlZF9mdW4gPC0gZnVuY3Rpb24oeCkgew0KICANCiAgc3BfbW9kZWwgPC0gbW9kZWxfbGlzdDFbW215X21vZGVsc1t4XV1dDQogIA0KICBwcmVkaWN0KHNwX21vZGVsLCBkZlRlc3QpIC0+IHByZWRpDQogIA0KICBkYXRhLmZyYW1lKHByZWRpY3RlZCA9IHByZWRpLCBhY3R1YWwgPSBkZlRlc3QkTUVEViwgTW9kZWwgPSBteV9tb2RlbHNbeF0pIC0+IGZpbmFsREYNCiAgDQogIHJldHVybihmaW5hbERGKQ0KICANCiAgDQp9DQoNCg0KIyBVc2UgdGhlIGZ1bmN0aW9uIGFuZCBleHRyYWN0IHJlc3VsdHM6IA0KDQpsYXBwbHkoMTpsZW5ndGgobXlfbW9kZWxzKSwgcHJlZF9mdW4pIC0+IGxpc3RfcHJlZGljdGlvbnMNCg0KZG8uY2FsbCgiYmluZF9yb3dzIiwgbGlzdF9wcmVkaWN0aW9ucykgLT4gZGZfcHJlZGljdGlvbnNfb25fdGVzdERhdGENCmBgYA0KDQoNCmBgYHtyLCBlY2hvPUZBTFNFLCBldmFsPVRSVUV9DQoNCnJlYWRfY3N2KCJkZl9wcmVkaWN0aW9uc19vbl90ZXN0RGF0YS5jc3YiKSAtPiBkZl9wcmVkaWN0aW9uc19vbl90ZXN0RGF0YQ0KYGBgDQoNClRyw6puIHRlc3QgZGF0YSwgUmFuZG9tIEZvcmVzdCBjw7MgUi1zcXVhcmVkIGzDoCA5MSUgdsOgIMSRw6J5IGzDoCBtw7QgaMOsbmggZOG7sSBiw6FvIGNow61uaCB4w6FjIG5o4bqldCB0cm9uZyBz4buRIDEyIHRodeG6rXQgdG/DoW4gTUwgKEZpZ3VyZSAxKS4gS+G6v3QgcXXhuqMgbsOgeSBsw6AgbmjhuqV0IHF1w6FuIHbhu5tpIHRyZW5kIMSRw6MgxJHGsOG7o2MgY2jhu4kgcmEgdHJvbmcgVGFibGUgMSDhu58gdHLDqm4uIETGsOG7m2kgxJHDonkgbMOgIFIgY29kZXM6IA0KDQoNCmBgYHtyLCBldmFsPVRSVUV9DQoNCiMgUmFuayBtb2RlbCBwZXJmb3JtYW5jZTogDQoNCnNwYWNlREYgPC0gZGF0YS5mcmFtZSgpDQoNCmZvciAobW9kZWwgaW4gbXlfbW9kZWxzKSB7DQogIA0KICBkZl9wcmVkaWN0aW9uc19vbl90ZXN0RGF0YSAlPiUgDQogICAgZmlsdGVyKE1vZGVsID09IG1vZGVsKSAlPiUgDQogICAgc2VsZWN0KC1Nb2RlbCkgJT4lIA0KICAgIGNvcigpICU+JSANCiAgICBhcy5kYXRhLmZyYW1lKCkgJT4lIA0KICAgIC5bMSwgMl0gLT4gbXlDb3JyDQogIA0KICBkYXRhLmZyYW1lKG15UnNxdWFyZWQgPSBteUNvcnJeMiwgTW9kZWwgPSBtb2RlbCkgLT4gZGZfaQ0KICANCiAgYmluZF9yb3dzKGRmX2ksIHNwYWNlREYpIC0+IHNwYWNlREYNCiAgDQp9DQoNCg0Kc3BhY2VERiAlPiUgDQogIGFycmFuZ2UoLW15UnNxdWFyZWQpICU+JSANCiAgcHVsbChNb2RlbCkgLT4gbW9kZWxzT3JkZXJlZA0KDQojIFZpc3VhbGl6ZSBwcmVkaWN0ZWQgdmFsdWVzIGFuZCBhY3R1YWxzOiANCg0KZGZfcHJlZGljdGlvbnNfb25fdGVzdERhdGEgJT4lIA0KICBtdXRhdGUoTW9kZWwgPSBmYWN0b3IoTW9kZWwsIGxldmVscyA9IG1vZGVsc09yZGVyZWQpKSAlPiUgDQogIGdncGxvdChhZXMoYWN0dWFsLCBwcmVkaWN0ZWQsIGNvbG9yID0gcHJlZGljdGVkKSkgKyANCiAgZ2VvbV9wb2ludChhbHBoYSA9IDAuMjUpICsgDQogIGZhY2V0X3dyYXAofiBNb2RlbCkgKyANCiAgc3RhdF9zbW9vdGgobWV0aG9kID0gImxtIiwgZm9ybXVsYSA9IHkgfiB4LCBnZW9tID0gInNtb290aCIsIA0KICAgICAgICAgICAgICBjb2xvciA9ICJyZWQiLCBzaXplID0gMC40KSArIA0KICB0aGVtZShsZWdlbmQucG9zaXRpb24gPSAidG9wIikgKyANCiAgc3RhdF9wb2x5X2VxKCkgKyANCiAgc2NhbGVfY29sb3JfdmlyaWRpcyhkaXJlY3Rpb24gPSAtMSwgDQogICAgICAgICAgICAgICAgICAgICAgb3B0aW9uID0gIkQiLCANCiAgICAgICAgICAgICAgICAgICAgICBuYW1lID0gIlByaWNlIiwgDQogICAgICAgICAgICAgICAgICAgICAgZ3VpZGUgPSBndWlkZV9jb2xvdXJiYXIoZGlyZWN0aW9uID0gImhvcml6b250YWwiLA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIGJhcmhlaWdodCA9IHVuaXQoMiwgdW5pdHMgPSAibW0iKSwNCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBiYXJ3aWR0aCA9IHVuaXQoMzUsIHVuaXRzID0gIm1tIiksDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgdGl0bGUuaGp1c3QgPSAwLjUsDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgbGFiZWwuaGp1c3QgPSAwLjUsIA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIHRpdGxlLnBvc2l0aW9uID0gInRvcCIpKSArIA0KICBsYWJzKHggPSAiQWN0dWFsIiwgeSA9ICJQcmVkaWN0ZWQiLCANCiAgICAgICB0aXRsZSA9ICJGaWd1cmUgMTogQWN0dWFsIGFuZCBQcmVkaWN0ZWQgYnkgTUwgTW9kZWxzIG9uIFRlc3QgRGF0YSIsIA0KICAgICAgIGNhcHRpb24gPSAiU291cmNlOiBodHRwczovL2FyY2hpdmUuaWNzLnVjaS5lZHUvZGF0YXNldHMiKQ0KDQpgYGANCg0KDQojIFN1bW1hcnkNCg0KVGjhu60gbmdoaeG7h20gMTIgdGh14bqtdCB0b8OhbiBjaG8gZOG7sSBiw6FvIGdpw6EgbmjDoCDhu58gduG7m2kgYuG7mSBz4buRIGxp4buHdSBCb3N0b24gSG91c2luZyBEYXRhc2V0IGNobyB0aOG6pXkgUmFuZG9tIEZvcmVzdCBsw6AgbmjDs20gbcO0IGjDrG5oIE1MIGPDsyBraOG6oyBuxINuZyBk4buxIGLDoW8gY2jDrW5oIHjDoWMgIG5o4bqldC4gVmnhu4djIHRo4butIG5naGnhu4dtIDEyIHRodeG6rXQgdG/DoW4gTUwgbsOgeSB0csOqbiBi4buZIGThu68gbGnhu4d1IG7DoHkgY8OzIHRo4buDIGN1bmcgY+G6pXAgbeG7mXQgdsOgaSBpbnNpZ2h0cyB2w6AgZ+G7o2kgw70gxJHhu4MgxJHhu4EgcmEgcGjGsMahbmcgw6FuIHBow7kgaOG7o3AgY2hvIGN14buZYyB0aGkgWmlsbG934oCZcyBIb21lIFZhbHVlIFByZWRpY3Rpb24uIA0KDQoNCiMgUmVmZXJlbmNlDQoNCjEuIFtIb3VzaW5nIFByaWNlIFByZWRpY3Rpb24gdmlhIEltcHJvdmVkIE1hY2hpbmUgTGVhcm5pbmcgVGVjaG5pcXVlc10oaHR0cHM6Ly93d3cuc2NpZW5jZWRpcmVjdC5jb20vc2NpZW5jZS9hcnRpY2xlL3BpaS9TMTg3NzA1MDkyMDMxNjMxOCkNCg0KMi4gW1VzaW5nIG1hY2hpbmUgbGVhcm5pbmcgYWxnb3JpdGhtcyBmb3IgaG91c2luZyBwcmljZSBwcmVkaWN0aW9uOiBUaGUgY2FzZSBvZiBGYWlyZmF4IENvdW50eSwgVmlyZ2luaWEgaG91c2luZyBkYXRhXShodHRwczovL3d3dy5zY2llbmNlZGlyZWN0LmNvbS9zY2llbmNlL2FydGljbGUvYWJzL3BpaS9TMDk1NzQxNzQxNDAwNzMyNSkNCg0KMy4gW1Byb3BlcnR5IHZhbHVhdGlvbiB1c2luZyBtYWNoaW5lIGxlYXJuaW5nIGFsZ29yaXRobXMgb24gc3RhdGlzdGljYWwgYXJlYXMgaW4gR3JlYXRlciBTeWRuZXksIEF1c3RyYWxpYV0oaHR0cHM6Ly93d3cuc2NpZW5jZWRpcmVjdC5jb20vc2NpZW5jZS9hcnRpY2xlL2Ficy9waWkvUzAyNjQ4Mzc3MjIwMDQzNjcpDQoNCjQuIFtBIGh5YnJpZCBtYWNoaW5lIGxlYXJuaW5nIGZyYW1ld29yayBmb3IgZm9yZWNhc3RpbmcgaG91c2UgcHJpY2VdKGh0dHBzOi8vd3d3LnNjaWVuY2VkaXJlY3QuY29tL3NjaWVuY2UvYXJ0aWNsZS9hYnMvcGlpL1MwOTU3NDE3NDIzMDE0ODM1KQ0K