Movie Recommender System
The following project builds out a Movie recommender system. The dataset comes from GroupLens, a research lab in the Department of Computer Science and Engineering at the University of Minnesota. About GroupLens. GroupLens compiled data from the MovieLens web site. A user rated the movies on a scale of 1 to 5.
About the Data
For this project, I chose reviews of seven highly popular films. The dataset that I downloaded consists of the following features:
userid : the Id of the user rating the films
movieid : a unique, numerical identifier for the film
move title: the title of the film
user ratins: user ratings on a scale of 1 to 5
movie genre(s) : the genres of the films, “Action|Comedy” etc.
## 'data.frame': 29085 obs. of 5 variables:
## $ userid : int 43 43 43 43 43 43 43 120 120 120 ...
## $ movieId: int 110 2959 356 527 260 1 1307 110 2959 356 ...
## $ title : Factor w/ 7 levels "Braveheart (1995)",..: 1 2 3 4 5 6 7 1 2 3 ...
## $ rating : num 4.5 5 5 3.5 4.5 4 4 5 5 5 ...
## $ genres : Factor w/ 7 levels "Action|Adventure|Sci-Fi",..: 3 2 5 7 1 4 6 3 2 5 ...
Data Preparation
For the purposes of this project, I removed the movieId and genres features.
Additionally, the data is in a long format. I changed it to a wide format so that the form of the dataset is a dense, user-matrix form.
Long Format
|
userid
|
title
|
rating
|
|
43
|
Braveheart (1995)
|
4.5
|
|
43
|
Fight Club (1999)
|
5.0
|
|
43
|
Forrest Gump (1994)
|
5.0
|
|
43
|
Schindler’s List (1993)
|
3.5
|
|
43
|
Star Wars: Episode IV - A New Hope (1977)
|
4.5
|
|
43
|
Toy Story (1995)
|
4.0
|
|
43
|
When Harry Met Sally… (1989)
|
4.0
|
ratings_wide <- spread(ratings, title, rating)
colnames(ratings_wide) <- c("UserId", "Braveheart", "Fight_Club", "Forrest_Gump", "Schindler's_List", "Star_Wars_Episode_IV_A_New_Hope", "Toy_Story", "When_Harry_Met_Sally")
kable(head(ratings_wide,5), caption = "Dense, User-Matrix Form") %>%
kable_styling(bootstrap_options = "striped", full_width = F)
Dense, User-Matrix Form
|
UserId
|
Braveheart
|
Fight_Club
|
Forrest_Gump
|
Schindler’s_List
|
Star_Wars_Episode_IV_A_New_Hope
|
Toy_Story
|
When_Harry_Met_Sally
|
|
43
|
4.5
|
5.0
|
5.0
|
3.5
|
4.5
|
4.0
|
4.0
|
|
120
|
5.0
|
5.0
|
5.0
|
5.0
|
5.0
|
5.0
|
5.0
|
|
171
|
4.5
|
4.5
|
4.5
|
4.0
|
4.5
|
4.5
|
4.0
|
|
426
|
4.5
|
5.0
|
4.0
|
4.5
|
4.5
|
2.5
|
3.5
|
|
431
|
4.5
|
5.0
|
3.5
|
1.5
|
4.0
|
3.0
|
1.5
|
## [1] 4155 8
I still have 4,155 which is too unruly for this project, so I trimmed it down to 100 users.
The last step in data preparation was to randomly assign “NAs” to the data as per project instructions. In the code block below, a random index number is generated between 1 and 100.
A for loop is created that does 10 iterations that randomly assigns NA values.
Dense, User-Matrix Form with NAs
|
UserId
|
Braveheart
|
Fight_Club
|
Forrest_Gump
|
Schindler’s_List
|
Star_Wars_Episode_IV_A_New_Hope
|
Toy_Story
|
When_Harry_Met_Sally
|
|
43
|
4.5
|
5.0
|
5.0
|
3.5
|
4.5
|
4.0
|
4.0
|
|
120
|
5.0
|
5.0
|
5.0
|
5.0
|
5.0
|
5.0
|
NA
|
|
171
|
4.5
|
4.5
|
4.5
|
4.0
|
4.5
|
4.5
|
4.0
|
|
426
|
4.5
|
NA
|
4.0
|
NA
|
4.5
|
NA
|
3.5
|
|
431
|
4.5
|
5.0
|
3.5
|
1.5
|
4.0
|
3.0
|
1.5
|
|
440
|
4.0
|
5.0
|
4.5
|
4.0
|
5.0
|
3.5
|
4.5
|
|
462
|
5.0
|
4.5
|
4.0
|
3.5
|
3.5
|
3.5
|
4.5
|
|
519
|
4.5
|
3.0
|
4.0
|
3.5
|
3.5
|
4.5
|
3.0
|
|
607
|
3.0
|
4.0
|
4.0
|
3.5
|
4.0
|
NA
|
3.5
|
|
707
|
NA
|
4.0
|
4.0
|
5.0
|
4.0
|
3.0
|
3.0
|
Break your ratings into separate training and test datasets
The data is split 50-50 into “train” and “test”
## [1] 50 8
## [1] 50 8
Using your training data, calculate the raw average (mean) rating for every user-item combination
User Averages
User Averages
|
|
user_avg
|
|
43
|
4.357143
|
|
171
|
4.357143
|
|
440
|
4.357143
|
|
707
|
3.833333
|
|
847
|
3.700000
|
|
896
|
4.250000
|
|
939
|
4.214286
|
|
947
|
4.500000
|
|
997
|
4.142857
|
|
1482
|
4.000000
|
Movie Averages
movie_avg <- colMeans(train[2:8], na.rm = T)
movie_avg_df <- as.data.frame(movie_avg)
rownames(movie_avg_df) <- c("Braveheart", "Fight_Club", "Forrest_Gump", "Schindler's_List", "Star_Wars_Episode_IV_A_New_Hope", "Toy_Story", "When_Harry_Met_Sally")
kable(head(movie_avg_df,10), caption = "Movie Averages") %>%
kable_styling(bootstrap_options = "striped", full_width = F)
Movie Averages
|
|
movie_avg
|
|
Braveheart
|
4.011628
|
|
Fight_Club
|
4.184783
|
|
Forrest_Gump
|
4.068182
|
|
Schindler’s_List
|
4.200000
|
|
Star_Wars_Episode_IV_A_New_Hope
|
4.114583
|
|
Toy_Story
|
3.777778
|
|
When_Harry_Met_Sally
|
3.944444
|
Raw averages for train set
For both the train and test sets, I converted them from a data.frame to a matrix while excluding the userid feature. Next, I calculated the mean for the entire matrixes and stored them in variables, train_raw_mean and test_raw_mean.
## [1] 4.044304
Raw averages for test set
## [1] 4
Calculate the RMSE for raw average for both your training data and your test data
In the code block below, I subtracted the raw mean from both the train and test data frames, then squared that result, converted that result to a matrix, took the mean, and finally took the square root of that result.
I got a train raw RMSE of 0.9528102 and a test raw RMSE of 0.9163092
## [1] 0.9528102
## [1] 0.9163092
Using your training data, calculate the bias for each user and each item.
In the next two code blocks, I subtracted the mean of the train dataset from from the average user and movie means.
User Biases - Top 10
|
userId
|
user_avg
|
|
43
|
0.3128391
|
|
171
|
0.3128391
|
|
440
|
0.3128391
|
|
707
|
-0.2109705
|
|
847
|
-0.3443038
|
|
896
|
0.2056962
|
|
939
|
0.1699819
|
|
947
|
0.4556962
|
|
997
|
0.0985533
|
|
1482
|
-0.0443038
|
We see that the users in the train set are negatively biased against Braveheart, Toy_Story, and When Harry Met Sally, and they have a positive bias to Schindler’s List and Fight Club.
Movie Biases
|
movie
|
movie_avg
|
|
Braveheart
|
-0.0326759
|
|
Fight_Club
|
0.1404788
|
|
Forrest_Gump
|
0.0238780
|
|
Schindler’s_List
|
0.1556962
|
|
Star_Wars_Episode_IV_A_New_Hope
|
0.0702795
|
|
Toy_Story
|
-0.2665260
|
|
When_Harry_Met_Sally
|
-0.0998594
|
From the raw average, and the appropriate user and item biases, calculate the baseline predictorsfor every user-item combination
The function below, create_baseline_predictors_df, takes in an item_bias and user_bias dataframes as well as the raw mean. It then creates and populate a data frame, baseline_predictors_df, with the raw mean plus the user and item biases. Additionally, it forces scores above 5 to be five and scores below 1 to be 1.
Baseline Predictors
|
|
Braveheart
|
Fight_Club
|
Forrest_Gump
|
Schindler’s_List
|
Star_Wars_Episode_IV_A_New_Hope
|
Toy_Story
|
When_Harry_Met_Sally
|
|
43
|
4.324467
|
4.497622
|
4.381021
|
4.512839
|
4.427422
|
4.090617
|
4.257283
|
|
171
|
4.324467
|
4.497622
|
4.381021
|
4.512839
|
4.427422
|
4.090617
|
4.257283
|
|
440
|
4.324467
|
4.497622
|
4.381021
|
4.512839
|
4.427422
|
4.090617
|
4.257283
|
|
707
|
3.800657
|
3.973812
|
3.857211
|
3.989030
|
3.903613
|
3.566807
|
3.733474
|
|
847
|
3.667324
|
3.840479
|
3.723878
|
3.855696
|
3.770280
|
3.433474
|
3.600141
|
|
896
|
4.217324
|
4.390479
|
4.273878
|
4.405696
|
4.320279
|
3.983474
|
4.150141
|
|
939
|
4.181610
|
4.354764
|
4.238164
|
4.369982
|
4.284565
|
3.947760
|
4.114426
|
|
947
|
4.467324
|
4.640479
|
4.523878
|
4.655696
|
4.570279
|
4.233474
|
4.400141
|
|
997
|
4.110181
|
4.283336
|
4.166735
|
4.298553
|
4.213137
|
3.876331
|
4.042998
|
|
1482
|
3.967324
|
4.140479
|
4.023878
|
4.155696
|
4.070279
|
3.733474
|
3.900141
|
Calculate the RMSE for the baseline predictors for both your training data and your test data
Here, I found something odd. Even though the RMSE improved for the training set, it got much worse for the test set.
## [1] 0.6759492
## [1] 1.154198
Summary
Accounting for usr bias improved the RMSE over using just the raw averages. I saw a 29% improvement of the RMSE.
## [1] 0.2905731
However, the same cannot be said for the test set. Here, I saw a 26% decline in the RMSE from using the raw average to adding in the item-user bias.
## [1] -0.2596158
I don’t have a ready explanation as to why, so I checked the movie averages and biases for the test set. I found that the test set had a stronger negative bias towards “When Harry Met Sally” than the train set. Also, the train set had a stronger negative bias against Toy Story than the train set. These may account for the train set’s RMSE decline.
movie_avg2 <- colMeans(test[2:8], na.rm = T)
movie_avg_df2 <- as.data.frame(movie_avg2)
rownames(movie_avg_df) <- c("Braveheart", "Fight_Club", "Forrest_Gump", "Schindler's_List", "Star_Wars_Episode_IV_A_New_Hope", "Toy_Story", "When_Harry_Met_Sally")
kable(head(movie_avg_df2,10), caption = "Movie Averages") %>%
kable_styling(bootstrap_options = "striped", full_width = F)
Movie Averages
|
|
movie_avg2
|
|
Braveheart
|
3.916667
|
|
Fight_Club
|
4.233333
|
|
Forrest_Gump
|
4.032609
|
|
Schindler’s_List
|
4.211111
|
|
Star_Wars_Episode_IV_A_New_Hope
|
4.093023
|
|
Toy_Story
|
4.000000
|
|
When_Harry_Met_Sally
|
3.522222
|
Movie Biases
|
movie
|
movie_avg2
|
|
Braveheart
|
-0.0833333
|
|
Fight_Club
|
0.2333333
|
|
Forrest_Gump
|
0.0326087
|
|
Schindler’s_List
|
0.2111111
|
|
Star_Wars_Episode_IV_A_New_Hope
|
0.0930233
|
|
Toy_Story
|
0.0000000
|
|
When_Harry_Met_Sally
|
-0.4777778
|
LS0tDQp0aXRsZTogIkNVTlkgREFUQSA2MTIgUHJvamVjdCBPbmUgU3VtbWVyIDIwMjAiDQphdXRob3I6ICJKb2huIEsuIEhhbmNvY2siDQpkYXRlOiAiNi8zLzIwMjAiDQpvdXRwdXQ6DQogIGh0bWxfZG9jdW1lbnQ6DQogICAgY29kZV9kb3dubG9hZDogeWVzDQogICAgY29kZV9mb2xkaW5nOiBoaWRlDQogICAgaGlnaGxpZ2h0OiBweWdtZW50cw0KICAgIG51bWJlcl9zZWN0aW9uczogeWVzDQogICAgdGhlbWU6IHBhcGVyDQogICAgdG9jOiB5ZXMNCiAgICB0b2NfZmxvYXQ6IHllcw0KICBwZGZfZG9jdW1lbnQ6DQogICAgdG9jOiBubw0KIA0KLS0tDQoNCmBgYHtyLCBpbmNsdWRlPUZBTFNFfQ0KbGlicmFyeShyZXNoYXBlMikNCmxpYnJhcnkodGlkeXIpDQpsaWJyYXJ5KGNhVG9vbHMpDQpsaWJyYXJ5KGthYmxlRXh0cmEpDQpsaWJyYXJ5KHJtZGZvcm1hdHMpDQpgYGANCg0KIyMgTW92aWUgUmVjb21tZW5kZXIgU3lzdGVtDQoNClRoZSBmb2xsb3dpbmcgcHJvamVjdCBidWlsZHMgb3V0IGEgTW92aWUgcmVjb21tZW5kZXIgc3lzdGVtLiAgVGhlIGRhdGFzZXQgY29tZXMgZnJvbSBHcm91cExlbnMsIGEgcmVzZWFyY2ggbGFiIGluIHRoZSBEZXBhcnRtZW50IG9mIENvbXB1dGVyIFNjaWVuY2UgYW5kIEVuZ2luZWVyaW5nIGF0IHRoZSBVbml2ZXJzaXR5IG9mIE1pbm5lc290YS4gW0Fib3V0IEdyb3VwTGVuc10oaHR0cHM6Ly9ncm91cGxlbnMub3JnL2Fib3V0L3doYXQtaXMtZ3JvdXBsZW5zLykuICBHcm91cExlbnMgY29tcGlsZWQgZGF0YSBmcm9tIHRoZSBbTW92aWVMZW5zIHdlYiBzaXRlXShodHRwczovL21vdmllbGVucy5vcmcvKS4gIEEgdXNlciByYXRlZCB0aGUgbW92aWVzIG9uIGEgc2NhbGUgb2YgMSB0byA1LiANCg0KIyMgQWJvdXQgdGhlIERhdGENCg0KYGBge3IsIGluY2x1ZGU9RkFMU0V9DQpyYXRpbmdzIDwtIHJlYWQuY3N2KCJodHRwczovL3Jhdy5naXRodWJ1c2VyY29udGVudC5jb20vSm9obktIYW5jb2NrL3Jhdy5naXRodWIvbWFzdGVyL0NVTlklMjBEQVRBNjEyL01MX1Jldmlld3MuY3N2IikNCmBgYA0KDQpGb3IgdGhpcyBwcm9qZWN0LCBJIGNob3NlIHJldmlld3Mgb2Ygc2V2ZW4gaGlnaGx5IHBvcHVsYXIgZmlsbXMuIFRoZSBkYXRhc2V0IHRoYXQgSSBkb3dubG9hZGVkIGNvbnNpc3RzIG9mIHRoZSBmb2xsb3dpbmcgZmVhdHVyZXM6DQoNCnVzZXJpZCA6IHRoZSBJZCBvZiB0aGUgdXNlciByYXRpbmcgdGhlIGZpbG1zIDxicj4NCm1vdmllaWQgOiBhIHVuaXF1ZSwgbnVtZXJpY2FsIGlkZW50aWZpZXIgZm9yIHRoZSBmaWxtIDxicj4NCm1vdmUgdGl0bGU6IHRoZSB0aXRsZSBvZiB0aGUgZmlsbSA8YnI+DQp1c2VyIHJhdGluczogdXNlciByYXRpbmdzIG9uIGEgc2NhbGUgb2YgMSB0byA1IDxicj4NCm1vdmllIGdlbnJlKHMpIDogdGhlIGdlbnJlcyBvZiB0aGUgZmlsbXMsICJBY3Rpb258Q29tZWR5IiBldGMuPGJyPiANCg0KYGBge3J9DQpzdHIocmF0aW5ncykNCmBgYA0KDQoNCg0KIyMgRGF0YSBQcmVwYXJhdGlvbg0KDQoNCkZvciB0aGUgcHVycG9zZXMgb2YgdGhpcyBwcm9qZWN0LCBJIHJlbW92ZWQgdGhlIG1vdmllSWQgYW5kIGdlbnJlcyBmZWF0dXJlcy4gDQoNCg0KDQpgYGB7ciwgaW5jbHVkZT1GQUxTRX0NCnJhdGluZ3MkZ2VucmVzIDwtIE5VTEwNCnJhdGluZ3MkbW92aWVJZCA8LSBOVUxMDQpgYGANCg0KQWRkaXRpb25hbGx5LCB0aGUgZGF0YSBpcyBpbiBhIGxvbmcgZm9ybWF0LiAgSSBjaGFuZ2VkIGl0IHRvIGEgd2lkZSBmb3JtYXQgc28gdGhhdCB0aGUgZm9ybSBvZiB0aGUgZGF0YXNldCBpcyBhIGRlbnNlLCB1c2VyLW1hdHJpeCBmb3JtLiANCg0KYGBge3J9DQprYWJsZShoZWFkKHJhdGluZ3MsNyksIGNhcHRpb24gPSAiTG9uZyBGb3JtYXQiKSAlPiUNCiAga2FibGVfc3R5bGluZyhib290c3RyYXBfb3B0aW9ucyA9ICJzdHJpcGVkIiwgZnVsbF93aWR0aCA9IEYpDQpgYGANCg0KDQpgYGB7ciwgd2FybmluZz1GQUxTRX0NCnJhdGluZ3Nfd2lkZSA8LSBzcHJlYWQocmF0aW5ncywgdGl0bGUsIHJhdGluZykNCmNvbG5hbWVzKHJhdGluZ3Nfd2lkZSkgPC0gYygiVXNlcklkIiwgIkJyYXZlaGVhcnQiLCAiRmlnaHRfQ2x1YiIsICJGb3JyZXN0X0d1bXAiLCAiU2NoaW5kbGVyJ3NfTGlzdCIsICJTdGFyX1dhcnNfRXBpc29kZV9JVl9BX05ld19Ib3BlIiwgIlRveV9TdG9yeSIsICJXaGVuX0hhcnJ5X01ldF9TYWxseSIpDQoNCmthYmxlKGhlYWQocmF0aW5nc193aWRlLDUpLCBjYXB0aW9uID0gIkRlbnNlLCBVc2VyLU1hdHJpeCBGb3JtIikgJT4lDQogIGthYmxlX3N0eWxpbmcoYm9vdHN0cmFwX29wdGlvbnMgPSAic3RyaXBlZCIsIGZ1bGxfd2lkdGggPSBGKQ0KYGBgDQoNCmBgYHtyfQ0KZGltKHJhdGluZ3Nfd2lkZSkNCmBgYA0KSSBzdGlsbCBoYXZlIDQsMTU1IHdoaWNoIGlzIHRvbyB1bnJ1bHkgZm9yIHRoaXMgcHJvamVjdCwgc28gSSB0cmltbWVkIGl0IGRvd24gdG8gMTAwIHVzZXJzLg0KDQoNCmBgYHtyfQ0KdG9wXzEwMCA8LSByYXRpbmdzX3dpZGVbMToxMDAsXQ0KYGBgDQoNCg0KVGhlIGxhc3Qgc3RlcCBpbiBkYXRhIHByZXBhcmF0aW9uIHdhcyB0byByYW5kb21seSBhc3NpZ24gIk5BcyIgdG8gdGhlIGRhdGEgYXMgcGVyIHByb2plY3QgaW5zdHJ1Y3Rpb25zLiBJbiB0aGUgY29kZSBibG9jayBiZWxvdywgYSByYW5kb20gaW5kZXggbnVtYmVyIGlzIGdlbmVyYXRlZCBiZXR3ZWVuIDEgYW5kIDEwMC4gIA0KDQpgYGB7cn0NCmdldF9JbmRleCA8LWZ1bmN0aW9uKCl7DQogICAgICAgIHJldHVybihmbG9vcihydW5pZigxLG1pbj0wLCBtYXg9MTAxKSkpDQogIA0KICB9DQpgYGANCg0KQSBmb3IgbG9vcCBpcyBjcmVhdGVkIHRoYXQgZG9lcyAxMCBpdGVyYXRpb25zIHRoYXQgcmFuZG9tbHkgYXNzaWducyBOQSB2YWx1ZXMuIA0KDQpgYGB7cn0NCiNSYW5kb21seSBhc3NpZ24gTkENCnNldC5zZWVkKDEyMykNCmZvciAoaSBpbiAxOjEwKXsNCiAgdG9wXzEwMCRCcmF2ZWhlYXJ0W2dldF9JbmRleCgpXSA9IE5BDQogIHRvcF8xMDAkRmlnaHRfQ2x1YltnZXRfSW5kZXgoKV0gPSBOQQ0KICB0b3BfMTAwJEZvcnJlc3RfR3VtcFtnZXRfSW5kZXgoKV0gPSBOQQ0KICB0b3BfMTAwJGBTY2hpbmRsZXInc19MaXN0YFtnZXRfSW5kZXgoKV0gPSBOQQ0KICB0b3BfMTAwJFN0YXJfV2Fyc19FcGlzb2RlX0lWX0FfTmV3X0hvcGVbZ2V0X0luZGV4KCldID0gTkENCiAgdG9wXzEwMCRUb3lfU3RvcnlbZ2V0X0luZGV4KCldID0gTkENCiAgdG9wXzEwMCRXaGVuX0hhcnJ5X01ldF9TYWxseVtnZXRfSW5kZXgoKV0gPSBOQQ0KICANCiAgDQogIA0KICANCn0NCg0KDQoNCg0KYGBgDQoNCg0KYGBge3J9DQoNCmthYmxlKGhlYWQodG9wXzEwMCwxMCksIGNhcHRpb24gPSAiRGVuc2UsIFVzZXItTWF0cml4IEZvcm0gd2l0aCBOQXMiKSAlPiUNCiAga2FibGVfc3R5bGluZyhib290c3RyYXBfb3B0aW9ucyA9ICJzdHJpcGVkIiwgZnVsbF93aWR0aCA9IEYpDQoNCmBgYA0KDQoNCiMjIEJyZWFrIHlvdXIgcmF0aW5ncyBpbnRvIHNlcGFyYXRlIHRyYWluaW5nIGFuZCB0ZXN0IGRhdGFzZXRzDQoNClRoZSBkYXRhIGlzIHNwbGl0IDUwLTUwIGludG8gInRyYWluIiBhbmQgInRlc3QiDQoNCmBgYHtyfQ0KI1NwbGl0IHRoZSBkYXRhIGludG8gVHJhaW5pbmcgYW5kIFRlc3QgU2V0IHVzaW5nIHRoZSBjYVRvb2xzIHBhY2thZ2UNCnNldC5zZWVkKDEyMykNCnNhbXBsZSA9IHNhbXBsZS5zcGxpdCh0b3BfMTAwJFVzZXJJZCwgU3BsaXRSYXRpbyA9IC41KQ0KdHJhaW4gPSBzdWJzZXQodG9wXzEwMCwgc2FtcGxlID09IFRSVUUpDQp0ZXN0ICA9IHN1YnNldCh0b3BfMTAwLCBzYW1wbGUgPT0gRkFMU0UpIA0KYGBgDQoNCg0KYGBge3J9DQpkaW0odHJhaW4pDQpgYGANCmBgYHtyfQ0KZGltKHRlc3QpDQpgYGANCiMjIFVzaW5nIHlvdXIgdHJhaW5pbmcgZGF0YSwgY2FsY3VsYXRlIHRoZSByYXcgYXZlcmFnZSAobWVhbikgcmF0aW5nIGZvciBldmVyeSB1c2VyLWl0ZW0gY29tYmluYXRpb24NCg0KIyMjIFVzZXIgQXZlcmFnZXMNCg0KYGBge3J9DQp1c2VyX2F2ZyA8LSByb3dNZWFucyh0cmFpblsyOjhdLCBuYS5ybSA9IFQpDQp1c2VyX2F2Z19kZiA8LSBhcy5kYXRhLmZyYW1lKHVzZXJfYXZnKQ0Kcm93bmFtZXModXNlcl9hdmdfZGYpIDwtIHRyYWluJFVzZXJJZA0KDQprYWJsZShoZWFkKHVzZXJfYXZnX2RmLDEwKSwgY2FwdGlvbiA9ICJVc2VyIEF2ZXJhZ2VzIikgJT4lDQogIGthYmxlX3N0eWxpbmcoYm9vdHN0cmFwX29wdGlvbnMgPSAic3RyaXBlZCIsIGZ1bGxfd2lkdGggPSBGKQ0KYGBgDQoNCg0KDQoNCg0KDQoNCiMjIyBNb3ZpZSBBdmVyYWdlcw0KYGBge3J9DQptb3ZpZV9hdmcgPC0gIGNvbE1lYW5zKHRyYWluWzI6OF0sIG5hLnJtID0gVCkNCm1vdmllX2F2Z19kZiA8LSBhcy5kYXRhLmZyYW1lKG1vdmllX2F2ZykNCnJvd25hbWVzKG1vdmllX2F2Z19kZikgPC0gYygiQnJhdmVoZWFydCIsICJGaWdodF9DbHViIiwgIkZvcnJlc3RfR3VtcCIsICJTY2hpbmRsZXInc19MaXN0IiwgIlN0YXJfV2Fyc19FcGlzb2RlX0lWX0FfTmV3X0hvcGUiLCAiVG95X1N0b3J5IiwgIldoZW5fSGFycnlfTWV0X1NhbGx5IikNCg0Ka2FibGUoaGVhZChtb3ZpZV9hdmdfZGYsMTApLCBjYXB0aW9uID0gIk1vdmllIEF2ZXJhZ2VzIikgJT4lDQogIGthYmxlX3N0eWxpbmcoYm9vdHN0cmFwX29wdGlvbnMgPSAic3RyaXBlZCIsIGZ1bGxfd2lkdGggPSBGKQ0KDQpgYGANCg0KDQoNCg0KIyMjIFJhdyBhdmVyYWdlcyBmb3IgdHJhaW4gc2V0DQoNCkZvciBib3RoIHRoZSB0cmFpbiBhbmQgdGVzdCBzZXRzLCBJIGNvbnZlcnRlZCB0aGVtIGZyb20gYSBkYXRhLmZyYW1lIHRvIGEgbWF0cml4IHdoaWxlIGV4Y2x1ZGluZyB0aGUgdXNlcmlkIGZlYXR1cmUuIE5leHQsIEkgY2FsY3VsYXRlZCB0aGUgbWVhbiBmb3IgdGhlIGVudGlyZSBtYXRyaXhlcyBhbmQgc3RvcmVkIHRoZW0gaW4gdmFyaWFibGVzLCB0cmFpbl9yYXdfbWVhbiBhbmQgdGVzdF9yYXdfbWVhbi4gDQoNCmBgYHtyfQ0KdHJhaW5fbWF0cml4IDwtIGFzLm1hdHJpeCh0cmFpblsyOjhdKQ0KdHJhaW5fcmF3X21lYW4gPC0gbWVhbih0cmFpbl9tYXRyaXgsIG5hLnJtID0gVCkNCnRyYWluX3Jhd19tZWFuDQpgYGANCiMjIyBSYXcgYXZlcmFnZXMgZm9yIHRlc3Qgc2V0DQoNCmBgYHtyfQ0KdGVzdF9tYXRyaXggPC0gYXMubWF0cml4KHRlc3RbMjo4XSkNCnRlc3RfcmF3X21lYW4gPC1tZWFuKHRlc3RfbWF0cml4LCBuYS5ybSA9IFQpDQp0ZXN0X3Jhd19tZWFuDQpgYGANCg0KIyMgQ2FsY3VsYXRlIHRoZSBSTVNFIGZvciByYXcgYXZlcmFnZSBmb3IgYm90aCB5b3VyIHRyYWluaW5nIGRhdGEgYW5kIHlvdXIgdGVzdCBkYXRhDQoNCkluIHRoZSBjb2RlIGJsb2NrIGJlbG93LCBJIHN1YnRyYWN0ZWQgdGhlIHJhdyBtZWFuIGZyb20gYm90aCB0aGUgdHJhaW4gYW5kIHRlc3QgZGF0YSBmcmFtZXMsIHRoZW4gc3F1YXJlZCB0aGF0IHJlc3VsdCwgY29udmVydGVkIHRoYXQgcmVzdWx0IHRvIGEgbWF0cml4LCB0b29rIHRoZSBtZWFuLCBhbmQgZmluYWxseSB0b29rIHRoZSBzcXVhcmUgcm9vdCBvZiB0aGF0IHJlc3VsdC4gDQoNCkkgZ290IGEgdHJhaW4gcmF3IFJNU0Ugb2YgMC45NTI4MTAyIGFuZCBhIHRlc3QgcmF3IFJNU0Ugb2YgMC45MTYzMDkyDQoNCmBgYHtyfQ0KdHJhaW5fcmF3X1JNU0UgPC0gc3FydChtZWFuKGFzLm1hdHJpeCgodHJhaW5bMjo4XS10cmFpbl9yYXdfbWVhbileMiksbmEucm0gPSBUKSkNCnRyYWluX3Jhd19STVNFDQpgYGANCg0KYGBge3J9DQp0ZXN0X3Jhd19STVNFIDwtIHNxcnQobWVhbihhcy5tYXRyaXgoKHRlc3RbMjo4XS10ZXN0X3Jhd19tZWFuKV4yKSxuYS5ybSA9IFQpKQ0KdGVzdF9yYXdfUk1TRQ0KYGBgDQoNCg0KIyMgVXNpbmcgeW91ciB0cmFpbmluZyBkYXRhLCBjYWxjdWxhdGUgdGhlIGJpYXMgZm9yIGVhY2ggdXNlciBhbmQgZWFjaCBpdGVtLg0KDQpJbiB0aGUgbmV4dCB0d28gY29kZSBibG9ja3MsIEkgc3VidHJhY3RlZCB0aGUgbWVhbiBvZiB0aGUgdHJhaW4gZGF0YXNldCBmcm9tIGZyb20gdGhlIGF2ZXJhZ2UgdXNlciBhbmQgbW92aWUgbWVhbnMuICANCg0KYGBge3J9DQp1c2VyX2JpYXNfZGYgPC0gdXNlcl9hdmdfZGYtdHJhaW5fcmF3X21lYW4gDQp1c2VyX2JpYXNfZGYgPC0gY2JpbmQoInVzZXJJZCIgPSByb3duYW1lcyh1c2VyX2JpYXNfZGYpLCB1c2VyX2JpYXNfZGYpDQpyb3duYW1lcyh1c2VyX2JpYXNfZGYpIDwtIE5VTEwNCg0Ka2FibGUoaGVhZCh1c2VyX2JpYXNfZGYsMTApLCBjYXB0aW9uID0gIlVzZXIgQmlhc2VzIC0gVG9wIDEwIikgJT4lDQogIGthYmxlX3N0eWxpbmcoYm9vdHN0cmFwX29wdGlvbnMgPSAic3RyaXBlZCIsIGZ1bGxfd2lkdGggPSBGKQ0KDQpgYGANCg0KV2Ugc2VlIHRoYXQgdGhlIHVzZXJzIGluIHRoZSB0cmFpbiBzZXQgYXJlIG5lZ2F0aXZlbHkgYmlhc2VkIGFnYWluc3QgQnJhdmVoZWFydCwgVG95X1N0b3J5LCBhbmQgV2hlbiBIYXJyeSBNZXQgU2FsbHksIGFuZCB0aGV5IGhhdmUgYSBwb3NpdGl2ZSBiaWFzIHRvIFNjaGluZGxlcidzIExpc3QgYW5kIEZpZ2h0IENsdWIuIA0KDQoNCmBgYHtyfQ0KbW92aWVfYmlhc19kZiA8LSBtb3ZpZV9hdmdfZGYtdHJhaW5fcmF3X21lYW4gDQptb3ZpZV9iaWFzX2RmIDwtIGNiaW5kKCJtb3ZpZSIgPSByb3duYW1lcyhtb3ZpZV9iaWFzX2RmKSwgbW92aWVfYmlhc19kZikNCnJvd25hbWVzKG1vdmllX2JpYXNfZGYpIDwtIE5VTEwNCg0KDQprYWJsZShoZWFkKG1vdmllX2JpYXNfZGYsMTApLCBjYXB0aW9uID0gIk1vdmllIEJpYXNlcyAiKSAlPiUNCiAga2FibGVfc3R5bGluZyhib290c3RyYXBfb3B0aW9ucyA9ICJzdHJpcGVkIiwgZnVsbF93aWR0aCA9IEYpDQoNCmBgYA0KDQojIyBGcm9tIHRoZSByYXcgYXZlcmFnZSwgYW5kIHRoZSBhcHByb3ByaWF0ZSB1c2VyIGFuZCBpdGVtIGJpYXNlcywgY2FsY3VsYXRlIHRoZSBiYXNlbGluZSBwcmVkaWN0b3JzZm9yIGV2ZXJ5IHVzZXItaXRlbSBjb21iaW5hdGlvbg0KDQpUaGUgZnVuY3Rpb24gYmVsb3csIGNyZWF0ZV9iYXNlbGluZV9wcmVkaWN0b3JzX2RmLCB0YWtlcyBpbiBhbiBpdGVtX2JpYXMgYW5kIHVzZXJfYmlhcyBkYXRhZnJhbWVzIGFzIHdlbGwgYXMgdGhlIHJhdyBtZWFuLiBJdCB0aGVuIGNyZWF0ZXMgYW5kIHBvcHVsYXRlIGEgZGF0YSBmcmFtZSwgYmFzZWxpbmVfcHJlZGljdG9yc19kZiwgd2l0aCB0aGUgcmF3IG1lYW4gcGx1cyB0aGUgdXNlciBhbmQgaXRlbSBiaWFzZXMuIEFkZGl0aW9uYWxseSwgaXQgZm9yY2VzIHNjb3JlcyBhYm92ZSA1IHRvIGJlIGZpdmUgYW5kIHNjb3JlcyBiZWxvdyAxIHRvIGJlIDEuIA0KDQpgYGB7cn0NCmNyZWF0ZV9iYXNlbGluZV9wcmVkaWN0b3JzX2RmIDwtIGZ1bmN0aW9uKGl0ZW1fQmlhcywgdXNlcl9CaWFzLCByYXdfbWVhbil7DQogICAgICAgIGJhc2VsaW5lX3ByZWRpY3RvcnNfZGYgPC0gZGF0YS5mcmFtZSgpDQogICAgICAgIA0KICAgICAgICBmb3IgKGkgaW4gMTpucm93KHVzZXJfQmlhcykpew0KICAgICAgICAgIGFycnkgPC0gYyhyYXdfbWVhbiArIHVzZXJfQmlhc1tpLDJdICsgaXRlbV9CaWFzWzJdKQ0KICAgICAgICAgIGFycnkgPC0gYXJyeVtbMV1dDQogICAgICAgICAgYXJyeVthcnJ5IDwgMV0gPC0gMS4wMA0KICAgICAgICAgIGFycnlbYXJyeSA+IDVdIDwtIDUuMDANCiAgICAgICAgICBiYXNlbGluZV9wcmVkaWN0b3JzX2RmIDwtIHJiaW5kKGJhc2VsaW5lX3ByZWRpY3RvcnNfZGYsYXJyeSkNCiAgICAgICAgICB9DQogICAgICAgIA0KICAgICAgICByZXR1cm4oYmFzZWxpbmVfcHJlZGljdG9yc19kZikNCiAgfQ0KYGBgDQoNCg0KDQpgYGB7cn0NCmJhc2VsaW5lX3ByZWRpY3RvcnNfZGY8LSBjcmVhdGVfYmFzZWxpbmVfcHJlZGljdG9yc19kZihtb3ZpZV9iaWFzX2RmLCB1c2VyX2JpYXNfZGYsIHRyYWluX3Jhd19tZWFuKSANCg0KY29sbmFtZXMoYmFzZWxpbmVfcHJlZGljdG9yc19kZikgPC0gYXMuY2hhcmFjdGVyKG1vdmllX2JpYXNfZGYkbW92aWUpDQpyb3duYW1lcyhiYXNlbGluZV9wcmVkaWN0b3JzX2RmKSA8LSB1c2VyX2JpYXNfZGYkdXNlcklkDQoNCmthYmxlKGhlYWQoYmFzZWxpbmVfcHJlZGljdG9yc19kZiwxMCksIGNhcHRpb24gPSAiQmFzZWxpbmUgUHJlZGljdG9ycyAiKSAlPiUNCiAga2FibGVfc3R5bGluZyhib290c3RyYXBfb3B0aW9ucyA9ICJzdHJpcGVkIiwgZnVsbF93aWR0aCA9IEYpDQpgYGANCiMjIENhbGN1bGF0ZSB0aGUgUk1TRSBmb3IgdGhlIGJhc2VsaW5lIHByZWRpY3RvcnMgZm9yIGJvdGggeW91ciB0cmFpbmluZyBkYXRhIGFuZCB5b3VyIHRlc3QgZGF0YQ0KDQpIZXJlLCBJIGZvdW5kIHNvbWV0aGluZyBvZGQuICBFdmVuIHRob3VnaCB0aGUgUk1TRSBpbXByb3ZlZCBmb3IgdGhlIHRyYWluaW5nIHNldCwgaXQgZ290IG11Y2ggd29yc2UgZm9yIHRoZSB0ZXN0IHNldC4gDQoNCmBgYHtyfQ0KdHJhaW5fYmFzZWxpbmVfUk1TRTwtc3FydChtZWFuKChhcy5tYXRyaXgodHJhaW5bMjo4XSAtIGJhc2VsaW5lX3ByZWRpY3RvcnNfZGYpKV4yLG5hLnJtPVQpKQ0KdHJhaW5fYmFzZWxpbmVfUk1TRQ0KYGBgDQoNCmBgYHtyfQ0KdGVzdF9iYXNlbGluZV9STVNFPC1zcXJ0KG1lYW4oKGFzLm1hdHJpeCh0ZXN0WzI6OF0gLSBiYXNlbGluZV9wcmVkaWN0b3JzX2RmKSleMixuYS5ybT1UKSkNCnRlc3RfYmFzZWxpbmVfUk1TRQ0KYGBgDQoNCiMjIFN1bW1hcnkNCg0KQWNjb3VudGluZyBmb3IgdXNyIGJpYXMgaW1wcm92ZWQgdGhlIFJNU0Ugb3ZlciB1c2luZyBqdXN0IHRoZSByYXcgYXZlcmFnZXMuIEkgc2F3IGEgMjklIGltcHJvdmVtZW50IG9mIHRoZSBSTVNFLg0KDQpgYGB7cn0NCjEgLSAodHJhaW5fYmFzZWxpbmVfUk1TRSAvIHRyYWluX3Jhd19STVNFKQ0KDQpgYGANCg0KSG93ZXZlciwgdGhlIHNhbWUgY2Fubm90IGJlIHNhaWQgZm9yIHRoZSB0ZXN0IHNldC4gIEhlcmUsIEkgc2F3IGEgMjYlIGRlY2xpbmUgaW4gdGhlIFJNU0UgZnJvbSB1c2luZyB0aGUgcmF3IGF2ZXJhZ2UgdG8gYWRkaW5nIGluIHRoZSBpdGVtLXVzZXIgYmlhcy4gDQoNCmBgYHtyfQ0KMS0odGVzdF9iYXNlbGluZV9STVNFIC8gdGVzdF9yYXdfUk1TRSkNCmBgYA0KDQpJIGRvbid0IGhhdmUgYSByZWFkeSBleHBsYW5hdGlvbiBhcyB0byB3aHksIHNvIEkgY2hlY2tlZCB0aGUgbW92aWUgYXZlcmFnZXMgYW5kIGJpYXNlcyBmb3IgdGhlIHRlc3Qgc2V0LiAgSSBmb3VuZCB0aGF0IHRoZSB0ZXN0IHNldCBoYWQgYSBzdHJvbmdlciBuZWdhdGl2ZSBiaWFzIHRvd2FyZHMgIldoZW4gSGFycnkgTWV0IFNhbGx5IiB0aGFuIHRoZSB0cmFpbiBzZXQuICBBbHNvLCB0aGUgdHJhaW4gc2V0IGhhZCBhIHN0cm9uZ2VyIG5lZ2F0aXZlIGJpYXMgYWdhaW5zdCBUb3kgU3RvcnkgdGhhbiB0aGUgdHJhaW4gc2V0LiBUaGVzZSBtYXkgYWNjb3VudCBmb3IgdGhlIHRyYWluIHNldCdzIFJNU0UgZGVjbGluZS4gDQoNCmBgYHtyfQ0KbW92aWVfYXZnMiA8LSAgY29sTWVhbnModGVzdFsyOjhdLCBuYS5ybSA9IFQpDQptb3ZpZV9hdmdfZGYyIDwtIGFzLmRhdGEuZnJhbWUobW92aWVfYXZnMikNCnJvd25hbWVzKG1vdmllX2F2Z19kZikgPC0gYygiQnJhdmVoZWFydCIsICJGaWdodF9DbHViIiwgIkZvcnJlc3RfR3VtcCIsICJTY2hpbmRsZXInc19MaXN0IiwgIlN0YXJfV2Fyc19FcGlzb2RlX0lWX0FfTmV3X0hvcGUiLCAiVG95X1N0b3J5IiwgIldoZW5fSGFycnlfTWV0X1NhbGx5IikNCg0Ka2FibGUoaGVhZChtb3ZpZV9hdmdfZGYyLDEwKSwgY2FwdGlvbiA9ICJNb3ZpZSBBdmVyYWdlcyIpICU+JQ0KICBrYWJsZV9zdHlsaW5nKGJvb3RzdHJhcF9vcHRpb25zID0gInN0cmlwZWQiLCBmdWxsX3dpZHRoID0gRikNCg0KYGBgDQoNCg0KYGBge3J9DQp0ZXN0X21vdmllX2JpYXNfZGYgPC0gbW92aWVfYXZnX2RmMi10ZXN0X3Jhd19tZWFuIA0KdGVzdF9tb3ZpZV9iaWFzX2RmIDwtIGNiaW5kKCJtb3ZpZSIgPSByb3duYW1lcyh0ZXN0X21vdmllX2JpYXNfZGYpLCB0ZXN0X21vdmllX2JpYXNfZGYpDQpyb3duYW1lcyh0ZXN0X21vdmllX2JpYXNfZGYpIDwtIE5VTEwNCg0KDQprYWJsZShoZWFkKHRlc3RfbW92aWVfYmlhc19kZiwxMCksIGNhcHRpb24gPSAiTW92aWUgQmlhc2VzICIpICU+JQ0KICBrYWJsZV9zdHlsaW5nKGJvb3RzdHJhcF9vcHRpb25zID0gInN0cmlwZWQiLCBmdWxsX3dpZHRoID0gRikNCg0KYGBgDQo=