-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathMini-Challenge_2.Rmd
More file actions
823 lines (624 loc) · 34.6 KB
/
Mini-Challenge_2.Rmd
File metadata and controls
823 lines (624 loc) · 34.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
---
title: "Content-based Movie Recommender"
output:
html_document:
toc: yes
df_print: paged
theme: spacelab
html_notebook:
toc: yes
---
```{r Imports, include=FALSE}
INSTALL_LIBS = FALSE
if (INSTALL_LIBS){
source("install_packages.R")
}
#install packages
library("reshape2")
library("recommenderlab")
library("dplyr")
library("tidyr")
library("ggplot2")
library("vegan")
library("coop")
library("bench")
library("gridExtra")
library("renv")
library("testthat")
library("devtools")
library("fmsb")
library("ggridges")
source("./src/helper.R")
#renv::snapshot()
#renv::restore()
```
## 1. Erzeugung von Film- & Nutzerprofilen
### 1.1. MovieLense Daten einlesen
```{r}
data("MovieLense")
MovieLense
```
### 1.2. Binäre User-Liked-Items Matrix für alle Nutzer erzeugen.
```{r}
########### Param ##########
MINIMUM_RATING = 4
##############################
MovieLense_binary <- recommenderlab::binarize(x = MovieLense, minRating = MINIMUM_RATING)
# Test
check_binary_matrix <- test_that(
"Check binary matrix", {
expect_equal(class(as(MovieLense_binary, "matrix")[1]), "logical")
}
)
```
### 1.3. Dimension der User-Liked-Items Matrix prüfen und ausgeben.
```{r}
dim(MovieLense_binary)
MovieLense_binary_matrix = as(MovieLense_binary, "matrix")
```
### 1.4. Movie-Genre Matrix für alle Filme erzeugen.
```{r}
movie_genre <- MovieLenseMeta %>%
dplyr::select(-c(url, year))
head(movie_genre)
```
### 1.5. Dimension der Movie-Genre Matrix prüfen und ausgeben.
```{r}
dim(movie_genre)
```
**Beschreibung**
Diese Dimensionen ergeben Sinn; Wir haben 1664 Filme und 19 Genres im Datensatz (eine Spalte wird für den Filmnamen gebraucht).
### 1.6. Anzahl unterschiedlicher Filmprofile bestimmen und visualisieren.
```{r}
number_of_genres <- movie_genre %>%
dplyr::select(-title) %>%
rowSums()
ggplot(data.frame(data.frame(number_of_genres)), aes(number_of_genres)) +
geom_bar() +
ggplot2::theme_bw() +
ggplot2::xlab("Number of different genres per Movie") +
ggplot2::ylab("Count") +
ggplot2::ggtitle('Count of different Genres per Movie') +
ggplot2::theme(panel.border = element_blank(),
axis.line = element_line(colour = "black")) +
ggplot2::scale_x_continuous(breaks = seq(1, 6, by = 1)) +
geom_text(aes(label = ..count..), stat = "count", vjust = -0.5)
```
**Beschreibung:**
Anhand dieses Plots kann man sehen, dass die Mehrheit der Filme nur ein Genre haben. Es gibt jedoch zahlreiche Filme, die mit mehr als einem Genre beschrieben sind. 3 Filme haben sogar 6 verschiedene Genres.
```{r}
# Test that the number of Genres are consistent between objects.
check_dims_genres <- test_that(
"Check dims of movie_genre fit with new generated number_of_genres", {
expect_equal(dim(movie_genre)[1], nrow(data.frame(data.frame(number_of_genres))))
}
)
```
```{r}
tmp <- tidyr::pivot_longer(data = movie_genre, cols = 2:dim(movie_genre)[[2]], names_to = "genre")
tmp <- tmp[tmp$value > 0, c("title", "genre")] %>%
group_by(title) %>%
mutate(combinations = paste0(genre, collapse="-")) %>%
distinct(title, .keep_all = TRUE) %>%
select(-genre)
combinations <- tmp %>%
dplyr::group_by(combinations) %>%
dplyr::count() %>%
dplyr::arrange(n)
total_unique_combinations <- length(unique(combinations$combinations))
total_unique_combinations
all_combinations <- combinations
combinations$combinations[combinations$n <= 11] <- "Others combined"
combinations <-combinations %>% group_by(combinations) %>% summarise(n = sum(n))
combinations <- combinations[order(combinations$n),]
combinations <- rbind(combinations[(nrow(combinations)),], combinations[-(nrow(combinations)),])
# use helper file to transform
tmp <- turn_into_wide(df = tmp)
combinations$combinations <- factor(combinations$combinations, levels = combinations$combinations)
ggplot2::ggplot(combinations, ggplot2::aes(x = n, y = combinations, label = n)) +
ggplot2::geom_segment(ggplot2::aes(x = 0, y = combinations, xend = n, yend = combinations), color = "grey50") +
ggplot2::geom_point(size=3, color = "grey") +
ggplot2::geom_text(nudge_x = 25, size = 3) +
ggplot2::theme_bw() +
ggplot2::xlab("Counts") +
ggplot2::ylab("") +
ggplot2::ggtitle(paste("Distribution of Genre Combinations (Total: ", total_unique_combinations, ")")) +
ggplot2::theme(panel.border = element_blank(),
axis.line = element_line(colour = "black"))
```
**Beschreibung:**
Der Plot zeigt die Anzahl der Genres in unserem Datensatz. Ingesamten kommen Filme mit "Drama" und "Comedy" als Genre am häufigsten vor. Falls eine Kombination weniger als 12 mal vorkommt, wird diese zusammengefasst als "Others combined", da sonst der obere Plot aus 216 Kombinationen bestehen wurde.
```{r}
# Test
check_dims_genres <- test_that(
"Check dims of genre matrices fit with new generated wide df", {
expect_equal(total_unique_combinations, dim(tmp)[2]-1)
}
)
```
### 1.7. User-Genre-Profil Matrix mit Nutzerprofilen im Genre-Vektorraum erzeugen.
```{r}
########## Param ##########
NORMALIZED_BY_VIEWS <- FALSE
###########################
# Convert both into matrices
user_movie_mat <- as(MovieLense_binary, "matrix")
movie_genre_mat <- movie_genre %>%
dplyr::select(-title) %>%
as.matrix()
# Create user genre profile
if (NORMALIZED_BY_VIEWS){
user_genre_mat <- ((user_movie_mat / as.vector(rowSums(user_movie_mat))) %*% movie_genre_mat)
} else {
user_genre_mat <- user_movie_mat %*% movie_genre_mat
}
# Test
check_both_matrices <- test_that(
"Check matrices types", {
expect_equal(class(user_movie_mat), class(movie_genre_mat))
}
)
check_mat_dim <- test_that(
"Check mat dims", {
expect_equal(dim(user_movie_mat)[2], dim(movie_genre)[1])
}
)
check_mat_dim_out <- test_that(
"Check mat dims output", {
expect_equal(dim(user_genre_mat), c(dim(user_movie_mat)[1], dim(movie_genre_mat)[2]))
}
)
```
Achtung! Die Werte können, trotz Normalisierung der Views, grösser als 1 sein, da gewisse Filme über mehrere Genres verfügen.
### 1.8. Dimension der User-Genre-Profil Matrix prüfen und ausgeben.
```{r}
dim(user_genre_mat)
```
**Beschreibung**
Die Matrix zeigt über die Reihen hinweg alle User und über die Spalten hinweg alle erzeugten Scores bezüglich ihrer Relation zu einem bestimmten Genre. Wir haben 943 User und 19 Genres.
### 1.9. Anzahl unterschiedlicher Nutzerprofile bestimmen, wenn Stärke der GenreKombination (a) vollständig bzw. (b) nur binär berücksichtigt wird.
```{r}
BINARY_THRESHOLD <- 0
# Create Binaray User-Genre Matrix
user_genre_mat_binary <- ifelse(user_genre_mat > BINARY_THRESHOLD, 1, 0)
random_idx <- sample(seq(1, dim(user_genre_mat_binary)[[1]]),
size = 4)
plot_user_profiles(user_genre_mat_binary, user_idx = random_idx,
sample_size = 5, n_cols = 5)
plot_user_profiles(user_genre_mat, user_idx = random_idx,
sample_size = 5, n_cols = 5)
```
**Beschreibung:**
Die beiden Plots unterscheiden sich stark in ihren Wertebereichen. Der Wertebereich des binärisierten Plots reicht bei jedem Nutzer von 0 bis nach 1. Die Grafik des binären Plots verändert sich, falls der BINARY_THRESHOLD angepasst wird. Der Plot mit den normalen Ratings sieht wie erwartet aus. Die Spiderdiagramme zeigen die stärkste Ausprägung meistens bei dem Genre Drama, da es das meistgesehene Genre im Datensatz ist und auch am meisten Filme unter diesem Genre vorkommen.
## 2. Ähnlichkeit von Nutzern und Filmen
### 2.1. Cosinus-Ähnlichkeit zwischen User-Genre- und Movie-Genre-Matrix berechnen.
```{r}
# Test
similarity_matrix <- calc_cosine_similarity(user_genre_mat, movie_genre_mat)
# Check similarity results with package coop
check_similarity_results <- test_that(
"Check mat dims with users", {
expect_equal(similarity_matrix[1,1], coop::cosine(user_genre_mat[1, ] ,movie_genre_mat[1, ]) )
}
)
```
`user_gerne_mat` beinhaltet, welche Genre ein User gern hat und `movie_gerne_mat`, welche Genre ein Film hat.
### 2.2. Dimension der Matrix der Cosinus-Ähnlichkeiten von Nutzern und Filmen prüfen und ausgeben.
```{r}
dim(similarity_matrix)
# Run Tests on Dimensionality fo Similarity Matrix
check_mat_dim <- test_that(
"Check mat dims with users", {
expect_equal(dim(similarity_matrix)[1], dim(user_genre_mat)[1])
expect_equal(dim(similarity_matrix)[2], dim(movie_genre_mat)[1])
}
)
```
### 2.3. 5-Zahlen Statistik für Matrix der Cosinus-Ähnlichkeiten prüfen uns ausgeben.
```{r}
summary(as.numeric(similarity_matrix))
```
**Beschreibung**
Interessant sind die 1664 NA's. Diese kommen vom User `685`. Wir sehen, dass der User `685` keine Filme als gut bewertet hat und somit entstehen bei der similarity_matrix für den User `685` nur NA's, da er keine "Lieblingsgenres" hat. Genau entstehen die NA's dadurch, dass die Cosine-Similarity mit einem Nullvektor nicht definiert ist, da im Zähler und im Nenner eine 0 steht und 0 durch 0 nicht definiert ist.
```{r}
user_genre_mat[685,]
```
### 2.4. Cosinus-Ähnlichkeiten von Nutzern und Filmen mit Dichteplot visualisieren.
```{r}
tmp <- as.vector(similarity_matrix)
tmp <- as.data.frame(tmp)
ggplot2::ggplot(data = tmp, aes(x = tmp)) +
ggplot2::geom_density(fill = "grey", color = "black") +
ggplot2::labs(title = "Distribution of Similarity Matrix") +
ggplot2::xlab("Cosine Similarities") +
theme_classic()
```
**Beschreibung:**
Der Plot zeigt die Verteilung der berechneten Ähnlichkeitsmasse pro User und Film innerhalb des Datensatzes. Man erkennt eine starke Ausprägung der Nullwerte (gar keine Similarity zwischen Nutzer und Filmen). Dies lässt sich mit der These vereinbaren, dass es Nutzer gibt, die fast keine Filme geschaut haben und vice-versa Filme, die fast nie geschaut wurden. Ohne die starke Ausprägung bei der tiefen Cosine Similaritie wäre das Histogram ggf. normalverteilt.
### 2.5. Cosinus-Ähnlichkeiten von Nutzern und Filmen mit Dichteplot für Nutzer “241”, “414”, “477”, “526”, “640” und “710” visualisieren.
```{r}
user_selection <- similarity_matrix[c(241, 414, 477, 526, 640, 710), ]
user_selection <- as.data.frame(t(user_selection)) %>%
reshape2::melt(value.name = "similarity")
colnames(user_selection) <- c("user", "similarity")
ggplot2::ggplot(data = user_selection, ggplot2::aes(x = similarity)) +
ggplot2::geom_boxplot(fill="grey", color = "black", notch = TRUE) +
facet_grid(rows = vars(user)) +
xlim(0, 1) +
labs(title = "Distribution of Similarities between User and Movies") +
theme(axis.title.y=element_blank(),
axis.text.y=element_blank(),
axis.ticks.y=element_blank())
ggplot2::ggplot(data = user_selection, ggplot2::aes(x = similarity)) +
ggplot2::geom_density(fill="grey", color = "black") +
facet_wrap(~user, nrow = 3) +
xlim(0, 1) +
labs(title = "Distribution of Similarities between User and Movies") +
ggplot2::ylab("density") +
theme(axis.text.y=element_blank(),
axis.ticks.y=element_blank())
```
**Beschreibung:**
Die Plots zeigt die Verteilung der Similarites zwischen einem User und allen Filmen. Die Daten wurden anhand eines Boxplots und Dichteplot visualisiert, um Unterschiede zwischen den Usern besser zu erkennen. Auf der ersten Visualisierung (Boxplot) kann man anhand des Notches (95% Konfidenzintervall vom Median) erkennen, dass sich die Nutzer statistisch signifikant voneinander unterscheiden hinsichtlich ihrer Ähnlichkeit zwischen den Filmen. Eine weitere Auffälligkeit sind die einzelnen Ausreisser der Ähnlickeitsmasse von Nutzer `640`, die aber auch auf die kleine Streuung der Similarities für diesen Nutzer zurückzuführen sind. Auf der zweiten Visualisierung (Dichteplot) sehen wir, dass wir jeweils bei jedem Nutzer mehrere Ausprägungen haben. Diese sind jeh nach Nutzer unterschiedlich stark ausgeprägt und verteilt.
## 3. Empfehlbare Filme
### 3.1. Bewertete Filme maskieren, d.h. “Negativabzug” der User-Items Matrix erzeugen, um anschliessend Empfehlungen herzuleiten.
```{r}
MovieLenseMat <- as(MovieLense, "matrix")
MovieLenseMat_mask <- ifelse(is.na(MovieLenseMat), 1, 0)
image(MovieLenseMat, col = gray.colors(2), main = "Movie Lense Matrix")
image(MovieLenseMat_mask, col = gray.colors(2), main = "Binary Movie Lense Matrix")
```
**Beschreibung:**
Oben können wir die beiden Matrizen als Bild betrachten: Das Erste ist der normale Movielense-Datensatz und das Zweite ist der "Negativabzug". Im "Negativabzug" sind alle NA-Werte (Film nicht bewertet/geschaut) eine 1 und alle anderen eine 0. Da der durchschnittliche Nutzer nur wenige Filme bewertet, hat das erste Bild nur wenige Datenpunkte.
### 3.2. Zeilensumme des “Negativabzuges” der User-Items Matrix für die User “5”, “25”, “50” und “150” ausgeben.
```{r}
SELECTED_USERS = c(5, 25, 50, 150)
###
Mask_sum = MovieLenseMat_mask
Mask_sum = data.frame(Sum = rowSums(Mask_sum))
Mask_sum$User <- factor(row.names(Mask_sum))
### Select users
sum_mask_users = Mask_sum[Mask_sum$User %in% SELECTED_USERS,]
sum_mask_users$User <- factor(sum_mask_users$User, levels = SELECTED_USERS)
#orginal-dimension of the matrix
dim(MovieLense)
ggplot(data=sum_mask_users, aes(x=User, y=Sum)) +
ggplot2::geom_bar(stat="identity", alpha = .9) +
ggplot2::geom_text(aes(label=Sum), vjust=1.6, color="white", size=3.5) +
ggplot2::ggtitle('Sum of unrated Movies of selected Users') +
ggplot2::theme_bw() +
ggplot2::theme(panel.border = element_blank(),
panel.grid.major = element_blank(),
axis.line = element_line(colour = "black")) +
ggplot2::ylim(0, max(sum_mask_users$Sum))
```
**Beschreibung:**
Es scheint so, als gäbe es keine grossen Unterschiede zwischen den Nutzern. Das liegt daran, dass wir uns die Filme ansehen, die von den Nutzern nicht bewertet wurden. Da es so viele verschiedene Filme gibt (1664), ist es normal, dass jeder eine so hohe Anzahl an nicht bewerteten Filmen hat. Zum Beispiel können wir sagen, dass der Nutzer `5` die meisten Filme bewertet hat und deshalb die niedrigste Anzahl an nicht bewerteten Filmen aufweist.
### 3.3. 5-Zahlen Statistik der Zeilensumme des “Negativabzuges” der User-Items Matrix bestimmen.
```{r}
summary(as.numeric(Mask_sum$Sum))
```
Die Statistik zeigt die wichtigsten Komponenten der Daten Minimum, Q1, Median, Mittelwert, Q3 und Maximum der Daten.
Ausserdem sieht man hier auch, dass die Verteilung eher linksschief sein muss, da das Minimum weiter weg vom Q1 und Median ist, als das Maximum vom Q3 und Median.
```{r}
# Horizontal
ggplot(Mask_sum, aes(y=Sum)) +
ggplot2::geom_boxplot(fill = "grey", alpha = .5) +
ggplot2::ggtitle('Boxplot of Sum of unrated Movies per User') +
ggplot2::theme_bw() +
coord_flip() + theme_bw() +
theme(axis.text.y=element_blank(),
axis.ticks.y=element_blank(),
panel.border = element_blank(),
axis.line = element_line())
```
**Beschreibung:**
Der Plot unterstützt die Annahmen der statistischen Kennzahlen nochmals. Mit Hilfe der Grafik können wir feststellen, dass wir die meisten Daten um 1600 haben. wir können auch sagen, dass dies eine Zahl ist, die sehr nahe an der maximalen Anzahl von Filmen (1664) liegt. wir haben viele Ausreißer in den unteren Werten. Der Minimalwert liegt bei 929. Es gibt also einen Nutzer, der 929 Filme nicht bewertet hat. Der höchste Wert liegt bei 1645. Das ist sehr verwunderlich, weil wir dachten, dass er bei 1664 liegen würde, weil es immer Leute geben sollte, die nie einen Film bewerten (z.B. neu hinzugefügte User). Aber in diesem Datensatz ist dies nicht der Fall.
## 4. Top-N Empfehlungen
### 4.1. Matrix für Bewertung aller Filme durch element-weise Multiplikation der Matrix der Cosinus-Ähnlichkeiten von Nutzern und Filmen und “Negativabzug” der User-Items Matrix erzeugen.
```{r}
similarity_users <- MovieLenseMat_mask * similarity_matrix
df <- melt(t(similarity_users[1:5,1:5]))
colnames(df) <- c("x", "y", "value")
df <- melt(t(similarity_users[1,]))
colnames(df) <- c("x", "y", "value")
ggplot(df, aes(x = y, y = x, fill = value)) +
geom_tile() +
theme(axis.text.x=element_blank(),
axis.text.y=element_blank(),
axis.ticks.x=element_blank(),
axis.ticks.y=element_blank()
) +
ggplot2::ggtitle('Genre-similarties for unseen movies for a selected user') +
ggplot2::xlab('Movie ID') + ggplot2::ylab("User 1")
similarity_users_melted <- melt(t(similarity_users))
users_num = c(1,2,3,4,5,6,7,8,9,10)
df1 <- similarity_users_melted[similarity_users_melted$Var2 == 1,]
df1 <- similarity_users_melted[similarity_users_melted$Var2 %in% users_num, ]
colnames(df1) <- c("x", "y", "value")
ggplot(df1, aes(x = x, y = y, fill = value)) +
geom_tile() +
theme(axis.text.x=element_blank(),
axis.text.y=element_blank(),
axis.ticks.x=element_blank(),
axis.ticks.y=element_blank()
) +
ggplot2::ggtitle('Genre-similarties for unseen movies for the first 10 users') +
ggplot2::xlab('Movie ID') +
ggplot2::ylab('User ID') +
facet_grid(rows = df1$y, scales = "free")
```
**Beschreibung:**
Wir betrachten hier oben verschiedene Visualisierungen über die Genre-Ähnlichkeiten für ungesehene Filme für verschiedene Nutzer.
In der ersten Grafik sieht man, dass es für den User 1 entweder gar keine Ähnlichkeiten zwischen den Filmen und seinem Profil besteht oder er diese alle gesehen hat und durch den Negativabzug zu 0 wurden. Die weiteren Linien, die aus der Ferne wie ein Barcode aussehen, sind Filme mit einer anderen Recommendation für den User 1. Es könnte also sein, dass der User 1 die Filme nach ID anschaut.
Die letzte Abbildung ist eine Übersicht über die ersten 10 Nutzer und alle Genre-Ähnlichkeiten für die ungesehenen Filme. Man kann hier sehen, dass alle User komplett unterschiedliche Empfehlungen erhalten. Im zweiten Plot sieht man User mit einem ähnlichen Verhalten analog zum User 1.
### 4.2. Dimension der Matrix für die Bewertung aller Filme prüfen.
```{r}
dim(similarity_users)
```
**Beschreibung**
Diese Dimensionen der Similarity ergeben Sinn: 943 User, 1664 Filme.
### 4.3. Top-20 Listen pro Nutzer extrahieren.
```{r}
top_top20_recos <- get_topn_recos(similarity_users_melted, 20)
head(top_top20_recos)
```
`Var1` entspricht dem `Movie-title`, `Var2` entspricht der `User-ID` und `value` der Similarity.
### 4.4. Länge der Top-20 Listen pro Nutzer prüfen.
```{r}
#test with a user
top_top20_recos_user_42 <- top_top20_recos[top_top20_recos$Var2 %in% 42, ]
#5-Zahlen Statistik
fivenum(top_top20_recos_user_42$value)
```
**Beschreibung:**
Man kann anhand der Kennzahlen sehen, dass die Similarites sehr eng streuen. Die Spannweite der Similarities reicht ist im Wertebereich von 0.68 - 0.77. Es war jedoch auch zu erwarten, dass der Wertebereich der Ähnlichkeiten von den Top-20 Empfehlungen hoch ausfallen, da sonst der Recommender nicht funktioniert (oder es keine Filme mehr gibt für diesen spezifischen User).
```{r}
top20_mean_group_size <- top_top20_recos %>%
group_by(Var2) %>%
summarise(group_size = n())
expect_equal(all(top20_mean_group_size$group_size == 20), TRUE)
```
**Beschreibung:**
Wie erwartet, besteht die Top20 Liste für jeden User aus 20 Filmen.
### 4.5. Verteilung der minimalen Ähnlichkeit für Top-N Listen für N = 10, 20, 50 und 100 für alle Nutzer visuell vergleichen.
```{r}
top_n_intervall = c(10, 20, 50, 100)
top_n_col_names = c('1. Top 10', '2. Top 20', '3. Top 50', '4. Top 100')
#create data frame with 0 rows and 5 columns
min_similarity_top_n <- data.frame(matrix(ncol = 1,
nrow = length(row.names(similarity_users))))
#provide column names
colnames(min_similarity_top_n) <- c('User')
min_similarity_top_n$User <- row.names(similarity_users)
for (i in 1:4) {
top_n_recos = get_topn_recos(similarity_users_melted, top_n_intervall[i])
min_top_n_recos = top_n_recos %>%
select(Var2, value) %>%
group_by(Var2) %>%
mutate(
MinSimilarity = min(value, na.rm = T),
User = min(Var2, na.rm = T)
) %>% select(User, MinSimilarity) %>%
distinct(.keep_all = TRUE)
min_similarity_top_n[,top_n_col_names[i]] <- min_top_n_recos$MinSimilarity
}
min_similarity_top_n_long <- pivot_longer(min_similarity_top_n,
cols=all_of(top_n_col_names),
names_to='TopN',
values_to='MinSimilarity')
head(min_similarity_top_n_long)
```
```{r}
ggplot2::ggplot(min_similarity_top_n_long, ggplot2::aes(x=MinSimilarity)) +
ggplot2::geom_histogram(bins=50)+
ggplot2::labs(title = "Min Similarity Distribution per Top N List")+
ggplot2::facet_grid(TopN ~ .) +
ggplot2::theme_bw()
```
**Beschreibung:**
Im oberen Plot sehen wir, dass der Modus der Mindest-Similarity abnimmt bei einem grösseren N bei der TopN Liste. Bei der Top100 sehen wir, dass die Mindest-Similarity bis under 0.4 fällt. Das führt wahrscheinlich zu ungeeigneten Empfehlungen. Somit müsste man ggf. das N pro User bestimmen und nicht das selbe N für alle User benutzen.
### 4.6. Top-20 Empfehlungen für Nutzer “5”, “25”, “50” und “150” visuell evaluieren.
```{r}
top_top20_recos_users <- top_top20_recos[top_top20_recos$Var2 %in% c(5, 25, 50, 150), ]
top_top20_recos_users$user = top_top20_recos_users$Var2
top_top20_recos_users$Similarity = top_top20_recos_users$value
ggplot2::ggplot(top_top20_recos_users, ggplot2::aes(x=Similarity)) +
ggplot2::geom_histogram(bins=20)+
ggplot2::labs(title = "Similarity Distribution for selected Users") +
ggplot2::facet_grid(user ~ .) +
ggplot2::theme_bw()
```
**Beschreibung:**
Es ist interessant zu sehen, wie sehr die Top20-Similarity für verschiedene User variiert. Das zeigt uns, dass unser Recommender für verschiedene User verschiedene Empfehlungen macht und zumindest in diesem Aspekt funktioniert.
```{r}
user_profiles <- analyze_topn_recos(top_top20_recos[top_top20_recos$Var2 %in% c(5, 25, 50, 150), ], 20)
create_cleveland_plot(user_profiles)
```
**Beschreibung:**
Rot sind unsere Top20-Recommendations und grün ist die Ground Truth. Wir sehen, dass für die User 5, 25, 50 und 150, Filme empfohlen bekommt mit Genres, welche der User über mehrere Filme als gut bewertet hat.
Aus der Distanz können wir auch den `mean squared error` bilden. Der MSE ist somit der Durchschnitt der `squared error` zwischen der Verteilung der geschauten Genres in Prozenten (ground truth) und der Verteilung der Genres der empfohlenen Filme in Prozenten. Es entspricht also der `squared distance` zwischen den grünen und roten Punkten im oberen Plot.
```{r}
squared_errors <- create_recomm_truth_MSE(user_profiles)
```
```{r}
p <-ggplot(data=squared_errors, aes(x=User, y=MSE)) +
ggplot2::geom_bar(stat="identity") +
ggplot2::labs(title = "MSE between Genres of watched vs. recommended movies.") +
ggplot2::theme_bw() +
ggplot2::theme(panel.border = element_blank(),
panel.grid.major = element_blank(),
axis.line = element_line(colour = "black"))
p
mean(squared_errors$MSE)
```
**Beschreibung:**
Wir sehen also, dass wir uns überall in einem kleinen Bereich aufhalten. Pro User und Genre hat es im Schnitt ca. einen MSE von 0.0045. Interessant ist der hohe Wert beim User 50.
```{r}
user_profiles <- analyze_topn_recos(top_top20_recos[top_top20_recos$Var2 %in% c(50), ], 20)
create_cleveland_plot(user_profiles)
```
**Beschreibung:**
Unser Recommender empfiehlt sehr oft Romances. Eine Vermutung ist, dass das Genre "Drama-Romance" sehr oft vorkommt. Da der User `50` Drama und Romances gut bewertet hat, schaut der Recommender, dass diese auch empfohlen werden. Wenn wir uns die Top20 Liste vom user `50` anschauen, sehen wir da auch nur "Drama-Romances". Ebenfalls hat User `50` nur 20 Filme als gut bewertet, 7 davon in Drama und 3 davon in Romance. Somit bekommt er zuerst Filme empfohlen, welche diese beiden Genres auch beinhalten. Hier ist nun die Frage, ob es optimal ist, dass der User `50` nur Drama-Romances empfohlen bekommt.
```{r}
# Anzahl als gut Bewertete Filme pro Genre für den USer 50.
user_genre_mat[50,]
```
### 4.7. Für Nutzer “133” und “555” Profil mit Top-N Empfehlungen für N = 20, 30, 40, 50 analysieren, visualisieren und diskutieren.
```{r}
top_n_intervall = c(20, 30, 40, 50)
top_n_col_names = c('Top 20', 'Top 30', 'Top 40', 'Top 50')
users = c(133, 555)
#create data frame with 0 rows and 5 columns
top_n_list_selected_users <- data.frame(matrix(ncol = 3, nrow = 0))
#provide column names
colnames(min_similarity_top_n) <- c('User', 'TopN', 'Similarity')
selected_users = similarity_users_melted[similarity_users_melted$Var2 %in% users, ]
for (i in 1:4) {
top_n_recos = get_topn_recos(selected_users, top_n_intervall[i])
top_n_recos$User = top_n_recos$Var2
top_n_recos$Similarity = top_n_recos$value
top_n_recos$TopN = top_n_col_names[i]
top_n_recos <- top_n_recos %>%
select(User, TopN, Similarity) %>%
arrange(desc(Similarity))
top_n_list_selected_users <- rbind(top_n_list_selected_users,top_n_recos)
}
head(top_n_list_selected_users)
```
```{r}
ggplot2::ggplot(data = top_n_list_selected_users,
ggplot2::aes(x = factor(User), y = Similarity, fill=factor(TopN))) +
ggplot2::geom_boxplot() +
ggplot2::scale_fill_brewer(palette = "Blues") +
labs(title = "Similarity Distribution for selected Users and selected TopN List",
fill = "Top-n") +
ggplot2::theme_bw() +
ggplot2::theme(panel.border = element_blank(),
axis.line = element_line()) +
ggplot2::xlab("User")
```
**Beschreibung:**
Wir sehen, dass das Minimum der Similarities mit mehr Empfehlungen abnimmt. Dies ergibt auch Sinn, da die TopN-Empfehlungen die Filme empfiehlt, welche die höchste Genre-Similarity haben zu Filmen, welche man als gut bewertet hat. Somit werden bei einem höheren N auch Filme empfohlen mit einer niedrigeren Similarity.
Hier muss man also aufpassen, dass man ein geschicktes N wählt: Ein User soll viele Filme empfohlen bekommen, welche aber auch für den User passen.
Eine Möglichkeit wäre, dass man das N von TopN pro User anhand einer mindest-Similarity berechnet. N soll man also so wählen, dass die tiefeste Similarity für den User nicht einen Threshold unterschreitet. Somit soll jeder User mindestens N Filme empfohlen bekommen, aber danach nur Filme, welche über dieser mindest-Similarity sind. Man könnte hierbei auch verschiedene Sets zur Offline-Evaluation verwenden und anhand von Top-N Metriken entscheiden, welche Threshold für User sich am besten eignen.
```{r}
user133 <- as.data.frame(similarity_users[133, ])
colnames(user133) <- c("Similarity")
ggplot2::ggplot(user133, ggplot2::aes(x = Similarity)) +
ggplot2::geom_histogram() +
ggplot2::theme_bw() +
ggplot2::theme(panel.border = element_blank(),
axis.line = element_line()) +
ggplot2::labs(title = "Similarities of User 133")
```
**Beschreibung:**
Der Plot zeigt klar, dass die meisten Similarities vom User `133` tatsächlich den Wert `0.8333333` haben, was im oberen Boxplot der Top-N Empfehlungen pro User eine Streuung von 0 für alle Top-N Resultate impliziert.
Wir können uns da auch noch das Minimum der Similarity von der Top50 Liste vom User `133` anschauen, um dies zu bestätigen.
```{r}
top_50_user_133 = head(arrange(user133, desc(Similarity)), 50)
min(top_50_user_133)
```
**Beschreibung:**
Unsere Annahme stimmt also; User `133` hat viele Similarites mit dem Wert `0.8333333` Da unsere Recommendations über Genres laufen, können wir uns anschauen, ob es da eine grosse Überschneidung gibt zwischen den Top50 Filmen und ihren Genres.
```{r}
top_50_user_133$title <- rownames(top_50_user_133)
top_50_user_133_genre <- left_join(top_50_user_133, movie_genre, by='title')
# Keep only columns which have at least one 1 in any row.
top_50_user_133_genre = top_50_user_133_genre[, colSums(top_50_user_133_genre == 1) > 0]
head(top_50_user_133_genre,10)
```
**Beschreibung:**
Ja, es stimmt! Die Top50 Resultate vom User 133 besteht nur aus dem Filmen mit dem Genre `Comedy` und `Drama` und zwar immer beide zusammen. Die `0.8333333` entsteht somit aus der Summe der Bewertungen pro Genre für den User `133` und der Genreverteilung dieser Top50-Filme.
Wir können die Cosine Similarity vom Film `Doom Generation, The (1995)` (Movie-ID 34) mit den Genres `Comedy` und `Drama` und dem User `133` und überprüfen, dass es `0.8333333` gibt.
```{r}
coop::cosine(user_genre_mat[133, ], movie_genre_mat[34, ])
```
```{r}
user_profiles <- analyze_topn_recos(selected_users, 5)
create_cleveland_plot(user_profiles)
```
**Beschreibung:**
Rot sind unsere Recommendations und grün ist die Ground Truth. Wir sehen, dass der User 555 Triller, Drama, Comedy, Adventure und Action gern schaut und diese auch häufiger empfohlen bekommt. Dasselbe sehen wir mit User 133, welcher Drama liebt und dieses auch das häufigste Genre der empfohlenen Filme ist. Unser Recommender empfiehlt also Filme, welche zu den beiden Usern passen!
Beim zweiten Nutzer sieht man klar, dass dieser Nutzer mehrheitlich auf zwei Genres fokussiert ist und meistens diese Genres geschaut hat. Dies macht auch die hohen Similarities im oberen Histogram für den Nutzer klar, weil er die meistgesehen Genres geschaut hat, so hat er auch mit den meisten Filmen eine hohe Similarity, aufgrund des ähnlichen Profils. Hier muss auch beachtet werden, dass Filme über meherere kombinierte Genres verfügen und deshalb auch Filme empfohlen bekommt, die bspw. Drama-Action sind, die in dieser Grafik auch als solche gewertet werden.
Wir können für die Top20, 30, 40 und 50 (und noch mehr, damit wir das Verhalten des MSE besser verstehen) je den MSE über den User `133` und `555` berechnen.
```{r}
top_n_intervall = c(20, 30, 40, 50)
top_n_col_names = c('MSE Top 20', 'MSE Top 30', 'MSE Top 40', 'MSE Top 50')
users = c(133, 555)
#create data frame with 0 rows and 5 columns
top_n_list_selected_users <- data.frame(matrix(ncol = 2, nrow = 0))
for (i in 1:length(top_n_col_names)) {
top_n_recos <- analyze_topn_recos(similarity_users_melted[similarity_users_melted$Var2 %in% users, ], top_n_intervall[i])
top_n_recos <- create_recomm_truth_MSE(top_n_recos)
top_n_recos$TopN <- top_n_col_names[i]
top_n_recos <- top_n_recos %>%
group_by(TopN) %>%
summarise(MSE = mean(MSE))
top_n_list_selected_users <- rbind(top_n_list_selected_users, top_n_recos)
}
p<-ggplot(data=top_n_list_selected_users, aes(x=TopN, y=MSE)) +
ggplot2::geom_bar(stat="identity") +
ggplot2::labs(title = "TopN MSE Distribution of Genres of Watched vs. Recommended movies.") +
ggplot2::theme_bw() +
ggplot2::theme(panel.border = element_blank(),
axis.line = element_line())
p
```
**Beschreibung:**
Wir sehen, dass der MSE ca. leicht abnimmt, wenn das N von TopN grösser wird. Dies lässt sich dadurch erklären, dass es nicht immer eine perfekte Genre-Verteilung gibt für eine TopN-Liste. Desto mehr Möglichkeiten der Recommender hat, Empfehlungen zu machen, desto mehr nähert sich die Verteilung der Genres vom Recommender an die perfekte Genre-Verteilung des Users an. Falls aber das N zu gross wird, nimmt der Fehler zu, da der Recommender nur noch Filme empfehlen kann, welche gar keine Überschneidung haben mit den "Lieblingsgenres" der Benutzer. Wir können uns das hier anschauen:
```{r}
top_n_intervall = c(10, 100, 200)
top_n_col_names = c('MSE Top 10', 'MSE Top 100', 'MSE Top 200')
users = c(133, 555)
#create data frame with 0 rows and 5 columns
top_n_list_selected_users <- data.frame(matrix(ncol = 2, nrow = 0))
for (i in 1:length(top_n_col_names)) {
top_n_recos <- analyze_topn_recos(similarity_users_melted[similarity_users_melted$Var2 %in% users, ], top_n_intervall[i])
top_n_recos <- create_recomm_truth_MSE(top_n_recos)
top_n_recos$TopN <- top_n_col_names[i]
top_n_recos <- top_n_recos %>%
group_by(TopN) %>%
summarise(MSE = mean(MSE))
top_n_list_selected_users <- rbind(top_n_list_selected_users, top_n_recos)
}
p<-ggplot(data=top_n_list_selected_users, aes(x=TopN, y=MSE)) +
ggplot2::geom_bar(stat="identity") +
ggplot2::labs(title = "TopN MSE Distribution of Genres of Watched vs. Recommended movies.") +
ggplot2::theme_bw() +
ggplot2::theme(panel.border = element_blank(),
axis.line = element_line())
p
```
**Beschreibung:**
Interessanterweise hat es also ein Minimum für den MSE. Daraus liesse sich theoretischerweise ein optimales N pro User erstellen.
```{r}
user_profiles <- analyze_topn_recos(selected_users, 50)
user_profiles <- user_profiles[1:2, ] %>%
select(-c(ground_truth, user))
user_watched_total <- as.data.frame(rowSums(user_profiles))
user_watched_total$user <- rownames(user_watched_total)
row.names(user_watched_total) <- NULL
colnames(user_watched_total) <- c("liked", "user")
ggplot2::ggplot(user_watched_total, aes(x = user, y = liked)) +
geom_col() +
ggplot2::theme_bw() +
ggplot2::theme(panel.border = element_blank(),
axis.line = element_line()) +
ggplot2::labs(title = "Counts of Movies Watched") +
ggplot2::ylab("Movies liked")
```
**Beschreibung:**
Wenn wir einen Vergleich der Anzahl der Filme machen, die die User geliked haben, können wir auch feststellen, dass viel weniger Informationen über die Präferenz des Nutzers 133 vorhanden ist. Dies hat zur Folge, dass weniger Informationen in unser Recommender System als Input geliefert werden, deshalb vermuten wir, dass darum eher grössere Abweichungen zwischen dem empfohlenen Profil zum tatsächlich gesehenen Profil erkennbar sind. Ebenfalls wird duch diese wenigen guten Bewertungen das genre-Profil" von User 133 sehr einseitig.
```{r}
# Create Binaray User-Genre Matrix
plot_user_profiles(user_genre_mat, user_idx = c(133, 555),
sample_size = 4)
```
**Beschreibung:**
Das "genre-Profil" vom User `133` ist weniger ausgeprägt als vom User 555, da es nur "Spitzen" im Radarplot bei Drama und Comedy hat.
----------------------------------------------------------------------------------------------
#### © Copyright 2022 Vincenzo Timmel, Simon Staehli, Firat Saritas
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.