Dating Recommender System

Some comments about the dataset:

  1. There are 135,359 users and 168,791 profiles with 17,359,346 ratings.
  2. userId is the user who provided rating.
  3. profileID is the user who has been rated.
  4. Ratings are on a scale of 1 - 10 where 10 is the highest.
  5. Only users who provide at least 20 ratings were included. User gender information is also available with idenfiers (M, F, U-Unknown).

Libraries

library(recommenderlab)
library(tidyverse)
library(kableExtra)

Data Pre-processing

Import files

The files are too large to upload to my github even when zipped so you can find it here.

d_ratings <- read.csv("C:/Users/Javern/Documents/Data Science MS/DATA612/libimseti-complete/libimseti/ratings.dat", header = F, sep = ",")
colnames(d_ratings) <- c("userId", "profileId", "rating")
d_ratings$userId <- as.factor(d_ratings$userId)
d_ratings$profileId <- as.factor(d_ratings$profileId)
gender <- read.csv("C:/Users/Javern/Documents/Data Science MS/DATA612/libimseti-complete/libimseti/gender.dat", header = F, sep = ",")
colnames(gender) <- c("userId", "type")
gender$userId <- as.factor(gender$userId)

EDA

Preview of datasets

head(d_ratings); head(gender)

Top 10 users who provided the most ratings

d_ratings %>% 
  group_by(userId)%>% 
  summarise(user_count = length(userId)) %>% 
  top_n(10) %>% 
  ggplot(aes(userId, user_count, fill = userId)) + geom_bar(stat = "identity") + geom_text(aes(label=user_count), vjust=-0.3, size=3.5)
package <U+393C><U+3E31>bindrcpp<U+393C><U+3E32> was built under R version 3.5.2Selecting by user_count

Top rated profiles

d_ratings %>% 
  group_by(profileId) %>% 
  summarise(profile_count = length(profileId)) %>% 
  top_n(10) -> topprofiles
Selecting by profile_count
  ggplot(topprofiles, aes(profileId, profile_count, fill = profileId)) + 
    geom_bar(stat = "identity") + 
    geom_text(aes(label=profile_count), vjust=-0.3, size=3.5)

How scores are distributed

d_ratings %>% 
   group_by(rating)%>% 
   summarise(rating_count = sum(rating)) %>% 
   ggplot(aes(rating, rating_count, fill = rating)) + geom_bar(stat = "identity") +   scale_x_continuous(breaks = seq(1, 10)) + geom_text(aes(label=rating_count), vjust=-0.3, size=3.5)

Gender ratio

gender %>% 
  group_by(type) %>% 
  summarise(type_count = length(type)) %>% 
  ggplot(aes(type, type_count, fill = type)) + geom_bar(stat = "identity", color = "purple")

NA

Dating Matrix

As we move forward in the analysis, the ratings dataframe will be converted into a matrix to build and evaluate the recommendation systems.

dmatrix <- as(d_ratings, "realRatingMatrix")
dmatrix
135359 x 168791 rating matrix of class ‘realRatingMatrix’ with 17359346 ratings.

Dimensions of the matrix

dim(dmatrix@data)
[1] 135359 168791

Size of matrix data

object.size(dmatrix)
228455328 bytes

About 228 MB

The file is large so we’ll cut down on the number of attributes.

# users who rated at least 500 profiles 
# profiles that are rated at least 1000 times
dmatrix <- dmatrix[rowCounts(dmatrix) > 500, colCounts(dmatrix) > 800]
dmatrix
3894 x 3391 rating matrix of class ‘realRatingMatrix’ with 1134236 ratings.

Average Profile Ratings

avg_profile_ratings <- data.frame("avg_rating" = colMeans(dmatrix)) %>% 
  ggplot(aes(x = avg_rating)) + 
  geom_histogram(color = "red", fill = "lightblue") + 
  ggtitle("Distribution of Average Ratings for Profiles")
avg_profile_ratings

The distribution is nearly normal with most rating falling between 5 and 7.5.

How similar are the first 300 users?

sim <- similarity(dmatrix[1:300, ], method = "cosine", which = "users")
image(as.matrix(sim), main = "User Similarity")

How similar are the first 300 profiles?

sim2 <- similarity(dmatrix[, 1:300], method = "cosine", which = "items")
image(as.matrix(sim2), main = "Profile Similarity")

Training and Test Sets

So we are going to split the data 90:10, train and test respectively, keeping 3 items and running the evaluation 4 times.

#min(rowCounts(dmatrix))= 6 so we can keep 5 items per user
dmat_eval <- evaluationScheme(data = dmatrix, method = "split", train = 0.9, given = 5, goodRating = 5, k = 4) 
dmat_eval
Evaluation scheme with 5 items given
Method: ‘split’ with 4 run(s).
Training set proportion: 0.900
Good ratings: >=5.000000
Data set: 3894 x 3391 rating matrix of class ‘realRatingMatrix’ with 1134236 ratings.

Compare Recommender System Algorithms

algorithms <- list(
  IBCF = list(name = "IBCF", param = list(method = "cosine")),
  UBCF = list(name = "UBCF", param = list(method = "cosine")),
  SVD = list(name = "SVD", param = list(k = 30)),
  POPULAR = list(name = "POPULAR", param = NULL), #serendipity
  RANDOM = list(name = "RANDOM", param = NULL)
)

Test the models by varying the number of profiles to recommend.

# run algorithms, predict next n profile
eval_results <- evaluate(dmat_eval, algorithms, type = "topNList", n = c(1, 3, 5, 10, 15, 20))
IBCF run fold/sample [model time/prediction time]
     1  [497.85sec/0.43sec] 
     2  [493.34sec/0.41sec] 
     3  [492.62sec/0.29sec] 
     4  [495.82sec/0.2sec] 
UBCF run fold/sample [model time/prediction time]
     1  [0.17sec/41.13sec] 
     2  [0.17sec/42.32sec] 
     3  [0.16sec/39.14sec] 
     4  [0.23sec/40.22sec] 
SVD run fold/sample [model time/prediction time]
     1  [3.33sec/1.24sec] 
     2  [3.31sec/1.13sec] 
     3  [3.58sec/1.27sec] 
     4  [3.45sec/1.17sec] 
POPULAR run fold/sample [model time/prediction time]
     1  [0.19sec/4.05sec] 
     2  [0.19sec/4.03sec] 
     3  [0.22sec/3.88sec] 
     4  [0.2sec/3.89sec] 
RANDOM run fold/sample [model time/prediction time]
     1  [0sec/1.28sec] 
     2  [0sec/1.11sec] 
     3  [0sec/1.28sec] 
     4  [0sec/1.28sec] 
averages <- avg(eval_results)

IBCF took approximately 25 minutes to run and do prediction.

Results

TP - True Positive FP - False Positive FN - False Negative TN - True Negative

kable(averages$IBCF) %>% kable_styling(bootstrap_options = c("striped", "bordered"), full_width = F, font_size = 11) %>% add_header_above(c(" ", "IBCF" = 8))

IBCF
TP FP FN TN precision recall TPR FPR
1 0.0179487 0.975641 222.5199 3162.487 0.0180648 0.0000925 0.0000925 0.0003090
3 0.0596154 2.882051 222.4782 3160.580 0.0200050 0.0002740 0.0002740 0.0009123
5 0.0967949 4.723718 222.4410 3158.738 0.0195725 0.0004463 0.0004463 0.0014955
10 0.1794872 9.063462 222.3583 3154.399 0.0184412 0.0007813 0.0007813 0.0028689
15 0.2717949 13.057692 222.2660 3150.404 0.0190787 0.0011761 0.0011761 0.0041339
20 0.3391026 16.832051 222.1987 3146.630 0.0182538 0.0014670 0.0014670 0.0053300

kable(averages$UBCF) %>% kable_styling(bootstrap_options = c("striped", "bordered"), full_width = F, font_size = 11) %>% add_header_above(c(" ", "UBCF" = 8))

UBCF
TP FP FN TN precision recall TPR FPR
1 0.449359 0.5487179 222.0885 3162.913 0.4502025 0.0024331 0.0024331 0.0001721
3 1.327564 1.6666667 221.2103 3161.796 0.4433765 0.0070320 0.0070320 0.0005221
5 2.157692 2.8326923 220.3801 3160.629 0.4323913 0.0113597 0.0113597 0.0008872
10 4.025000 5.9557692 218.5128 3157.506 0.4033029 0.0208295 0.0208295 0.0018655
15 5.716667 9.2544872 216.8212 3154.208 0.3818708 0.0289989 0.0289989 0.0028990
20 7.313461 12.6480769 215.2244 3150.814 0.3664033 0.0366339 0.0366339 0.0039625

kable(averages$SVD) %>% kable_styling(bootstrap_options = c("striped", "bordered"), full_width = F, font_size = 11) %>% add_header_above(c(" ", "SVD" = 8))

SVD
TP FP FN TN precision recall TPR FPR
1 0.2500000 0.750000 222.2878 3162.712 0.2500000 0.0014511 0.0014511 0.0002370
3 0.7108974 2.289103 221.8269 3161.173 0.2369658 0.0039949 0.0039949 0.0007227
5 1.1538462 3.846154 221.3840 3159.616 0.2307692 0.0064196 0.0064196 0.0012140
10 2.2198718 7.780128 220.3179 3155.682 0.2219872 0.0124932 0.0124932 0.0024563
15 3.2980769 11.701923 219.2397 3151.760 0.2198718 0.0183008 0.0183008 0.0036935
20 4.3525641 15.647436 218.1853 3147.815 0.2176282 0.0239089 0.0239089 0.0049379

kable(averages$POPULAR) %>% kable_styling(bootstrap_options = c("striped", "bordered"), full_width = F, font_size = 11) %>% add_header_above(c(" ", "POPULAR" = 8))

POPULAR
TP FP FN TN precision recall TPR FPR
1 0.4057692 0.5942308 222.1321 3162.868 0.4057692 0.0018452 0.0018452 0.0001856
3 1.2275641 1.7724359 221.3103 3161.690 0.4091880 0.0058259 0.0058259 0.0005546
5 2.0128205 2.9871795 220.5250 3160.475 0.4025641 0.0092910 0.0092910 0.0009331
10 3.7782051 6.2217949 218.7596 3157.240 0.3778205 0.0175165 0.0175165 0.0019462
15 5.4551282 9.5448718 217.0827 3153.917 0.3636752 0.0253210 0.0253210 0.0029850
20 7.1230769 12.8769231 215.4147 3150.585 0.3561538 0.0322594 0.0322594 0.0040242

kable(averages$RANDOM) %>% kable_styling(bootstrap_options = c("striped", "bordered"), full_width = F, font_size = 11) %>% add_header_above(c(" ", "RANDOM" = 8))
RANDOM
TP FP FN TN precision recall TPR FPR
1 0.0692308 0.9307692 222.4686 3162.531 0.0692308 0.0003498 0.0003498 0.0002944
3 0.2198718 2.7801282 222.3179 3160.682 0.0732906 0.0010129 0.0010129 0.0008787
5 0.3660256 4.6339744 222.1718 3158.828 0.0732051 0.0016633 0.0016633 0.0014648
10 0.7179487 9.2820513 221.8199 3154.180 0.0717949 0.0033660 0.0033660 0.0029348
15 1.0525641 13.9474359 221.4853 3149.515 0.0701709 0.0049277 0.0049277 0.0044104
20 1.4205128 18.5794872 221.1173 3144.883 0.0710256 0.0066586 0.0066586 0.0058743

ROC curve

The ROC curve is created by plotting the true positive rate against the false positive rate. The closer an ROC curve is to the upper left corner, the more efficient is the test.

plot(eval_results, annotate = T, legend="topleft")
title("ROC Curve")

Based on the graph visualization above, the UBCF is better than the others.

Precision-Recall

Precision expresses the proportion of the data points our model says was relevant and are actually were relevant. Calculated as: TP / (TP + FP)

Recall expresses the ability to find all relevant instances in a dataset or the model’s ability to find all the data points of interest in a dataset. TP / (TP + FN)

The closer the curve or line is to the top right, the better the performance of the algorithm.

# precision / recall
plot(eval_results, "prec/rec", annotate = 2)
title("Precision-recall")

User Based model is still better than the other algorithms.

#Predict top-N recommendation lists
eval_results2 <- evaluate(dmat_eval, algorithms, type = "ratings")
IBCF run fold/sample [model time/prediction time]
     1  [493.46sec/0.12sec] 
     2  [505.67sec/0.14sec] 
     3  [500.72sec/0.12sec] 
     4  [499.53sec/0.14sec] 
UBCF run fold/sample [model time/prediction time]
     1  [0.17sec/41.48sec] 
     2  [0.17sec/40.88sec] 
     3  [0.19sec/39.89sec] 
     4  [0.17sec/41.02sec] 
SVD run fold/sample [model time/prediction time]
     1  [3.41sec/0.93sec] 
     2  [3.33sec/0.74sec] 
     3  [3.77sec/0.7sec] 
     4  [3.37sec/0.63sec] 
POPULAR run fold/sample [model time/prediction time]
     1  [0.2sec/0.44sec] 
     2  [0.35sec/0.36sec] 
     3  [0.19sec/0.44sec] 
     4  [0.19sec/0.41sec] 
RANDOM run fold/sample [model time/prediction time]
     1  [0.01sec/0.74sec] 
     2  [0sec/0.87sec] 
     3  [0sec/0.88sec] 
     4  [0.01sec/0.79sec] 
avg(eval_results2)
$`IBCF`
        RMSE      MSE      MAE
res 2.487833 6.196339 1.179454

$UBCF
        RMSE     MSE      MAE
res 2.784005 7.75109 2.235006

$SVD
        RMSE     MSE      MAE
res 2.986838 8.92139 2.420337

$POPULAR
        RMSE      MSE      MAE
res 2.187636 4.786568 1.673285

$RANDOM
        RMSE     MSE      MAE
res 3.707756 13.7477 2.933583

Online Evaluation

As noted, offline evaluations use precompiled offline datasets from which data was removed hence evaluations are used to analyze the algorthims’ ablity to predict missing data. On the other hand, in online evaluations, recommendations are shown to real users of the system during their session and so users do not rate recommendations but the recommender system observes how often a user accepts a recommendation. One metric that could be used to evaluate online evaluation is click-through rate (CTR). This measures the ratio of clicks to the number of recommendation lists provided. So for instance if the recommender recommends 1000 profiles and the user clicks only 5 then the CTR would be 0.5%.

Summary

Based on the output shown for each algorithms, the IBCF took a longer time to learn the data than the others but took a shorter time to predict the data. According to the error rates, Popular has the lowest RMSE which means that it performed the best with lower error rates. However, as shown in the visualizations, UBCF performed best. The Random algorithm continued to perform worst through out the process.

LS0tDQp0aXRsZTogIkRBVEE2MTIgUHJvamVjdCA0IHwgQWNjdXJhY3kgYW5kIEJleW9uZCINCmF1dGhvcjogIkphdmVybiBXaWxzb24iDQpkYXRlOiAiSnVuZSAyOSwgMjAxOSINCm91dHB1dDogDQogIGh0bWxfbm90ZWJvb2s6DQogICAgdGhlbWU6IHBhcGVyDQogICAgdG9jOiB0cnVlDQogICAgY29kZV9mb2xkaW5nOiBzaG93DQogICAgdG9jX2Zsb2F0Og0KICAgICAgY29sbGFwc2VkOiBmYWxzZQ0KICAgICAgc21vb3RoX3Njcm9sbDogZmFsc2UNCg0KLS0tDQoNCiMjIERhdGluZyBSZWNvbW1lbmRlciBTeXN0ZW0NCg0KU29tZSBjb21tZW50cyBhYm91dCB0aGUgZGF0YXNldDoNCg0KICAxLiBUaGVyZSBhcmUgMTM1LDM1OSB1c2VycyBhbmQgMTY4LDc5MSBwcm9maWxlcyB3aXRoIDE3LDM1OSwzNDYgcmF0aW5ncy4NCiAgMi4gdXNlcklkIGlzIHRoZSB1c2VyIHdobyBwcm92aWRlZCByYXRpbmcuDQogIDMuIHByb2ZpbGVJRCBpcyB0aGUgdXNlciB3aG8gaGFzIGJlZW4gcmF0ZWQuDQogIDIuIFJhdGluZ3MgYXJlIG9uIGEgc2NhbGUgb2YgMSAtIDEwIHdoZXJlIDEwIGlzIHRoZSBoaWdoZXN0LiANCiAgMy4gT25seSB1c2VycyB3aG8gcHJvdmlkZSBhdCBsZWFzdCAyMCByYXRpbmdzIHdlcmUgaW5jbHVkZWQuIFVzZXIgZ2VuZGVyIGluZm9ybWF0aW9uIGlzIGFsc28gYXZhaWxhYmxlIHdpdGggaWRlbmZpZXJzIChNLCBGLCBVLVVua25vd24pLg0KDQojIyBMaWJyYXJpZXMNCg0KYGBge3IgbWVzc2FnZT1GQUxTRSwgd2FybmluZz1GQUxTRX0NCg0KbGlicmFyeShyZWNvbW1lbmRlcmxhYikNCmxpYnJhcnkodGlkeXZlcnNlKQ0KbGlicmFyeShrYWJsZUV4dHJhKQ0KYGBgDQoNCg0KIyMgRGF0YSBQcmUtcHJvY2Vzc2luZw0KDQpJbXBvcnQgZmlsZXMgPGJyLz4NCg0KVGhlIGZpbGVzIGFyZSB0b28gbGFyZ2UgdG8gdXBsb2FkIHRvIG15IGdpdGh1YiBldmVuIHdoZW4gemlwcGVkIHNvIHlvdSBjYW4gZmluZCBpdCBbaGVyZV0oaHR0cDovL3d3dy5vY2NhbXNsYWIuY29tL3BldHJpY2VrL2RhdGEvKS4NCg0KYGBge3J9DQpkX3JhdGluZ3MgPC0gcmVhZC5jc3YoIkM6L1VzZXJzL0phdmVybi9Eb2N1bWVudHMvRGF0YSBTY2llbmNlIE1TL0RBVEE2MTIvbGliaW1zZXRpLWNvbXBsZXRlL2xpYmltc2V0aS9yYXRpbmdzLmRhdCIsIGhlYWRlciA9IEYsIHNlcCA9ICIsIikNCg0KY29sbmFtZXMoZF9yYXRpbmdzKSA8LSBjKCJ1c2VySWQiLCAicHJvZmlsZUlkIiwgInJhdGluZyIpDQpkX3JhdGluZ3MkdXNlcklkIDwtIGFzLmZhY3RvcihkX3JhdGluZ3MkdXNlcklkKQ0KZF9yYXRpbmdzJHByb2ZpbGVJZCA8LSBhcy5mYWN0b3IoZF9yYXRpbmdzJHByb2ZpbGVJZCkNCg0KZ2VuZGVyIDwtIHJlYWQuY3N2KCJDOi9Vc2Vycy9KYXZlcm4vRG9jdW1lbnRzL0RhdGEgU2NpZW5jZSBNUy9EQVRBNjEyL2xpYmltc2V0aS1jb21wbGV0ZS9saWJpbXNldGkvZ2VuZGVyLmRhdCIsIGhlYWRlciA9IEYsIHNlcCA9ICIsIikNCg0KY29sbmFtZXMoZ2VuZGVyKSA8LSBjKCJ1c2VySWQiLCAidHlwZSIpDQpnZW5kZXIkdXNlcklkIDwtIGFzLmZhY3RvcihnZW5kZXIkdXNlcklkKQ0KDQpgYGANCg0KDQojIyBFREEgey50YWJzZXQgLnRhYnNldC1mYWRlfQ0KDQojIyMgUHJldmlldyBvZiBkYXRhc2V0cw0KYGBge3J9DQpoZWFkKGRfcmF0aW5ncyk7IGhlYWQoZ2VuZGVyKQ0KYGBgDQoNCg0KIyMjIFRvcCAxMCB1c2VycyB3aG8gcHJvdmlkZWQgdGhlIG1vc3QgcmF0aW5ncw0KYGBge3J9DQpkX3JhdGluZ3MgJT4lIA0KICBncm91cF9ieSh1c2VySWQpJT4lIA0KICBzdW1tYXJpc2UodXNlcl9jb3VudCA9IGxlbmd0aCh1c2VySWQpKSAlPiUgDQogIHRvcF9uKDEwKSAlPiUgDQogIGdncGxvdChhZXModXNlcklkLCB1c2VyX2NvdW50LCBmaWxsID0gdXNlcklkKSkgKyBnZW9tX2JhcihzdGF0ID0gImlkZW50aXR5IikgKyBnZW9tX3RleHQoYWVzKGxhYmVsPXVzZXJfY291bnQpLCB2anVzdD0tMC4zLCBzaXplPTMuNSkNCg0KYGBgDQoNCg0KIyMjIFRvcCByYXRlZCBwcm9maWxlcw0KYGBge3J9DQpkX3JhdGluZ3MgJT4lIA0KICBncm91cF9ieShwcm9maWxlSWQpICU+JSANCiAgc3VtbWFyaXNlKHByb2ZpbGVfY291bnQgPSBsZW5ndGgocHJvZmlsZUlkKSkgJT4lIA0KICB0b3BfbigxMCkgLT4gdG9wcHJvZmlsZXMNCiAgZ2dwbG90KHRvcHByb2ZpbGVzLCBhZXMocHJvZmlsZUlkLCBwcm9maWxlX2NvdW50LCBmaWxsID0gcHJvZmlsZUlkKSkgKyANCiAgICBnZW9tX2JhcihzdGF0ID0gImlkZW50aXR5IikgKyANCiAgICBnZW9tX3RleHQoYWVzKGxhYmVsPXByb2ZpbGVfY291bnQpLCB2anVzdD0tMC4zLCBzaXplPTMuNSkNCmBgYA0KDQojIyMgSG93IHNjb3JlcyBhcmUgZGlzdHJpYnV0ZWQNCmBgYHtyfQ0KZF9yYXRpbmdzICU+JSANCiAgIGdyb3VwX2J5KHJhdGluZyklPiUgDQogICBzdW1tYXJpc2UocmF0aW5nX2NvdW50ID0gc3VtKHJhdGluZykpICU+JSANCiAgIGdncGxvdChhZXMocmF0aW5nLCByYXRpbmdfY291bnQsIGZpbGwgPSByYXRpbmcpKSArIGdlb21fYmFyKHN0YXQgPSAiaWRlbnRpdHkiKSArICAgc2NhbGVfeF9jb250aW51b3VzKGJyZWFrcyA9IHNlcSgxLCAxMCkpICsgZ2VvbV90ZXh0KGFlcyhsYWJlbD1yYXRpbmdfY291bnQpLCB2anVzdD0tMC4zLCBzaXplPTMuNSkNCmBgYA0KDQoNCiMjIyBHZW5kZXIgcmF0aW8NCg0KYGBge3J9DQpnZW5kZXIgJT4lIA0KICBncm91cF9ieSh0eXBlKSAlPiUgDQogIHN1bW1hcmlzZSh0eXBlX2NvdW50ID0gbGVuZ3RoKHR5cGUpKSAlPiUgDQogIGdncGxvdChhZXModHlwZSwgdHlwZV9jb3VudCwgZmlsbCA9IHR5cGUpKSArIGdlb21fYmFyKHN0YXQgPSAiaWRlbnRpdHkiLCBjb2xvciA9ICJwdXJwbGUiKQ0KICANCmBgYA0KDQoNCiMjIERhdGluZyBNYXRyaXgNCg0KQXMgd2UgbW92ZSBmb3J3YXJkIGluIHRoZSBhbmFseXNpcywgdGhlIHJhdGluZ3MgZGF0YWZyYW1lIHdpbGwgYmUgY29udmVydGVkIGludG8gYSBtYXRyaXggdG8gYnVpbGQgYW5kIGV2YWx1YXRlIHRoZSByZWNvbW1lbmRhdGlvbiBzeXN0ZW1zLg0KDQpgYGB7cn0NCmRtYXRyaXggPC0gYXMoZF9yYXRpbmdzLCAicmVhbFJhdGluZ01hdHJpeCIpDQpkbWF0cml4DQpgYGANCg0KRGltZW5zaW9ucyBvZiB0aGUgbWF0cml4DQpgYGB7cn0NCmRpbShkbWF0cml4QGRhdGEpDQpgYGANCg0KU2l6ZSBvZiBtYXRyaXggZGF0YQ0KYGBge3J9DQpvYmplY3Quc2l6ZShkbWF0cml4KQ0KYGBgDQpBYm91dCAyMjggTUINCg0KDQpUaGUgZmlsZSBpcyBsYXJnZSBzbyB3ZSdsbCBjdXQgZG93biBvbiB0aGUgbnVtYmVyIG9mIGF0dHJpYnV0ZXMuDQpgYGB7cn0NCiMgdXNlcnMgd2hvIHJhdGVkIGF0IGxlYXN0IDUwMCBwcm9maWxlcyANCiMgcHJvZmlsZXMgdGhhdCBhcmUgcmF0ZWQgYXQgbGVhc3QgODAwIHRpbWVzDQoNCmRtYXRyaXggPC0gZG1hdHJpeFtyb3dDb3VudHMoZG1hdHJpeCkgPiA1MDAsIGNvbENvdW50cyhkbWF0cml4KSA+IDgwMF0NCmRtYXRyaXgNCmBgYA0KDQojIyMjIEF2ZXJhZ2UgUHJvZmlsZSBSYXRpbmdzDQoNCmBgYHtyfQ0KYXZnX3Byb2ZpbGVfcmF0aW5ncyA8LSBkYXRhLmZyYW1lKCJhdmdfcmF0aW5nIiA9IGNvbE1lYW5zKGRtYXRyaXgpKSAlPiUgDQogIGdncGxvdChhZXMoeCA9IGF2Z19yYXRpbmcpKSArIA0KICBnZW9tX2hpc3RvZ3JhbShjb2xvciA9ICJyZWQiLCBmaWxsID0gImxpZ2h0Ymx1ZSIpICsgDQogIGdndGl0bGUoIkRpc3RyaWJ1dGlvbiBvZiBBdmVyYWdlIFJhdGluZ3MgZm9yIFByb2ZpbGVzIikNCg0KYXZnX3Byb2ZpbGVfcmF0aW5ncw0KYGBgDQoNClRoZSBkaXN0cmlidXRpb24gaXMgbmVhcmx5IG5vcm1hbCB3aXRoIG1vc3QgcmF0aW5nIGZhbGxpbmcgYmV0d2VlbiA1IGFuZCA3LjUuDQoNCiMjIyMgSG93IHNpbWlsYXIgYXJlIHRoZSBmaXJzdCAzMDAgdXNlcnM/DQpgYGB7cn0NCnNpbSA8LSBzaW1pbGFyaXR5KGRtYXRyaXhbMTozMDAsIF0sIG1ldGhvZCA9ICJjb3NpbmUiLCB3aGljaCA9ICJ1c2VycyIpDQppbWFnZShhcy5tYXRyaXgoc2ltKSwgbWFpbiA9ICJVc2VyIFNpbWlsYXJpdHkiKQ0KDQpgYGANCg0KIyMjIyBIb3cgc2ltaWxhciBhcmUgdGhlIGZpcnN0IDMwMCBwcm9maWxlcz8NCmBgYHtyfQ0KDQpzaW0yIDwtIHNpbWlsYXJpdHkoZG1hdHJpeFssIDE6MzAwXSwgbWV0aG9kID0gImNvc2luZSIsIHdoaWNoID0gIml0ZW1zIikNCmltYWdlKGFzLm1hdHJpeChzaW0yKSwgbWFpbiA9ICJQcm9maWxlIFNpbWlsYXJpdHkiKQ0KDQpgYGANCg0KIyMgVHJhaW5pbmcgYW5kIFRlc3QgU2V0cw0KDQpTbyB3ZSBhcmUgZ29pbmcgdG8gc3BsaXQgdGhlIGRhdGEgOTA6MTAsIHRyYWluIGFuZCB0ZXN0IHJlc3BlY3RpdmVseSwga2VlcGluZyAzIGl0ZW1zIGFuZCBydW5uaW5nIHRoZSBldmFsdWF0aW9uIDQgdGltZXMuDQpgYGB7cn0NCg0KI21pbihyb3dDb3VudHMoZG1hdHJpeCkpPSA2IHNvIHdlIGNhbiBrZWVwIDUgaXRlbXMgcGVyIHVzZXINCmRtYXRfZXZhbCA8LSBldmFsdWF0aW9uU2NoZW1lKGRhdGEgPSBkbWF0cml4LCBtZXRob2QgPSAic3BsaXQiLCB0cmFpbiA9IDAuOSwgZ2l2ZW4gPSA1LCBnb29kUmF0aW5nID0gNSwgayA9IDQpIA0KZG1hdF9ldmFsDQpgYGANCg0KIyMgQ29tcGFyZSBSZWNvbW1lbmRlciBTeXN0ZW0gQWxnb3JpdGhtcw0KDQpgYGB7cn0NCmFsZ29yaXRobXMgPC0gbGlzdCgNCiAgSUJDRiA9IGxpc3QobmFtZSA9ICJJQkNGIiwgcGFyYW0gPSBsaXN0KG1ldGhvZCA9ICJjb3NpbmUiKSksDQogIFVCQ0YgPSBsaXN0KG5hbWUgPSAiVUJDRiIsIHBhcmFtID0gbGlzdChtZXRob2QgPSAiY29zaW5lIikpLA0KICBTVkQgPSBsaXN0KG5hbWUgPSAiU1ZEIiwgcGFyYW0gPSBsaXN0KGsgPSAzMCkpLA0KICBQT1BVTEFSID0gbGlzdChuYW1lID0gIlBPUFVMQVIiLCBwYXJhbSA9IE5VTEwpLCAjc2VyZW5kaXBpdHkNCiAgUkFORE9NID0gbGlzdChuYW1lID0gIlJBTkRPTSIsIHBhcmFtID0gTlVMTCkNCikNCmBgYA0KDQpUZXN0IHRoZSBtb2RlbHMgYnkgdmFyeWluZyB0aGUgbnVtYmVyIG9mIHByb2ZpbGVzIHRvIHJlY29tbWVuZC4NCmBgYHtyfQ0KIyBydW4gYWxnb3JpdGhtcywgcHJlZGljdCBuZXh0IG4gcHJvZmlsZQ0KZXZhbF9yZXN1bHRzIDwtIGV2YWx1YXRlKGRtYXRfZXZhbCwgYWxnb3JpdGhtcywgdHlwZSA9ICJ0b3BOTGlzdCIsIG4gPSBjKDEsIDMsIDUsIDEwLCAxNSwgMjApKQ0KDQphdmVyYWdlcyA8LSBhdmcoZXZhbF9yZXN1bHRzKQ0KYGBgDQoNCklCQ0YgdG9vayBhcHByb3hpbWF0ZWx5IDI1IG1pbnV0ZXMgdG8gcnVuIGFuZCBkbyBwcmVkaWN0aW9uLg0KDQojIyMjIFJlc3VsdHMNCg0KVFAgLSBUcnVlIFBvc2l0aXZlDQpGUCAtIEZhbHNlIFBvc2l0aXZlDQpGTiAtIEZhbHNlIE5lZ2F0aXZlDQpUTiAtIFRydWUgTmVnYXRpdmUNCg0KYGBge3J9DQprYWJsZShhdmVyYWdlcyRJQkNGKSAlPiUga2FibGVfc3R5bGluZyhib290c3RyYXBfb3B0aW9ucyA9IGMoInN0cmlwZWQiLCAiYm9yZGVyZWQiKSwgZnVsbF93aWR0aCA9IEYsIGZvbnRfc2l6ZSA9IDExKSAlPiUgYWRkX2hlYWRlcl9hYm92ZShjKCIgIiwgIklCQ0YiID0gOCkpDQprYWJsZShhdmVyYWdlcyRVQkNGKSAlPiUga2FibGVfc3R5bGluZyhib290c3RyYXBfb3B0aW9ucyA9IGMoInN0cmlwZWQiLCAiYm9yZGVyZWQiKSwgZnVsbF93aWR0aCA9IEYsIGZvbnRfc2l6ZSA9IDExKSAlPiUgYWRkX2hlYWRlcl9hYm92ZShjKCIgIiwgIlVCQ0YiID0gOCkpDQprYWJsZShhdmVyYWdlcyRTVkQpICU+JSBrYWJsZV9zdHlsaW5nKGJvb3RzdHJhcF9vcHRpb25zID0gYygic3RyaXBlZCIsICJib3JkZXJlZCIpLCBmdWxsX3dpZHRoID0gRiwgZm9udF9zaXplID0gMTEpICU+JSBhZGRfaGVhZGVyX2Fib3ZlKGMoIiAiLCAiU1ZEIiA9IDgpKQ0Ka2FibGUoYXZlcmFnZXMkUE9QVUxBUikgJT4lIGthYmxlX3N0eWxpbmcoYm9vdHN0cmFwX29wdGlvbnMgPSBjKCJzdHJpcGVkIiwgImJvcmRlcmVkIiksIGZ1bGxfd2lkdGggPSBGLCBmb250X3NpemUgPSAxMSkgJT4lIGFkZF9oZWFkZXJfYWJvdmUoYygiICIsICJQT1BVTEFSIiA9IDgpKQ0Ka2FibGUoYXZlcmFnZXMkUkFORE9NKSAlPiUga2FibGVfc3R5bGluZyhib290c3RyYXBfb3B0aW9ucyA9IGMoInN0cmlwZWQiLCAiYm9yZGVyZWQiKSwgZnVsbF93aWR0aCA9IEYsIGZvbnRfc2l6ZSA9IDExKSAlPiUgYWRkX2hlYWRlcl9hYm92ZShjKCIgIiwgIlJBTkRPTSIgPSA4KSkNCmBgYA0KDQoNCiMjIyMgUk9DIGN1cnZlDQoNClRoZSBST0MgY3VydmUgaXMgY3JlYXRlZCBieSBwbG90dGluZyB0aGUgdHJ1ZSBwb3NpdGl2ZSByYXRlIGFnYWluc3QgdGhlIGZhbHNlIHBvc2l0aXZlIHJhdGUuIFRoZSBjbG9zZXIgYW4gUk9DIGN1cnZlIGlzIHRvIHRoZSB1cHBlciBsZWZ0IGNvcm5lciwgdGhlIG1vcmUgZWZmaWNpZW50IGlzIHRoZSB0ZXN0Lg0KDQpgYGB7cn0NCnBsb3QoZXZhbF9yZXN1bHRzLCBhbm5vdGF0ZSA9IFQsIGxlZ2VuZD0idG9wbGVmdCIpDQp0aXRsZSgiUk9DIEN1cnZlIikNCmBgYA0KDQpCYXNlZCBvbiB0aGUgZ3JhcGggdmlzdWFsaXphdGlvbiBhYm92ZSwgdGhlIFVCQ0YgaXMgYmV0dGVyIHRoYW4gdGhlIG90aGVycy4NCg0KIyMjIyBQcmVjaXNpb24tUmVjYWxsDQoNClByZWNpc2lvbiBleHByZXNzZXMgdGhlIHByb3BvcnRpb24gb2YgdGhlIGRhdGEgcG9pbnRzIG91ciBtb2RlbCBzYXlzIHdhcyByZWxldmFudCBhbmQgYXJlIGFjdHVhbGx5IHdlcmUgcmVsZXZhbnQuIENhbGN1bGF0ZWQgYXM6ICoqVFAgLyAoVFAgKyBGUCkqKg0KDQpSZWNhbGwgZXhwcmVzc2VzIHRoZSBhYmlsaXR5IHRvIGZpbmQgYWxsIHJlbGV2YW50IGluc3RhbmNlcyBpbiBhIGRhdGFzZXQgb3IgdGhlIG1vZGVsJ3MgYWJpbGl0eSB0byBmaW5kIGFsbCB0aGUgZGF0YSBwb2ludHMgb2YgaW50ZXJlc3QgaW4gYSBkYXRhc2V0LiAqKlRQIC8gKFRQICsgRk4pKioNCg0KVGhlIGNsb3NlciB0aGUgY3VydmUgb3IgbGluZSBpcyB0byB0aGUgdG9wIHJpZ2h0LCB0aGUgYmV0dGVyIHRoZSBwZXJmb3JtYW5jZSBvZiB0aGUgYWxnb3JpdGhtLg0KDQpgYGB7cn0NCiMgcHJlY2lzaW9uIC8gcmVjYWxsDQpwbG90KGV2YWxfcmVzdWx0cywgInByZWMvcmVjIiwgYW5ub3RhdGUgPSAyKQ0KdGl0bGUoIlByZWNpc2lvbi1yZWNhbGwiKQ0KYGBgDQoNClVzZXIgQmFzZWQgbW9kZWwgaXMgc3RpbGwgYmV0dGVyIHRoYW4gdGhlIG90aGVyIGFsZ29yaXRobXMuDQoNCg0KDQpgYGB7cn0NCiNQcmVkaWN0IHRvcC1OIHJlY29tbWVuZGF0aW9uIGxpc3RzDQpldmFsX3Jlc3VsdHMyIDwtIGV2YWx1YXRlKGRtYXRfZXZhbCwgYWxnb3JpdGhtcywgdHlwZSA9ICJyYXRpbmdzIikNCmBgYA0KDQpgYGB7cn0NCmF2ZyhldmFsX3Jlc3VsdHMyKQ0KYGBgDQoNCiMjIyMgT25saW5lIEV2YWx1YXRpb24NCg0KQXMgbm90ZWQsIG9mZmxpbmUgZXZhbHVhdGlvbnMgdXNlIHByZWNvbXBpbGVkIG9mZmxpbmUgZGF0YXNldHMgZnJvbSB3aGljaCBkYXRhIHdhcyByZW1vdmVkIGhlbmNlIGV2YWx1YXRpb25zIGFyZSB1c2VkIHRvIGFuYWx5emUgdGhlIGFsZ29ydGhpbXMnIGFibGl0eSB0byBwcmVkaWN0IG1pc3NpbmcgZGF0YS4gT24gdGhlIG90aGVyIGhhbmQsIGluIG9ubGluZSBldmFsdWF0aW9ucywgcmVjb21tZW5kYXRpb25zIGFyZSBzaG93biB0byByZWFsIHVzZXJzIG9mIHRoZSBzeXN0ZW0gZHVyaW5nIHRoZWlyIHNlc3Npb24gYW5kIHNvIHVzZXJzIGRvIG5vdCByYXRlIHJlY29tbWVuZGF0aW9ucyBidXQgdGhlIHJlY29tbWVuZGVyIHN5c3RlbSBvYnNlcnZlcyBob3cgb2Z0ZW4gYSB1c2VyIGFjY2VwdHMgYSByZWNvbW1lbmRhdGlvbi4gT25lIG1ldHJpYyB0aGF0IGNvdWxkIGJlIHVzZWQgdG8gZXZhbHVhdGUgb25saW5lIGV2YWx1YXRpb24gaXMgKipjbGljay10aHJvdWdoIHJhdGUgKENUUikqKi4gVGhpcyBtZWFzdXJlcyB0aGUgcmF0aW8gb2YgY2xpY2tzIHRvIHRoZSBudW1iZXIgb2YgcmVjb21tZW5kYXRpb24gbGlzdHMgcHJvdmlkZWQuIFNvIGZvciBpbnN0YW5jZSBpZiB0aGUgcmVjb21tZW5kZXIgcmVjb21tZW5kcyAxMDAwIHByb2ZpbGVzIGFuZCB0aGUgdXNlciBjbGlja3Mgb25seSA1ICB0aGVuIHRoZSBDVFIgd291bGQgYmUgMC41JS4NCg0KIyMgU3VtbWFyeQ0KDQpCYXNlZCBvbiB0aGUgb3V0cHV0IHNob3duIGZvciBlYWNoIGFsZ29yaXRobXMsIHRoZSBJQkNGIHRvb2sgYSBsb25nZXIgdGltZSB0byBsZWFybiB0aGUgZGF0YSB0aGFuIHRoZSBvdGhlcnMgYnV0IHRvb2sgYSBzaG9ydGVyIHRpbWUgdG8gcHJlZGljdCB0aGUgZGF0YS4gQWNjb3JkaW5nIHRvIHRoZSBlcnJvciByYXRlcywgUG9wdWxhciBoYXMgdGhlIGxvd2VzdCBSTVNFIHdoaWNoIG1lYW5zIHRoYXQgaXQgcGVyZm9ybWVkIHRoZSBiZXN0IHdpdGggbG93ZXIgZXJyb3IgcmF0ZXMuIEhvd2V2ZXIsIGFzIHNob3duIGluIHRoZSB2aXN1YWxpemF0aW9ucywgVUJDRiBwZXJmb3JtZWQgYmVzdC4gVGhlIFJhbmRvbSBhbGdvcml0aG0gY29udGludWVkIHRvIHBlcmZvcm0gd29yc3QgdGhyb3VnaCBvdXQgdGhlIHByb2Nlc3MuDQoNCiMjIFJlZmVyZW5jZXMNCg0KaHR0cHM6Ly90b3dhcmRzZGF0YXNjaWVuY2UuY29tL2JleW9uZC1hY2N1cmFjeS1wcmVjaXNpb24tYW5kLXJlY2FsbC0zZGEwNmJlYTlmNmMNCmh0dHBzOi8vYWN1dGVjYXJldGVzdGluZy5vcmcvZW4vYXJ0aWNsZXMvcm9jLWN1cnZlcy13aGF0LWFyZS10aGV5LWFuZC1ob3ctYXJlLXRoZXktdXNlZA0KaHR0cHM6Ly9wZGZzLnNlbWFudGljc2Nob2xhci5vcmcvOTRjMi8wMGNlYzFlMmY5NTQ3ZWE2MDYzZTA4MDE5ZjcyODk1YmZiYTgucGRmDQpodHRwczovL2xpbmsuc3ByaW5nZXIuY29tL2FydGljbGUvMTAuMTAwNy9zMTMwNDItMDE3LTA3NjItOQ0K