8. Statistical learning and predictive analytics

8.1 Supervised learning

The basic goal of supervised learning is to find a function that accurately describes how different measured explanatory variables can be combined to make a prediction about a response variable.

Supervised learning의 목적은 반응 변수(response variables)에 대한 예측을 위해 설명 변수(explanatory variables)를 결합할 수 있는 방법을 설명하는 함수를 찾는 것이다.

예를 들면, 다음과 같은 식을 많이 보았을 것이다.
diabetic ~ age + weight + height



8.2 Classifiers

quantitative response variables(정량적 응답 변수)에 대한 회귀 모델은 결과를 ’실수’로 반환하지만, categorial response variables(범주형 변수)에서는 결과를 0 또는 1로 반환하고 이를 classifier, 분류기라고 한다. 분류기는 머신러닝 및 예측 모델링에서 중요한 역할을 할 수 있다.

Decision trees

의사결정 나무는 각 관찰 표본에 클래스 레이블을 할당하는 순서도라고 한다.
지정하는 변수의 수에 따라 가지의 수가 매우 증가하는 특징이 있고, 여기서는 “rpart” 패키지를 사용한다.

의사결정나무에서 child nodes의 순도(purity)를 결정하는 방법은 두 가지가 있다.
1) 지니 계수, Gini measurement
2) 정보 획득량, Information gain


의사결정나무에 대한 예시 문제를 풀어보자. 데이터는 1994년, 미국 32,561명의 인구조사 데이터이다.
자신이 마케팅 담당자라면 잠재고객이 고소득자($50,000이상)인지 관심이 있을 수 있다.

library(mdsr)

census <- read.csv(
"http://archive.ics.uci.edu/ml/machine-learning-databases/adult/adult.data", header = FALSE, stringsAsFactors = TRUE)

names(census) <- c("age", "workclass", "fnlwgt", "education","education.num", "marital.status",
                   "occupation", "relationship","race", "sex", "capital.gain", "capital.loss",
                   "hours.per.week","native.country", "income")
glimpse(census)
Rows: 32,561
Columns: 15
$ age            <int> 39, 50, 38, 53, 28, 37, 49, 52, 31, 42, 37, 30, 23, 32, 40, 34, 25, 32, 38, 43, 40, 54, 35, …
$ workclass      <fct>  State-gov,  Self-emp-not-inc,  Private,  Private,  Private,  Private,  Private,  Self-emp-n…
$ fnlwgt         <int> 77516, 83311, 215646, 234721, 338409, 284582, 160187, 209642, 45781, 159449, 280464, 141297,…
$ education      <fct>  Bachelors,  Bachelors,  HS-grad,  11th,  Bachelors,  Masters,  9th,  HS-grad,  Masters,  Ba…
$ education.num  <int> 13, 13, 9, 7, 13, 14, 5, 9, 14, 13, 10, 13, 13, 12, 11, 4, 9, 9, 7, 14, 16, 9, 5, 7, 9, 13, …
$ marital.status <fct>  Never-married,  Married-civ-spouse,  Divorced,  Married-civ-spouse,  Married-civ-spouse,  M…
$ occupation     <fct>  Adm-clerical,  Exec-managerial,  Handlers-cleaners,  Handlers-cleaners,  Prof-specialty,  E…
$ relationship   <fct>  Not-in-family,  Husband,  Not-in-family,  Husband,  Wife,  Wife,  Not-in-family,  Husband, …
$ race           <fct>  White,  White,  White,  Black,  Black,  White,  Black,  White,  White,  White,  Black,  Asi…
$ sex            <fct>  Male,  Male,  Male,  Male,  Female,  Female,  Female,  Male,  Female,  Male,  Male,  Male, …
$ capital.gain   <int> 2174, 0, 0, 0, 0, 0, 0, 0, 14084, 5178, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0…
$ capital.loss   <int> 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2042, 0, 0, 0, 0, 0, 0,…
$ hours.per.week <int> 40, 13, 40, 40, 40, 40, 16, 45, 50, 40, 80, 40, 30, 50, 40, 45, 35, 40, 50, 45, 60, 20, 40, …
$ native.country <fct>  United-States,  United-States,  United-States,  United-States,  Cuba,  United-States,  Jama…
$ income         <fct>  <=50K,  <=50K,  <=50K,  <=50K,  <=50K,  <=50K,  <=50K,  >50K,  >50K,  >50K,  >50K,  >50K,  …

데이터를 불러올 때 책과 다른 점은 read.csv에 (stringsAsFactors = TRUE)를 추가한 것이다. R 버전이 바뀌며 문자열이 factor로 지정되지 않고 character 타입으로 지정되면서 본 도서에서 많은 오류를 발생시키므로 위 코드를 추가하여 오류를 해결하였다.
glimpse()함수는 내장함수인 str()처럼 데이터의 구조를 확인하는 함수이다. 가독성이 좋다는 특징이 있다.

set.seed(364)
n <- nrow(census)
test_idx <- sample.int(n, size = round(0.2 * n))
train <- census[-test_idx, ]
nrow(train)
[1] 26049
test <- census[test_idx, ]
nrow(test)
[1] 6512

데이터를 80%/20% 비율로 학습과 테스트 데이터로 분리하였다.
행의 개수를 세기 위한 tally()함수를 사용하였다.

mosaic::tally(~income, data = train, format = "percent")
income
   <=50K     >50K 
76.17567 23.82433 

생성한 Null Model에서 50,000불 이상의 소득을 버는 인구 비율이 약 24%로 판명되었다.
(Null Model은 설명변수를 하나도 지정하지 않은 모델을 말한다.)
여기에 capital.gain 변수를 추가하여 고려해보자.

library(rpart)
rpart(income ~ capital.gain, data = train)
n= 26049 

node), split, n, loss, yval, (yprob)
      * denotes terminal node

1) root 26049 6206  <=50K (0.76175669 0.23824331)  
  2) capital.gain< 5119 24805 5030  <=50K (0.79721830 0.20278170) *
  3) capital.gain>=5119 1244   68  >50K (0.05466238 0.94533762) *

결과를 보면, capital.gain이 $5,119 미만인 경우 소득이 5만불 이하일 확률이 약 80%, capital.gain이 $5,119 이상인 경우 소득이 5만불 이상일 확률이 약 95%로 나타났다. $5,119라는 기준은 알고리즘이 지니계수를 가장 낮추는 값으로 계산한 것이며, 이 기준을 활용하여 시각화 하면 다음과 같다.

split <- 5119
train <- train %>% mutate(hi_cap_gains = capital.gain >= split)

ggplot(data = train, aes(x = capital.gain, y = income)) +
  geom_count(aes(color = hi_cap_gains),
             position = position_jitter(width = 0, height = 0.1), alpha = 0.5) +
  geom_vline(xintercept = split, color = "dodgerblue", lty = 2) +
  scale_x_log10(labels = scales::dollar)

다른 변수들도 고려하여 의사결정 나무를 만들 수 있다.

form <- as.formula("income ~ age + workclass + education + marital.status + occupation + relationship +   
                   race + sex + capital.gain + capital.loss + hours.per.week")
mod_tree <- rpart(form, data = train)
mod_tree
n= 26049 

node), split, n, loss, yval, (yprob)
      * denotes terminal node

 1) root 26049 6206  <=50K (0.76175669 0.23824331)  
   2) relationship= Not-in-family, Other-relative, Own-child, Unmarried 14310  940  <=50K (0.93431167 0.06568833)  
     4) capital.gain< 7073.5 14055  694  <=50K (0.95062255 0.04937745) *
     5) capital.gain>=7073.5 255    9  >50K (0.03529412 0.96470588) *
   3) relationship= Husband, Wife 11739 5266  <=50K (0.55140983 0.44859017)  
     6) education= 10th, 11th, 12th, 1st-4th, 5th-6th, 7th-8th, 9th, Assoc-acdm, Assoc-voc, HS-grad, Preschool, Some-college 8199 2717  <=50K (0.66861812 0.33138188)  
      12) capital.gain< 5095.5 7796 2321  <=50K (0.70228322 0.29771678) *
      13) capital.gain>=5095.5 403    7  >50K (0.01736973 0.98263027) *
     7) education= Bachelors, Doctorate, Masters, Prof-school 3540  991  >50K (0.27994350 0.72005650) *

추가한 변수들에 대한 정보가 relationship부터 education까지 출력되었다.

위의 복잡한 식을 plot()함수로 나타낼 수 있다.

plot(mod_tree)
text(mod_tree, use.n = TRUE, all = TRUE, cex = 0.7)

의사결정나무 알고리즘은 가능한 많은 분할(가지 수)을 고려하지만, 모델의 예측력을 충분히 향상시키지 못하는 경우 가지치기(pruning)를 한다. 디폴트 값으로 각 가지는 오류를 1%로 줄여야 하고, 이는 과적합을 방지하는데 도움이 된다.


Confusion Matrix

모델의 예측 정확도를 확인하는 방법 중 하나이다. 위 모델을 예시로 사용하면,

train <- train %>% mutate(income_dtree = predict(mod_tree, type = "class"))
confusion <- mosaic::tally(income_dtree ~ income, data = train,
                           format = "count")
confusion
            income
income_dtree  <=50K  >50K
       <=50K  18836  3015
       >50K    1007  3191
sum(diag(confusion)) / nrow(train)
[1] 0.8455987

여기서 의사결정나무의 정확도는 84.56%로, 76%였던 Null model보다 크게 향상되었음을 알 수 있다.
본 예시에서 의사결정나무의 오류 임계값이 1%라고 했는데, 이를 0.2%로 낮추면 어떻게 될까? 나무가 더욱 복잡해질 것이다.

mod_tree2 <- rpart(form, data = train, control = rpart.control(cp = 0.002))

train <- train %>%
mutate(income_dtree = predict(mod_tree2, type = "class"))
confusion <- mosaic::tally(income_dtree ~ income, data = train,
                           format = "count")
confusion
            income
income_dtree  <=50K  >50K
       <=50K  18846  2571
       >50K     997  3635
sum(diag(confusion)) / nrow(train)
[1] 0.8630274

정확도가 86.3%로 약 2%가량 증가하였다.


Random Forests

다음 분류기는 랜덤포레스트로, 다수결 규칙에 의해 집계된 의사결정나무의 모음을 뜻한다.
앞의 7장에서 배운 “부트스트랩”을 생각해보자. 랜덤포레스트는 의사결정나무의 부트스트랩 결과 모음과 같다.

library(randomForest)
mod_forest <- randomForest(form, data = train, ntree = 201, mtry = 3)
mod_forest

Call:
 randomForest(formula = form, data = train, ntree = 201, mtry = 3) 
               Type of random forest: classification
                     Number of trees: 201
No. of variables tried at each split: 3

        OOB estimate of  error rate: 13.43%
Confusion matrix:
        <=50K  >50K class.error
 <=50K  18550  1293  0.06516152
 >50K    2205  4001  0.35530132
sum(diag(mod_forest$confusion)) / nrow(train)
[1] 0.8657146

랜덤포레스트의 정확도는 약 86.67%가 나왔다. 앞의 의사결정나무 모델보다 0.3%정도 상승한 결과이다.
랜덤포레스트의 각 트리는 서로 다른 변수 집합을 사용하기 때문에, 어떤 변수가 지속적으로 영향력이 높은지 알 수 있다.

library(tibble)
importance(mod_forest) %>%
  as.data.frame() %>%
  rownames_to_column() %>%
  arrange(desc(MeanDecreaseGini))

importance()함수를 통해 확인해보면, 소득과 나이의 영향이 크고 인종은 그렇지 않다는 것을 알 수 있다.


Nearest neighbor

지금까지의 모델링과는 조금 다른 방법이다. 모델을 구성하지 않는 대신 관측치 사이의 거리를 통해 결과를 설명하는데, “거리가 서로 가까운 관측값이 유사한 결과를 가진다.”는 가정하에 진행된다. 여기서 ’거리’란, p attribute를 가진 데이터는 p차원 공간의 점으로 나타낼 수 있는데, 이 두 점 사이의 거리를 구하는 것을 말한다.

k-NN분류기는 학습 데이터를 분류할 필요없이 바로 처리할 수 있으며 ’class’패키지의 knn()함수를 사용한다.

library(class)
# distance metric only works with quantitative variables
train_q <- train %>%
  select(age, education.num, capital.gain, capital.loss, hours.per.week)
income_knn <- knn(train_q, test = train_q, cl = train$income, k = 10)
confusion <- mosaic::tally(income_knn ~ income, data = train, format = "count")
confusion
          income
income_knn  <=50K  >50K
     <=50K  18988  2988
     >50K     855  3218
sum(diag(confusion)) / nrow(train)
[1] 0.8524703

정확도가 85.3% 출력되었다.

k-NN에서 k의 수는 데이터에 따라 다르다. k를 최적화하여 결정하기 위해 교차검증(cross-validation)을 사용한다.

knn_error_rate <- function(x, y, numNeighbors, z = x) {
  y_hat <- knn(train = x, test = z, cl = y, k = numNeighbors)
  return(sum(y_hat != y) / nrow(x))
}

ks <- c(1:15, 20, 30, 40, 50)
train_rates <- sapply(ks, FUN = knn_error_rate, x = train_q, y = train$income)
knn_error_rates <- data.frame(k = ks, train_rate = train_rates)
ggplot(data = knn_error_rates, aes(x = k, y = train_rate)) +
  geom_point() + geom_line() + ylab("Misclassification Rate")

위 그래프는 k값에 따른 오분류율을 보여준다. 이 경우 최적 k값은 1로 정할 수 있다.


Naive Bayes

조건부 확률을 다른 조건부 확률로부터 계산할 수 있게 해주는 분류기이다.

\(p(y|x) = {p(xy) \over p(x)} = {p(x|y)p(y) \over p(x)}\)

위 조건부 확률 식을 다양한 수업을 통해 익혔을 것이라고 생각한다.

Naive Bayes 분류기를 실제로 돌려보자. “e1071” 패키지의 naiveBayes()함수를 사용한다.

library(e1071)
mod_nb <- naiveBayes(form, data = train)
income_nb <- predict(mod_nb, newdata = train)
confusion <- mosaic::tally(income_nb ~ income, data = train, format = "count")
confusion
         income
income_nb  <=50K  >50K
    <=50K  18724  3591
    >50K    1119  2615
sum(diag(confusion)) / nrow(train)
[1] 0.8191869

나이브 베이즈에 경우 약 81.9%의 정확도를 보였다.


Artificial neural networks (ANN), 인공신경망 모델

인간의 뇌 구조에서 착안한 분류기이다. 하지만 생물학적인 요소보다는 전적으로 수학적 계산에 기반하는 모델이다.

library(nnet)
mod_nn <- nnet(form, data = train, size = 5)
# weights:  296
initial  value 29667.656582 
iter  10 value 13252.698876
iter  20 value 13118.667120
iter  30 value 11615.433866
iter  40 value 10873.110995
iter  50 value 10471.136127
iter  60 value 10177.670234
iter  70 value 9928.453475
iter  80 value 9321.154933
iter  90 value 9292.163384
iter 100 value 9187.213290
final  value 9187.213290 
stopped after 100 iterations


여기서는 57개의 입력변수 다음으로 5개의 Hidden Layer을 두었고, 최종적으로 한 개의 출력이 결정되어 나오게 된다. 본 ANN 알고리즘은 이 모든 경우(edge)를 반복 계산하여 새로운 input에도 결과를 예측할 수 있다.

income_nn <- predict(mod_nn, newdata = train, type = "class")
confusion <- mosaic::tally(income_nn ~ income, data = train, format = "count")
confusion
         income
income_nn  <=50K  >50K
    <=50K  17388  1750
    >50K    2455  4456
sum(diag(confusion)) / nrow(train)
[1] 0.8385735

ANN 모델을 평가하면 약 83.47%의 정확도를 보인다.
여기까지가 다양한 분류기, Classifer들의 모음이다.



8.3 Ensemble method, 앙상블

앙상블이란, 앞서 나열한 분류기들을 결합하여 사용하는 것을 말한다. 앙상블은 단일 분류기로 분리가 쉽고, 오류율 측면에서 성능을 높일 수 있다는 장점이 있다.

income_ensemble <- ifelse((income_knn == " >50K") +
                            (income_nb == " >50K") +
                            (income_nn == " >50K") >= 2, " >50K", " <=50K")
confusion <- mosaic::tally(income_ensemble ~ income, data = train,
                           format = "count")
confusion
               income
income_ensemble  <=50K  >50K
          <=50K  18731  2975
          >50K    1112  3231
sum(diag(confusion)) / nrow(train)
[1] 0.8431034

본 예시에서는 k-nn과 나이브베이즈, ann분류기를 앙상블한 모습입니다. 앙상블의 정확도는 84.7%로 앞서 knn이 보여준 85.3%의 수치보다는 낮지만, 거의 근소하며 간단하기에 좋은 방법임에는 분명하다고 한다.

8.4 Evaluating models

Cross-validation, 교차검증

“과적합”이란 무엇일까??
본 교재에서는 ’학습 데이터에 대한 최적의 매개변수 세트를 결정하여 높은 정확도를 가지지만, 새로운 데이터가 들어오면 예측력이 현저히 떨어지는 것.’이라고 설명하였다.

교차검증기법으로 2-fold cross-validation은 데이터를 동일하게 X1, X2로 나눈다. 이후 X1에서 모델을 만들고 X2에서 측정했을 때, 잘 돌아간다는 보장이 없다. 그 다음, X1과 X2의 역할을 바꿔서 X2에서 학습하고 X1에서 테스트할 때, 첫 모델이 과적합이라면 교차검증 시에도 잘 수행되지 않을 확률이 높다는 것을 활용한 검증법이다. 같은 방식으로 k-fold corss-validation이 작동하며, 위의 두 개로 분류한 것을 동일한 크기의 k개로 일반화하여 적용한다.

Measuring prediction Error

예측 오차를 검증하는 방법으로 다음 네 가지가 있다.

  1. RMSE: Root Mean Squared Error
    \(\sqrt({1 \over n} \sum (y- \hat y)^2)\)

RMSE는 \(\hat y\)(관측값)이 y와 동일한 단위에 있고 과대평가와 과소평가를 동시에 포착 가능하며, 큰 오차에 큰 패널티를 부과한다는 장점이 있다.

  1. MAE: Mean Absolute Error
    \({1 \over n} \sum \left\vert y - {\hat y} \right\vert\)

RMSE와 유사하지만, 큰 오차에 큰 패널티를 주는 방식이 아니다. 위 수식에 제곱형이 없어서 그렇다.

  1. Correlation

\((y- \hat y)\)을 최소화하려는 방법보다는, 추세를 확인하고자 할 때 적절한 방법이다. \(y_i와 {\hat y_i}\)가 동일한 상대 순서상에 있는지 확인하여 측정한다.

  1. Coefficient of determination

흔히 \(R^2\)으로 알고 있는 결정계수를 의미한다. 0과 1사이의 수치로 표현하며 1이면 y와 동일함을 뜻한다.


ROC curves

Receiver Operating Characteristic curve의 약자로, 모든 가능한 임계값을 고려하고 민감도와 특이성 사이의 균형을 나타내는 곡선을 말한다. 책에서는 ROCR 패키지를 사용한다.

income_probs <- mod_nb %>%
predict(newdata = train, type = "raw") %>%
as.data.frame()

mosaic::tally(~` >50K` > 0.24, data = income_probs, format = "percent")
` >50K` > 0.24
    TRUE    FALSE 
19.31744 80.68256 
pred <- ROCR::prediction(income_probs[,2], train$income)
perf <- ROCR::performance(pred, 'tpr', 'fpr')
class(perf)
[1] "performance"
attr(,"package")
[1] "ROCR"
perf_df <- data.frame(perf@x.values, perf@y.values)
names(perf_df) <- c("fpr", "tpr")
roc <- ggplot(data = perf_df, aes(x = fpr, y = tpr)) +
  geom_line(color="blue") + 
  geom_abline(intercept=0, slope=1, lty=3) +
  ylab(perf@y.name) + xlab(perf@x.name)

roc



Bias-variance trade-off

편향과 분산을 모두 최소화하는 모델을 원하지만, 이는 상호 배타적인 목표이다. 복잡한 모델은 편향이 감소하지만 분산이 증가하고, 간단한 모델은 분산이 감소하지만 편향이 증가하기 때문이다. 따라서 정규화를 통해 적정 균형을 맞춰야 한다.

정규화는 과적합을 방지하기 위해 회귀모형에 제약조건을 추가하는 기술을 말한다. 예측변수 집합이 클 때 특히 유용하며, ridge regression과 Lasso 방식이 있다.

EX: 소득 모델 평가 먼저 “모든 사람의 소득이 5만불 이하”라고 예측하는 Null model을 구축한다.

favstats(~ capital.gain, data = train)
favstats(~ capital.gain, data = test)
mod_null <- glm(income ~ 1, data = train, family = binomial)
mods <- list(mod_null, mod_tree, mod_forest, mod_nn, mod_nb)

lapply(mods, class)
[[1]]
[1] "glm" "lm" 

[[2]]
[1] "rpart"

[[3]]
[1] "randomForest.formula" "randomForest"        

[[4]]
[1] "nnet.formula" "nnet"        

[[5]]
[1] "naiveBayes"
predict_methods <- methods("predict")
predict_methods[grepl(pattern = "(glm|rpart|randomForest|nnet|naive)", predict_methods)]
[1] "predict.glm"          "predict.glmmPQL"      "predict.glmtree"      "predict.naiveBayes"  
[5] "predict.nnet"         "predict.randomForest" "predict.rpart"       

추가한 모델 목록을 반복하고 각 개체에 적절한 predict() 메서드를 적용한다.

library(tidyr)
predictions_train <-
  data.frame(y = as.character(train$income),
             type = "train",
             mod_null = predict(mod_null, type ="response"),
             mod_tree = predict(mod_tree, type = "class"),
             mod_forest = predict(mod_forest, type = "class"),
             mod_nn = predict(mod_nn, type = "class"),
             mod_nb = predict(mod_nb, newdata = train, type = "class"))

predictions_test <- 
  data.frame(y = as.character(test$income),
             type = "test",
             mod_null = predict(mod_null, newdata = test, type = "response"),
             mod_tree = predict(mod_tree, newdata = test, type = "class"),
             mod_forest = predict(mod_forest, newdata = test, type = "class"),
             mod_nn = predict(mod_nn, newdata = test, type = "class"),
             mod_nb = predict(mod_nb, newdata = test, type = "class"))

predictions <- bind_rows(predictions_train, predictions_test)
glimpse(predictions)
Rows: 32,561
Columns: 7
$ y          <chr> " <=50K", " <=50K", " <=50K", " <=50K", " <=50K", " <=50K", " >50K", " >50K", " >50K", " <=50K",…
$ type       <chr> "train", "train", "train", "train", "train", "train", "train", "train", "train", "train", "train…
$ mod_null   <dbl> 0.2382433, 0.2382433, 0.2382433, 0.2382433, 0.2382433, 0.2382433, 0.2382433, 0.2382433, 0.238243…
$ mod_tree   <fct>  <=50K,  >50K,  <=50K,  <=50K,  >50K,  <=50K,  >50K,  <=50K,  >50K,  <=50K,  <=50K,  <=50K,  <=5…
$ mod_forest <fct>  <=50K,  <=50K,  <=50K,  <=50K,  >50K,  <=50K,  >50K,  >50K,  >50K,  <=50K,  <=50K,  >50K,  <=50…
$ mod_nn     <chr> " <=50K", " <=50K", " <=50K", " <=50K", " >50K", " <=50K", " >50K", " >50K", " >50K", " <=50K", …
$ mod_nb     <fct>  <=50K,  <=50K,  <=50K,  <=50K,  <=50K,  <=50K,  >50K,  <=50K,  <=50K,  <=50K,  <=50K,  <=50K,  …
predictions_tidy <- predictions %>%
  mutate(mod_null = ifelse(mod_null < 0.5, " <=50K", " >50K")) %>%
  gather(key = "model", value = "y_hat", -type, -y)
glimpse(predictions_tidy)
Rows: 162,805
Columns: 4
$ y     <chr> " <=50K", " <=50K", " <=50K", " <=50K", " <=50K", " <=50K", " >50K", " >50K", " >50K", " <=50K", " <=…
$ type  <chr> "train", "train", "train", "train", "train", "train", "train", "train", "train", "train", "train", "t…
$ model <chr> "mod_null", "mod_null", "mod_null", "mod_null", "mod_null", "mod_null", "mod_null", "mod_null", "mod_…
$ y_hat <chr> " <=50K", " <=50K", " <=50K", " <=50K", " <=50K", " <=50K", " <=50K", " <=50K", " <=50K", " <=50K", "…

각 모델에 대한 예측값을 얻었으므로, 실제 y와 비교하고 결과를 집계한다.

predictions_summary <- predictions_tidy %>%
  group_by(model, type) %>%
  summarize(N = n(), correct = sum(y == y_hat, 0),
            positives = sum(y == " >50K"),
            true_pos = sum(y_hat == " >50K" & y == y_hat),
            false_pos = sum(y_hat == " >50K" & y != y_hat)) %>%
  mutate(accuracy = correct / N,
         tpr = true_pos / positives,
         fpr = false_pos / (N - positives)) %>%
  ungroup() %>%
  gather(val_type, val, -model, -type) %>%
  unite(temp1, type, val_type, sep = "_") %>% # glue variables
  spread(temp1, val) %>%
  arrange(desc(test_accuracy)) %>%
  select(model, train_accuracy, test_accuracy, test_tpr, test_fpr)

predictions_summary

모든 모델의 정확도는 학습과 테스트 세트에서 거의 비슷했다. 이후 ROC 곡선을 계산해보면 다음과 같다.

Naive Bayes 모델이 가장 좋은 성능으로 나타났다!


추가 문제

NHANES에서 조사한 성인의 연령, 체질량지수(BMI), 당뇨병 간의 관계를 보이시오

library(NHANES)
people <- NHANES %>%
  select(Age, Gender, Diabetes, BMI, HHIncome, PhysActive) %>%
  na.omit()
glimpse(people)
Rows: 7,555
Columns: 6
$ Age        <int> 34, 34, 34, 49, 45, 45, 45, 66, 58, 54, 58, 50, 33, 60, 56, 56, 54, 54, 38, 36, 44, 44, 64, 26, …
$ Gender     <fct> male, male, male, female, female, female, female, male, male, male, female, male, male, male, fe…
$ Diabetes   <fct> No, No, No, No, No, No, No, No, No, No, No, No, No, No, No, No, No, No, No, No, Yes, Yes, Yes, N…
$ BMI        <dbl> 32.22, 32.22, 32.22, 30.57, 27.24, 27.24, 27.24, 23.67, 23.69, 26.03, 26.22, 26.60, 28.54, 25.84…
$ HHIncome   <fct> 25000-34999, 25000-34999, 25000-34999, 35000-44999, 75000-99999, 75000-99999, 75000-99999, 25000…
$ PhysActive <fct> No, No, No, No, Yes, Yes, Yes, Yes, Yes, Yes, Yes, Yes, No, No, Yes, Yes, Yes, Yes, No, Yes, No,…
whoIsDiabetic <- rpart(Diabetes ~ Age + BMI + Gender + PhysActive,
                       data = people, control = rpart.control(cp = 0.005,
                                                              minbucket = 30))
whoIsDiabetic
n= 7555 

node), split, n, loss, yval, (yprob)
      * denotes terminal node

 1) root 7555 684 No (0.90946393 0.09053607)  
   2) Age< 52.5 5092 188 No (0.96307934 0.03692066) *
   3) Age>=52.5 2463 496 No (0.79861957 0.20138043)  
     6) BMI< 39.985 2301 416 No (0.81920904 0.18079096) *
     7) BMI>=39.985 162  80 No (0.50617284 0.49382716)  
      14) Age>=67.5 50  18 No (0.64000000 0.36000000) *
      15) Age< 67.5 112  50 Yes (0.44642857 0.55357143)  
        30) Age< 60.5 71  30 No (0.57746479 0.42253521) *
        31) Age>=60.5 41   9 Yes (0.21951220 0.78048780) *
library(partykit)
plot(as.party(whoIsDiabetic))

partykit 패키지를 활용하였다. 소득을 제외한 모든 변수를 포함시킨 의사결정나무이다.

ggplot(data = people, aes(x = Age, y = BMI)) +
  geom_count(aes(color = Diabetes), alpha = 0.5) +
  geom_vline(xintercept = 52.5) +
  geom_segment(x = 52.5, xend = 100, y = 39.985, yend = 39.985) +
  geom_segment(x = 67.5, xend = 67.5, y = 39.985, yend = Inf) +
  geom_segment(x = 60.5, xend = 60.5, y = 39.985, yend = Inf) +
  annotate("rect", xmin = 60.5, xmax = 67.5, ymin = 39.985,
           ymax = Inf, fill = "blue", alpha = 0.1)

도표를 보면, 고령자와 BMI가 높은 사람이 당뇨과 연관이 있다고 판단할 수 있다. 52세 이하는 당뇨가 없을 확률이 더 높으며 61세에서 67세 사이가 가장 높다. BMI는 40 이상일 때 위험도가 증가한다.

다음은 6개 모델을 모두 활용하여 모델간의 차이를 알아보았다.

ggplot(data = res, aes(x = Age, y = BMI)) +
  geom_tile(aes(fill = y_hat), color = NA) +
  geom_count(aes(color = Diabetes), alpha = 0.4, data = people) +
  scale_fill_gradient(low = "white", high = "dodgerblue") +
  scale_color_manual(values = c("gray", "gold")) +
  scale_size(range = c(0, 2)) +
  scale_x_continuous(expand = c(0.02,0)) +
  scale_y_continuous(expand = c(0.02,0)) +
  facet_wrap(~model)

의사결정나무는 분명한 직선으로 확률을 구분하였으며 k-NN은 유연하게 이진예측을 하였다. 나이브베이즈는 비선형 범위를 생성하였고, 랜덤포레스트는 k-NN과 유연성은 비슷하지만 뉘앙스가 더 많았다. Null모델은 균일하게 예측하였다.




LS0tDQp0aXRsZTogIk1vZGVybiBEYXRhIFNjaWVuY2Ugd2l0aCBSX0NwdDgiDQphdXRob3I6IHNlb25nc3UsIGtpbQ0KZGF0ZTogMjAyMy0wMS0xNw0Kb3V0cHV0OiBodG1sX25vdGVib29rDQotLS0NCg0KIyA4LiBTdGF0aXN0aWNhbCBsZWFybmluZyBhbmQgcHJlZGljdGl2ZSBhbmFseXRpY3MNCg0KIyMgOC4xIFN1cGVydmlzZWQgbGVhcm5pbmcNClRoZSBiYXNpYyBnb2FsIG9mIHN1cGVydmlzZWQgbGVhcm5pbmcgaXMgdG8gZmluZCBhIGZ1bmN0aW9uIHRoYXQgYWNjdXJhdGVseSBkZXNjcmliZXMgaG93IGRpZmZlcmVudCBtZWFzdXJlZCBleHBsYW5hdG9yeSB2YXJpYWJsZXMgY2FuIGJlIGNvbWJpbmVkIHRvIG1ha2UgYSBwcmVkaWN0aW9uIGFib3V0IGEgcmVzcG9uc2UgdmFyaWFibGUuXA0KDQpTdXBlcnZpc2VkIGxlYXJuaW5n7J2YIOuqqeyggeydgCDrsJjsnZEg67OA7IiYKHJlc3BvbnNlIHZhcmlhYmxlcynsl5Ag64yA7ZWcIOyYiOy4oeydhCDsnITtlbQg7ISk66qFIOuzgOyImChleHBsYW5hdG9yeSB2YXJpYWJsZXMp66W8IOqysO2Vqe2VoCDsiJgg7J6I64qUIOuwqeuyleydhCDshKTrqoXtlZjripQg7ZWo7IiY66W8IOywvuuKlCDqsoPsnbTri6QuXA0KXA0KDQrsmIjrpbwg65Ok66m0LCDri6TsnYzqs7wg6rCZ7J2AIOyLneydhCDrp47snbQg67O07JWY7J2EIOqyg+ydtOuLpC4NClwNCipkaWFiZXRpYyB+IGFnZSArIHdlaWdodCArIGhlaWdodCpcDQpcDQpcDQpcDQoNCiMjIDguMiBDbGFzc2lmaWVycw0KcXVhbnRpdGF0aXZlIHJlc3BvbnNlIHZhcmlhYmxlcyjsoJXrn4nsoIEg7J2R64u1IOuzgOyImCnsl5Ag64yA7ZWcIO2ajOq3gCDrqqjrjbjsnYAg6rKw6rO866W8ICfsi6TsiJgn66GcIOuwmO2ZmO2VmOyngOunjCwgY2F0ZWdvcmlhbCByZXNwb25zZSB2YXJpYWJsZXMo67KU7KO87ZiVIOuzgOyImCnsl5DshJzripQg6rKw6rO866W8IDAg65iQ64qUIDHroZwg67CY7ZmY7ZWY6rOgIOydtOulvCBjbGFzc2lmaWVyLCDrtoTrpZjquLDrnbzqs6Ag7ZWc64ukLiDrtoTrpZjquLDripQg66i47Iug65+s64udIOuwjyDsmIjsuKEg66qo642466eB7JeQ7IScIOykkeyalO2VnCDsl63tlaDsnYQg7ZWgIOyImCDsnojri6QuDQpcDQpcDQoNCiMjIyBEZWNpc2lvbiB0cmVlcw0K7J2Y7IKs6rKw7KCVIOuCmOustOuKlCDqsIEg6rSA7LCwIO2RnOuzuOyXkCDtgbTrnpjsiqQg66CI7J2067iU7J2EIO2VoOuLue2VmOuKlCDsiJzshJzrj4Trnbzqs6Ag7ZWc64ukLg0KXA0K7KeA7KCV7ZWY64qUIOuzgOyImOydmCDsiJjsl5Ag65Sw6528IOqwgOyngOydmCDsiJjqsIAg66ek7JqwIOymneqwgO2VmOuKlCDtirnsp5XsnbQg7J6I6rOgLCDsl6zquLDshJzripQgInJwYXJ0IiDtjKjtgqTsp4Drpbwg7IKs7Jqp7ZWc64ukLg0KXA0KXA0KDQrsnZjsgqzqsrDsoJXrgpjrrLTsl5DshJwgY2hpbGQgbm9kZXPsnZgg7Iic64+EKHB1cml0eSnrpbwg6rKw7KCV7ZWY64qUIOuwqeuyleydgCDrkZAg6rCA7KeA6rCAIOyeiOuLpC5cDQoxKSDsp4Dri4gg6rOE7IiYLCBHaW5pIG1lYXN1cmVtZW50XA0KMikg7KCV67O0IO2ajeuTneufiSwgSW5mb3JtYXRpb24gZ2FpblwNClwNClwNCg0K7J2Y7IKs6rKw7KCV64KY66y07JeQIOuMgO2VnCDsmIjsi5wg66y47KCc66W8IO2SgOyWtOuztOyekC4g642w7J207YSw64qUIDE5OTTrhYQsIOuvuOq1rSAzMiw1NjHrqoXsnZgg7J246rWs7KGw7IKsIOuNsOydtO2EsOydtOuLpC5cDQrsnpDsi6DsnbQg66eI7LyA7YyFIOuLtOuLueyekOudvOuptCDsnqDsnqzqs6DqsJ3snbQg6rOg7IaM65Od7J6QKCQ1MCwwMDDsnbTsg4Ep7J247KeAIOq0gOyLrOydtCDsnojsnYQg7IiYIOyeiOuLpC4NCmBgYHtyIG1lc3NhZ2U9RkFMU0UsIHdhcm5pbmc9RkFMU0V9DQpsaWJyYXJ5KG1kc3IpDQoNCmNlbnN1cyA8LSByZWFkLmNzdigNCiJodHRwOi8vYXJjaGl2ZS5pY3MudWNpLmVkdS9tbC9tYWNoaW5lLWxlYXJuaW5nLWRhdGFiYXNlcy9hZHVsdC9hZHVsdC5kYXRhIiwgaGVhZGVyID0gRkFMU0UsIHN0cmluZ3NBc0ZhY3RvcnMgPSBUUlVFKQ0KDQpuYW1lcyhjZW5zdXMpIDwtIGMoImFnZSIsICJ3b3JrY2xhc3MiLCAiZm5sd2d0IiwgImVkdWNhdGlvbiIsImVkdWNhdGlvbi5udW0iLCAibWFyaXRhbC5zdGF0dXMiLA0KICAgICAgICAgICAgICAgICAgICJvY2N1cGF0aW9uIiwgInJlbGF0aW9uc2hpcCIsInJhY2UiLCAic2V4IiwgImNhcGl0YWwuZ2FpbiIsICJjYXBpdGFsLmxvc3MiLA0KICAgICAgICAgICAgICAgICAgICJob3Vycy5wZXIud2VlayIsIm5hdGl2ZS5jb3VudHJ5IiwgImluY29tZSIpDQpnbGltcHNlKGNlbnN1cykNCmBgYA0K642w7J207YSw66W8IOu2iOufrOyYrCDrlYwg7LGF6rO8IOuLpOuluCDsoJDsnYAgcmVhZC5jc3bsl5AgKHN0cmluZ3NBc0ZhY3RvcnMgPSBUUlVFKeulvCDstpTqsIDtlZwg6rKD7J2064ukLiBSIOuyhOyghOydtCDrsJTrgIzrqbAg66y47J6Q7Je07J20IGZhY3RvcuuhnCDsp4DsoJXrkJjsp4Ag7JWK6rOgIGNoYXJhY3RlciDtg4DsnoXsnLzroZwg7KeA7KCV65CY66m07IScIOuzuCDrj4TshJzsl5DshJwg66eO7J2AIOyYpOulmOulvCDrsJzsg53si5ztgqTrr4DroZwg7JyEIOy9lOuTnOulvCDstpTqsIDtlZjsl6wg7Jik66WY66W8IO2VtOqysO2VmOyYgOuLpC5cDQpnbGltcHNlKCntlajsiJjripQg64K07J6l7ZWo7IiY7J24IHN0cigp7LKY65+8IOuNsOydtO2EsOydmCDqtazsobDrpbwg7ZmV7J247ZWY64qUIO2VqOyImOydtOuLpC4g6rCA64+F7ISx7J20IOyii+uLpOuKlCDtirnsp5XsnbQg7J6I64ukLg0KXA0KDQpgYGB7cn0NCnNldC5zZWVkKDM2NCkNCm4gPC0gbnJvdyhjZW5zdXMpDQp0ZXN0X2lkeCA8LSBzYW1wbGUuaW50KG4sIHNpemUgPSByb3VuZCgwLjIgKiBuKSkNCnRyYWluIDwtIGNlbnN1c1stdGVzdF9pZHgsIF0NCm5yb3codHJhaW4pDQoNCnRlc3QgPC0gY2Vuc3VzW3Rlc3RfaWR4LCBdDQpucm93KHRlc3QpDQpgYGANCuuNsOydtO2EsOulvCA4MCUvMjAlIOu5hOycqOuhnCDtlZnsirXqs7wg7YWM7Iqk7Yq4IOuNsOydtO2EsOuhnCDrtoTrpqztlZjsmIDri6QuXA0K7ZaJ7J2YIOqwnOyImOulvCDshLjquLAg7JyE7ZWcIHRhbGx5KCntlajsiJjrpbwg7IKs7Jqp7ZWY7JiA64ukLg0KDQpgYGB7cn0NCm1vc2FpYzo6dGFsbHkofmluY29tZSwgZGF0YSA9IHRyYWluLCBmb3JtYXQgPSAicGVyY2VudCIpDQpgYGANCuyDneyEse2VnCBOdWxsIE1vZGVs7JeQ7IScIDUwLDAwMOu2iCDsnbTsg4HsnZgg7IaM65Od7J2EIOuyhOuKlCDsnbjqtawg67mE7Jyo7J20IOyVvSAyNCXroZwg7YyQ66qF65CY7JeI64ukLlwNCihOdWxsIE1vZGVs7J2AIOyEpOuqheuzgOyImOulvCDtlZjrgpjrj4Qg7KeA7KCV7ZWY7KeAIOyViuydgCDrqqjrjbjsnYQg66eQ7ZWc64ukLilcDQrsl6zquLDsl5AgY2FwaXRhbC5nYWluIOuzgOyImOulvCDstpTqsIDtlZjsl6wg6rOg66Ck7ZW067O07J6QLlwNCg0KYGBge3J9DQpsaWJyYXJ5KHJwYXJ0KQ0KcnBhcnQoaW5jb21lIH4gY2FwaXRhbC5nYWluLCBkYXRhID0gdHJhaW4pDQpgYGANCuqysOqzvOulvCDrs7TrqbQsIGNhcGl0YWwuZ2FpbuydtCAkNSwxMTkg66+466eM7J24IOqyveyasCDshozrk53snbQgNeunjOu2iCDsnbTtlZjsnbwg7ZmV66Wg7J20IOyVvSA4MCUsDQpjYXBpdGFsLmdhaW7snbQgJDUsMTE5IOydtOyDgeyduCDqsr3smrAg7IaM65Od7J20IDXrp4zrtogg7J207IOB7J28ICDtmZXrpaDsnbQg7JW9IDk1JeuhnCDrgpjtg4Drgqzri6QuDQokNSwxMTnrnbzripQg6riw7KSA7J2AIOyVjOqzoOumrOymmOydtCDsp4Dri4jqs4TsiJjrpbwg6rCA7J6lIOuCruy2lOuKlCDqsJLsnLzroZwg6rOE7IKw7ZWcIOqyg+ydtOupsCwg7J20IOq4sOykgOydhCDtmZzsmqntlZjsl6wg7Iuc6rCB7ZmUIO2VmOuptCDri6TsnYzqs7wg6rCZ64ukLg0KXA0KXA0KDQpgYGB7ciBtZXNzYWdlPUZBTFNFLCB3YXJuaW5nPUZBTFNFfQ0Kc3BsaXQgPC0gNTExOQ0KdHJhaW4gPC0gdHJhaW4gJT4lIG11dGF0ZShoaV9jYXBfZ2FpbnMgPSBjYXBpdGFsLmdhaW4gPj0gc3BsaXQpDQoNCmdncGxvdChkYXRhID0gdHJhaW4sIGFlcyh4ID0gY2FwaXRhbC5nYWluLCB5ID0gaW5jb21lKSkgKw0KICBnZW9tX2NvdW50KGFlcyhjb2xvciA9IGhpX2NhcF9nYWlucyksDQogICAgICAgICAgICAgcG9zaXRpb24gPSBwb3NpdGlvbl9qaXR0ZXIod2lkdGggPSAwLCBoZWlnaHQgPSAwLjEpLCBhbHBoYSA9IDAuNSkgKw0KICBnZW9tX3ZsaW5lKHhpbnRlcmNlcHQgPSBzcGxpdCwgY29sb3IgPSAiZG9kZ2VyYmx1ZSIsIGx0eSA9IDIpICsNCiAgc2NhbGVfeF9sb2cxMChsYWJlbHMgPSBzY2FsZXM6OmRvbGxhcikNCmBgYA0KDQrri6Trpbgg67OA7IiY65Ok64+EIOqzoOugpO2VmOyXrCDsnZjsgqzqsrDsoJUg64KY66y066W8IOunjOuTpCDsiJgg7J6I64ukLlwNClwNCg0KYGBge3J9DQpmb3JtIDwtIGFzLmZvcm11bGEoImluY29tZSB+IGFnZSArIHdvcmtjbGFzcyArIGVkdWNhdGlvbiArIG1hcml0YWwuc3RhdHVzICsgb2NjdXBhdGlvbiArIHJlbGF0aW9uc2hpcCArICAgDQogICAgICAgICAgICAgICAgICAgcmFjZSArIHNleCArIGNhcGl0YWwuZ2FpbiArIGNhcGl0YWwubG9zcyArIGhvdXJzLnBlci53ZWVrIikNCm1vZF90cmVlIDwtIHJwYXJ0KGZvcm0sIGRhdGEgPSB0cmFpbikNCm1vZF90cmVlDQpgYGANCuy2lOqwgO2VnCDrs4DsiJjrk6Tsl5Ag64yA7ZWcIOygleuztOqwgCByZWxhdGlvbnNoaXDrtoDthLAgZWR1Y2F0aW9u6rmM7KeAIOy2nOugpeuQmOyXiOuLpC4NClwNClwNCg0K7JyE7J2YIOuzteyeoe2VnCDsi53snYQgcGxvdCgp7ZWo7IiY66GcIOuCmO2DgOuCvCDsiJgg7J6I64ukLg0KYGBge3J9DQpwbG90KG1vZF90cmVlKQ0KdGV4dChtb2RfdHJlZSwgdXNlLm4gPSBUUlVFLCBhbGwgPSBUUlVFLCBjZXggPSAwLjcpDQpgYGANCuydmOyCrOqysOygleuCmOustCDslYzqs6DrpqzsppjsnYAg6rCA64ql7ZWcIOunjuydgCDrtoTtlaAo6rCA7KeAIOyImCnsnYQg6rOg66Ck7ZWY7KeA66eMLCDrqqjrjbjsnZgg7JiI7Lih66Cl7J2EIOy2qeu2hO2eiCDtlqXsg4Hsi5ztgqTsp4Ag66q77ZWY64qUIOqyveyasCAq6rCA7KeA7LmY6riwKHBydW5pbmcpKuulvCDtlZzri6QuIOuUlO2PtO2KuCDqsJLsnLzroZwg6rCBIOqwgOyngOuKlCDsmKTrpZjrpbwgMSXroZwg7KSE7Jes7JW8IO2VmOqzoCwg7J2064qUIOqzvOygge2VqeydhCDrsKnsp4DtlZjripTrjbAg64+E7JuA7J20IOuQnOuLpC4NClwNClwNClwNCg0KIyMjIENvbmZ1c2lvbiBNYXRyaXgNCuuqqOuNuOydmCDsmIjsuKEg7KCV7ZmV64+E66W8IO2ZleyduO2VmOuKlCDrsKnrspUg7KSRIO2VmOuCmOydtOuLpC4g7JyEIOuqqOuNuOydhCDsmIjsi5zroZwg7IKs7Jqp7ZWY66m0LA0KYGBge3J9DQp0cmFpbiA8LSB0cmFpbiAlPiUgbXV0YXRlKGluY29tZV9kdHJlZSA9IHByZWRpY3QobW9kX3RyZWUsIHR5cGUgPSAiY2xhc3MiKSkNCmNvbmZ1c2lvbiA8LSBtb3NhaWM6OnRhbGx5KGluY29tZV9kdHJlZSB+IGluY29tZSwgZGF0YSA9IHRyYWluLA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgZm9ybWF0ID0gImNvdW50IikNCmNvbmZ1c2lvbg0KDQpzdW0oZGlhZyhjb25mdXNpb24pKSAvIG5yb3codHJhaW4pDQpgYGANCuyXrOq4sOyEnCDsnZjsgqzqsrDsoJXrgpjrrLTsnZgg7KCV7ZmV64+E64qUIDg0LjU2JeuhnCwgNzYl7JiA642YIE51bGwgbW9kZWzrs7Tri6Qg7YGs6rKMIO2WpeyDgeuQmOyXiOydjOydhCDslYwg7IiYIOyeiOuLpC4NClwNCuuzuCDsmIjsi5zsl5DshJwg7J2Y7IKs6rKw7KCV64KY66y07J2YIOyYpOulmCDsnoTqs4TqsJLsnbQgMSXrnbzqs6Ag7ZaI64qU642wLCDsnbTrpbwgMC4yJeuhnCDrgq7stpTrqbQg7Ja065a76rKMIOuQoOq5jD8g64KY66y06rCAIOuNlOyasSDrs7XsnqHtlbTsp4gg6rKD7J2064ukLg0KXA0KYGBge3J9DQptb2RfdHJlZTIgPC0gcnBhcnQoZm9ybSwgZGF0YSA9IHRyYWluLCBjb250cm9sID0gcnBhcnQuY29udHJvbChjcCA9IDAuMDAyKSkNCg0KdHJhaW4gPC0gdHJhaW4gJT4lDQptdXRhdGUoaW5jb21lX2R0cmVlID0gcHJlZGljdChtb2RfdHJlZTIsIHR5cGUgPSAiY2xhc3MiKSkNCmNvbmZ1c2lvbiA8LSBtb3NhaWM6OnRhbGx5KGluY29tZV9kdHJlZSB+IGluY29tZSwgZGF0YSA9IHRyYWluLA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgZm9ybWF0ID0gImNvdW50IikNCmNvbmZ1c2lvbg0KDQpzdW0oZGlhZyhjb25mdXNpb24pKSAvIG5yb3codHJhaW4pDQpgYGANCuygle2ZleuPhOqwgCA4Ni4zJeuhnCDslb0gMiXqsIDrn4kg7Kad6rCA7ZWY7JiA64ukLg0KXA0KXA0KXA0KDQojIyMgUmFuZG9tIEZvcmVzdHMNCuuLpOydjCDrtoTrpZjquLDripQg656c642k7Y+s66CI7Iqk7Yq466GcLCDri6TsiJjqsrAg6rec7LmZ7JeQIOydmO2VtCDsp5Hqs4TrkJwg7J2Y7IKs6rKw7KCV64KY66y07J2YIOuqqOydjOydhCDrnLvtlZzri6QuXA0K7JWe7J2YIDfsnqXsl5DshJwg67Cw7Jq0ICLrtoDtirjsiqTtirjrnqki7J2EIOyDneqwge2VtOuztOyekC4g656c642k7Y+s66CI7Iqk7Yq464qUIOydmOyCrOqysOygleuCmOustOydmCDrtoDtirjsiqTtirjrnqkg6rKw6rO8IOuqqOydjOqzvCDqsJnri6QuXA0KYGBge3IgbWVzc2FnZT1GQUxTRSwgd2FybmluZz1GQUxTRX0NCmxpYnJhcnkocmFuZG9tRm9yZXN0KQ0KbW9kX2ZvcmVzdCA8LSByYW5kb21Gb3Jlc3QoZm9ybSwgZGF0YSA9IHRyYWluLCBudHJlZSA9IDIwMSwgbXRyeSA9IDMpDQptb2RfZm9yZXN0DQoNCnN1bShkaWFnKG1vZF9mb3Jlc3QkY29uZnVzaW9uKSkgLyBucm93KHRyYWluKQ0KYGBgDQrrnpzrjaTtj6zroIjsiqTtirjsnZgg7KCV7ZmV64+E64qUIOyVvSA4Ni42NyXqsIAg64KY7JmU64ukLiDslZ7snZgg7J2Y7IKs6rKw7KCV64KY66y0IOuqqOuNuOuztOuLpCAwLjMl7KCV64+EIOyDgeyKue2VnCDqsrDqs7zsnbTri6QuXA0K656c642k7Y+s66CI7Iqk7Yq47J2YIOqwgSDtirjrpqzripQg7ISc66GcIOuLpOuluCDrs4DsiJgg7KeR7ZWp7J2EIOyCrOyaqe2VmOq4sCDrlYzrrLjsl5AsIOyWtOuWpCDrs4DsiJjqsIAg7KeA7IaN7KCB7Jy866GcIOyYge2WpeugpeydtCDrhpLsnYDsp4Ag7JWMIOyImCDsnojri6QuDQoNCmBgYHtyfQ0KbGlicmFyeSh0aWJibGUpDQppbXBvcnRhbmNlKG1vZF9mb3Jlc3QpICU+JQ0KICBhcy5kYXRhLmZyYW1lKCkgJT4lDQogIHJvd25hbWVzX3RvX2NvbHVtbigpICU+JQ0KICBhcnJhbmdlKGRlc2MoTWVhbkRlY3JlYXNlR2luaSkpDQpgYGANCmltcG9ydGFuY2UoKe2VqOyImOulvCDthrXtlbQg7ZmV7J247ZW067O066m0LCDshozrk53qs7wg64KY7J207J2YIOyYge2WpeydtCDtgazqs6Ag7J247KKF7J2AIOq3uOugh+yngCDslYrri6TripQg6rKD7J2EIOyVjCDsiJgg7J6I64ukLg0KXA0KXA0KXA0KDQojIyMgTmVhcmVzdCBuZWlnaGJvcg0K7KeA6riI6rmM7KeA7J2YIOuqqOuNuOungeqzvOuKlCDsobDquIgg64uk66W4IOuwqeuyleydtOuLpC4g66qo64247J2EIOq1rOyEse2VmOyngCDslYrripQg64yA7IugIOq0gOy4oey5mCDsgqzsnbTsnZgg6rGw66as66W8IO2Gte2VtCDqsrDqs7zrpbwg7ISk66qF7ZWY64qU642wLCAi6rGw66as6rCAIOyEnOuhnCDqsIDquYzsmrQg6rSA7Lih6rCS7J20IOycoOyCrO2VnCDqsrDqs7zrpbwg6rCA7KeE64ukLiLripQg6rCA7KCV7ZWY7JeQIOynhO2WieuQnOuLpC4g7Jes6riw7IScICfqsbDrpqwn656ALCBwIGF0dHJpYnV0ZeulvCDqsIDsp4Qg642w7J207YSw64qUIHDssKjsm5Ag6rO16rCE7J2YIOygkOycvOuhnCDrgpjtg4Drgrwg7IiYIOyeiOuKlOuNsCwg7J20IOuRkCDsoJAg7IKs7J207J2YIOqxsOumrOulvCDqtaztlZjripQg6rKD7J2EIOunkO2VnOuLpC5cDQpcDQoNCmstTk7rtoTrpZjquLDripQg7ZWZ7Iq1IOuNsOydtO2EsOulvCDrtoTrpZjtlaAg7ZWE7JqU7JeG7J20IOuwlOuhnCDsspjrpqztlaAg7IiYIOyeiOycvOupsCAnY2xhc3Mn7Yyo7YKk7KeA7J2YIGtubigp7ZWo7IiY66W8IOyCrOyaqe2VnOuLpC4NCmBgYHtyfQ0KbGlicmFyeShjbGFzcykNCiMgZGlzdGFuY2UgbWV0cmljIG9ubHkgd29ya3Mgd2l0aCBxdWFudGl0YXRpdmUgdmFyaWFibGVzDQp0cmFpbl9xIDwtIHRyYWluICU+JQ0KICBzZWxlY3QoYWdlLCBlZHVjYXRpb24ubnVtLCBjYXBpdGFsLmdhaW4sIGNhcGl0YWwubG9zcywgaG91cnMucGVyLndlZWspDQppbmNvbWVfa25uIDwtIGtubih0cmFpbl9xLCB0ZXN0ID0gdHJhaW5fcSwgY2wgPSB0cmFpbiRpbmNvbWUsIGsgPSAxMCkNCmNvbmZ1c2lvbiA8LSBtb3NhaWM6OnRhbGx5KGluY29tZV9rbm4gfiBpbmNvbWUsIGRhdGEgPSB0cmFpbiwgZm9ybWF0ID0gImNvdW50IikNCmNvbmZ1c2lvbg0KDQpzdW0oZGlhZyhjb25mdXNpb24pKSAvIG5yb3codHJhaW4pDQpgYGANCuygle2ZleuPhOqwgCA4NS4zJSDstpzroKXrkJjsl4jri6QuDQpcDQpcDQoNCmstTk7sl5DshJwga+ydmCDsiJjripQg642w7J207YSw7JeQIOuUsOudvCDri6TrpbTri6QuIGvrpbwg7LWc7KCB7ZmU7ZWY7JesIOqysOygle2VmOq4sCDsnITtlbQg6rWQ7LCo6rKA7KadKGNyb3NzLXZhbGlkYXRpb24p7J2EIOyCrOyaqe2VnOuLpC4NCmBgYHtyfQ0Ka25uX2Vycm9yX3JhdGUgPC0gZnVuY3Rpb24oeCwgeSwgbnVtTmVpZ2hib3JzLCB6ID0geCkgew0KICB5X2hhdCA8LSBrbm4odHJhaW4gPSB4LCB0ZXN0ID0geiwgY2wgPSB5LCBrID0gbnVtTmVpZ2hib3JzKQ0KICByZXR1cm4oc3VtKHlfaGF0ICE9IHkpIC8gbnJvdyh4KSkNCn0NCg0Ka3MgPC0gYygxOjE1LCAyMCwgMzAsIDQwLCA1MCkNCnRyYWluX3JhdGVzIDwtIHNhcHBseShrcywgRlVOID0ga25uX2Vycm9yX3JhdGUsIHggPSB0cmFpbl9xLCB5ID0gdHJhaW4kaW5jb21lKQ0Ka25uX2Vycm9yX3JhdGVzIDwtIGRhdGEuZnJhbWUoayA9IGtzLCB0cmFpbl9yYXRlID0gdHJhaW5fcmF0ZXMpDQpnZ3Bsb3QoZGF0YSA9IGtubl9lcnJvcl9yYXRlcywgYWVzKHggPSBrLCB5ID0gdHJhaW5fcmF0ZSkpICsNCiAgZ2VvbV9wb2ludCgpICsgZ2VvbV9saW5lKCkgKyB5bGFiKCJNaXNjbGFzc2lmaWNhdGlvbiBSYXRlIikNCmBgYA0K7JyEIOq3uOuemO2UhOuKlCBr6rCS7JeQIOuUsOuluCDsmKTrtoTrpZjsnKjsnYQg67O07Jes7KSA64ukLiDsnbQg6rK97JqwIOy1nOyggSBr6rCS7J2AIDHroZwg7KCV7ZWgIOyImCDsnojri6QuDQpcDQpcDQpcDQoNCiMjIyBOYWl2ZSBCYXllcw0K7KGw6rG067aAIO2ZleuloOydhCDri6Trpbgg7KGw6rG067aAIO2ZleuloOuhnOu2gO2EsCDqs4TsgrDtlaAg7IiYIOyeiOqyjCDtlbTso7zripQg67aE66WY6riw7J2064ukLg0KXA0KXA0KDQokcCh5fHgpID0ge3AoeHkpIFxvdmVyIHAoeCl9ID0ge3AoeHx5KXAoeSkgXG92ZXIgcCh4KX0kXA0KXA0KDQrsnIQg7KGw6rG067aAIO2ZleuloCDsi53snYQg64uk7JaR7ZWcIOyImOyXheydhCDthrXtlbQg7J217ZiU7J2EIOqyg+ydtOudvOqzoCDsg53qsIHtlZzri6QuXA0KXA0KDQpOYWl2ZSBCYXllcyDrtoTrpZjquLDrpbwg7Iuk7KCc66GcIOuPjOugpOuztOyekC4gImUxMDcxIiDtjKjtgqTsp4DsnZggbmFpdmVCYXllcygp7ZWo7IiY66W8IOyCrOyaqe2VnOuLpC4NCmBgYHtyfQ0KbGlicmFyeShlMTA3MSkNCm1vZF9uYiA8LSBuYWl2ZUJheWVzKGZvcm0sIGRhdGEgPSB0cmFpbikNCmluY29tZV9uYiA8LSBwcmVkaWN0KG1vZF9uYiwgbmV3ZGF0YSA9IHRyYWluKQ0KY29uZnVzaW9uIDwtIG1vc2FpYzo6dGFsbHkoaW5jb21lX25iIH4gaW5jb21lLCBkYXRhID0gdHJhaW4sIGZvcm1hdCA9ICJjb3VudCIpDQpjb25mdXNpb24NCg0Kc3VtKGRpYWcoY29uZnVzaW9uKSkgLyBucm93KHRyYWluKQ0KYGBgDQrrgpjsnbTruIwg67Kg7J207KaI7JeQIOqyveyasCDslb0gODEuOSXsnZgg7KCV7ZmV64+E66W8IOuztOyYgOuLpC5cDQpcDQpcDQoNCiMjIEFydGlmaWNpYWwgbmV1cmFsIG5ldHdvcmtzIChBTk4pLCDsnbjqs7Xsi6Dqsr3rp50g66qo6424DQrsnbjqsITsnZgg64eMIOq1rOyhsOyXkOyEnCDssKnslYjtlZwg67aE66WY6riw7J2064ukLiDtlZjsp4Drp4wg7IOd66y87ZWZ7KCB7J24IOyalOyGjOuztOuLpOuKlCDsoITsoIHsnLzroZwg7IiY7ZWZ7KCBIOqzhOyCsOyXkCDquLDrsJjtlZjripQg66qo64247J2064ukLlwNCg0KYGBge3J9DQpsaWJyYXJ5KG5uZXQpDQptb2Rfbm4gPC0gbm5ldChmb3JtLCBkYXRhID0gdHJhaW4sIHNpemUgPSA1KQ0KYGBgDQohW10oQzovVXNlcnMvdXNlci9EZXNrdG9wL0xhYl9kcml2ZS9SX3Byb2plY3QvWzIwMjNdTW9kZXJuIERhdGEgU2NpZW5jZSBXaXRoIFIvaW1hZ2UvQU5OX21vZGVsLnBuZykgXA0K7Jes6riw7ISc64qUIDU36rCc7J2YIOyeheugpeuzgOyImCDri6TsnYzsnLzroZwgNeqwnOydmCBIaWRkZW4gTGF5ZXLsnYQg65GQ7JeI6rOgLCDstZzsooXsoIHsnLzroZwg7ZWcIOqwnOydmCDstpzroKXsnbQg6rKw7KCV65CY7Ja0IOuCmOyYpOqyjCDrkJzri6QuIOuzuCBBTk4g7JWM6rOg66as7KaY7J2AIOydtCDrqqjrk6Ag6rK97JqwKGVkZ2Up66W8IOuwmOuztSDqs4TsgrDtlZjsl6wg7IOI66Gc7Jq0IGlucHV07JeQ64+EIOqysOqzvOulvCDsmIjsuKHtlaAg7IiYIOyeiOuLpC5cDQpcDQpgYGB7cn0NCmluY29tZV9ubiA8LSBwcmVkaWN0KG1vZF9ubiwgbmV3ZGF0YSA9IHRyYWluLCB0eXBlID0gImNsYXNzIikNCmNvbmZ1c2lvbiA8LSBtb3NhaWM6OnRhbGx5KGluY29tZV9ubiB+IGluY29tZSwgZGF0YSA9IHRyYWluLCBmb3JtYXQgPSAiY291bnQiKQ0KY29uZnVzaW9uDQoNCnN1bShkaWFnKGNvbmZ1c2lvbikpIC8gbnJvdyh0cmFpbikNCmBgYA0KQU5OIOuqqOuNuOydhCDtj4nqsIDtlZjrqbQg7JW9IDgzLjQ3JeydmCDsoJXtmZXrj4Trpbwg67O07J2464ukLlwNCuyXrOq4sOq5jOyngOqwgCDri6TslpHtlZwg67aE66WY6riwLCBDbGFzc2lmZXLrk6TsnZgg66qo7J2M7J2064ukLlwNClwNClwNClwNCg0KIyMgOC4zIEVuc2VtYmxlIG1ldGhvZCwg7JWZ7IOB67iUDQrslZnsg4HruJTsnbTrnoAsIOyVnuyEnCDrgpjsl7TtlZwg67aE66WY6riw65Ok7J2EIOqysO2Vqe2VmOyXrCDsgqzsmqntlZjripQg6rKD7J2EIOunkO2VnOuLpC4g7JWZ7IOB67iU7J2AIOuLqOydvCDrtoTrpZjquLDroZwg67aE66as6rCAIOyJveqzoCwg7Jik66WY7JyoIOy4oeuptOyXkOyEnCDshLHriqXsnYQg64aS7J28IOyImCDsnojri6TripQg7J6l7KCQ7J20IOyeiOuLpC4NCmBgYHtyfQ0KaW5jb21lX2Vuc2VtYmxlIDwtIGlmZWxzZSgoaW5jb21lX2tubiA9PSAiID41MEsiKSArDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgKGluY29tZV9uYiA9PSAiID41MEsiKSArDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgKGluY29tZV9ubiA9PSAiID41MEsiKSA+PSAyLCAiID41MEsiLCAiIDw9NTBLIikNCmNvbmZ1c2lvbiA8LSBtb3NhaWM6OnRhbGx5KGluY29tZV9lbnNlbWJsZSB+IGluY29tZSwgZGF0YSA9IHRyYWluLA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgZm9ybWF0ID0gImNvdW50IikNCmNvbmZ1c2lvbg0KDQpzdW0oZGlhZyhjb25mdXNpb24pKSAvIG5yb3codHJhaW4pDQpgYGANCuuzuCDsmIjsi5zsl5DshJzripQgay1ubuqzvCDrgpjsnbTruIzrsqDsnbTspogsIGFubuu2hOulmOq4sOulvCDslZnsg4HruJTtlZwg66qo7Iq17J6F64uI64ukLiDslZnsg4HruJTsnZgg7KCV7ZmV64+E64qUIDg0Ljcl66GcIOyVnuyEnCBrbm7snbQg67O07Jes7KSAIDg1LjMl7J2YIOyImOy5mOuztOuLpOuKlCDrgq7sp4Drp4wsIOqxsOydmCDqt7zshoztlZjrqbAg6rCE64uo7ZWY6riw7JeQIOyii+ydgCDrsKnrspXsnoTsl5DripQg67aE66qF7ZWY64uk6rOgIO2VnOuLpC5cDQpcDQoNCiMjIDguNCBFdmFsdWF0aW5nIG1vZGVscw0KDQojIyMgQ3Jvc3MtdmFsaWRhdGlvbiwg6rWQ7LCo6rKA7KadDQoi6rO87KCB7ZWpIuydtOuegCDrrLTsl4fsnbzquYw/Pw0KXA0K67O4IOq1kOyerOyXkOyEnOuKlCAn7ZWZ7Iq1IOuNsOydtO2EsOyXkCDrjIDtlZwg7LWc7KCB7J2YIOunpOqwnOuzgOyImCDshLjtirjrpbwg6rKw7KCV7ZWY7JesIOuGkuydgCDsoJXtmZXrj4Trpbwg6rCA7KeA7KeA66eMLCDsg4jroZzsmrQg642w7J207YSw6rCAIOuTpOyWtOyYpOuptCDsmIjsuKHroKXsnbQg7ZiE7KCA7Z6IIOuWqOyWtOyngOuKlCDqsoMuJ+ydtOudvOqzoCDshKTrqoXtlZjsmIDri6QuDQpcDQpcDQoNCuq1kOywqOqygOymneq4sOuyleycvOuhnCAyLWZvbGQgY3Jvc3MtdmFsaWRhdGlvbuydgCDrjbDsnbTthLDrpbwg64+Z7J287ZWY6rKMIFgxLCBYMuuhnCDrgpjriIjri6QuIOydtO2bhCBYMeyXkOyEnCDrqqjrjbjsnYQg66eM65Ok6rOgIFgy7JeQ7IScIOy4oeygle2WiOydhCDrlYwsIOyemCDrj4zslYTqsITri6TripQg67O07J6l7J20IOyXhuuLpC4g6re4IOuLpOydjCwgWDHqs7wgWDLsnZgg7Jet7ZWg7J2EIOuwlOq/lOyEnCBYMuyXkOyEnCDtlZnsirXtlZjqs6AgWDHsl5DshJwg7YWM7Iqk7Yq47ZWgIOuVjCwg7LKrIOuqqOuNuOydtCDqs7zsoIHtlansnbTrnbzrqbQg6rWQ7LCo6rKA7KadIOyLnOyXkOuPhCDsnpgg7IiY7ZaJ65CY7KeAIOyViuydhCDtmZXrpaDsnbQg64aS64uk64qUIOqyg+ydhCDtmZzsmqntlZwg6rKA7Kad67KV7J2064ukLiDqsJnsnYAg67Cp7Iud7Jy866GcIGstZm9sZCBjb3Jzcy12YWxpZGF0aW9u7J20IOyekeuPme2VmOupsCwg7JyE7J2YIOuRkCDqsJzroZwg67aE66WY7ZWcIOqyg+ydhCDrj5nsnbztlZwg7YGs6riw7J2YIGvqsJzroZwg7J2867CY7ZmU7ZWY7JesIOyggeyaqe2VnOuLpC4NClwNClwNCg0KIyMjIE1lYXN1cmluZyBwcmVkaWN0aW9uIEVycm9yDQrsmIjsuKEg7Jik7LCo66W8IOqygOymne2VmOuKlCDrsKnrspXsnLzroZwg64uk7J2MIOuEpCDqsIDsp4DqsIAg7J6I64ukLlwNCg0KMSkgUk1TRTogUm9vdCBNZWFuIFNxdWFyZWQgRXJyb3INClwNCiRcc3FydCh7MSBcb3ZlciBufSBcc3VtICh5LSBcaGF0IHkpXjIpJFwNCg0KUk1TReuKlCAkXGhhdCB5JCjqtIDsuKHqsJIp7J20IHnsmYAg64+Z7J287ZWcIOuLqOychOyXkCDsnojqs6Ag6rO864yA7Y+J6rCA7JmAIOqzvOyGjO2PieqwgOulvCDrj5nsi5zsl5Ag7Y+s7LCpIOqwgOuKpe2VmOupsCwg7YGwIOyYpOywqOyXkCDtgbAg7Yyo64SQ7Yuw66W8IOu2gOqzvO2VnOuLpOuKlCDsnqXsoJDsnbQg7J6I64ukLlwNClwNCg0KMikgTUFFOiBNZWFuIEFic29sdXRlIEVycm9yDQpcDQokezEgXG92ZXIgbn0gXHN1bSBcbGVmdFx2ZXJ0IHkgLSB7XGhhdCB5fSBccmlnaHRcdmVydCRcDQoNClJNU0XsmYAg7Jyg7IKs7ZWY7KeA66eMLCDtgbAg7Jik7LCo7JeQIO2BsCDtjKjrhJDti7Drpbwg7KO864qUIOuwqeyLneydtCDslYTri4jri6QuIOychCDsiJjsi53sl5Ag7KCc6rOx7ZiV7J20IOyXhuyWtOyEnCDqt7jroIfri6QuXA0KXA0KDQozKSBDb3JyZWxhdGlvbg0KXA0KDQokKHktIFxoYXQgeSkk7J2EIOy1nOyGjO2ZlO2VmOugpOuKlCDrsKnrspXrs7Tri6TripQsIOy2lOyEuOulvCDtmZXsnbjtlZjqs6DsnpAg7ZWgIOuVjCDsoIHsoIjtlZwg67Cp67KV7J2064ukLiAkeV9p7JmAIHtcaGF0IHlfaX0k6rCAIOuPmeydvO2VnCDsg4HrjIAg7Iic7ISc7IOB7JeQIOyeiOuKlOyngCDtmZXsnbjtlZjsl6wg7Lih7KCV7ZWc64ukLlwNCg0KNCkgQ29lZmZpY2llbnQgb2YgZGV0ZXJtaW5hdGlvbg0KXA0KDQrtnZTtnoggJFJeMiTsnLzroZwg7JWM6rOgIOyeiOuKlCDqsrDsoJXqs4TsiJjrpbwg7J2Y66+47ZWc64ukLiAw6rO8IDHsgqzsnbTsnZgg7IiY7LmY66GcIO2RnO2YhO2VmOupsCAx7J2066m0IHnsmYAg64+Z7J287ZWo7J2EIOucu+2VnOuLpC5cDQpcDQpcDQoNCiMjIyBST0MgY3VydmVzDQpSZWNlaXZlciBPcGVyYXRpbmcgQ2hhcmFjdGVyaXN0aWMgY3VydmXsnZgg7JW97J6Q66GcLCDrqqjrk6Ag6rCA64ql7ZWcIOyehOqzhOqwkuydhCDqs6DroKTtlZjqs6Ag66+86rCQ64+E7JmAIO2KueydtOyEsSDsgqzsnbTsnZgg6reg7ZiV7J2EIOuCmO2DgOuCtOuKlCDqs6HshKDsnYQg66eQ7ZWc64ukLiDssYXsl5DshJzripQgUk9DUiDtjKjtgqTsp4Drpbwg7IKs7Jqp7ZWc64ukLg0KYGBge3IgbWVzc2FnZT1GQUxTRSwgd2FybmluZz1GQUxTRX0NCmluY29tZV9wcm9icyA8LSBtb2RfbmIgJT4lDQpwcmVkaWN0KG5ld2RhdGEgPSB0cmFpbiwgdHlwZSA9ICJyYXciKSAlPiUNCmFzLmRhdGEuZnJhbWUoKQ0KDQptb3NhaWM6OnRhbGx5KH5gID41MEtgID4gMC4yNCwgZGF0YSA9IGluY29tZV9wcm9icywgZm9ybWF0ID0gInBlcmNlbnQiKQ0KDQpwcmVkIDwtIFJPQ1I6OnByZWRpY3Rpb24oaW5jb21lX3Byb2JzWywyXSwgdHJhaW4kaW5jb21lKQ0KcGVyZiA8LSBST0NSOjpwZXJmb3JtYW5jZShwcmVkLCAndHByJywgJ2ZwcicpDQpjbGFzcyhwZXJmKQ0KDQpwZXJmX2RmIDwtIGRhdGEuZnJhbWUocGVyZkB4LnZhbHVlcywgcGVyZkB5LnZhbHVlcykNCm5hbWVzKHBlcmZfZGYpIDwtIGMoImZwciIsICJ0cHIiKQ0Kcm9jIDwtIGdncGxvdChkYXRhID0gcGVyZl9kZiwgYWVzKHggPSBmcHIsIHkgPSB0cHIpKSArDQogIGdlb21fbGluZShjb2xvcj0iYmx1ZSIpICsgDQogIGdlb21fYWJsaW5lKGludGVyY2VwdD0wLCBzbG9wZT0xLCBsdHk9MykgKw0KICB5bGFiKHBlcmZAeS5uYW1lKSArIHhsYWIocGVyZkB4Lm5hbWUpDQoNCnJvYw0KYGBgDQpcDQpcDQoNCiMjIyBCaWFzLXZhcmlhbmNlIHRyYWRlLW9mZg0K7Y647Zal6rO8IOu2hOyCsOydhCDrqqjrkZAg7LWc7IaM7ZmU7ZWY64qUIOuqqOuNuOydhCDsm5DtlZjsp4Drp4wsIOydtOuKlCDsg4HtmLgg67Cw7YOA7KCB7J24IOuqqe2RnOydtOuLpC4g67O17J6h7ZWcIOuqqOuNuOydgCDtjrjtlqXsnbQg6rCQ7IaM7ZWY7KeA66eMIOu2hOyCsOydtCDspp3qsIDtlZjqs6AsIOqwhOuLqO2VnCDrqqjrjbjsnYAg67aE7IKw7J20IOqwkOyGjO2VmOyngOunjCDtjrjtlqXsnbQg7Kad6rCA7ZWY6riwIOuVjOusuOydtOuLpC4g65Sw65287IScIOygleq3nO2ZlOulvCDthrXtlbQg7KCB7KCVIOq3oO2YleydhCDrp57strDslbwg7ZWc64ukLg0KXA0KXA0KDQrsoJXqt5ztmZTripQg6rO87KCB7ZWp7J2EIOuwqeyngO2VmOq4sCDsnITtlbQg7ZqM6reA66qo7ZiV7JeQIOygnOyVveyhsOqxtOydhCDstpTqsIDtlZjripQg6riw7Iig7J2EIOunkO2VnOuLpC4g7JiI7Lih67OA7IiYIOynke2VqeydtCDtgbQg65WMIO2Kue2eiCDsnKDsmqntlZjrqbAsIHJpZGdlIHJlZ3Jlc3Npb27qs7wgTGFzc28g67Cp7Iud7J20IOyeiOuLpC4NClwNClwNCg0KKipFWDog7IaM65OdIOuqqOuNuCDtj4nqsIAqKg0K66i87KCAICLrqqjrk6Ag7IKs656M7J2YIOyGjOuTneydtCA166eM67aIIOydtO2VmCLrnbzqs6Ag7JiI7Lih7ZWY64qUIE51bGwgbW9kZWzsnYQg6rWs7LaV7ZWc64ukLg0KYGBge3J9DQpmYXZzdGF0cyh+IGNhcGl0YWwuZ2FpbiwgZGF0YSA9IHRyYWluKQ0KYGBgDQpgYGB7cn0NCmZhdnN0YXRzKH4gY2FwaXRhbC5nYWluLCBkYXRhID0gdGVzdCkNCmBgYA0KYGBge3J9DQptb2RfbnVsbCA8LSBnbG0oaW5jb21lIH4gMSwgZGF0YSA9IHRyYWluLCBmYW1pbHkgPSBiaW5vbWlhbCkNCm1vZHMgPC0gbGlzdChtb2RfbnVsbCwgbW9kX3RyZWUsIG1vZF9mb3Jlc3QsIG1vZF9ubiwgbW9kX25iKQ0KDQpsYXBwbHkobW9kcywgY2xhc3MpDQoNCnByZWRpY3RfbWV0aG9kcyA8LSBtZXRob2RzKCJwcmVkaWN0IikNCnByZWRpY3RfbWV0aG9kc1tncmVwbChwYXR0ZXJuID0gIihnbG18cnBhcnR8cmFuZG9tRm9yZXN0fG5uZXR8bmFpdmUpIiwgcHJlZGljdF9tZXRob2RzKV0NCmBgYA0KDQrstpTqsIDtlZwg66qo6424IOuqqeuhneydhCDrsJjrs7XtlZjqs6Ag6rCBIOqwnOyytOyXkCDsoIHsoIjtlZwgcHJlZGljdCgpIOuplOyEnOuTnOulvCDsoIHsmqntlZzri6QuXA0KDQpgYGB7ciBtZXNzYWdlPUZBTFNFLCB3YXJuaW5nPUZBTFNFfQ0KbGlicmFyeSh0aWR5cikNCnByZWRpY3Rpb25zX3RyYWluIDwtDQogIGRhdGEuZnJhbWUoeSA9IGFzLmNoYXJhY3Rlcih0cmFpbiRpbmNvbWUpLA0KICAgICAgICAgICAgIHR5cGUgPSAidHJhaW4iLA0KICAgICAgICAgICAgIG1vZF9udWxsID0gcHJlZGljdChtb2RfbnVsbCwgdHlwZSA9InJlc3BvbnNlIiksDQogICAgICAgICAgICAgbW9kX3RyZWUgPSBwcmVkaWN0KG1vZF90cmVlLCB0eXBlID0gImNsYXNzIiksDQogICAgICAgICAgICAgbW9kX2ZvcmVzdCA9IHByZWRpY3QobW9kX2ZvcmVzdCwgdHlwZSA9ICJjbGFzcyIpLA0KICAgICAgICAgICAgIG1vZF9ubiA9IHByZWRpY3QobW9kX25uLCB0eXBlID0gImNsYXNzIiksDQogICAgICAgICAgICAgbW9kX25iID0gcHJlZGljdChtb2RfbmIsIG5ld2RhdGEgPSB0cmFpbiwgdHlwZSA9ICJjbGFzcyIpKQ0KDQpwcmVkaWN0aW9uc190ZXN0IDwtIA0KICBkYXRhLmZyYW1lKHkgPSBhcy5jaGFyYWN0ZXIodGVzdCRpbmNvbWUpLA0KICAgICAgICAgICAgIHR5cGUgPSAidGVzdCIsDQogICAgICAgICAgICAgbW9kX251bGwgPSBwcmVkaWN0KG1vZF9udWxsLCBuZXdkYXRhID0gdGVzdCwgdHlwZSA9ICJyZXNwb25zZSIpLA0KICAgICAgICAgICAgIG1vZF90cmVlID0gcHJlZGljdChtb2RfdHJlZSwgbmV3ZGF0YSA9IHRlc3QsIHR5cGUgPSAiY2xhc3MiKSwNCiAgICAgICAgICAgICBtb2RfZm9yZXN0ID0gcHJlZGljdChtb2RfZm9yZXN0LCBuZXdkYXRhID0gdGVzdCwgdHlwZSA9ICJjbGFzcyIpLA0KICAgICAgICAgICAgIG1vZF9ubiA9IHByZWRpY3QobW9kX25uLCBuZXdkYXRhID0gdGVzdCwgdHlwZSA9ICJjbGFzcyIpLA0KICAgICAgICAgICAgIG1vZF9uYiA9IHByZWRpY3QobW9kX25iLCBuZXdkYXRhID0gdGVzdCwgdHlwZSA9ICJjbGFzcyIpKQ0KDQpwcmVkaWN0aW9ucyA8LSBiaW5kX3Jvd3MocHJlZGljdGlvbnNfdHJhaW4sIHByZWRpY3Rpb25zX3Rlc3QpDQpnbGltcHNlKHByZWRpY3Rpb25zKQ0KDQpwcmVkaWN0aW9uc190aWR5IDwtIHByZWRpY3Rpb25zICU+JQ0KICBtdXRhdGUobW9kX251bGwgPSBpZmVsc2UobW9kX251bGwgPCAwLjUsICIgPD01MEsiLCAiID41MEsiKSkgJT4lDQogIGdhdGhlcihrZXkgPSAibW9kZWwiLCB2YWx1ZSA9ICJ5X2hhdCIsIC10eXBlLCAteSkNCmdsaW1wc2UocHJlZGljdGlvbnNfdGlkeSkNCmBgYA0KDQrqsIEg66qo64247JeQIOuMgO2VnCDsmIjsuKHqsJLsnYQg7Ja77JeI7Jy866+A66GcLCDsi6TsoJwgeeyZgCDruYTqtZDtlZjqs6Ag6rKw6rO866W8IOynkeqzhO2VnOuLpC5cDQoNCmBgYHtyIG1lc3NhZ2U9RkFMU0UsIHdhcm5pbmc9RkFMU0V9DQpwcmVkaWN0aW9uc19zdW1tYXJ5IDwtIHByZWRpY3Rpb25zX3RpZHkgJT4lDQogIGdyb3VwX2J5KG1vZGVsLCB0eXBlKSAlPiUNCiAgc3VtbWFyaXplKE4gPSBuKCksIGNvcnJlY3QgPSBzdW0oeSA9PSB5X2hhdCwgMCksDQogICAgICAgICAgICBwb3NpdGl2ZXMgPSBzdW0oeSA9PSAiID41MEsiKSwNCiAgICAgICAgICAgIHRydWVfcG9zID0gc3VtKHlfaGF0ID09ICIgPjUwSyIgJiB5ID09IHlfaGF0KSwNCiAgICAgICAgICAgIGZhbHNlX3BvcyA9IHN1bSh5X2hhdCA9PSAiID41MEsiICYgeSAhPSB5X2hhdCkpICU+JQ0KICBtdXRhdGUoYWNjdXJhY3kgPSBjb3JyZWN0IC8gTiwNCiAgICAgICAgIHRwciA9IHRydWVfcG9zIC8gcG9zaXRpdmVzLA0KICAgICAgICAgZnByID0gZmFsc2VfcG9zIC8gKE4gLSBwb3NpdGl2ZXMpKSAlPiUNCiAgdW5ncm91cCgpICU+JQ0KICBnYXRoZXIodmFsX3R5cGUsIHZhbCwgLW1vZGVsLCAtdHlwZSkgJT4lDQogIHVuaXRlKHRlbXAxLCB0eXBlLCB2YWxfdHlwZSwgc2VwID0gIl8iKSAlPiUgIyBnbHVlIHZhcmlhYmxlcw0KICBzcHJlYWQodGVtcDEsIHZhbCkgJT4lDQogIGFycmFuZ2UoZGVzYyh0ZXN0X2FjY3VyYWN5KSkgJT4lDQogIHNlbGVjdChtb2RlbCwgdHJhaW5fYWNjdXJhY3ksIHRlc3RfYWNjdXJhY3ksIHRlc3RfdHByLCB0ZXN0X2ZwcikNCg0KcHJlZGljdGlvbnNfc3VtbWFyeQ0KYGBgDQrrqqjrk6Ag66qo64247J2YIOygle2ZleuPhOuKlCDtlZnsirXqs7wg7YWM7Iqk7Yq4IOyEuO2KuOyXkOyEnCDqsbDsnZgg67mE7Iq37ZaI64ukLiDsnbTtm4QgUk9DIOqzoeyEoOydhCDqs4TsgrDtlbTrs7TrqbQg64uk7J2M6rO8IOqwmeuLpC5cDQpcDQpgYGB7ciBpbmNsdWRlPUZBTFNFfQ0Kb3V0cHV0cyA8LSBjKCJyZXNwb25zZSIsICJwcm9iIiwgInByb2IiLCAicmF3IiwgInJhdyIpDQpyb2NfdGVzdCA8LSBtYXBwbHkocHJlZGljdCwgbW9kcywgdHlwZSA9IG91dHB1dHMsDQogICAgICAgICAgICAgICAgICAgTW9yZUFyZ3MgPSBsaXN0KG5ld2RhdGEgPSB0ZXN0KSkgJT4lDQogIGFzLmRhdGEuZnJhbWUoKSAlPiUNCiAgc2VsZWN0KDEsMyw1LDYsOCkNCm5hbWVzKHJvY190ZXN0KSA8LSBjKCJtb2RfbnVsbCIsICJtb2RfdHJlZSIsICJtb2RfZm9yZXN0IiwgIm1vZF9ubiIsICJtb2RfbmIiKQ0KZ2xpbXBzZShyb2NfdGVzdCkNCg0KDQpnZXRfcm9jIDwtIGZ1bmN0aW9uKHgsIHkpIHsNCiAgcHJlZCA8LSBST0NSOjpwcmVkaWN0aW9uKHgkeV9oYXQsIHkpDQogIHBlcmYgPC0gUk9DUjo6cGVyZm9ybWFuY2UocHJlZCwgJ3RwcicsICdmcHInKQ0KICBwZXJmX2RmIDwtIGRhdGEuZnJhbWUocGVyZkB4LnZhbHVlcywgcGVyZkB5LnZhbHVlcykNCiAgbmFtZXMocGVyZl9kZikgPC0gYygiZnByIiwgInRwciIpDQogIHJldHVybihwZXJmX2RmKQ0KfQ0Kcm9jX3RpZHkgPC0gcm9jX3Rlc3QgJT4lDQogIGdhdGhlcihrZXkgPSAibW9kZWwiLCB2YWx1ZSA9ICJ5X2hhdCIpICU+JQ0KICBncm91cF9ieShtb2RlbCkgJT4lDQogIGRwbHlyOjpkbyhnZXRfcm9jKC4sIHkgPSB0ZXN0JGluY29tZSkpDQoNCg0KZ2dwbG90KGRhdGEgPSByb2NfdGlkeSwgYWVzKHggPSBmcHIsIHkgPSB0cHIpKSArDQogIGdlb21fbGluZShhZXMoY29sb3IgPSBtb2RlbCkpICsNCiAgZ2VvbV9hYmxpbmUoaW50ZXJjZXB0ID0gMCwgc2xvcGUgPSAxLCBsdHkgPSAzKSArDQogIHlsYWIocGVyZkB5Lm5hbWUpICsgeGxhYihwZXJmQHgubmFtZSkgKw0KICBnZW9tX3BvaW50KGRhdGEgPSBwcmVkaWN0aW9uc19zdW1tYXJ5LCBzaXplID0gMywNCiAgICAgICAgICAgICBhZXMoeCA9IHRlc3RfZnByLCB5ID0gdGVzdF90cHIsIGNvbG9yID0gbW9kZWwpKQ0KYGBgDQpOYWl2ZSBCYXllcyDrqqjrjbjsnbQg6rCA7J6lIOyii+ydgCDshLHriqXsnLzroZwg64KY7YOA64Ks64ukIQ0KXA0KXA0KXA0KDQojIyMg7LaU6rCAIOusuOygnA0KTkhBTkVT7JeQ7IScIOyhsOyCrO2VnCDshLHsnbjsnZgg7Jew66C5LCDssrTsp4jrn4nsp4DsiJgoQk1JKSwg64u564eo67ORIOqwhOydmCDqtIDqs4Trpbwg67O07J207Iuc7JikDQpgYGB7cn0NCmxpYnJhcnkoTkhBTkVTKQ0KcGVvcGxlIDwtIE5IQU5FUyAlPiUNCiAgc2VsZWN0KEFnZSwgR2VuZGVyLCBEaWFiZXRlcywgQk1JLCBISEluY29tZSwgUGh5c0FjdGl2ZSkgJT4lDQogIG5hLm9taXQoKQ0KZ2xpbXBzZShwZW9wbGUpDQpgYGANCmBgYHtyfQ0Kd2hvSXNEaWFiZXRpYyA8LSBycGFydChEaWFiZXRlcyB+IEFnZSArIEJNSSArIEdlbmRlciArIFBoeXNBY3RpdmUsDQogICAgICAgICAgICAgICAgICAgICAgIGRhdGEgPSBwZW9wbGUsIGNvbnRyb2wgPSBycGFydC5jb250cm9sKGNwID0gMC4wMDUsDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIG1pbmJ1Y2tldCA9IDMwKSkNCndob0lzRGlhYmV0aWMNCmBgYA0KYGBge3IgbWVzc2FnZT1GQUxTRSwgd2FybmluZz1GQUxTRX0NCmxpYnJhcnkocGFydHlraXQpDQpwbG90KGFzLnBhcnR5KHdob0lzRGlhYmV0aWMpKQ0KYGBgDQpwYXJ0eWtpdCDtjKjtgqTsp4Drpbwg7Zmc7Jqp7ZWY7JiA64ukLiDshozrk53snYQg7KCc7Jm47ZWcIOuqqOuToCDrs4DsiJjrpbwg7Y+s7ZWo7Iuc7YKoIOydmOyCrOqysOygleuCmOustOydtOuLpC4NClwNClwNCiAgDQpgYGB7cn0NCmdncGxvdChkYXRhID0gcGVvcGxlLCBhZXMoeCA9IEFnZSwgeSA9IEJNSSkpICsNCiAgZ2VvbV9jb3VudChhZXMoY29sb3IgPSBEaWFiZXRlcyksIGFscGhhID0gMC41KSArDQogIGdlb21fdmxpbmUoeGludGVyY2VwdCA9IDUyLjUpICsNCiAgZ2VvbV9zZWdtZW50KHggPSA1Mi41LCB4ZW5kID0gMTAwLCB5ID0gMzkuOTg1LCB5ZW5kID0gMzkuOTg1KSArDQogIGdlb21fc2VnbWVudCh4ID0gNjcuNSwgeGVuZCA9IDY3LjUsIHkgPSAzOS45ODUsIHllbmQgPSBJbmYpICsNCiAgZ2VvbV9zZWdtZW50KHggPSA2MC41LCB4ZW5kID0gNjAuNSwgeSA9IDM5Ljk4NSwgeWVuZCA9IEluZikgKw0KICBhbm5vdGF0ZSgicmVjdCIsIHhtaW4gPSA2MC41LCB4bWF4ID0gNjcuNSwgeW1pbiA9IDM5Ljk4NSwNCiAgICAgICAgICAgeW1heCA9IEluZiwgZmlsbCA9ICJibHVlIiwgYWxwaGEgPSAwLjEpDQpgYGANCuuPhO2RnOulvCDrs7TrqbQsIOqzoOugueyekOyZgCBCTUnqsIAg64aS7J2AIOyCrOuejOydtCDri7nrh6jqs7wg7Jew6rSA7J20IOyeiOuLpOqzoCDtjJDri6jtlaAg7IiYIOyeiOuLpC4gNTLshLgg7J207ZWY64qUIOuLueuHqOqwgCDsl4bsnYQg7ZmV66Wg7J20IOuNlCDrhpLsnLzrqbAgNjHshLjsl5DshJwgNjfshLgg7IKs7J206rCAIOqwgOyepSDrhpLri6QuIEJNSeuKlCA0MCDsnbTsg4Hsnbwg65WMIOychO2XmOuPhOqwgCDspp3qsIDtlZzri6QuDQpcDQpcDQoNCuuLpOydjOydgCA26rCcIOuqqOuNuOydhCDrqqjrkZAg7Zmc7Jqp7ZWY7JesIOuqqOuNuOqwhOydmCDssKjsnbTrpbwg7JWM7JWE67O07JWY64ukLg0KYGBge3IgaW5jbHVkZT1GQUxTRX0NCmFnZXMgPC0gcmFuZ2UofiBBZ2UsIGRhdGEgPSBwZW9wbGUpDQpibWlzIDwtIHJhbmdlKH4gQk1JLCBkYXRhID0gcGVvcGxlKQ0KcmVzIDwtIDEwMA0KZmFrZV9ncmlkIDwtIGV4cGFuZC5ncmlkKA0KQWdlID0gc2VxKGZyb20gPSBhZ2VzWzFdLCB0byA9IGFnZXNbMl0sIGxlbmd0aC5vdXQgPSByZXMpLA0KQk1JID0gc2VxKGZyb20gPSBibWlzWzFdLCB0byA9IGJtaXNbMl0sIGxlbmd0aC5vdXQgPSByZXMpKQ0KDQpmb3JtIDwtIGFzLmZvcm11bGEoIkRpYWJldGVzIH4gQWdlICsgQk1JIikNCmRtb2RfdHJlZSA8LSBycGFydChmb3JtLCBkYXRhID0gcGVvcGxlLA0KY29udHJvbCA9IHJwYXJ0LmNvbnRyb2woY3AgPSAwLjAwNSwgbWluYnVja2V0ID0gMzApKQ0KDQpkbW9kX2ZvcmVzdCA8LSByYW5kb21Gb3Jlc3QoZm9ybSwgZGF0YSA9IHBlb3BsZSwgbnRyZWUgPSAyMDEsIG10cnkgPSAzKQ0KZG1vZF9ubmV0IDwtIG5uZXQoZm9ybSwgZGF0YSA9IHBlb3BsZSwgc2l6ZSA9IDYpDQoNCmRtb2RfbmIgPC0gbmFpdmVCYXllcyhmb3JtLCBkYXRhID0gcGVvcGxlKQ0KcHJlZF90cmVlIDwtIHByZWRpY3QoZG1vZF90cmVlLCBuZXdkYXRhID0gZmFrZV9ncmlkKVssICJZZXMiXQ0KcHJlZF9mb3Jlc3QgPC0gcHJlZGljdChkbW9kX2ZvcmVzdCwgbmV3ZGF0YSA9IGZha2VfZ3JpZCwgdHlwZSA9ICJwcm9iIilbLCAiWWVzIl0NCnByZWRfa25uIDwtIHBlb3BsZSAlPiUNCiAgc2VsZWN0KEFnZSwgQk1JKSAlPiUNCiAga25uKHRlc3QgPSBzZWxlY3QoZmFrZV9ncmlkLCBBZ2UsIEJNSSksIGNsID0gcGVvcGxlJERpYWJldGVzLCBrID0gNSkgJT4lDQogIGFzLm51bWVyaWMoKSAtIDENCg0KcHJlZF9ubmV0IDwtIHByZWRpY3QoZG1vZF9ubmV0LCBuZXdkYXRhID0gZmFrZV9ncmlkLCB0eXBlID0gInJhdyIpICU+JQ0KICBhcy5udW1lcmljKCkNCg0KcHJlZF9uYiA8LSBwcmVkaWN0KGRtb2RfbmIsIG5ld2RhdGEgPSBmYWtlX2dyaWQsIHR5cGUgPSAicmF3IilbLCAiWWVzIl0NCmBgYA0KDQoNCmBgYHtyIGluY2x1ZGU9RkFMU0V9DQpwIDwtIG1vc2FpYzo6dGFsbHkofiBEaWFiZXRlcywgZGF0YSA9IHBlb3BsZSwgZm9ybWF0ID0gInByb3BvcnRpb24iKVsiWWVzIl0NCg0KcmVzIDwtIGZha2VfZ3JpZCAlPiUNCiAgbXV0YXRlKCJOdWxsIiA9IHJlcChwLCBucm93KGZha2VfZ3JpZCkpLCAiRGVjaXNpb24gVHJlZSIgPSBwcmVkX3RyZWUsDQogICAgICAgICAiUmFuZG9tIEZvcmVzdCIgPSBwcmVkX2ZvcmVzdCwgImstTmVhcmVzdCBOZWlnaGJvciIgPSBwcmVkX2tubiwNCiAgICAgICAgICJOZXVyYWwgTmV0d29yayIgPSBwcmVkX25uZXQsICJOYWl2ZSBCYXllcyIgPSBwcmVkX25iKSAlPiUNCiAgZ2F0aGVyKGtleSA9ICJtb2RlbCIsIHZhbHVlID0gInlfaGF0IiwgLUFnZSwgLUJNSSkNCmBgYA0KDQoNCmBgYHtyfQ0KZ2dwbG90KGRhdGEgPSByZXMsIGFlcyh4ID0gQWdlLCB5ID0gQk1JKSkgKw0KICBnZW9tX3RpbGUoYWVzKGZpbGwgPSB5X2hhdCksIGNvbG9yID0gTkEpICsNCiAgZ2VvbV9jb3VudChhZXMoY29sb3IgPSBEaWFiZXRlcyksIGFscGhhID0gMC40LCBkYXRhID0gcGVvcGxlKSArDQogIHNjYWxlX2ZpbGxfZ3JhZGllbnQobG93ID0gIndoaXRlIiwgaGlnaCA9ICJkb2RnZXJibHVlIikgKw0KICBzY2FsZV9jb2xvcl9tYW51YWwodmFsdWVzID0gYygiZ3JheSIsICJnb2xkIikpICsNCiAgc2NhbGVfc2l6ZShyYW5nZSA9IGMoMCwgMikpICsNCiAgc2NhbGVfeF9jb250aW51b3VzKGV4cGFuZCA9IGMoMC4wMiwwKSkgKw0KICBzY2FsZV95X2NvbnRpbnVvdXMoZXhwYW5kID0gYygwLjAyLDApKSArDQogIGZhY2V0X3dyYXAofm1vZGVsKQ0KYGBgDQoNCuydmOyCrOqysOygleuCmOustOuKlCDrtoTrqoXtlZwg7KeB7ISg7Jy866GcIO2ZleuloOydhCDqtazrtoTtlZjsmIDsnLzrqbAgay1OTuydgCDsnKDsl7DtlZjqsowg7J207KeE7JiI7Lih7J2EIO2VmOyYgOuLpC4g64KY7J2067iM67Kg7J207KaI64qUIOu5hOyEoO2YlSDrspTsnITrpbwg7IOd7ISx7ZWY7JiA6rOgLCDrnpzrjaTtj6zroIjsiqTtirjripQgay1OTuqzvCDsnKDsl7DshLHsnYAg67mE7Iq37ZWY7KeA66eMIOuJmOyVmeyKpOqwgCDrjZQg66eO7JWY64ukLiBOdWxs66qo64247J2AIOq3oOydvO2VmOqyjCDsmIjsuKHtlZjsmIDri6QuDQpcDQpcDQpcDQpcDQpcDQo=