### Load standardpackages
library(tidyverse) # Collection of all the good stuff like dplyr, ggplot2 ect.
Registered S3 methods overwritten by 'dbplyr':
  method         from
  print.tbl_lazy     
  print.tbl_sql      
── Attaching packages ─────────────────────────────────────────────────────────────────────────────────────────────────────── tidyverse 1.3.1 ──
✓ ggplot2 3.3.5     ✓ purrr   0.3.4
✓ tibble  3.1.5     ✓ dplyr   1.0.7
✓ tidyr   1.1.4     ✓ stringr 1.4.0
✓ readr   2.0.2     ✓ forcats 0.5.1
── Conflicts ────────────────────────────────────────────────────────────────────────────────────────────────────────── tidyverse_conflicts() ──
x dplyr::filter() masks stats::filter()
x dplyr::lag()    masks stats::lag()
library(magrittr) # For extra-piping operators (eg. %<>%)

Attaching package: ‘magrittr’

The following object is masked from ‘package:purrr’:

    set_names

The following object is masked from ‘package:tidyr’:

    extract
library(tidytext)

Download the data

# download and open some Trump tweets from trump_tweet_data_archive
library(jsonlite)

Attaching package: ‘jsonlite’

The following object is masked from ‘package:purrr’:

    flatten
tmp <- tempfile()
download.file("https://github.com/SDS-AAU/SDS-master/raw/master/M2/data/pol_tweets.gz", tmp)
trying URL 'https://github.com/SDS-AAU/SDS-master/raw/master/M2/data/pol_tweets.gz'
Content type 'application/octet-stream' length 7342085 bytes (7.0 MB)
==================================================
downloaded 7.0 MB
tweets_raw <- stream_in(gzfile(tmp, "pol_tweets"))

 Found 1 records...
 Imported 1 records. Simplifying...
Registered S3 method overwritten by 'data.table':
  method           from
  print.data.table     
Registered S3 methods overwritten by 'themis':
  method                  from   
  bake.step_downsample    recipes
  bake.step_upsample      recipes
  prep.step_downsample    recipes
  prep.step_upsample      recipes
  tidy.step_downsample    recipes
  tidy.step_upsample      recipes
  tunable.step_downsample recipes
  tunable.step_upsample   recipes
tweets_raw %>% glimpse()
Rows: 1
Columns: 2
$ text   <df[,50000]> <data.frame[1 x 50000]>
$ labels <df[,50000]> <data.frame[1 x 50000]>
tweets <- tibble(ID = colnames(tweets_raw[[1]]), 
                 text = tweets_raw[[1]] %>% as.character(), 
                 labels = tweets_raw[[2]] %>% as.logical())
#rm(tweets_raw)
tweets %>% glimpse()
Rows: 50,000
Columns: 3
$ ID     <chr> "340675", "289492", "371088", "82212", "476047", "220741", "379074", "633731", "103805", "401277", "493433", "578814", "570425"…
$ text   <chr> "RT @GreenBeretFound Today we remember Sgt. 1st Class Ryan J. Savard killed in action on this day eight years ago. SFC Savard w…
$ labels <lgl> FALSE, TRUE, TRUE, FALSE, FALSE, TRUE, TRUE, TRUE, TRUE, FALSE, TRUE, FALSE, FALSE, FALSE, TRUE, TRUE, TRUE, FALSE, TRUE, TRUE,…
tweets %<>%
  filter(!(text %>% str_detect('^RT'))) # Filter retweets
tweets %>% head()

Tidying

tweets_tidy <- tweets %>%
  unnest_tokens(word, text, token = "tweets") 
Using `to_lower = TRUE` with `token = 'tweets'` may not preserve URLs.
tweets_tidy %>% head(50)
tweets_tidy %>% count(word, sort = TRUE)

Preprocessing

# preprocessing
tweets_tidy %<>%
  filter(!(word %>% str_detect('@'))) %>% # remove hashtags and mentions
  filter(!(word %>% str_detect('^amp|^http|^t\\.co'))) %>% # Twitter specific stuff
#  mutate(word = word %>% str_remove_all('[^[:alnum:]]')) %>% ## remove all special characters
  filter(str_length(word) > 2 ) %>% # Remove words with less than  3 characters
  group_by(word) %>%
  filter(n() > 100) %>% # remove words occuring less than 100 times
  ungroup() %>%
  anti_join(stop_words, by = 'word') # remove stopwords

TFIDF

TFIDF weighting

# top words
tweets_tidy %>%
  count(word, sort = TRUE) %>%
  head(20)
# TFIDF weights
tweets_tidy %<>%
  add_count(ID, word) %>%
  bind_tf_idf(term = word,
              document = ID,
              n = n)
# TFIDF topwords
tweets_tidy %>%
  count(word, wt = tf_idf, sort = TRUE) %>%
  head(20)

Inspecting

Words by party affiliation

labels_words <- tweets_tidy %>%
  group_by(labels) %>%
  count(word, wt = tf_idf, sort = TRUE, name = "tf_idf") %>%
  slice(1:100) %>%
  ungroup() 
labels_words %>%
  mutate(word = reorder_within(word, by = tf_idf, within = labels)) %>%
  ggplot(aes(x = word, y = tf_idf, fill = labels)) +
  geom_col(show.legend = FALSE) +
  labs(x = NULL, y = "tf-idf") +
  facet_wrap(~labels, ncol = 2, scales = "free") +
  coord_flip() +
  scale_x_reordered()

Distance

tweets_tidy %>% head()

Predictive model

library(tidymodels)
Registered S3 method overwritten by 'tune':
  method                   from   
  required_pkgs.model_spec parsnip
── Attaching packages ────────────────────────────────────────────────────────────────────────────────────────────────────── tidymodels 0.1.3 ──
✓ broom        0.7.9      ✓ rsample      0.1.0 
✓ dials        0.0.10     ✓ tune         0.1.6 
✓ infer        1.0.0      ✓ workflows    0.2.3 
✓ modeldata    0.1.1      ✓ workflowsets 0.1.0 
✓ parsnip      0.1.7      ✓ yardstick    0.0.8 
✓ recipes      0.1.16     
── Conflicts ───────────────────────────────────────────────────────────────────────────────────────────────────────── tidymodels_conflicts() ──
x scales::discard()     masks purrr::discard()
x magrittr::extract()   masks tidyr::extract()
x dplyr::filter()       masks stats::filter()
x recipes::fixed()      masks stringr::fixed()
x jsonlite::flatten()   masks purrr::flatten()
x dplyr::lag()          masks stats::lag()
x magrittr::set_names() masks purrr::set_names()
x yardstick::spec()     masks readr::spec()
x recipes::step()       masks stats::step()
• Use tidymodels_prefer() to resolve common conflicts.

Simple manual baseline

words_classifier <- labels_words %>%
  arrange(desc(tf_idf)) %>%
  distinct(word, .keep_all = TRUE) %>%
  select(-tf_idf)
tweet_null_model <- tweets_tidy %>%
  inner_join(labels_words, by = 'word')
null_res <- tweet_null_model %>%
  group_by(ID) %>%
  summarise(truth = mean(labels.x, na.rm = TRUE) %>% round(0),
         pred = mean(labels.y, na.rm = TRUE) %>% round(0))
table(null_res$truth, null_res$pred)
   
        0     1
  0  8842  2609
  1 11327  9235

Preprocessing

# Notice, we use the initial untokenized tweets
data <- tweets %>%
  select(labels, text) %>%
  rename(y = labels) %>%
  mutate(y = y  %>% as.factor()) 

Training & Test split

data_split <- initial_split(data, prop = 0.75, strata = y)

data_train <- data_split  %>%  training()
data_test <- data_split %>% testing()

Preprocessing pipeline

library(textrecipes) # Adittional recipes for working with text data
# This recipe pretty much reconstructs all preprocessing we did so far
data_recipe <- data_train %>%
  recipe(y ~.) %>%
  themis::step_downsample(y) %>% # For downsampling class imbalances (optimal)
  step_filter(!(text %>% str_detect('^RT'))) %>% # Upfront filtering retweets
  step_filter(text != "") %>%
  step_tokenize(text, token = "tweets") %>% # tokenize
  step_tokenfilter(text, min_times = 75) %>%  # Filter out rare words
  step_stopwords(text, keep = FALSE) %>% # Filter stopwords
  step_tfidf(text) %>% # TFIDF weighting
  #step_pca(all_predictors()) %>% # Dimensionality reduction via PCA (optional)
  prep() # NOTE: Only prep the recipe when not using in a workflow
data_recipe
Data Recipe

Inputs:

Training data contained 26239 data points and no missing data.

Operations:

Down-sampling based on y [trained]
Row filtering [trained]
Row filtering [trained]
Tokenization for text [trained]
Text filtering for text [trained]
Stop word removal for text [trained]
Term frequency-inverse document frequency with text [trained]

Since we will not do hyperparameter tuning, we directly bake/juice the recipe

data_train_prep <- data_recipe %>% juice()
data_test_prep <- data_recipe %>% bake(data_test)

Defining the models

model_null <- null_model(mode = 'classification')
model_en <- logistic_reg(mode = 'classification', 
                         mixture = 0.5, 
                         penalty = 0.5) %>%
  set_engine('glm', family = binomial) 

Define the workflow

We will skip the workflow step this time, since we do not evaluate different models against each others.

fit the model

fit_en <- model_en %>% fit(formula = y ~., data = data_train_prep)
pred_collected <- tibble(
  truth = data_train_prep %>% pull(y),
  pred = fit_en %>% predict(new_data = data_train_prep) %>% pull(.pred_class),
  pred_prob = fit_en %>% predict(new_data = data_train_prep, type = "prob") %>% pull(.pred_TRUE),
  ) 
pred_collected %>% conf_mat(truth, pred)
          Truth
Prediction FALSE TRUE
     FALSE  6178 4301
     TRUE   3420 5297
pred_collected %>% conf_mat(truth, pred) %>% summary()

Well… soso

Using the model for new prediction

Simple test

# How would the model predict given some tweet text
pred_own = tibble(text = 'USA USA WE NEED A WALL TO MAKE AMERICA GREAT AGAIN AND KEEP THE MEXICANS AND ALL REALLY BAD COUNTRIES OUT! AMNERICA FIRST')
fit_en %>% predict(new_data = data_recipe %>% bake(pred_own))

Endnotes

Packages & Ecosystem

Further NLP packages ecosystem

References

  • Julia Silge and David Robinson (2020). Text Mining with R: A Tidy Approach, O’Reilly. Online available here
  • Emil Hvidfeldt and Julia Silge (2020). Supervised Machine Learning for Text Analysis in R, online available here

Further sources

Datacamp

Other online

  • Julia Silge’s Blog: Full of great examples of predictive modeling, NLP, and the combination fo both, using tidy ecosystems

Session Info

sessionInfo()
R version 4.1.1 (2021-08-10)
Platform: x86_64-apple-darwin17.0 (64-bit)
Running under: macOS Catalina 10.15.7

Matrix products: default
BLAS:   /System/Library/Frameworks/Accelerate.framework/Versions/A/Frameworks/vecLib.framework/Versions/A/libBLAS.dylib
LAPACK: /Library/Frameworks/R.framework/Versions/4.1/Resources/lib/libRlapack.dylib

locale:
[1] en_US.UTF-8/en_US.UTF-8/en_US.UTF-8/C/en_US.UTF-8/en_US.UTF-8

attached base packages:
[1] stats     graphics  grDevices utils     datasets  methods   base     

other attached packages:
 [1] textrecipes_0.4.1  yardstick_0.0.8    workflowsets_0.1.0 workflows_0.2.3    tune_0.1.6         rsample_0.1.0      recipes_0.1.16    
 [8] parsnip_0.1.7      modeldata_0.1.1    infer_1.0.0        dials_0.0.10       scales_1.1.1       broom_0.7.9        tidymodels_0.1.3  
[15] jsonlite_1.7.2     tidytext_0.3.1     magrittr_2.0.1     forcats_0.5.1      stringr_1.4.0      dplyr_1.0.7        purrr_0.3.4       
[22] readr_2.0.2        tidyr_1.1.4        tibble_3.1.5       ggplot2_3.3.5      tidyverse_1.3.1    knitr_1.36        

loaded via a namespace (and not attached):
 [1] colorspace_2.0-2   ellipsis_0.3.2     class_7.3-19       fs_1.5.0           rstudioapi_0.13    listenv_0.8.0      furrr_0.2.3       
 [8] farver_2.1.0       ParamHelpers_1.14  SnowballC_0.7.0    prodlim_2019.11.13 fansi_0.5.0        lubridate_1.7.10   xml2_1.3.2        
[15] codetools_0.2-18   splines_4.1.1      doParallel_1.0.16  pROC_1.18.0        dbplyr_2.1.1       compiler_4.1.1     httr_1.4.2        
[22] backports_1.2.1    assertthat_0.2.1   Matrix_1.3-4       cli_3.0.1          tools_4.1.1        gtable_0.3.0       glue_1.4.2        
[29] RANN_2.6.1         fastmatch_1.1-3    Rcpp_1.0.7         parallelMap_1.5.1  cellranger_1.1.0   DiceDesign_1.9     vctrs_0.3.8       
[36] iterators_1.0.13   timeDate_3043.102  gower_0.2.2        xfun_0.26          mlr_2.19.0         stopwords_2.2      globals_0.14.0    
[43] rvest_1.0.1        lifecycle_1.0.1    future_1.22.1      MASS_7.3-54        ipred_0.9-12       hms_1.1.1          parallel_4.1.1    
[50] BBmisc_1.11        rpart_4.1-15       stringi_1.7.4      tokenizers_0.2.1   foreach_1.5.1      checkmate_2.0.0    lhs_1.1.3         
[57] hardhat_0.1.6      lava_1.6.10        rlang_0.4.11       pkgconfig_2.0.3    lattice_0.20-44    labeling_0.4.2     tidyselect_1.1.1  
[64] parallelly_1.28.1  plyr_1.8.6         R6_2.5.1           themis_0.1.4       generics_0.1.0     DBI_1.1.1          pillar_1.6.3      
[71] haven_2.4.3        withr_2.4.2        survival_3.2-13    nnet_7.3-16        future.apply_1.8.1 ROSE_0.0-4         janeaustenr_0.1.5 
[78] modelr_0.1.8       crayon_1.4.1       unbalanced_2.0     utf8_1.2.2         tzdb_0.1.2         grid_4.1.1         readxl_1.3.1      
[85] data.table_1.14.2  FNN_1.1.3          reprex_2.0.1       digest_0.6.28      munsell_0.5.0      GPfit_1.0-8       
LS0tCnRpdGxlOiAnTkxQIHdvcmtzaG9wIC0gRXhwbG9yaW5nIFByZXNpZGVudGlhbCBEZWJhdGUgb24gdHdpdHRlcicKYXV0aG9yOiAiRGFuaWVsIFMuIEhhaW4gKGRzaEBidXNpbmVzcy5hYXUuZGspIgpkYXRlOiAiVXBkYXRlZCBgciBmb3JtYXQoU3lzLnRpbWUoKSwgJyVCICVkLCAlWScpYCIKb3V0cHV0OgogIGh0bWxfbm90ZWJvb2s6CiAgICBjb2RlX2ZvbGRpbmc6IHNob3cKICAgIGRmX3ByaW50OiBwYWdlZAogICAgdG9jOiB0cnVlCiAgICB0b2NfZGVwdGg6IDIKICAgIHRvY19mbG9hdDoKICAgICAgY29sbGFwc2VkOiBmYWxzZQogICAgdGhlbWU6IGZsYXRseQotLS0KCmBgYHtyIHNldHVwLCBpbmNsdWRlPUZBTFNFfQojIyMgR2VuZXJpYyBwcmVhbWJsZQpybShsaXN0PWxzKCkpClN5cy5zZXRlbnYoTEFORyA9ICJlbiIpICMgRm9yIGVuZ2xpc2ggbGFuZ3VhZ2UKb3B0aW9ucyhzY2lwZW4gPSA1KSAjIFRvIGRlYWN0aXZhdGUgYW5ub3lpbmcgc2NpZW50aWZpYyBudW1iZXIgbm90YXRpb24KCiMjIyBLbml0ciBvcHRpb25zCmxpYnJhcnkoa25pdHIpICMgRm9yIGRpc3BsYXkgb2YgdGhlIG1hcmtkb3duCmtuaXRyOjpvcHRzX2NodW5rJHNldCh3YXJuaW5nPUZBTFNFLAogICAgICAgICAgICAgICAgICAgICBtZXNzYWdlPUZBTFNFLAogICAgICAgICAgICAgICAgICAgICBjb21tZW50PUZBTFNFLCAKICAgICAgICAgICAgICAgICAgICAgZmlnLmFsaWduPSJjZW50ZXIiCiAgICAgICAgICAgICAgICAgICAgICkKYGBgCgpgYGB7cn0KIyMjIExvYWQgc3RhbmRhcmRwYWNrYWdlcwpsaWJyYXJ5KHRpZHl2ZXJzZSkgIyBDb2xsZWN0aW9uIG9mIGFsbCB0aGUgZ29vZCBzdHVmZiBsaWtlIGRwbHlyLCBnZ3Bsb3QyIGVjdC4KbGlicmFyeShtYWdyaXR0cikgIyBGb3IgZXh0cmEtcGlwaW5nIG9wZXJhdG9ycyAoZWcuICU8PiUpCmBgYAoKYGBge3J9CmxpYnJhcnkodGlkeXRleHQpCmBgYAoKCiMgRG93bmxvYWQgdGhlIGRhdGEKCmBgYHtyfQojIGRvd25sb2FkIGFuZCBvcGVuIHNvbWUgVHJ1bXAgdHdlZXRzIGZyb20gdHJ1bXBfdHdlZXRfZGF0YV9hcmNoaXZlCmxpYnJhcnkoanNvbmxpdGUpCnRtcCA8LSB0ZW1wZmlsZSgpCmRvd25sb2FkLmZpbGUoImh0dHBzOi8vZ2l0aHViLmNvbS9TRFMtQUFVL1NEUy1tYXN0ZXIvcmF3L21hc3Rlci9NMi9kYXRhL3BvbF90d2VldHMuZ3oiLCB0bXApCgp0d2VldHNfcmF3IDwtIHN0cmVhbV9pbihnemZpbGUodG1wLCAicG9sX3R3ZWV0cyIpKQpgYGAKCmBgYHtyfQp0d2VldHNfcmF3ICU+JSBnbGltcHNlKCkKYGBgCgpgYGB7cn0KdHdlZXRzIDwtIHRpYmJsZShJRCA9IGNvbG5hbWVzKHR3ZWV0c19yYXdbWzFdXSksIAogICAgICAgICAgICAgICAgIHRleHQgPSB0d2VldHNfcmF3W1sxXV0gJT4lIGFzLmNoYXJhY3RlcigpLCAKICAgICAgICAgICAgICAgICBsYWJlbHMgPSB0d2VldHNfcmF3W1syXV0gJT4lIGFzLmxvZ2ljYWwoKSkKI3JtKHR3ZWV0c19yYXcpCmBgYAoKYGBge3J9CnR3ZWV0cyAlPiUgZ2xpbXBzZSgpCmBgYAoKYGBge3J9CnR3ZWV0cyAlPD4lCiAgZmlsdGVyKCEodGV4dCAlPiUgc3RyX2RldGVjdCgnXlJUJykpKSAjIEZpbHRlciByZXR3ZWV0cwpgYGAKCmBgYHtyfQp0d2VldHMgJT4lIGhlYWQoKQpgYGAKCiMgVGlkeWluZwoKYGBge3J9CnR3ZWV0c190aWR5IDwtIHR3ZWV0cyAlPiUKICB1bm5lc3RfdG9rZW5zKHdvcmQsIHRleHQsIHRva2VuID0gInR3ZWV0cyIpIApgYGAKCmBgYHtyfQp0d2VldHNfdGlkeSAlPiUgaGVhZCg1MCkKYGBgCgoKYGBge3J9CnR3ZWV0c190aWR5ICU+JSBjb3VudCh3b3JkLCBzb3J0ID0gVFJVRSkKYGBgCgoKIyBQcmVwcm9jZXNzaW5nCgpgYGB7cn0KIyBwcmVwcm9jZXNzaW5nCnR3ZWV0c190aWR5ICU8PiUKICBmaWx0ZXIoISh3b3JkICU+JSBzdHJfZGV0ZWN0KCdAJykpKSAlPiUgIyByZW1vdmUgaGFzaHRhZ3MgYW5kIG1lbnRpb25zCiAgZmlsdGVyKCEod29yZCAlPiUgc3RyX2RldGVjdCgnXmFtcHxeaHR0cHxedFxcLmNvJykpKSAlPiUgIyBUd2l0dGVyIHNwZWNpZmljIHN0dWZmCiMgIG11dGF0ZSh3b3JkID0gd29yZCAlPiUgc3RyX3JlbW92ZV9hbGwoJ1teWzphbG51bTpdXScpKSAlPiUgIyMgcmVtb3ZlIGFsbCBzcGVjaWFsIGNoYXJhY3RlcnMKICBmaWx0ZXIoc3RyX2xlbmd0aCh3b3JkKSA+IDIgKSAlPiUgIyBSZW1vdmUgd29yZHMgd2l0aCBsZXNzIHRoYW4gIDMgY2hhcmFjdGVycwogIGdyb3VwX2J5KHdvcmQpICU+JQogIGZpbHRlcihuKCkgPiAxMDApICU+JSAjIHJlbW92ZSB3b3JkcyBvY2N1cmluZyBsZXNzIHRoYW4gMTAwIHRpbWVzCiAgdW5ncm91cCgpICU+JQogIGFudGlfam9pbihzdG9wX3dvcmRzLCBieSA9ICd3b3JkJykgIyByZW1vdmUgc3RvcHdvcmRzCmBgYAoKIyBURklERgoKVEZJREYgd2VpZ2h0aW5nCgpgYGB7cn0KIyB0b3Agd29yZHMKdHdlZXRzX3RpZHkgJT4lCiAgY291bnQod29yZCwgc29ydCA9IFRSVUUpICU+JQogIGhlYWQoMjApCmBgYAoKYGBge3J9CiMgVEZJREYgd2VpZ2h0cwp0d2VldHNfdGlkeSAlPD4lCiAgYWRkX2NvdW50KElELCB3b3JkKSAlPiUKICBiaW5kX3RmX2lkZih0ZXJtID0gd29yZCwKICAgICAgICAgICAgICBkb2N1bWVudCA9IElELAogICAgICAgICAgICAgIG4gPSBuKQpgYGAKCgpgYGB7cn0KIyBURklERiB0b3B3b3Jkcwp0d2VldHNfdGlkeSAlPiUKICBjb3VudCh3b3JkLCB3dCA9IHRmX2lkZiwgc29ydCA9IFRSVUUpICU+JQogIGhlYWQoMjApCmBgYAoKIyBJbnNwZWN0aW5nCgojIyBXb3JkcyBieSBwYXJ0eSBhZmZpbGlhdGlvbgoKYGBge3J9CmxhYmVsc193b3JkcyA8LSB0d2VldHNfdGlkeSAlPiUKICBncm91cF9ieShsYWJlbHMpICU+JQogIGNvdW50KHdvcmQsIHd0ID0gdGZfaWRmLCBzb3J0ID0gVFJVRSwgbmFtZSA9ICJ0Zl9pZGYiKSAlPiUKICBzbGljZSgxOjEwMCkgJT4lCiAgdW5ncm91cCgpIApgYGAKCmBgYHtyLCBmaWcud2lkdGg9MTB9CmxhYmVsc193b3JkcyAlPiUKICBtdXRhdGUod29yZCA9IHJlb3JkZXJfd2l0aGluKHdvcmQsIGJ5ID0gdGZfaWRmLCB3aXRoaW4gPSBsYWJlbHMpKSAlPiUKICBnZ3Bsb3QoYWVzKHggPSB3b3JkLCB5ID0gdGZfaWRmLCBmaWxsID0gbGFiZWxzKSkgKwogIGdlb21fY29sKHNob3cubGVnZW5kID0gRkFMU0UpICsKICBsYWJzKHggPSBOVUxMLCB5ID0gInRmLWlkZiIpICsKICBmYWNldF93cmFwKH5sYWJlbHMsIG5jb2wgPSAyLCBzY2FsZXMgPSAiZnJlZSIpICsKICBjb29yZF9mbGlwKCkgKwogIHNjYWxlX3hfcmVvcmRlcmVkKCkKYGBgCgojIyBEaXN0YW5jZQoKYGBge3J9CnR3ZWV0c190aWR5ICU+JSBoZWFkKCkKYGBgCgojIFByZWRpY3RpdmUgbW9kZWwKCmBgYHtyfQpsaWJyYXJ5KHRpZHltb2RlbHMpCmBgYAoKIyMgU2ltcGxlIG1hbnVhbCBiYXNlbGluZQoKYGBge3J9CndvcmRzX2NsYXNzaWZpZXIgPC0gbGFiZWxzX3dvcmRzICU+JQogIGFycmFuZ2UoZGVzYyh0Zl9pZGYpKSAlPiUKICBkaXN0aW5jdCh3b3JkLCAua2VlcF9hbGwgPSBUUlVFKSAlPiUKICBzZWxlY3QoLXRmX2lkZikKYGBgCgpgYGB7cn0KdHdlZXRfbnVsbF9tb2RlbCA8LSB0d2VldHNfdGlkeSAlPiUKICBpbm5lcl9qb2luKGxhYmVsc193b3JkcywgYnkgPSAnd29yZCcpCmBgYAoKYGBge3J9Cm51bGxfcmVzIDwtIHR3ZWV0X251bGxfbW9kZWwgJT4lCiAgZ3JvdXBfYnkoSUQpICU+JQogIHN1bW1hcmlzZSh0cnV0aCA9IG1lYW4obGFiZWxzLngsIG5hLnJtID0gVFJVRSkgJT4lIHJvdW5kKDApLAogICAgICAgICBwcmVkID0gbWVhbihsYWJlbHMueSwgbmEucm0gPSBUUlVFKSAlPiUgcm91bmQoMCkpCmBgYAoKYGBge3J9CnRhYmxlKG51bGxfcmVzJHRydXRoLCBudWxsX3JlcyRwcmVkKQpgYGAKCgojIyBQcmVwcm9jZXNzaW5nCgpgYGB7cn0KIyBOb3RpY2UsIHdlIHVzZSB0aGUgaW5pdGlhbCB1bnRva2VuaXplZCB0d2VldHMKZGF0YSA8LSB0d2VldHMgJT4lCiAgc2VsZWN0KGxhYmVscywgdGV4dCkgJT4lCiAgcmVuYW1lKHkgPSBsYWJlbHMpICU+JQogIG11dGF0ZSh5ID0geSAgJT4lIGFzLmZhY3RvcigpKSAKYGBgCgoKIyMgVHJhaW5pbmcgJiBUZXN0IHNwbGl0CgpgYGB7cn0KZGF0YV9zcGxpdCA8LSBpbml0aWFsX3NwbGl0KGRhdGEsIHByb3AgPSAwLjc1LCBzdHJhdGEgPSB5KQoKZGF0YV90cmFpbiA8LSBkYXRhX3NwbGl0ICAlPiUgIHRyYWluaW5nKCkKZGF0YV90ZXN0IDwtIGRhdGFfc3BsaXQgJT4lIHRlc3RpbmcoKQpgYGAKCiMjIFByZXByb2Nlc3NpbmcgcGlwZWxpbmUKCmBgYHtyfQpsaWJyYXJ5KHRleHRyZWNpcGVzKSAjIEFkaXR0aW9uYWwgcmVjaXBlcyBmb3Igd29ya2luZyB3aXRoIHRleHQgZGF0YQpgYGAKCmBgYHtyfQojIFRoaXMgcmVjaXBlIHByZXR0eSBtdWNoIHJlY29uc3RydWN0cyBhbGwgcHJlcHJvY2Vzc2luZyB3ZSBkaWQgc28gZmFyCmRhdGFfcmVjaXBlIDwtIGRhdGFfdHJhaW4gJT4lCiAgcmVjaXBlKHkgfi4pICU+JQogIHRoZW1pczo6c3RlcF9kb3duc2FtcGxlKHkpICU+JSAjIEZvciBkb3duc2FtcGxpbmcgY2xhc3MgaW1iYWxhbmNlcyAob3B0aW1hbCkKICBzdGVwX2ZpbHRlcighKHRleHQgJT4lIHN0cl9kZXRlY3QoJ15SVCcpKSkgJT4lICMgVXBmcm9udCBmaWx0ZXJpbmcgcmV0d2VldHMKICBzdGVwX2ZpbHRlcih0ZXh0ICE9ICIiKSAlPiUKICBzdGVwX3Rva2VuaXplKHRleHQsIHRva2VuID0gInR3ZWV0cyIpICU+JSAjIHRva2VuaXplCiAgc3RlcF90b2tlbmZpbHRlcih0ZXh0LCBtaW5fdGltZXMgPSA3NSkgJT4lICAjIEZpbHRlciBvdXQgcmFyZSB3b3JkcwogIHN0ZXBfc3RvcHdvcmRzKHRleHQsIGtlZXAgPSBGQUxTRSkgJT4lICMgRmlsdGVyIHN0b3B3b3JkcwogIHN0ZXBfdGZpZGYodGV4dCkgJT4lICMgVEZJREYgd2VpZ2h0aW5nCiAgI3N0ZXBfcGNhKGFsbF9wcmVkaWN0b3JzKCkpICU+JSAjIERpbWVuc2lvbmFsaXR5IHJlZHVjdGlvbiB2aWEgUENBIChvcHRpb25hbCkKICBwcmVwKCkgIyBOT1RFOiBPbmx5IHByZXAgdGhlIHJlY2lwZSB3aGVuIG5vdCB1c2luZyBpbiBhIHdvcmtmbG93CmBgYAoKCmBgYHtyfQpkYXRhX3JlY2lwZQpgYGAKClNpbmNlIHdlIHdpbGwgbm90IGRvIGh5cGVycGFyYW1ldGVyIHR1bmluZywgd2UgZGlyZWN0bHkgYmFrZS9qdWljZSB0aGUgcmVjaXBlCgpgYGB7cn0KZGF0YV90cmFpbl9wcmVwIDwtIGRhdGFfcmVjaXBlICU+JSBqdWljZSgpCmRhdGFfdGVzdF9wcmVwIDwtIGRhdGFfcmVjaXBlICU+JSBiYWtlKGRhdGFfdGVzdCkKYGBgCgoKIyMgRGVmaW5pbmcgdGhlIG1vZGVscwoKYGBge3J9Cm1vZGVsX251bGwgPC0gbnVsbF9tb2RlbChtb2RlID0gJ2NsYXNzaWZpY2F0aW9uJykKYGBgCgoKYGBge3J9Cm1vZGVsX2VuIDwtIGxvZ2lzdGljX3JlZyhtb2RlID0gJ2NsYXNzaWZpY2F0aW9uJywgCiAgICAgICAgICAgICAgICAgICAgICAgICBtaXh0dXJlID0gMC41LCAKICAgICAgICAgICAgICAgICAgICAgICAgIHBlbmFsdHkgPSAwLjUpICU+JQogIHNldF9lbmdpbmUoJ2dsbScsIGZhbWlseSA9IGJpbm9taWFsKSAKYGBgCgoKIyMgRGVmaW5lIHRoZSB3b3JrZmxvdwoKV2Ugd2lsbCBza2lwIHRoZSB3b3JrZmxvdyBzdGVwIHRoaXMgdGltZSwgc2luY2Ugd2UgZG8gbm90IGV2YWx1YXRlIGRpZmZlcmVudCBtb2RlbHMgYWdhaW5zdCBlYWNoIG90aGVycy4KCiMjIGZpdCB0aGUgbW9kZWwKCmBgYHtyfQpmaXRfZW4gPC0gbW9kZWxfZW4gJT4lIGZpdChmb3JtdWxhID0geSB+LiwgZGF0YSA9IGRhdGFfdHJhaW5fcHJlcCkKYGBgCgoKYGBge3J9CnByZWRfY29sbGVjdGVkIDwtIHRpYmJsZSgKICB0cnV0aCA9IGRhdGFfdHJhaW5fcHJlcCAlPiUgcHVsbCh5KSwKICBwcmVkID0gZml0X2VuICU+JSBwcmVkaWN0KG5ld19kYXRhID0gZGF0YV90cmFpbl9wcmVwKSAlPiUgcHVsbCgucHJlZF9jbGFzcyksCiAgcHJlZF9wcm9iID0gZml0X2VuICU+JSBwcmVkaWN0KG5ld19kYXRhID0gZGF0YV90cmFpbl9wcmVwLCB0eXBlID0gInByb2IiKSAlPiUgcHVsbCgucHJlZF9UUlVFKSwKICApIApgYGAKCmBgYHtyfQpwcmVkX2NvbGxlY3RlZCAlPiUgY29uZl9tYXQodHJ1dGgsIHByZWQpCmBgYAoKYGBge3J9CnByZWRfY29sbGVjdGVkICU+JSBjb25mX21hdCh0cnV0aCwgcHJlZCkgJT4lIHN1bW1hcnkoKQpgYGAKCldlbGwuLi4gc29zbwoKIyBVc2luZyB0aGUgbW9kZWwgZm9yIG5ldyBwcmVkaWN0aW9uCgojIyBTaW1wbGUgdGVzdAoKYGBge3J9CiMgSG93IHdvdWxkIHRoZSBtb2RlbCBwcmVkaWN0IGdpdmVuIHNvbWUgdHdlZXQgdGV4dApwcmVkX293biA9IHRpYmJsZSh0ZXh0ID0gJ1VTQSBVU0EgV0UgTkVFRCBBIFdBTEwgVE8gTUFLRSBBTUVSSUNBIEdSRUFUIEFHQUlOIEFORCBLRUVQIFRIRSBNRVhJQ0FOUyBBTkQgQUxMIFJFQUxMWSBCQUQgQ09VTlRSSUVTIE9VVCEgQU1ORVJJQ0EgRklSU1QnKQpgYGAKCgpgYGB7cn0KZml0X2VuICU+JSBwcmVkaWN0KG5ld19kYXRhID0gZGF0YV9yZWNpcGUgJT4lIGJha2UocHJlZF9vd24pKQpgYGAKCgojIEVuZG5vdGVzCgojIyMgUGFja2FnZXMgJiBFY29zeXN0ZW0KCiogW2B0aWR5dGV4dGBdKGh0dHBzOi8vZ2l0aHViLmNvbS9qdWxpYXNpbGdlL3RpZHl0ZXh0KQoqIFtgdGV4dHJlY2lwZXNgXShodHRwczovL3RleHRyZWNpcGVzLnRpZHltb2RlbHMub3JnLykKKiBbYHRvcGljbW9kZWxzYF0oaHR0cHM6Ly9jcmFuLnItcHJvamVjdC5vcmcvd2ViL3BhY2thZ2VzL3RvcGljbW9kZWxzL3ZpZ25ldHRlcy90b3BpY21vZGVscy5wZGYpCgpGdXJ0aGVyIE5MUCBwYWNrYWdlcyBlY29zeXN0ZW0KCiogYHRtYCBbaGVyZV0oaHR0cHM6Ly9jcmFuLnItcHJvamVjdC5vcmcvd2ViL3BhY2thZ2VzL3RtLykKKiBgcXVhbnRlZGFgIFtoZXJlXShodHRwczovL3F1YW50ZWRhLmlvLyksIGFuZCBtYW55IG1hbnkgZ3JlYXQgdHV0b3JpYWxzIFtoZXJlXShodHRwczovL3R1dG9yaWFscy5xdWFudGVkYS5pby8pCgoKIyMjIFJlZmVyZW5jZXMgCgoqIEp1bGlhIFNpbGdlIGFuZCBEYXZpZCBSb2JpbnNvbiAoMjAyMCkuIFRleHQgTWluaW5nIHdpdGggUjogQSBUaWR5IEFwcHJvYWNoLCBP4oCZUmVpbGx5LiBPbmxpbmUgYXZhaWxhYmxlIFtoZXJlXShodHRwczovL3d3dy50aWR5dGV4dG1pbmluZy5jb20vKQogICAqIFtDaGFwdGVyIDZdKGh0dHBzOi8vd3d3LnRpZHl0ZXh0bWluaW5nLmNvbS90b3BpY21vZGVsaW5nLmh0bWwpOiBJbnRyb2R1Y3Rpb24gdG9waWMgbW9kZWxzCiogRW1pbCBIdmlkZmVsZHQgYW5kIEp1bGlhIFNpbGdlICgyMDIwKS4gU3VwZXJ2aXNlZCBNYWNoaW5lIExlYXJuaW5nIGZvciBUZXh0IEFuYWx5c2lzIGluIFIsIG9ubGluZSBhdmFpbGFibGUgW2hlcmVdKGh0dHBzOi8vc21sdGFyLmNvbS8pCiAgICogW0NoYXB0ZXIgN10oaHR0cHM6Ly9zbWx0YXIuY29tL21sY2xhc3NpZmljYXRpb24uaHRtbCk6IENsYXNzaWZpY2F0aW9uCgojIyMgRnVydGhlciBzb3VyY2VzCgpEYXRhY2FtcAoKKiAgW1RvcGljIE1vZGVsaW5nIGluIFJdKGh0dHBzOi8vbGVhcm4uZGF0YWNhbXAuY29tL2NvdXJzZXMvdG9waWMtbW9kZWxpbmctaW4tcikgCgpPdGhlciBvbmxpbmUKCiogW0p1bGlhIFNpbGdlJ3MgQmxvZ10oaHR0cHM6Ly9qdWxpYXNpbGdlLmNvbS8pOiBGdWxsIG9mIGdyZWF0IGV4YW1wbGVzIG9mIHByZWRpY3RpdmUgbW9kZWxpbmcsIE5MUCwgYW5kIHRoZSBjb21iaW5hdGlvbiBmbyBib3RoLCB1c2luZyB0aWR5IGVjb3N5c3RlbXMKCiMjIyBTZXNzaW9uIEluZm8KCmBgYHtyfQpzZXNzaW9uSW5mbygpCmBgYAoK