File size: 84,695 Bytes
3284fa6
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
{
  "cells": [
    {
      "cell_type": "code",
      "execution_count": 1,
      "metadata": {
        "id": "uq9k8YYUKjnp"
      },
      "outputs": [],
      "source": [
        "import os\n",
        "import urllib.request\n",
        "import zipfile\n",
        "import json\n",
        "import pandas as pd\n",
        "import time\n",
        "import torch\n",
        "import numpy as np\n",
        "import pandas as pd\n",
        "import torch.nn as nn\n",
        "import torch.nn.functional as F\n",
        "import torch.optim as optim\n",
        "from torch.utils.data import DataLoader, TensorDataset\n",
        "from sklearn.model_selection import train_test_split\n",
        "import matplotlib.pyplot as plt"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 2,
      "metadata": {
        "id": "L5h3Tsa0LIoo"
      },
      "outputs": [],
      "source": [
        "def unzip_archive(filepath, dir_path):\n",
        "  with zipfile.ZipFile(f\"{filepath}\", 'r') as zip_ref:\n",
        "    zip_ref.extractall(dir_path)\n",
        "\n",
        "unzip_archive(os.getcwd() + '/data/raw/spotify_million_playlist_dataset.zip', os.getcwd() + '/data/raw/playlists')\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 3,
      "metadata": {},
      "outputs": [],
      "source": [
        "import shutil\n",
        "\n",
        "def make_dir(directory):\n",
        "    if os.path.exists(directory):\n",
        "        shutil.rmtree(directory)\n",
        "        os.makedirs(directory)\n",
        "    else:\n",
        "        os.makedirs(directory)\n",
        "        \n",
        "directory = os.getcwd() + '/data/raw/data'\n",
        "make_dir(directory)"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 4,
      "metadata": {},
      "outputs": [],
      "source": [
        "cols = [\n",
        "    'name',\n",
        "    'pid',\n",
        "    'num_followers',\n",
        "    'pos',\n",
        "    'artist_name',\n",
        "    'track_name',\n",
        "    'album_name'\n",
        "]"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 5,
      "metadata": {
        "colab": {
          "base_uri": "https://localhost:8080/"
        },
        "id": "qyCujIu8cDGg",
        "outputId": "0964ace3-2916-49e3-eebf-2e08e61d95d9"
      },
      "outputs": [
        {
          "name": "stdout",
          "output_type": "stream",
          "text": [
            "mpd.slice.188000-188999.json\t100/1000\t10.0%"
          ]
        }
      ],
      "source": [
        "\n",
        "directory = os.getcwd() + '/data/raw/playlists/data'\n",
        "df = pd.DataFrame()\n",
        "index = 0\n",
        "# Loop through all files in the directory\n",
        "for filename in os.listdir(directory):\n",
        "    # Check if the item is a file (not a subdirectory)\n",
        "    if os.path.isfile(os.path.join(directory, filename)):\n",
        "        if filename.find('.json') != -1 :\n",
        "          index += 1\n",
        "\n",
        "          # Print the filename or perform operations on the file\n",
        "          print(f'\\r{filename}\\t{index}/1000\\t{((index/1000)*100):.1f}%', end='')\n",
        "\n",
        "          # If you need the full file path, you can use:\n",
        "          full_path = os.path.join(directory, filename)\n",
        "\n",
        "          with open(full_path, 'r') as file:\n",
        "              json_data = json.load(file)\n",
        "\n",
        "          temp = pd.DataFrame(json_data['playlists'])\n",
        "          expanded_df = temp.explode('tracks').reset_index(drop=True)\n",
        "\n",
        "          # Normalize the JSON data\n",
        "          json_normalized = pd.json_normalize(expanded_df['tracks'])\n",
        "\n",
        "          # Concatenate the original DataFrame with the normalized JSON data\n",
        "          result = pd.concat([expanded_df.drop(columns=['tracks']), json_normalized], axis=1)\n",
        "          \n",
        "          result = result[cols]\n",
        "\n",
        "          df = pd.concat([df, result], axis=0, ignore_index=True)\n",
        "          \n",
        "        if index % 50 == 0:\n",
        "            df.to_parquet(f'{os.getcwd()}/data/raw/data/playlists_{index % 1000}.parquet')\n",
        "            del df\n",
        "            df = pd.DataFrame()\n",
        "            if index % 100 == 0:\n",
        "                break"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 6,
      "metadata": {},
      "outputs": [],
      "source": [
        "import pyarrow.parquet as pq\n",
        "\n",
        "def read_parquet_folder(folder_path):\n",
        "    dataframes = []\n",
        "    for file in os.listdir(folder_path):\n",
        "        if file.endswith('.parquet'):\n",
        "            file_path = os.path.join(folder_path, file)\n",
        "            df = pd.read_parquet(file_path)\n",
        "            dataframes.append(df)\n",
        "    \n",
        "    return pd.concat(dataframes, ignore_index=True)\n",
        "\n",
        "folder_path = os.getcwd() + '/data/raw/data'\n",
        "df = read_parquet_folder(folder_path)"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 7,
      "metadata": {},
      "outputs": [],
      "source": [
        "directory = os.getcwd() + '/data/raw/mappings'\n",
        "make_dir(directory)"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 8,
      "metadata": {},
      "outputs": [],
      "source": [
        "def create_ids(df, col, name):\n",
        "    # Create a dictionary mapping unique values to IDs\n",
        "    value_to_id = {val: i for i, val in enumerate(df[col].unique())}\n",
        "\n",
        "    # Create a new column with the IDs\n",
        "    df[f'{name}_id'] = df[col].map(value_to_id)\n",
        "    df[[f'{name}_id', col]].drop_duplicates().to_csv(os.getcwd() + f'/data/raw/mappings/{name}.csv')\n",
        "    # df = df.drop(col, axis=1)\n",
        "    return df"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 9,
      "metadata": {},
      "outputs": [],
      "source": [
        "df = create_ids(df, 'artist_name', 'artist')\n",
        "df = create_ids(df, 'pid', 'playlist')\n",
        "df = create_ids(df, 'track_name', 'song')\n",
        "df = create_ids(df, 'album_name', 'album')"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 10,
      "metadata": {},
      "outputs": [],
      "source": [
        "df['artist_count'] = df.groupby(['playlist_id','artist_id'])['song_id'].transform('nunique')\n",
        "df['album_count'] = df.groupby(['playlist_id','artist_id'])['album_id'].transform('nunique')\n",
        "df['song_count'] = df.groupby(['playlist_id','artist_id'])['song_id'].transform('count')"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 11,
      "metadata": {},
      "outputs": [],
      "source": [
        "df['playlist_songs'] = df.groupby(['playlist_id'])['pos'].transform('max')\n",
        "df['playlist_songs'] += 1"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 12,
      "metadata": {},
      "outputs": [],
      "source": [
        "df['artist_percent'] = df['artist_count'] / df['playlist_songs']\n",
        "df['song_percent'] = df['song_count'] / df['playlist_songs']\n",
        "df['album_percent'] = df['album_count'] / df['playlist_songs']"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 13,
      "metadata": {},
      "outputs": [
        {
          "data": {
            "text/html": [
              "<div>\n",
              "<style scoped>\n",
              "    .dataframe tbody tr th:only-of-type {\n",
              "        vertical-align: middle;\n",
              "    }\n",
              "\n",
              "    .dataframe tbody tr th {\n",
              "        vertical-align: top;\n",
              "    }\n",
              "\n",
              "    .dataframe thead th {\n",
              "        text-align: right;\n",
              "    }\n",
              "</style>\n",
              "<table border=\"1\" class=\"dataframe\">\n",
              "  <thead>\n",
              "    <tr style=\"text-align: right;\">\n",
              "      <th></th>\n",
              "      <th>name</th>\n",
              "      <th>pid</th>\n",
              "      <th>num_followers</th>\n",
              "      <th>pos</th>\n",
              "      <th>artist_name</th>\n",
              "      <th>track_name</th>\n",
              "      <th>album_name</th>\n",
              "      <th>artist_id</th>\n",
              "      <th>playlist_id</th>\n",
              "      <th>song_id</th>\n",
              "      <th>album_id</th>\n",
              "      <th>artist_count</th>\n",
              "      <th>album_count</th>\n",
              "      <th>song_count</th>\n",
              "      <th>playlist_songs</th>\n",
              "      <th>artist_percent</th>\n",
              "      <th>song_percent</th>\n",
              "      <th>album_percent</th>\n",
              "    </tr>\n",
              "  </thead>\n",
              "  <tbody>\n",
              "    <tr>\n",
              "      <th>212</th>\n",
              "      <td>throwbacks</td>\n",
              "      <td>143005</td>\n",
              "      <td>2</td>\n",
              "      <td>0</td>\n",
              "      <td>R. Kelly</td>\n",
              "      <td>Ignition - Remix</td>\n",
              "      <td>Chocolate Factory</td>\n",
              "      <td>108</td>\n",
              "      <td>5</td>\n",
              "      <td>203</td>\n",
              "      <td>152</td>\n",
              "      <td>1</td>\n",
              "      <td>1</td>\n",
              "      <td>1</td>\n",
              "      <td>193</td>\n",
              "      <td>0.005181</td>\n",
              "      <td>0.005181</td>\n",
              "      <td>0.005181</td>\n",
              "    </tr>\n",
              "    <tr>\n",
              "      <th>213</th>\n",
              "      <td>throwbacks</td>\n",
              "      <td>143005</td>\n",
              "      <td>2</td>\n",
              "      <td>1</td>\n",
              "      <td>Backstreet Boys</td>\n",
              "      <td>I Want It That Way</td>\n",
              "      <td>Original Album Classics</td>\n",
              "      <td>109</td>\n",
              "      <td>5</td>\n",
              "      <td>204</td>\n",
              "      <td>153</td>\n",
              "      <td>1</td>\n",
              "      <td>1</td>\n",
              "      <td>1</td>\n",
              "      <td>193</td>\n",
              "      <td>0.005181</td>\n",
              "      <td>0.005181</td>\n",
              "      <td>0.005181</td>\n",
              "    </tr>\n",
              "    <tr>\n",
              "      <th>214</th>\n",
              "      <td>throwbacks</td>\n",
              "      <td>143005</td>\n",
              "      <td>2</td>\n",
              "      <td>2</td>\n",
              "      <td>*NSYNC</td>\n",
              "      <td>Bye Bye Bye</td>\n",
              "      <td>No Strings Attached</td>\n",
              "      <td>110</td>\n",
              "      <td>5</td>\n",
              "      <td>205</td>\n",
              "      <td>154</td>\n",
              "      <td>1</td>\n",
              "      <td>1</td>\n",
              "      <td>1</td>\n",
              "      <td>193</td>\n",
              "      <td>0.005181</td>\n",
              "      <td>0.005181</td>\n",
              "      <td>0.005181</td>\n",
              "    </tr>\n",
              "    <tr>\n",
              "      <th>215</th>\n",
              "      <td>throwbacks</td>\n",
              "      <td>143005</td>\n",
              "      <td>2</td>\n",
              "      <td>3</td>\n",
              "      <td>Fountains Of Wayne</td>\n",
              "      <td>Stacy's Mom</td>\n",
              "      <td>Welcome Interstate Managers</td>\n",
              "      <td>111</td>\n",
              "      <td>5</td>\n",
              "      <td>206</td>\n",
              "      <td>155</td>\n",
              "      <td>1</td>\n",
              "      <td>1</td>\n",
              "      <td>1</td>\n",
              "      <td>193</td>\n",
              "      <td>0.005181</td>\n",
              "      <td>0.005181</td>\n",
              "      <td>0.005181</td>\n",
              "    </tr>\n",
              "    <tr>\n",
              "      <th>216</th>\n",
              "      <td>throwbacks</td>\n",
              "      <td>143005</td>\n",
              "      <td>2</td>\n",
              "      <td>4</td>\n",
              "      <td>Bowling For Soup</td>\n",
              "      <td>1985</td>\n",
              "      <td>A Hangover You Don't Deserve</td>\n",
              "      <td>112</td>\n",
              "      <td>5</td>\n",
              "      <td>207</td>\n",
              "      <td>156</td>\n",
              "      <td>1</td>\n",
              "      <td>1</td>\n",
              "      <td>1</td>\n",
              "      <td>193</td>\n",
              "      <td>0.005181</td>\n",
              "      <td>0.005181</td>\n",
              "      <td>0.005181</td>\n",
              "    </tr>\n",
              "    <tr>\n",
              "      <th>...</th>\n",
              "      <td>...</td>\n",
              "      <td>...</td>\n",
              "      <td>...</td>\n",
              "      <td>...</td>\n",
              "      <td>...</td>\n",
              "      <td>...</td>\n",
              "      <td>...</td>\n",
              "      <td>...</td>\n",
              "      <td>...</td>\n",
              "      <td>...</td>\n",
              "      <td>...</td>\n",
              "      <td>...</td>\n",
              "      <td>...</td>\n",
              "      <td>...</td>\n",
              "      <td>...</td>\n",
              "      <td>...</td>\n",
              "      <td>...</td>\n",
              "      <td>...</td>\n",
              "    </tr>\n",
              "    <tr>\n",
              "      <th>400</th>\n",
              "      <td>throwbacks</td>\n",
              "      <td>143005</td>\n",
              "      <td>2</td>\n",
              "      <td>188</td>\n",
              "      <td>JoJo</td>\n",
              "      <td>Too Little, Too Late - Radio Version</td>\n",
              "      <td>Too Little, Too Late</td>\n",
              "      <td>199</td>\n",
              "      <td>5</td>\n",
              "      <td>390</td>\n",
              "      <td>293</td>\n",
              "      <td>1</td>\n",
              "      <td>1</td>\n",
              "      <td>1</td>\n",
              "      <td>193</td>\n",
              "      <td>0.005181</td>\n",
              "      <td>0.005181</td>\n",
              "      <td>0.005181</td>\n",
              "    </tr>\n",
              "    <tr>\n",
              "      <th>401</th>\n",
              "      <td>throwbacks</td>\n",
              "      <td>143005</td>\n",
              "      <td>2</td>\n",
              "      <td>189</td>\n",
              "      <td>Spice Girls</td>\n",
              "      <td>Wannabe - Radio Edit</td>\n",
              "      <td>Spice</td>\n",
              "      <td>200</td>\n",
              "      <td>5</td>\n",
              "      <td>391</td>\n",
              "      <td>294</td>\n",
              "      <td>1</td>\n",
              "      <td>1</td>\n",
              "      <td>1</td>\n",
              "      <td>193</td>\n",
              "      <td>0.005181</td>\n",
              "      <td>0.005181</td>\n",
              "      <td>0.005181</td>\n",
              "    </tr>\n",
              "    <tr>\n",
              "      <th>402</th>\n",
              "      <td>throwbacks</td>\n",
              "      <td>143005</td>\n",
              "      <td>2</td>\n",
              "      <td>190</td>\n",
              "      <td>MiMS</td>\n",
              "      <td>This Is Why I'm Hot</td>\n",
              "      <td>Music Is My Savior</td>\n",
              "      <td>201</td>\n",
              "      <td>5</td>\n",
              "      <td>392</td>\n",
              "      <td>295</td>\n",
              "      <td>1</td>\n",
              "      <td>1</td>\n",
              "      <td>1</td>\n",
              "      <td>193</td>\n",
              "      <td>0.005181</td>\n",
              "      <td>0.005181</td>\n",
              "      <td>0.005181</td>\n",
              "    </tr>\n",
              "    <tr>\n",
              "      <th>403</th>\n",
              "      <td>throwbacks</td>\n",
              "      <td>143005</td>\n",
              "      <td>2</td>\n",
              "      <td>191</td>\n",
              "      <td>Rihanna</td>\n",
              "      <td>Disturbia</td>\n",
              "      <td>Good Girl Gone Bad</td>\n",
              "      <td>115</td>\n",
              "      <td>5</td>\n",
              "      <td>393</td>\n",
              "      <td>296</td>\n",
              "      <td>3</td>\n",
              "      <td>3</td>\n",
              "      <td>3</td>\n",
              "      <td>193</td>\n",
              "      <td>0.015544</td>\n",
              "      <td>0.015544</td>\n",
              "      <td>0.015544</td>\n",
              "    </tr>\n",
              "    <tr>\n",
              "      <th>404</th>\n",
              "      <td>throwbacks</td>\n",
              "      <td>143005</td>\n",
              "      <td>2</td>\n",
              "      <td>192</td>\n",
              "      <td>DEV</td>\n",
              "      <td>Bass Down Low</td>\n",
              "      <td>The Night The Sun Came Up</td>\n",
              "      <td>179</td>\n",
              "      <td>5</td>\n",
              "      <td>394</td>\n",
              "      <td>264</td>\n",
              "      <td>2</td>\n",
              "      <td>1</td>\n",
              "      <td>2</td>\n",
              "      <td>193</td>\n",
              "      <td>0.010363</td>\n",
              "      <td>0.010363</td>\n",
              "      <td>0.005181</td>\n",
              "    </tr>\n",
              "  </tbody>\n",
              "</table>\n",
              "<p>193 rows × 18 columns</p>\n",
              "</div>"
            ],
            "text/plain": [
              "           name     pid  num_followers  pos         artist_name  \\\n",
              "212  throwbacks  143005              2    0            R. Kelly   \n",
              "213  throwbacks  143005              2    1     Backstreet Boys   \n",
              "214  throwbacks  143005              2    2              *NSYNC   \n",
              "215  throwbacks  143005              2    3  Fountains Of Wayne   \n",
              "216  throwbacks  143005              2    4    Bowling For Soup   \n",
              "..          ...     ...            ...  ...                 ...   \n",
              "400  throwbacks  143005              2  188                JoJo   \n",
              "401  throwbacks  143005              2  189         Spice Girls   \n",
              "402  throwbacks  143005              2  190                MiMS   \n",
              "403  throwbacks  143005              2  191             Rihanna   \n",
              "404  throwbacks  143005              2  192                 DEV   \n",
              "\n",
              "                               track_name                    album_name  \\\n",
              "212                      Ignition - Remix             Chocolate Factory   \n",
              "213                    I Want It That Way       Original Album Classics   \n",
              "214                           Bye Bye Bye           No Strings Attached   \n",
              "215                           Stacy's Mom   Welcome Interstate Managers   \n",
              "216                                  1985  A Hangover You Don't Deserve   \n",
              "..                                    ...                           ...   \n",
              "400  Too Little, Too Late - Radio Version          Too Little, Too Late   \n",
              "401                  Wannabe - Radio Edit                         Spice   \n",
              "402                   This Is Why I'm Hot            Music Is My Savior   \n",
              "403                             Disturbia            Good Girl Gone Bad   \n",
              "404                         Bass Down Low     The Night The Sun Came Up   \n",
              "\n",
              "     artist_id  playlist_id  song_id  album_id  artist_count  album_count  \\\n",
              "212        108            5      203       152             1            1   \n",
              "213        109            5      204       153             1            1   \n",
              "214        110            5      205       154             1            1   \n",
              "215        111            5      206       155             1            1   \n",
              "216        112            5      207       156             1            1   \n",
              "..         ...          ...      ...       ...           ...          ...   \n",
              "400        199            5      390       293             1            1   \n",
              "401        200            5      391       294             1            1   \n",
              "402        201            5      392       295             1            1   \n",
              "403        115            5      393       296             3            3   \n",
              "404        179            5      394       264             2            1   \n",
              "\n",
              "     song_count  playlist_songs  artist_percent  song_percent  album_percent  \n",
              "212           1             193        0.005181      0.005181       0.005181  \n",
              "213           1             193        0.005181      0.005181       0.005181  \n",
              "214           1             193        0.005181      0.005181       0.005181  \n",
              "215           1             193        0.005181      0.005181       0.005181  \n",
              "216           1             193        0.005181      0.005181       0.005181  \n",
              "..          ...             ...             ...           ...            ...  \n",
              "400           1             193        0.005181      0.005181       0.005181  \n",
              "401           1             193        0.005181      0.005181       0.005181  \n",
              "402           1             193        0.005181      0.005181       0.005181  \n",
              "403           3             193        0.015544      0.015544       0.015544  \n",
              "404           2             193        0.010363      0.010363       0.005181  \n",
              "\n",
              "[193 rows x 18 columns]"
            ]
          },
          "execution_count": 13,
          "metadata": {},
          "output_type": "execute_result"
        }
      ],
      "source": [
        "df[df['playlist_id'] == 5]"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 14,
      "metadata": {},
      "outputs": [
        {
          "data": {
            "text/html": [
              "<div>\n",
              "<style scoped>\n",
              "    .dataframe tbody tr th:only-of-type {\n",
              "        vertical-align: middle;\n",
              "    }\n",
              "\n",
              "    .dataframe tbody tr th {\n",
              "        vertical-align: top;\n",
              "    }\n",
              "\n",
              "    .dataframe thead th {\n",
              "        text-align: right;\n",
              "    }\n",
              "</style>\n",
              "<table border=\"1\" class=\"dataframe\">\n",
              "  <thead>\n",
              "    <tr style=\"text-align: right;\">\n",
              "      <th></th>\n",
              "      <th>playlist_id</th>\n",
              "      <th>artist_id</th>\n",
              "      <th>artist_percent</th>\n",
              "    </tr>\n",
              "  </thead>\n",
              "  <tbody>\n",
              "    <tr>\n",
              "      <th>0</th>\n",
              "      <td>0</td>\n",
              "      <td>0</td>\n",
              "      <td>0.571429</td>\n",
              "    </tr>\n",
              "    <tr>\n",
              "      <th>1</th>\n",
              "      <td>0</td>\n",
              "      <td>0</td>\n",
              "      <td>0.571429</td>\n",
              "    </tr>\n",
              "    <tr>\n",
              "      <th>2</th>\n",
              "      <td>0</td>\n",
              "      <td>0</td>\n",
              "      <td>0.571429</td>\n",
              "    </tr>\n",
              "    <tr>\n",
              "      <th>3</th>\n",
              "      <td>0</td>\n",
              "      <td>0</td>\n",
              "      <td>0.571429</td>\n",
              "    </tr>\n",
              "    <tr>\n",
              "      <th>4</th>\n",
              "      <td>0</td>\n",
              "      <td>0</td>\n",
              "      <td>0.571429</td>\n",
              "    </tr>\n",
              "  </tbody>\n",
              "</table>\n",
              "</div>"
            ],
            "text/plain": [
              "   playlist_id  artist_id  artist_percent\n",
              "0            0          0        0.571429\n",
              "1            0          0        0.571429\n",
              "2            0          0        0.571429\n",
              "3            0          0        0.571429\n",
              "4            0          0        0.571429"
            ]
          },
          "execution_count": 14,
          "metadata": {},
          "output_type": "execute_result"
        }
      ],
      "source": [
        "artists = df.loc[:,['playlist_id','artist_id','album_id','album_percent']]\n",
        "artists.head()"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 15,
      "metadata": {},
      "outputs": [],
      "source": [
        "X = artists.loc[:,['playlist_id','artist_id','album_id']]\n",
        "y = artists.loc[:,'album_percent']\n",
        "\n",
        "# Split our data into training and test sets\n",
        "X_train, X_val, y_train, y_val = train_test_split(X,y,random_state=0, test_size=0.2)"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 16,
      "metadata": {},
      "outputs": [],
      "source": [
        "def prep_dataloaders(X_train,y_train,X_val,y_val,batch_size):\n",
        "    # Convert training and test data to TensorDatasets\n",
        "    trainset = TensorDataset(torch.from_numpy(np.array(X_train)).long(), \n",
        "                            torch.from_numpy(np.array(y_train)).float())\n",
        "    valset = TensorDataset(torch.from_numpy(np.array(X_val)).long(), \n",
        "                            torch.from_numpy(np.array(y_val)).float())\n",
        "\n",
        "    # Create Dataloaders for our training and test data to allow us to iterate over minibatches \n",
        "    trainloader = torch.utils.data.DataLoader(trainset, batch_size=batch_size, shuffle=True)\n",
        "    valloader = torch.utils.data.DataLoader(valset, batch_size=batch_size, shuffle=False)\n",
        "\n",
        "    return trainloader, valloader\n",
        "\n",
        "batchsize = 64\n",
        "trainloader,valloader = prep_dataloaders(X_train,y_train,X_val,y_val,batchsize)"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 17,
      "metadata": {},
      "outputs": [],
      "source": [
        "class NNColabFiltering(nn.Module):\n",
        "    \n",
        "    def __init__(self, n_playlists, n_artists, embedding_dim_users, embedding_dim_items, n_activations, rating_range):\n",
        "        super().__init__()\n",
        "        self.user_embeddings = nn.Embedding(num_embeddings=n_playlists,embedding_dim=embedding_dim_users)\n",
        "        self.item_embeddings = nn.Embedding(num_embeddings=n_artists,embedding_dim=embedding_dim_items)\n",
        "        self.fc1 = nn.Linear(embedding_dim_users+embedding_dim_items,n_activations)\n",
        "        self.fc2 = nn.Linear(n_activations,1)\n",
        "        self.rating_range = rating_range\n",
        "\n",
        "    def forward(self, X):\n",
        "        # Get embeddings for minibatch\n",
        "        embedded_users = self.user_embeddings(X[:,0])\n",
        "        embedded_items = self.item_embeddings(X[:,1])\n",
        "        # Concatenate user and item embeddings\n",
        "        embeddings = torch.cat([embedded_users,embedded_items],dim=1)\n",
        "        # Pass embeddings through network\n",
        "        preds = self.fc1(embeddings)\n",
        "        preds = F.relu(preds)\n",
        "        preds = self.fc2(preds)\n",
        "        # Scale predicted ratings to target-range [low,high]\n",
        "        preds = torch.sigmoid(preds) * (self.rating_range[1]-self.rating_range[0]) + self.rating_range[0]\n",
        "        return preds"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 19,
      "metadata": {},
      "outputs": [],
      "source": [
        "def train_model(model, criterion, optimizer, dataloaders, device, num_epochs=5, scheduler=None):\n",
        "    model = model.to(device) # Send model to GPU if available\n",
        "    since = time.time()\n",
        "\n",
        "    costpaths = {'train':[],'val':[]}\n",
        "\n",
        "    for epoch in range(num_epochs):\n",
        "        print('Epoch {}/{}'.format(epoch, num_epochs - 1))\n",
        "        print('-' * 10)\n",
        "\n",
        "        # Each epoch has a training and validation phase\n",
        "        for phase in ['train', 'val']:\n",
        "            if phase == 'train':\n",
        "                model.train()  # Set model to training mode\n",
        "            else:\n",
        "                model.eval()   # Set model to evaluate mode\n",
        "\n",
        "            running_loss = 0.0\n",
        "\n",
        "            # Get the inputs and labels, and send to GPU if available\n",
        "            index = 0\n",
        "            for (inputs,labels) in dataloaders[phase]:\n",
        "                inputs = inputs.to(device)\n",
        "                labels = labels.to(device)\n",
        "\n",
        "                # Zero the weight gradients\n",
        "                optimizer.zero_grad()\n",
        "\n",
        "                # Forward pass to get outputs and calculate loss\n",
        "                # Track gradient only for training data\n",
        "                with torch.set_grad_enabled(phase == 'train'):\n",
        "                    outputs = model.forward(inputs).view(-1)\n",
        "                    loss = criterion(outputs, labels)\n",
        "\n",
        "                    # Backpropagation to get the gradients with respect to each weight\n",
        "                    # Only if in train\n",
        "                    if phase == 'train':\n",
        "                        loss.backward()\n",
        "                        # Update the weights\n",
        "                        optimizer.step()\n",
        "\n",
        "                # Convert loss into a scalar and add it to running_loss\n",
        "                running_loss += np.sqrt(loss.item()) * labels.size(0)\n",
        "                print(f'\\r{running_loss} {index} {index / len(dataloaders[phase])}', end='')\n",
        "                index +=1\n",
        "\n",
        "            # Step along learning rate scheduler when in train\n",
        "            if (phase == 'train') and (scheduler is not None):\n",
        "                scheduler.step()\n",
        "\n",
        "            # Calculate and display average loss and accuracy for the epoch\n",
        "            epoch_loss = running_loss / len(dataloaders[phase].dataset)\n",
        "            costpaths[phase].append(epoch_loss)\n",
        "            print('{} loss: {:.4f}'.format(phase, epoch_loss))\n",
        "\n",
        "    time_elapsed = time.time() - since\n",
        "    print('Training complete in {:.0f}m {:.0f}s'.format(\n",
        "        time_elapsed // 60, time_elapsed % 60))\n",
        "\n",
        "    return costpaths"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {},
      "outputs": [
        {
          "name": "stdout",
          "output_type": "stream",
          "text": [
            "Epoch 0/2\n",
            "----------\n",
            "910724978601.7391 123493 100.00%\n",
            "train loss: 115229.4395\n",
            "227700857865.127 30873 100.00%\n",
            "val loss: 115239.3512\n",
            "Epoch 1/2\n",
            "----------\n",
            "910727409277.4519 123493 100.00%\n",
            "train loss: 115229.7471\n",
            "227700857865.127 30873 100.00%\n",
            "val loss: 115239.3512\n",
            "Epoch 2/2\n",
            "----------\n",
            "910734475316.9005 123493 100.00%\n",
            "train loss: 115230.6411\n",
            "227700857865.127 30873 100.00%\n",
            "val loss: 115239.3512\n",
            "Training complete in 71m 54s\n"
          ]
        }
      ],
      "source": [
        "dataloaders = {'train':trainloader, 'val':valloader}\n",
        "n_playlists = X.loc[:,'playlist_id'].max()+1\n",
        "n_artists = X.loc[:,'artist_id'].max()+1\n",
        "n_albums = X.loc[:,'album_id'].max()+1\n",
        "model = NNColabFiltering(\n",
        "    n_playlists,\n",
        "    n_artists,\n",
        "    embedding_dim_users=50,\n",
        "    embedding_dim_items=50,\n",
        "    n_activations = 100,\n",
        "    rating_range=[0.,n_albums]\n",
        ")\n",
        "criterion = nn.MSELoss()\n",
        "lr=0.001\n",
        "n_epochs=10\n",
        "wd=1e-3\n",
        "optimizer = optim.Adam(model.parameters(), lr=lr, weight_decay=wd)\n",
        "device = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\n",
        "\n",
        "costpaths = train_model(model,criterion,optimizer,dataloaders, device, n_epochs, scheduler=None)"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {},
      "outputs": [
        {
          "data": {
            "image/png": "iVBORw0KGgoAAAANSUhEUgAABNoAAAHWCAYAAAChceSWAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjcuMSwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy/bCgiHAAAACXBIWXMAAA9hAAAPYQGoP6dpAACCgElEQVR4nOzde1zUVf7H8fdwGxCZQRBEBJWyxCsK3ii7WAaVmRdctcxL2sVW3dTd1txKrbYs27bsl+m2a2oXy7xmWpppaiXeQLwmmnlDBDRkRlGuM78/XGeX9RIq+h3g9Xw85rE753vmzOfMmB7efM/3a3I6nU4BAAAAAAAAuCoeRhcAAAAAAAAAVAUEbQAAAAAAAEAFIGgDAAAAAAAAKgBBGwAAAAAAAFABCNoAAAAAAACACkDQBgAAAAAAAFQAgjYAAAAAAACgAhC0AQAAAAAAABWAoA0AAAAAAACoAARtAFBBGjZsqEGDBhldBgAAAK6hmTNnymQy6cCBA0aXAsANEbQBqFbWrVunCRMmKC8vz+hSAAAAAABVjJfRBQDA9bRu3Tq9+OKLGjRokAIDAyt07PT0dHl48PsLAAAAAKiu+IkQAC7A4XCooKDgsl5jNpvl7e19jSoCAAAAALg7gjYA1caECRP0zDPPSJKioqJkMplc19cwmUwaPny4PvnkEzVr1kxms1nLli2TJP3tb3/TLbfcouDgYPn5+SkuLk7z5s07b/z/vUbbuet3/Pjjjxo9erRCQkLk7++vHj166NixY9dlzgAAANXdvHnzZDKZtGbNmvOO/eMf/5DJZNKOHTu0bds2DRo0SDfccIN8fX0VFhamwYMH69dffzWgagCVFVtHAVQbPXv21J49e/Tpp5/qrbfeUu3atSVJISEhkqRVq1bp888/1/Dhw1W7dm01bNhQkjR58mQ9+OCD6tevn4qKivTZZ5/pd7/7nZYsWaIuXbr85vuOGDFCtWrV0vjx43XgwAG9/fbbGj58uObMmXPN5goAAICzunTpopo1a+rzzz/XHXfcUebYnDlz1KxZMzVv3lxvvvmmfvnlFz366KMKCwvTzp079f7772vnzp1av369TCaTQTMAUJkQtAGoNlq2bKnY2Fh9+umn6t69uytIOyc9PV3bt29X06ZNy7Tv2bNHfn5+rufDhw9XbGys/v73v5craAsODtY333zjWpw5HA698847stlsslqtVz8xAAAAXJSfn5+6du2qefPm6Z133pGnp6ckKSsrS2vWrNGECRMkSb///e/1xz/+scxrO3TooIceekg//PCDbrvttutdOoBKiK2jAPBvd9xxx3khm6QyIduJEydks9l02223KTU1tVzjPvHEE2V+A3rbbbeptLRUBw8evPqiAQAA8Jv69OmjnJwcrV692tU2b948ORwO9enTR1LZNV9BQYGOHz+uDh06SFK5130AQNBWgV555RXdcsstqlGjRrnvZrhgwQIlJCQoODhYJpNJaWlp5/W58847XdeSOvcYOnSo6/jWrVv10EMPKTIyUn5+fmrSpIkmT5582fX/1vsAVV1UVNQF25csWaIOHTrI19dXQUFBCgkJ0dSpU2Wz2co1bv369cs8r1WrlqSzoR0AAACuvXvvvVdWq7XMpTvmzJmjVq1a6eabb5Yk5ebm6umnn1adOnXk5+enkJAQ1/qwvOs+ACBou0x33nmnZs6cecFjRUVF+t3vfqennnqq3OPl5+erY8eOev311y/Z7/HHH9fRo0ddj0mTJrmOpaSkKDQ0VB9//LF27typ5557TmPHjtW7775b7jrK8z5AVfffv8U85/vvv9eDDz4oX19fvffee/rqq6+0YsUKPfzww3I6neUa99z2hP9V3tcDAADg6pjNZnXv3l0LFy5USUmJjhw5oh9//NF1Npsk9e7dW//85z81dOhQLViwQN98843r5lgOh8Oo0gFUMlyjrQK9+OKLknTRIO5C+vfvL0k6cODAJfvVqFFDYWFhFzw2ePDgMs9vuOEGJScna8GCBRo+fLir/YsvvtCLL76oXbt2KTw8XAMHDtRzzz0nL6///DG41PsAVcHlXsR2/vz58vX11fLly2U2m13tM2bMqOjSAAAAcA316dNHs2bN0sqVK/XTTz/J6XS6grYTJ05o5cqVevHFFzVu3DjXa/bu3WtUuQAqKc5oqyQ++eQT1a5dW82bN9fYsWN1+vTpS/a32WwKCgpyPf/+++81YMAAPf3009q1a5f+8Y9/aObMmXrllVeu6n2Aysbf31+SlJeXV67+np6eMplMKi0tdbUdOHBAixYtugbVAQAA4Frp3LmzgoKCNGfOHM2ZM0ft2rVzbQ09twPhf3ccvP3229e7TACVHGe0VQIPP/ywGjRooPDwcG3btk1jxoxRenq6FixY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",
            "text/plain": [
              "<Figure size 1500x500 with 2 Axes>"
            ]
          },
          "metadata": {},
          "output_type": "display_data"
        }
      ],
      "source": [
        "# Plot the cost over training and validation sets\n",
        "fig,ax = plt.subplots(1,2,figsize=(15,5))\n",
        "for i,key in enumerate(costpaths.keys()):\n",
        "    ax_sub=ax[i%3]\n",
        "    ax_sub.plot(costpaths[key])\n",
        "    ax_sub.set_title(key)\n",
        "    ax_sub.set_xlabel('Epoch')\n",
        "    ax_sub.set_ylabel('Loss')\n",
        "plt.show()"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 22,
      "metadata": {},
      "outputs": [],
      "source": [
        "# Save the entire model\n",
        "torch.save(model, os.getcwd() + '/models/recommender.pt')"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 24,
      "metadata": {},
      "outputs": [],
      "source": [
        "def generate_recommendations(artist_album, playlists, model, playlist_id, device, top_n=10, batch_size=1024):\n",
        "    model.eval()\n",
        "\n",
        "\n",
        "    all_movie_ids = torch.tensor(artist_album['artist_album_id'].values, dtype=torch.long, device=device)\n",
        "    user_ids = torch.full((len(all_movie_ids),), playlist_id, dtype=torch.long, device=device)\n",
        "\n",
        "    # Initialize tensor to store all predictions\n",
        "    all_predictions = torch.zeros(len(all_movie_ids), device=device)\n",
        "\n",
        "    # Generate predictions in batches\n",
        "    with torch.no_grad():\n",
        "        for i in range(0, len(all_movie_ids), batch_size):\n",
        "            batch_user_ids = user_ids[i:i+batch_size]\n",
        "            batch_movie_ids = all_movie_ids[i:i+batch_size]\n",
        "\n",
        "            input_tensor = torch.stack([batch_user_ids, batch_movie_ids], dim=1)\n",
        "            batch_predictions = model(input_tensor).squeeze()\n",
        "            all_predictions[i:i+batch_size] = batch_predictions\n",
        "\n",
        "    # Convert to numpy for easier handling\n",
        "    predictions = all_predictions.cpu().numpy()\n",
        "\n",
        "    albums_listened = set(playlists.loc[playlists['playlist_id'] == playlist_id, 'artist_album_id'].tolist())\n",
        "\n",
        "    unlistened_mask = np.isin(artist_album['artist_album_id'].values, list(albums_listened), invert=True)\n",
        "\n",
        "    # Get top N recommendations\n",
        "    top_indices = np.argsort(predictions[unlistened_mask])[-top_n:][::-1]\n",
        "    recs = artist_album['artist_album_id'].values[unlistened_mask][top_indices]\n",
        "\n",
        "    recs_names = artist_album.loc[artist_album['artist_album_id'].isin(recs)]\n",
        "    album, artist = recs_names['album_name'].values, recs_names['artist_name'].values\n",
        "\n",
        "    return album.tolist(), artist.tolist()   "
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {},
      "outputs": [
        {
          "name": "stdout",
          "output_type": "stream",
          "text": [
            "Precision: 5.0609978643478826e-06\n",
            "Recall: 5.0609978643478826e-06\n"
          ]
        }
      ],
      "source": [
        "from torchmetrics import Precision, Recall\n",
        "\n",
        "precision = Precision(task=\"multiclass\", num_classes=num_classes).to(device) \n",
        "recall = Recall(task=\"multiclass\", num_classes=num_classes).to(device) \n",
        "\n",
        "\n",
        "model.eval()\n",
        "with torch.no_grad():\n",
        "    for batch in dataloaders['val']:\n",
        "        inputs, targets = batch\n",
        "        inputs = inputs.to(device)\n",
        "        targets = targets.to(device)\n",
        "\n",
        "        outputs = model(inputs)\n",
        "\n",
        "        # For binary classification\n",
        "        preds = torch.argmax(outputs, dim=1)\n",
        "\n",
        "        # Update metrics\n",
        "        precision(preds, targets)\n",
        "        recall(preds, targets)\n",
        "\n",
        "# Compute final metrics\n",
        "final_precision = precision.compute()\n",
        "final_recall = recall.compute()\n",
        "\n",
        "print(f\"Precision: {final_precision}\")\n",
        "print(f\"Recall: {final_recall}\")"
      ]
    }
  ],
  "metadata": {
    "colab": {
      "machine_shape": "hm",
      "provenance": []
    },
    "kernelspec": {
      "display_name": "Python 3",
      "name": "python3"
    },
    "language_info": {
      "codemirror_mode": {
        "name": "ipython",
        "version": 3
      },
      "file_extension": ".py",
      "mimetype": "text/x-python",
      "name": "python",
      "nbconvert_exporter": "python",
      "pygments_lexer": "ipython3",
      "version": "3.9.19"
    }
  },
  "nbformat": 4,
  "nbformat_minor": 0
}