Goal

Predict if the user will click on an ad. Using history of clicks and searches for products.

Approch

create a user profile based on search information, and store impression profile.

Setup

library(caret)
library(tidyverse)
library(tidymodels)
library(lubridate)
library(skimr)

Fn_var_imp <- function(model.fit){
  varImp(model.fit)$importance %>%
    as.data.frame() %>%
    rownames_to_column() %>%
    arrange(desc(Overall)) %>% 
    filter(Overall>0)}

Import

skim(df)
Skim summary statistics
 n obs: 237609 
 n variables: 7 

-- Variable type:character -----------------------------------------------------
      variable missing complete      n min max empty n_unique
 impression_id       0   237609 237609  32  32     0   237609
    os_version       0   237609 237609   3  12     0        3

-- Variable type:numeric -------------------------------------------------------
 variable missing complete      n      mean       sd p0   p25   p50   p75  p100     hist
 app_code       0   237609 237609   249.1     135.21  0   163   213   385   522 ▅▂▃▇▂▇▁▂
    is_4G       0   237609 237609     0.36      0.48  0     0     0     1     1 ▇▁▁▁▁▁▁▅
 is_click       0   237609 237609     0.046     0.21  0     0     0     0     1 ▇▁▁▁▁▁▁▁
  user_id       0   237609 237609 46454.53  26802.73  0 23197 46597 69684 92586 ▇▇▇▇▇▇▇▇

-- Variable type:POSIXct -------------------------------------------------------
        variable missing complete      n        min        max     median n_unique
 impression_time       0   237609 237609 2018-11-15 2018-12-13 2018-11-29    36461

Merge item information with viewlog

viewlogitem <- viewlog %>% 
  left_join(items, by="item_id")

User search activity profile

user_search_profile <- viewlogitem %>% 
  group_by(user_id) %>% 
  summarise(n_search = n_distinct(item_id),
            n_product_type = n_distinct(product_type),
            n_top_cat = n_distinct(category_1),
            n_session = n_distinct(session_id),
            search_to_session = n_search / n_session,
            item_to_cat = n_search / n_top_cat,
            first_session = yday(min(as_date(server_time))),
            last_session = yday(max(as_date(server_time))),
            search_period = (last_session - first_session) + 1,
            avg_price = mean(item_price, na.rm=TRUE))

Store impression profile

store_imp_profile <- df %>% 
  group_by(app_code) %>% 
  summarise(n_store_imp = n_distinct(impression_id),
            n_store_user = n_distinct(user_id),
            imp_to_user = n_store_imp / n_store_user,
            first_store_imp = yday(min(as_date(impression_time))),
            last_store_imp = yday(max(as_date(impression_time))),
            imp_store_period = (last_store_imp - first_store_imp) + 1,
            n_store_yday = n_distinct(yday(impression_time)))

Build training set

train <- df %>% 
  left_join(user_search_profile, by="user_id") %>% 
  left_join(store_imp_profile, by="app_code") %>% 
  mutate(is_click = factor(if_else(is_click==1,"Yes","No")),
         last_session_to_first_impression = first_store_imp - last_session,
         last_session_to_last_impression = last_store_imp - last_session,
         imp_to_search_period = as.numeric(imp_store_period) / as.numeric(search_period)) %>%
  filter(!is.na(avg_price))

Outcome distribution

ggplot(train, aes(x=is_click)) +
  geom_bar() +
  geom_text(aes(label=scales::percent(..count../sum(..count..))),
            stat="count",position=position_stack(),vjust=1, color="yellow")

Preprocessing

preproc_recipe <- recipe(~ ., train) %>% 
  update_role(is_click, new_role = "outcome") %>%
  step_mutate(week = week(impression_time),
              yday = yday(impression_time),
              wday = wday(impression_time)) %>% 
  step_dummy(os_version) %>% 
  step_rm(impression_id, impression_time, user_id) %>% 
  step_scale(all_numeric())

Modeling RF

library(ranger)

fitControl <- trainControl(method = "cv",
                           number = 5,
                           classProbs = TRUE,
                           summaryFunction = twoClassSummary,
                           verboseIter = TRUE,
                           search = "random")

set.seed(1968)

rf.fit <- train(preproc_recipe, 
                data=train, 
                method="ranger", 
                importance="impurity",
                # tuneLength = 3,
                tuneGrid=expand.grid(mtry=c(15),
                                     splitrule=c('gini'),
                                     min.node.size=9),
                metric="ROC",
                trControl=fitControl)
Loading required namespace: e1071
Preparing recipe
+ Fold1: mtry=15, splitrule=gini, min.node.size=9 
Growing trees.. Progress: 20%. Estimated remaining time: 2 minutes, 5 seconds.
Growing trees.. Progress: 38%. Estimated remaining time: 1 minute, 39 seconds.
Growing trees.. Progress: 57%. Estimated remaining time: 1 minute, 11 seconds.
Growing trees.. Progress: 74%. Estimated remaining time: 43 seconds.
Growing trees.. Progress: 91%. Estimated remaining time: 15 seconds.
- Fold1: mtry=15, splitrule=gini, min.node.size=9 
+ Fold2: mtry=15, splitrule=gini, min.node.size=9 
Growing trees.. Progress: 16%. Estimated remaining time: 2 minutes, 40 seconds.
Growing trees.. Progress: 34%. Estimated remaining time: 2 minutes, 0 seconds.
Growing trees.. Progress: 53%. Estimated remaining time: 1 minute, 22 seconds.
Growing trees.. Progress: 72%. Estimated remaining time: 49 seconds.
Growing trees.. Progress: 90%. Estimated remaining time: 17 seconds.
- Fold2: mtry=15, splitrule=gini, min.node.size=9 
+ Fold3: mtry=15, splitrule=gini, min.node.size=9 
Growing trees.. Progress: 16%. Estimated remaining time: 2 minutes, 40 seconds.
Growing trees.. Progress: 35%. Estimated remaining time: 1 minute, 59 seconds.
Growing trees.. Progress: 51%. Estimated remaining time: 1 minute, 32 seconds.
Growing trees.. Progress: 66%. Estimated remaining time: 1 minute, 4 seconds.
Growing trees.. Progress: 83%. Estimated remaining time: 33 seconds.
Growing trees.. Progress: 98%. Estimated remaining time: 3 seconds.
- Fold3: mtry=15, splitrule=gini, min.node.size=9 
+ Fold4: mtry=15, splitrule=gini, min.node.size=9 
Growing trees.. Progress: 17%. Estimated remaining time: 2 minutes, 33 seconds.
Growing trees.. Progress: 33%. Estimated remaining time: 2 minutes, 9 seconds.
Growing trees.. Progress: 50%. Estimated remaining time: 1 minute, 35 seconds.
Growing trees.. Progress: 66%. Estimated remaining time: 1 minute, 6 seconds.
Growing trees.. Progress: 83%. Estimated remaining time: 31 seconds.
- Fold4: mtry=15, splitrule=gini, min.node.size=9 
+ Fold5: mtry=15, splitrule=gini, min.node.size=9 
Growing trees.. Progress: 16%. Estimated remaining time: 2 minutes, 40 seconds.
Growing trees.. Progress: 33%. Estimated remaining time: 2 minutes, 8 seconds.
Growing trees.. Progress: 49%. Estimated remaining time: 1 minute, 35 seconds.
Growing trees.. Progress: 67%. Estimated remaining time: 1 minute, 0 seconds.
Growing trees.. Progress: 84%. Estimated remaining time: 29 seconds.
- Fold5: mtry=15, splitrule=gini, min.node.size=9 
Aggregating results
Fitting final model on full training set
Growing trees.. Progress: 12%. Estimated remaining time: 3 minutes, 43 seconds.
Growing trees.. Progress: 25%. Estimated remaining time: 3 minutes, 9 seconds.
Growing trees.. Progress: 38%. Estimated remaining time: 2 minutes, 36 seconds.
Growing trees.. Progress: 51%. Estimated remaining time: 2 minutes, 2 seconds.
Growing trees.. Progress: 64%. Estimated remaining time: 1 minute, 28 seconds.
Growing trees.. Progress: 77%. Estimated remaining time: 55 seconds.
Growing trees.. Progress: 91%. Estimated remaining time: 20 seconds.
rf.fit
Random Forest 

237606 samples
    26 predictor
     2 classes: 'No', 'Yes' 

Recipe steps: mutate, dummy, rm, scale 
Resampling: Cross-Validated (5 fold) 
Summary of sample sizes: 190085, 190085, 190084, 190085, 190085 
Resampling results:

  ROC        Sens       Spec     
  0.7213679  0.9945622  0.0349838

Tuning parameter 'mtry' was held constant at a value of 15
Tuning parameter 'splitrule' was held constant at a value of gini
Tuning
 parameter 'min.node.size' was held constant at a value of 9
# Variable importance
Fn_var_imp(rf.fit)

# Confustion metric
confusionMatrix.train(rf.fit, norm="none")
Cross-Validated (5 fold) Confusion Matrix 

(entries are un-normalized aggregated counts)
 
          Reference
Prediction     No    Yes
       No  225511  10482
       Yes   1233    380
                            
 Accuracy (average) : 0.9507

Modeling XGB

xgb.fit
eXtreme Gradient Boosting 

237606 samples
    26 predictor
     2 classes: 'No', 'Yes' 

Recipe steps: mutate, dummy, rm, scale 
Resampling: Cross-Validated (5 fold) 
Summary of sample sizes: 190085, 190085, 190084, 190085, 190085 
Resampling results:

  ROC        Sens       Spec        
  0.7349856  0.9999824  0.0005524014

Tuning parameter 'nrounds' was held constant at a value of 1000
Tuning parameter 'max_depth' was held constant at a value of 6
 parameter 'colsample_bytree' was held constant at a value of 0.5
Tuning parameter 'min_child_weight' was held constant at a value of
 2
Tuning parameter 'subsample' was held constant at a value of 1

Submision

test <- read_csv('data/test.csv') %>% 
  left_join(user_search_profile, by="user_id") %>% 
  left_join(store_imp_profile, by="app_code") %>% 
  mutate(last_session_to_first_impression = first_store_imp - last_session,
         last_session_to_last_impression = last_store_imp - last_session,
         imp_to_search_period = as.numeric(imp_store_period) / as.numeric(search_period)) %>% 
  mutate_if(is.numeric, replace_na, replace = 0) %>%

test.pred.prob <- predict(xgb.fit, test, type = "prob")

submission <- bind_cols(impression_id=test$impression_id, is_click=test.pred.prob$Yes)

write_csv(submission,'submission7.csv')
LS0tDQp0aXRsZTogIkFWIFdOUyBBbmFseXRpY3MgV2l6YXJkIDIwMTkiDQpvdXRwdXQ6IGh0bWxfbm90ZWJvb2sNCi0tLQ0KDQojIyMgR29hbA0KUHJlZGljdCBpZiB0aGUgdXNlciB3aWxsIGNsaWNrIG9uIGFuIGFkLiBVc2luZyBoaXN0b3J5IG9mIGNsaWNrcyBhbmQgc2VhcmNoZXMgZm9yIHByb2R1Y3RzLg0KDQojIyMgQXBwcm9jaA0KY3JlYXRlIGEgdXNlciBwcm9maWxlIGJhc2VkIG9uIHNlYXJjaCBpbmZvcm1hdGlvbiwgYW5kIHN0b3JlIGltcHJlc3Npb24gcHJvZmlsZS4NCg0KIyMjIFNldHVwDQpgYGB7ciBtZXNzYWdlPUZBTFNFLCB3YXJuaW5nPUZBTFNFfQ0KbGlicmFyeShjYXJldCkNCmxpYnJhcnkodGlkeXZlcnNlKQ0KbGlicmFyeSh0aWR5bW9kZWxzKQ0KbGlicmFyeShsdWJyaWRhdGUpDQpsaWJyYXJ5KHNraW1yKQ0KDQpGbl92YXJfaW1wIDwtIGZ1bmN0aW9uKG1vZGVsLmZpdCl7DQogIHZhckltcChtb2RlbC5maXQpJGltcG9ydGFuY2UgJT4lDQogICAgYXMuZGF0YS5mcmFtZSgpICU+JQ0KICAgIHJvd25hbWVzX3RvX2NvbHVtbigpICU+JQ0KICAgIGFycmFuZ2UoZGVzYyhPdmVyYWxsKSkgJT4lIA0KICAgIGZpbHRlcihPdmVyYWxsPjApfQ0KYGBgDQoNCiMjIyBJbXBvcnQNCmBgYHtyIG1lc3NhZ2U9RkFMU0UsIHdhcm5pbmc9RkFMU0V9DQppdGVtcyA8LSByZWFkX2NzdignZGF0YS9pdGVtX2RhdGEuY3N2JykNCnZpZXdsb2cgPC0gcmVhZF9jc3YoJ2RhdGEvdmlld19sb2cuY3N2JykNCmRmIDwtIHJlYWRfY3N2KCdkYXRhL3RyYWluLmNzdicpDQoNCnNraW0oZGYpDQpgYGANCg0KIyMjIE1lcmdlIGl0ZW0gaW5mb3JtYXRpb24gd2l0aCB2aWV3bG9nDQpgYGB7cn0NCnZpZXdsb2dpdGVtIDwtIHZpZXdsb2cgJT4lIA0KICBsZWZ0X2pvaW4oaXRlbXMsIGJ5PSJpdGVtX2lkIikNCmBgYA0KDQojIyMgVXNlciBzZWFyY2ggYWN0aXZpdHkgcHJvZmlsZQ0KYGBge3J9DQp1c2VyX3NlYXJjaF9wcm9maWxlIDwtIHZpZXdsb2dpdGVtICU+JSANCiAgZ3JvdXBfYnkodXNlcl9pZCkgJT4lIA0KICBzdW1tYXJpc2Uobl9zZWFyY2ggPSBuX2Rpc3RpbmN0KGl0ZW1faWQpLA0KICAgICAgICAgICAgbl9wcm9kdWN0X3R5cGUgPSBuX2Rpc3RpbmN0KHByb2R1Y3RfdHlwZSksDQogICAgICAgICAgICBuX3RvcF9jYXQgPSBuX2Rpc3RpbmN0KGNhdGVnb3J5XzEpLA0KICAgICAgICAgICAgbl9zZXNzaW9uID0gbl9kaXN0aW5jdChzZXNzaW9uX2lkKSwNCiAgICAgICAgICAgIHNlYXJjaF90b19zZXNzaW9uID0gbl9zZWFyY2ggLyBuX3Nlc3Npb24sDQogICAgICAgICAgICBpdGVtX3RvX2NhdCA9IG5fc2VhcmNoIC8gbl90b3BfY2F0LA0KICAgICAgICAgICAgZmlyc3Rfc2Vzc2lvbiA9IHlkYXkobWluKGFzX2RhdGUoc2VydmVyX3RpbWUpKSksDQogICAgICAgICAgICBsYXN0X3Nlc3Npb24gPSB5ZGF5KG1heChhc19kYXRlKHNlcnZlcl90aW1lKSkpLA0KICAgICAgICAgICAgc2VhcmNoX3BlcmlvZCA9IChsYXN0X3Nlc3Npb24gLSBmaXJzdF9zZXNzaW9uKSArIDEsDQogICAgICAgICAgICBhdmdfcHJpY2UgPSBtZWFuKGl0ZW1fcHJpY2UsIG5hLnJtPVRSVUUpKQ0KYGBgDQoNCiMjIyBTdG9yZSBpbXByZXNzaW9uIHByb2ZpbGUNCmBgYHtyfQ0Kc3RvcmVfaW1wX3Byb2ZpbGUgPC0gZGYgJT4lIA0KICBncm91cF9ieShhcHBfY29kZSkgJT4lIA0KICBzdW1tYXJpc2Uobl9zdG9yZV9pbXAgPSBuX2Rpc3RpbmN0KGltcHJlc3Npb25faWQpLA0KICAgICAgICAgICAgbl9zdG9yZV91c2VyID0gbl9kaXN0aW5jdCh1c2VyX2lkKSwNCiAgICAgICAgICAgIGltcF90b191c2VyID0gbl9zdG9yZV9pbXAgLyBuX3N0b3JlX3VzZXIsDQogICAgICAgICAgICBmaXJzdF9zdG9yZV9pbXAgPSB5ZGF5KG1pbihhc19kYXRlKGltcHJlc3Npb25fdGltZSkpKSwNCiAgICAgICAgICAgIGxhc3Rfc3RvcmVfaW1wID0geWRheShtYXgoYXNfZGF0ZShpbXByZXNzaW9uX3RpbWUpKSksDQogICAgICAgICAgICBpbXBfc3RvcmVfcGVyaW9kID0gKGxhc3Rfc3RvcmVfaW1wIC0gZmlyc3Rfc3RvcmVfaW1wKSArIDEsDQogICAgICAgICAgICBuX3N0b3JlX3lkYXkgPSBuX2Rpc3RpbmN0KHlkYXkoaW1wcmVzc2lvbl90aW1lKSkpDQpgYGANCg0KIyMjIEJ1aWxkIHRyYWluaW5nIHNldA0KYGBge3J9DQp0cmFpbiA8LSBkZiAlPiUgDQogIGxlZnRfam9pbih1c2VyX3NlYXJjaF9wcm9maWxlLCBieT0idXNlcl9pZCIpICU+JSANCiAgbGVmdF9qb2luKHN0b3JlX2ltcF9wcm9maWxlLCBieT0iYXBwX2NvZGUiKSAlPiUgDQogIG11dGF0ZShpc19jbGljayA9IGZhY3RvcihpZl9lbHNlKGlzX2NsaWNrPT0xLCJZZXMiLCJObyIpKSwNCiAgICAgICAgIGxhc3Rfc2Vzc2lvbl90b19maXJzdF9pbXByZXNzaW9uID0gZmlyc3Rfc3RvcmVfaW1wIC0gbGFzdF9zZXNzaW9uLA0KICAgICAgICAgbGFzdF9zZXNzaW9uX3RvX2xhc3RfaW1wcmVzc2lvbiA9IGxhc3Rfc3RvcmVfaW1wIC0gbGFzdF9zZXNzaW9uLA0KICAgICAgICAgaW1wX3RvX3NlYXJjaF9wZXJpb2QgPSBhcy5udW1lcmljKGltcF9zdG9yZV9wZXJpb2QpIC8gYXMubnVtZXJpYyhzZWFyY2hfcGVyaW9kKSkgJT4lDQogIGZpbHRlcighaXMubmEoYXZnX3ByaWNlKSkNCmBgYA0KDQojIyMgT3V0Y29tZSBkaXN0cmlidXRpb24NCmBgYHtyfQ0KZ2dwbG90KHRyYWluLCBhZXMoeD1pc19jbGljaykpICsNCiAgZ2VvbV9iYXIoKSArDQogIGdlb21fdGV4dChhZXMobGFiZWw9c2NhbGVzOjpwZXJjZW50KC4uY291bnQuLi9zdW0oLi5jb3VudC4uKSkpLA0KICAgICAgICAgICAgc3RhdD0iY291bnQiLHBvc2l0aW9uPXBvc2l0aW9uX3N0YWNrKCksdmp1c3Q9MSwgY29sb3I9InllbGxvdyIpDQpgYGANCg0KIyMjIFByZXByb2Nlc3NpbmcNCmBgYHtyfQ0KcHJlcHJvY19yZWNpcGUgPC0gcmVjaXBlKH4gLiwgdHJhaW4pICU+JSANCiAgdXBkYXRlX3JvbGUoaXNfY2xpY2ssIG5ld19yb2xlID0gIm91dGNvbWUiKSAlPiUNCiAgc3RlcF9tdXRhdGUod2VlayA9IHdlZWsoaW1wcmVzc2lvbl90aW1lKSwNCiAgICAgICAgICAgICAgeWRheSA9IHlkYXkoaW1wcmVzc2lvbl90aW1lKSwNCiAgICAgICAgICAgICAgd2RheSA9IHdkYXkoaW1wcmVzc2lvbl90aW1lKSkgJT4lIA0KICBzdGVwX2R1bW15KG9zX3ZlcnNpb24pICU+JSANCiAgc3RlcF9ybShpbXByZXNzaW9uX2lkLCBpbXByZXNzaW9uX3RpbWUsIHVzZXJfaWQpICU+JSANCiAgc3RlcF9zY2FsZShhbGxfbnVtZXJpYygpKQ0KYGBgDQoNCiMjIyBNb2RlbGluZyBSRg0KYGBge3J9DQpsaWJyYXJ5KHJhbmdlcikNCg0KZml0Q29udHJvbCA8LSB0cmFpbkNvbnRyb2wobWV0aG9kID0gImN2IiwNCiAgICAgICAgICAgICAgICAgICAgICAgICAgIG51bWJlciA9IDUsDQogICAgICAgICAgICAgICAgICAgICAgICAgICBjbGFzc1Byb2JzID0gVFJVRSwNCiAgICAgICAgICAgICAgICAgICAgICAgICAgIHN1bW1hcnlGdW5jdGlvbiA9IHR3b0NsYXNzU3VtbWFyeSwNCiAgICAgICAgICAgICAgICAgICAgICAgICAgIHZlcmJvc2VJdGVyID0gVFJVRSwNCiAgICAgICAgICAgICAgICAgICAgICAgICAgIHNlYXJjaCA9ICJyYW5kb20iKQ0KDQpzZXQuc2VlZCgxOTY4KQ0KDQpyZi5maXQgPC0gdHJhaW4ocHJlcHJvY19yZWNpcGUsIA0KICAgICAgICAgICAgICAgIGRhdGE9dHJhaW4sIA0KICAgICAgICAgICAgICAgIG1ldGhvZD0icmFuZ2VyIiwgDQogICAgICAgICAgICAgICAgaW1wb3J0YW5jZT0iaW1wdXJpdHkiLA0KICAgICAgICAgICAgICAgICMgdHVuZUxlbmd0aCA9IDMsDQogICAgICAgICAgICAgICAgdHVuZUdyaWQ9ZXhwYW5kLmdyaWQobXRyeT1jKDE1KSwNCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBzcGxpdHJ1bGU9YygnZ2luaScpLA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIG1pbi5ub2RlLnNpemU9OSksDQogICAgICAgICAgICAgICAgbWV0cmljPSJST0MiLA0KICAgICAgICAgICAgICAgIHRyQ29udHJvbD1maXRDb250cm9sKQ0KDQpyZi5maXQNCg0KIyBWYXJpYWJsZSBpbXBvcnRhbmNlDQpGbl92YXJfaW1wKHJmLmZpdCkNCg0KIyBDb25mdXN0aW9uIG1ldHJpYw0KY29uZnVzaW9uTWF0cml4LnRyYWluKHJmLmZpdCwgbm9ybT0ibm9uZSIpDQpgYGANCg0KIyMjIE1vZGVsaW5nIFhHQg0KYGBge3J9DQpsaWJyYXJ5KHhnYm9vc3QpDQoNCmZpdENvbnRyb2wgPC0gdHJhaW5Db250cm9sKG1ldGhvZCA9ICJjdiIsDQogICAgICAgICAgICAgICAgICAgICAgICAgICBudW1iZXIgPSA1LA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgY2xhc3NQcm9icyA9IFRSVUUsDQogICAgICAgICAgICAgICAgICAgICAgICAgICBzdW1tYXJ5RnVuY3Rpb24gPSB0d29DbGFzc1N1bW1hcnksDQogICAgICAgICAgICAgICAgICAgICAgICAgICB2ZXJib3NlSXRlciA9IFRSVUUsDQogICAgICAgICAgICAgICAgICAgICAgICAgICBhbGxvd1BhcmFsbGVsID0gVFJVRSwNCiAgICAgICAgICAgICAgICAgICAgICAgICAgIHNlYXJjaCA9ICJyYW5kb20iKQ0KDQpwYXJhbWV0ZXJzR3JpZCA8LSAgZXhwYW5kLmdyaWQoZXRhID0gYygwLjAxKSwNCiAgICAgICAgICAgICAgICAgICAgICAgICAgICBjb2xzYW1wbGVfYnl0cmVlPWMoMC41KSwNCiAgICAgICAgICAgICAgICAgICAgICAgICAgICBtYXhfZGVwdGg9Yyg2KSwNCiAgICAgICAgICAgICAgICAgICAgICAgICAgICBucm91bmRzPWMoMTAwMCksDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgZ2FtbWE9MSwNCiAgICAgICAgICAgICAgICAgICAgICAgICAgICBtaW5fY2hpbGRfd2VpZ2h0PTIsDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgc3Vic2FtcGxlPTEpDQoNCnNldC5zZWVkKDE5NjgpDQoNCnhnYi5maXQgPC0gdHJhaW4ocHJlcHJvY19yZWNpcGUsIA0KICAgICAgICAgICAgICAgIGRhdGE9dHJhaW4sIA0KICAgICAgICAgICAgICAgIG1ldGhvZD0ieGdiVHJlZSIsIA0KICAgICAgICAgICAgICAgICMgdHVuZUxlbmd0aCA9IDUsDQogICAgICAgICAgICAgICAgdHVuZUdyaWQ9cGFyYW1ldGVyc0dyaWQsDQogICAgICAgICAgICAgICAgbWV0cmljPSJST0MiLA0KICAgICAgICAgICAgICAgIHRyQ29udHJvbD1maXRDb250cm9sKQ0KDQoNCiMgVmFyaWFibGUgaW1wb3J0YW5jZQ0KRm5fdmFyX2ltcCh4Z2IuZml0KQ0KDQojIENvbmZ1c3Rpb24gbWV0cmljDQpjb25mdXNpb25NYXRyaXgudHJhaW4oeGdiLmZpdCwgbm9ybT0ibm9uZSIpDQpgYGANCg0KIyMjIFN1Ym1pc2lvbg0KYGBge3J9DQp0ZXN0IDwtIHJlYWRfY3N2KCdkYXRhL3Rlc3QuY3N2JykgJT4lIA0KICBsZWZ0X2pvaW4odXNlcl9zZWFyY2hfcHJvZmlsZSwgYnk9InVzZXJfaWQiKSAlPiUgDQogIGxlZnRfam9pbihzdG9yZV9pbXBfcHJvZmlsZSwgYnk9ImFwcF9jb2RlIikgJT4lIA0KICBtdXRhdGUobGFzdF9zZXNzaW9uX3RvX2ZpcnN0X2ltcHJlc3Npb24gPSBmaXJzdF9zdG9yZV9pbXAgLSBsYXN0X3Nlc3Npb24sDQogICAgICAgICBsYXN0X3Nlc3Npb25fdG9fbGFzdF9pbXByZXNzaW9uID0gbGFzdF9zdG9yZV9pbXAgLSBsYXN0X3Nlc3Npb24sDQogICAgICAgICBpbXBfdG9fc2VhcmNoX3BlcmlvZCA9IGFzLm51bWVyaWMoaW1wX3N0b3JlX3BlcmlvZCkgLyBhcy5udW1lcmljKHNlYXJjaF9wZXJpb2QpKSAlPiUgDQogIG11dGF0ZV9pZihpcy5udW1lcmljLCByZXBsYWNlX25hLCByZXBsYWNlID0gMCkgJT4lDQoNCnRlc3QucHJlZC5wcm9iIDwtIHByZWRpY3QoeGdiLmZpdCwgdGVzdCwgdHlwZSA9ICJwcm9iIikNCg0Kc3VibWlzc2lvbiA8LSBiaW5kX2NvbHMoaW1wcmVzc2lvbl9pZD10ZXN0JGltcHJlc3Npb25faWQsIGlzX2NsaWNrPXRlc3QucHJlZC5wcm9iJFllcykNCg0Kd3JpdGVfY3N2KHN1Ym1pc3Npb24sJ3N1Ym1pc3Npb243LmNzdicpDQpgYGA=