0.1 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.

0.2 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 ...

0.3 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
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

0.5 Using your training data, calculate the raw average (mean) rating for every user-item combination

0.5.1 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

0.5.3 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

0.6 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

0.7 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

0.8 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

0.9 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

0.10 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 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=