Note that the directories used to store data are likely different on your computer, and such references will need to be changed before using any such code.

library(knitr)
library(kableExtra)
html_df <- function(text, cols=NULL, col1=FALSE, full=F) {
  if(!length(cols)) {
    cols=colnames(text)
  }
  if(!col1) {
    kable(text,"html", col.names = cols, align = c("l",rep('c',length(cols)-1))) %>%
      kable_styling(bootstrap_options = c("striped","hover"), full_width=full)
  } else {
    kable(text,"html", col.names = cols, align = c("l",rep('c',length(cols)-1))) %>%
      kable_styling(bootstrap_options = c("striped","hover"), full_width=full) %>%
      column_spec(1,bold=T)
  }
}
library(tidyverse)
```r
```r
df <- readRDS('../../Data/Session_6_models.rds')
library(xgboost)
# Prep data
train_x <- model.matrix(AAER ~ ., data=df[df$Test==0,-1])[,-1]
train_y <- model.frame(AAER ~ ., data=df[df$Test==0,])[,\AAER\]
test_x <- model.matrix(AAER ~ ., data=df[df$Test==1,-1])[,-1]
test_y <- model.frame(AAER ~ ., data=df[df$Test==1,])[,\AAER\]
set.seed(468435)  #for reproducibility
xgbCV <- xgb.cv(max_depth=5, eta=0.10, gamma=5, min_child_weight = 4,
                subsample = 0.57, objective = \binary:logistic\, data=train_x,
                label=train_y, nrounds=100, eval_metric=\auc\, nfold=10,
                stratified=TRUE, verbose=0)
fit_ens <- xgboost(params=xgbCV$params, data = train_x, label = train_y,
                   nrounds = which.max(xgbCV$evaluation_log$test_auc_mean),
                   verbose = 0)

library(yardstick)
all_x <- model.matrix(AAER ~ ., data=df[,-1])[,-1]
df$pred_ens <- predict(fit_ens, all_x, type="response")

df_test <- df %>% filter(Test==1)

aucs <- c(df_test %>% roc_auc(truth=AAER, estimate=pred_ens, event_level='second') %>% pull(.estimate),
          df_test %>% roc_auc(truth=AAER, estimate=pred_BCE, event_level='second') %>% pull(.estimate),
          df_test %>% roc_auc(truth=AAER, estimate=pred_lmin, event_level='second') %>% pull(.estimate),
          df_test %>% roc_auc(truth=AAER, estimate=pred_xgb, event_level='second') %>% pull(.estimate))
names(aucs) <- c("Ensemble", "Logit (BCE)", "Lasso (lambda.min)", "XGBoost")


curve_out_ens <- df_test %>% roc_curve(truth=AAER, estimate=pred_ens, event_level='second')
curve_out_BCE <- df_test %>% roc_curve(truth=AAER, estimate=pred_BCE, event_level='second')
curve_out_lmin <- df_test %>% roc_curve(truth=AAER, estimate=pred_lmin, event_level='second')
curve_out_xgb <- df_test %>% roc_curve(truth=AAER, estimate=pred_xgb, event_level='second')

ggplot() +
  geom_line(data=curve_out_BCE, aes(y=sensitivity, x=1-specificity, color="BCE")) + 
  geom_line(data=curve_out_lmin, aes(y=sensitivity, x=1-specificity, color="LASSO, lambda.min")) + 
  geom_line(data=curve_out_xgb, aes(y=sensitivity, x=1-specificity, color="XGBoost")) +
  geom_line(data=curve_out_ens, aes(y=sensitivity, x=1-specificity, color="Ensemble")) +
  geom_abline(slope=1)

aucs  # Out of sample
          Ensemble        Logit (BCE) Lasso (lambda.min)            XGBoost 
         0.8169036          0.7599594          0.7290185          0.8083503 
xgb.train.data = xgb.DMatrix(train_x, label = train_y, missing = NA)
col_names = attr(xgb.train.data, ".Dimnames")[[2]]
imp = xgb.importance(col_names, fit_ens)
# Variable importance
xgb.plot.importance(imp)

library(gender)
PLEASE NOTE: The method provided by this package must be used cautiously
and responsibly. Please be sure to see the guidelines and warnings about
usage in the README or the package documentation.
df <- read_csv('../../Data/City_of_Chicago_Salary_2019.04.csv')

-- Column specification ---------------------------------------------------------------------------------------------------------------------------------------
cols(
  Name = col_character(),
  `Job Titles` = col_character(),
  Department = col_character(),
  `Full or Part-Time` = col_character(),
  `Salary or Hourly` = col_character(),
  `Typical Hours` = col_double(),
  `Annual Salary` = col_double(),
  `Hourly Rate` = col_double()
)
names(df) <- make.names(names(df),unique = TRUE)
df <- df %>%
  mutate(Full.Time = ifelse(Full.or.Part.Time == 'F',1 ,0),
         Salaried = ifelse(Salary.or.Hourly == 'Salary', 1, 0),
         Salary = ifelse(is.na(Annual.Salary), Typical.Hours * Hourly.Rate, Annual.Salary),
         Obs = n()) %>%
  group_by(Job.Titles) %>%
  mutate(Job.Titles.Freq = n()/Obs) %>%
  ungroup() %>%
  group_by(Department) %>%
  mutate(Department.Freq = n()/Obs) %>%
  ungroup() %>%
  mutate(Job.Titles = ifelse(Job.Titles.Freq > 0.03, Job.Titles, 'Other'),
         Department = ifelse(Department.Freq > 0.05, Department, 'Other'),
         Job.Titles = factor(Job.Titles),
         Department = factor(Department),
         first_name = str_extract(df$Name, '(?<=,[:space:]{2})[:alpha:]+'))

genders <- gender(df$first_name) %>% distinct(name, .keep_all = T)
genders <- genders %>%
  mutate(Female = ifelse(proportion_female > proportion_male,1,0))
df <- left_join(df, genders[, c('name', 'Female')], by = c("first_name" = "name"))

train_x <- model.matrix(Salary ~ Job.Titles + Department + Full.Time + Salaried + Female, data=df)[,-1]
train_y <- model.frame(Salary ~ Job.Titles + Department + Full.Time + Salaried + Female, data=df)[, "Salary"]

set.seed(654687)  #for reproducibility
xgbCV <- xgb.cv(max_depth=6, eta=0.30, gamma=0, min_child_weight = 1,
                subsample = 1, booster = 'gblinear', data=train_x,
                label=train_y, nrounds=100, eval_metric="rmse", nfold=10,
                verbose=0)

fit_xgb <- xgboost(params=xgbCV$params, data = train_x, label = train_y,
                   nrounds = which.max(xgbCV$evaluation_log$test_rmse_mean))
[00:44:27] WARNING: amalgamation/../src/learner.cc:573: 
Parameters: { "gamma", "max_depth", "min_child_weight", "silent", "subsample" } might not be used.

  This may not be accurate due to some parameters are only used in language bindings but
  passed down to XGBoost core.  Or some parameters are not used but slip through this
  verification. Please open an issue if you find above cases.


[1] train-rmse:33171.421875 
library('SHAPforxgboost')
#https://liuyanguu.github.io/post/2019/07/18/visualization-of-shap-for-xgboost/
shap_values <- shap.values(xgb_model = fit_xgb, X_train = train_x)
shap_values$mean_shap_score
                                        Salaried                                        Full.Time                                 DepartmentPOLICE 
                                     15541.90119                                      14542.46170                                       8310.01701 
                                 Job.TitlesOther                         Job.TitlesPOLICE OFFICER                                           Female 
                                      6298.13637                                       5415.61501                                       3823.13352 
                                 DepartmentOther                               Job.TitlesSERGEANT Job.TitlesPOLICE OFFICER (ASSIGNED AS DETECTIVE) 
                                      2477.28417                                       1021.78531                                        773.86084 
                    Job.TitlesMOTOR TRUCK DRIVER                                   DepartmentOEMC                          DepartmentSTREETS & SAN 
                                       259.84506                                        175.65395                                         89.93510 
                           DepartmentWATER MGMNT 
                                        29.38586 
shap_long <- shap.prep(shap_contrib = shap_values$shap_score, X_train=train_x)
shap.plot.summary(shap_long)

plot_data <- shap.prep.stack.data(shap_contrib = shap_values$shap_score,
                                  top_n = 5, n_groups = 6)
The SHAP values of the Rest 8 features were summed into variable 'rest_variables'.
shap.plot.force_plot_bygroup(plot_data)

LS0tDQp0aXRsZTogIkNvZGUgZm9yIFNlc3Npb24gOSINCmF1dGhvcjogIkRyLiBSaWNoYXJkIE0uIENyb3dsZXkiDQpkYXRlOiAiIg0Kb3V0cHV0Og0KICBodG1sX25vdGVib29rDQotLS0NCg0KTm90ZSB0aGF0IHRoZSBkaXJlY3RvcmllcyB1c2VkIHRvIHN0b3JlIGRhdGEgYXJlIGxpa2VseSBkaWZmZXJlbnQgb24geW91ciBjb21wdXRlciwgYW5kIHN1Y2ggcmVmZXJlbmNlcyB3aWxsIG5lZWQgdG8gYmUgY2hhbmdlZCBiZWZvcmUgdXNpbmcgYW55IHN1Y2ggY29kZS4NCg0KYGBge3IgaGVscGVycywgd2FybmluZz1GQUxTRSwgbWVzc2FnZT1GfQ0KbGlicmFyeShrbml0cikNCmxpYnJhcnkoa2FibGVFeHRyYSkNCmh0bWxfZGYgPC0gZnVuY3Rpb24odGV4dCwgY29scz1OVUxMLCBjb2wxPUZBTFNFLCBmdWxsPUYpIHsNCiAgaWYoIWxlbmd0aChjb2xzKSkgew0KICAgIGNvbHM9Y29sbmFtZXModGV4dCkNCiAgfQ0KICBpZighY29sMSkgew0KICAgIGthYmxlKHRleHQsImh0bWwiLCBjb2wubmFtZXMgPSBjb2xzLCBhbGlnbiA9IGMoImwiLHJlcCgnYycsbGVuZ3RoKGNvbHMpLTEpKSkgJT4lDQogICAgICBrYWJsZV9zdHlsaW5nKGJvb3RzdHJhcF9vcHRpb25zID0gYygic3RyaXBlZCIsImhvdmVyIiksIGZ1bGxfd2lkdGg9ZnVsbCkNCiAgfSBlbHNlIHsNCiAgICBrYWJsZSh0ZXh0LCJodG1sIiwgY29sLm5hbWVzID0gY29scywgYWxpZ24gPSBjKCJsIixyZXAoJ2MnLGxlbmd0aChjb2xzKS0xKSkpICU+JQ0KICAgICAga2FibGVfc3R5bGluZyhib290c3RyYXBfb3B0aW9ucyA9IGMoInN0cmlwZWQiLCJob3ZlciIpLCBmdWxsX3dpZHRoPWZ1bGwpICU+JQ0KICAgICAgY29sdW1uX3NwZWMoMSxib2xkPVQpDQogIH0NCn0NCmBgYA0KDQpgYGB7cn0NCmxpYnJhcnkodGlkeXZlcnNlKQ0KYGBgDQoNCmBgYHtyLCBtZXNzYWdlPUYsIHdhcm5pbmdzPUYsIG1lc3NhZ2U9RiwgcmVzdWx0cz1GfQ0KZGYgPC0gcmVhZFJEUygnLi4vLi4vRGF0YS9TZXNzaW9uXzZfbW9kZWxzLnJkcycpDQpkZjIgPC0gZGYgJT4lIG11dGF0ZShBQUVSID0gaWZlbHNlKEFBRVI9PTAsMCwxKSkNCiNoZWFkKGRmKSAlPiUgc2VsZWN0KC1wcmVkX0YsIC1wcmVkX1MpICU+JSBzbGljZSgxOjIpICU+JSBodG1sX2RmKCkNCmxpYnJhcnkoeGdib29zdCkNCg0KIyBQcmVwIGRhdGENCnRyYWluX3ggPC0gbW9kZWwubWF0cml4KEFBRVIgfiAuLCBkYXRhPWRmMltkZjIkVGVzdD09MCwtMV0pWywtMV0NCnRyYWluX3kgPC0gbW9kZWwuZnJhbWUoQUFFUiB+IC4sIGRhdGE9ZGYyW2RmMiRUZXN0PT0wLF0pWywiQUFFUiJdDQp0ZXN0X3ggPC0gbW9kZWwubWF0cml4KEFBRVIgfiAuLCBkYXRhPWRmMltkZjIkVGVzdD09MSwtMV0pWywtMV0NCnRlc3RfeSA8LSBtb2RlbC5mcmFtZShBQUVSIH4gLiwgZGF0YT1kZjJbZGYyJFRlc3Q9PTEsXSlbLCJBQUVSIl0NCg0Kc2V0LnNlZWQoNDY4NDM1KSAgI2ZvciByZXByb2R1Y2liaWxpdHkNCnhnYkNWIDwtIHhnYi5jdihtYXhfZGVwdGg9NSwgZXRhPTAuMTAsIGdhbW1hPTUsIG1pbl9jaGlsZF93ZWlnaHQgPSA0LA0KICAgICAgICAgICAgICAgIHN1YnNhbXBsZSA9IDAuNSwgb2JqZWN0aXZlID0gImJpbmFyeTpsb2dpc3RpYyIsIGRhdGE9dHJhaW5feCwNCiAgICAgICAgICAgICAgICBsYWJlbD10cmFpbl95LCBucm91bmRzPTEwMCwgZXZhbF9tZXRyaWM9ImF1YyIsIG5mb2xkPTEwLA0KICAgICAgICAgICAgICAgIHN0cmF0aWZpZWQ9VFJVRSwgdmVyYm9zZT0wKQ0KDQpmaXRfZW5zIDwtIHhnYm9vc3QocGFyYW1zPXhnYkNWJHBhcmFtcywgZGF0YSA9IHRyYWluX3gsIGxhYmVsID0gdHJhaW5feSwNCiAgICAgICAgICAgICAgICAgICBucm91bmRzID0gd2hpY2gubWF4KHhnYkNWJGV2YWx1YXRpb25fbG9nJHRlc3RfYXVjX21lYW4pLA0KICAgICAgICAgICAgICAgICAgIHZlcmJvc2UgPSAwKQ0KYGBgDQoNCmBgYHtyLCB3YXJuaW5nPUYsIG1lc3NhZ2U9RiwgZmlnLmhlaWdodD0zLjV9DQpsaWJyYXJ5KHlhcmRzdGljaykNCmFsbF94IDwtIG1vZGVsLm1hdHJpeChBQUVSIH4gLiwgZGF0YT1kZlssLTFdKVssLTFdDQpkZiRwcmVkX2VucyA8LSBwcmVkaWN0KGZpdF9lbnMsIGFsbF94LCB0eXBlPSJyZXNwb25zZSIpDQoNCmRmX3Rlc3QgPC0gZGYgJT4lIGZpbHRlcihUZXN0PT0xKQ0KDQphdWNzIDwtIGMoZGZfdGVzdCAlPiUgcm9jX2F1Yyh0cnV0aD1BQUVSLCBlc3RpbWF0ZT1wcmVkX2VucywgZXZlbnRfbGV2ZWw9J3NlY29uZCcpICU+JSBwdWxsKC5lc3RpbWF0ZSksDQogICAgICAgICAgZGZfdGVzdCAlPiUgcm9jX2F1Yyh0cnV0aD1BQUVSLCBlc3RpbWF0ZT1wcmVkX0JDRSwgZXZlbnRfbGV2ZWw9J3NlY29uZCcpICU+JSBwdWxsKC5lc3RpbWF0ZSksDQogICAgICAgICAgZGZfdGVzdCAlPiUgcm9jX2F1Yyh0cnV0aD1BQUVSLCBlc3RpbWF0ZT1wcmVkX2xtaW4sIGV2ZW50X2xldmVsPSdzZWNvbmQnKSAlPiUgcHVsbCguZXN0aW1hdGUpLA0KICAgICAgICAgIGRmX3Rlc3QgJT4lIHJvY19hdWModHJ1dGg9QUFFUiwgZXN0aW1hdGU9cHJlZF94Z2IsIGV2ZW50X2xldmVsPSdzZWNvbmQnKSAlPiUgcHVsbCguZXN0aW1hdGUpKQ0KbmFtZXMoYXVjcykgPC0gYygiRW5zZW1ibGUiLCAiTG9naXQgKEJDRSkiLCAiTGFzc28gKGxhbWJkYS5taW4pIiwgIlhHQm9vc3QiKQ0KDQoNCmN1cnZlX291dF9lbnMgPC0gZGZfdGVzdCAlPiUgcm9jX2N1cnZlKHRydXRoPUFBRVIsIGVzdGltYXRlPXByZWRfZW5zLCBldmVudF9sZXZlbD0nc2Vjb25kJykNCmN1cnZlX291dF9CQ0UgPC0gZGZfdGVzdCAlPiUgcm9jX2N1cnZlKHRydXRoPUFBRVIsIGVzdGltYXRlPXByZWRfQkNFLCBldmVudF9sZXZlbD0nc2Vjb25kJykNCmN1cnZlX291dF9sbWluIDwtIGRmX3Rlc3QgJT4lIHJvY19jdXJ2ZSh0cnV0aD1BQUVSLCBlc3RpbWF0ZT1wcmVkX2xtaW4sIGV2ZW50X2xldmVsPSdzZWNvbmQnKQ0KY3VydmVfb3V0X3hnYiA8LSBkZl90ZXN0ICU+JSByb2NfY3VydmUodHJ1dGg9QUFFUiwgZXN0aW1hdGU9cHJlZF94Z2IsIGV2ZW50X2xldmVsPSdzZWNvbmQnKQ0KDQpnZ3Bsb3QoKSArDQogIGdlb21fbGluZShkYXRhPWN1cnZlX291dF9CQ0UsIGFlcyh5PXNlbnNpdGl2aXR5LCB4PTEtc3BlY2lmaWNpdHksIGNvbG9yPSJCQ0UiKSkgKyANCiAgZ2VvbV9saW5lKGRhdGE9Y3VydmVfb3V0X2xtaW4sIGFlcyh5PXNlbnNpdGl2aXR5LCB4PTEtc3BlY2lmaWNpdHksIGNvbG9yPSJMQVNTTywgbGFtYmRhLm1pbiIpKSArIA0KICBnZW9tX2xpbmUoZGF0YT1jdXJ2ZV9vdXRfeGdiLCBhZXMoeT1zZW5zaXRpdml0eSwgeD0xLXNwZWNpZmljaXR5LCBjb2xvcj0iWEdCb29zdCIpKSArDQogIGdlb21fbGluZShkYXRhPWN1cnZlX291dF9lbnMsIGFlcyh5PXNlbnNpdGl2aXR5LCB4PTEtc3BlY2lmaWNpdHksIGNvbG9yPSJFbnNlbWJsZSIpKSArDQogIGdlb21fYWJsaW5lKHNsb3BlPTEpDQpgYGANCg0KYGBge3J9DQphdWNzICAjIE91dCBvZiBzYW1wbGUNCmBgYA0KDQpgYGB7ciwgZmlnLmhlaWdodD00LjV9DQp4Z2IudHJhaW4uZGF0YSA9IHhnYi5ETWF0cml4KHRyYWluX3gsIGxhYmVsID0gdHJhaW5feSwgbWlzc2luZyA9IE5BKQ0KY29sX25hbWVzID0gYXR0cih4Z2IudHJhaW4uZGF0YSwgIi5EaW1uYW1lcyIpW1syXV0NCmltcCA9IHhnYi5pbXBvcnRhbmNlKGNvbF9uYW1lcywgZml0X2VucykNCiMgVmFyaWFibGUgaW1wb3J0YW5jZQ0KeGdiLnBsb3QuaW1wb3J0YW5jZShpbXApDQpgYGANCg0KYGBge3IsIG1lc3NhZ2U9Riwgd2FybmluZz1GfQ0KbGlicmFyeShnZW5kZXIpDQpkZiA8LSByZWFkX2NzdignLi4vLi4vRGF0YS9DaXR5X29mX0NoaWNhZ29fU2FsYXJ5XzIwMTkuMDQuY3N2JykNCm5hbWVzKGRmKSA8LSBtYWtlLm5hbWVzKG5hbWVzKGRmKSx1bmlxdWUgPSBUUlVFKQ0KZGYgPC0gZGYgJT4lDQogIG11dGF0ZShGdWxsLlRpbWUgPSBpZmVsc2UoRnVsbC5vci5QYXJ0LlRpbWUgPT0gJ0YnLDEgLDApLA0KICAgICAgICAgU2FsYXJpZWQgPSBpZmVsc2UoU2FsYXJ5Lm9yLkhvdXJseSA9PSAnU2FsYXJ5JywgMSwgMCksDQogICAgICAgICBTYWxhcnkgPSBpZmVsc2UoaXMubmEoQW5udWFsLlNhbGFyeSksIFR5cGljYWwuSG91cnMgKiBIb3VybHkuUmF0ZSwgQW5udWFsLlNhbGFyeSksDQogICAgICAgICBPYnMgPSBuKCkpICU+JQ0KICBncm91cF9ieShKb2IuVGl0bGVzKSAlPiUNCiAgbXV0YXRlKEpvYi5UaXRsZXMuRnJlcSA9IG4oKS9PYnMpICU+JQ0KICB1bmdyb3VwKCkgJT4lDQogIGdyb3VwX2J5KERlcGFydG1lbnQpICU+JQ0KICBtdXRhdGUoRGVwYXJ0bWVudC5GcmVxID0gbigpL09icykgJT4lDQogIHVuZ3JvdXAoKSAlPiUNCiAgbXV0YXRlKEpvYi5UaXRsZXMgPSBpZmVsc2UoSm9iLlRpdGxlcy5GcmVxID4gMC4wMywgSm9iLlRpdGxlcywgJ090aGVyJyksDQogICAgICAgICBEZXBhcnRtZW50ID0gaWZlbHNlKERlcGFydG1lbnQuRnJlcSA+IDAuMDUsIERlcGFydG1lbnQsICdPdGhlcicpLA0KICAgICAgICAgSm9iLlRpdGxlcyA9IGZhY3RvcihKb2IuVGl0bGVzKSwNCiAgICAgICAgIERlcGFydG1lbnQgPSBmYWN0b3IoRGVwYXJ0bWVudCksDQogICAgICAgICBmaXJzdF9uYW1lID0gc3RyX2V4dHJhY3QoZGYkTmFtZSwgJyg/PD0sWzpzcGFjZTpdezJ9KVs6YWxwaGE6XSsnKSkNCg0KZ2VuZGVycyA8LSBnZW5kZXIoZGYkZmlyc3RfbmFtZSkgJT4lIGRpc3RpbmN0KG5hbWUsIC5rZWVwX2FsbCA9IFQpDQpnZW5kZXJzIDwtIGdlbmRlcnMgJT4lDQogIG11dGF0ZShGZW1hbGUgPSBpZmVsc2UocHJvcG9ydGlvbl9mZW1hbGUgPiBwcm9wb3J0aW9uX21hbGUsMSwwKSkNCmRmIDwtIGxlZnRfam9pbihkZiwgZ2VuZGVyc1ssIGMoJ25hbWUnLCAnRmVtYWxlJyldLCBieSA9IGMoImZpcnN0X25hbWUiID0gIm5hbWUiKSkNCg0KdHJhaW5feCA8LSBtb2RlbC5tYXRyaXgoU2FsYXJ5IH4gSm9iLlRpdGxlcyArIERlcGFydG1lbnQgKyBGdWxsLlRpbWUgKyBTYWxhcmllZCArIEZlbWFsZSwgZGF0YT1kZilbLC0xXQ0KdHJhaW5feSA8LSBtb2RlbC5mcmFtZShTYWxhcnkgfiBKb2IuVGl0bGVzICsgRGVwYXJ0bWVudCArIEZ1bGwuVGltZSArIFNhbGFyaWVkICsgRmVtYWxlLCBkYXRhPWRmKVssICJTYWxhcnkiXQ0KDQpzZXQuc2VlZCg2NTQ2ODcpICAjZm9yIHJlcHJvZHVjaWJpbGl0eQ0KeGdiQ1YgPC0geGdiLmN2KG1heF9kZXB0aD02LCBldGE9MC4zMCwgZ2FtbWE9MCwgbWluX2NoaWxkX3dlaWdodCA9IDEsDQogICAgICAgICAgICAgICAgc3Vic2FtcGxlID0gMSwgYm9vc3RlciA9ICdnYmxpbmVhcicsIGRhdGE9dHJhaW5feCwNCiAgICAgICAgICAgICAgICBsYWJlbD10cmFpbl95LCBucm91bmRzPTEwMCwgZXZhbF9tZXRyaWM9InJtc2UiLCBuZm9sZD0xMCwNCiAgICAgICAgICAgICAgICB2ZXJib3NlPTApDQoNCmZpdF94Z2IgPC0geGdib29zdChwYXJhbXM9eGdiQ1YkcGFyYW1zLCBkYXRhID0gdHJhaW5feCwgbGFiZWwgPSB0cmFpbl95LA0KICAgICAgICAgICAgICAgICAgIG5yb3VuZHMgPSB3aGljaC5tYXgoeGdiQ1YkZXZhbHVhdGlvbl9sb2ckdGVzdF9ybXNlX21lYW4pKQ0KYGBgDQoNCmBgYHtyLCB3YXJuaW5nPUZ9DQpsaWJyYXJ5KCdTSEFQZm9yeGdib29zdCcpDQojaHR0cHM6Ly9saXV5YW5ndXUuZ2l0aHViLmlvL3Bvc3QvMjAxOS8wNy8xOC92aXN1YWxpemF0aW9uLW9mLXNoYXAtZm9yLXhnYm9vc3QvDQpzaGFwX3ZhbHVlcyA8LSBzaGFwLnZhbHVlcyh4Z2JfbW9kZWwgPSBmaXRfeGdiLCBYX3RyYWluID0gdHJhaW5feCkNCnNoYXBfdmFsdWVzJG1lYW5fc2hhcF9zY29yZQ0KYGBgDQoNCmBgYHtyLCBmaWcuaGVpZ2h0ID0gNC41LCBmaWcud2lkdGggPSA4LCBtZXNzYWdlPUZ9DQpzaGFwX2xvbmcgPC0gc2hhcC5wcmVwKHNoYXBfY29udHJpYiA9IHNoYXBfdmFsdWVzJHNoYXBfc2NvcmUsIFhfdHJhaW49dHJhaW5feCkNCnNoYXAucGxvdC5zdW1tYXJ5KHNoYXBfbG9uZykNCmBgYA0KDQpgYGB7ciwgZmlnLmhlaWdodD00LjUsIGZpZy53aWR0aD02LCBtZXNzYWdlPUZ9DQpwbG90X2RhdGEgPC0gc2hhcC5wcmVwLnN0YWNrLmRhdGEoc2hhcF9jb250cmliID0gc2hhcF92YWx1ZXMkc2hhcF9zY29yZSwNCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICB0b3BfbiA9IDUsIG5fZ3JvdXBzID0gNikNCnNoYXAucGxvdC5mb3JjZV9wbG90X2J5Z3JvdXAocGxvdF9kYXRhKQ0KYGBgDQoNCg==