1. Data Preprocessing & Artist Profiles (spotify.csv)
We start with $114,000$ Spotify track rows. After dropping missing entries and duplicate songs per artist (drop_duplicates(subset=['artists', 'track_name'])) so re-releases do not skew averages, we obtain $81,343$ unique songs across $31,437$ artists.
For each artist (groupby('artists')), we compute the mean of the $9$ audio features (danceability, energy, loudness, speechiness, acousticness, instrumentalness, liveness, valence, tempo) and extract the dominant genre (track_genre via mode), keeping $11,402$ filtered artist profiles ($\ge 2$ songs or present in Last.fm) for reliable averages.
2. Feature Scaling (MinMaxScaler)
Spotify features span very different ranges (danceability $\in [0, 1]$, loudness $\in [-60, 0]\text{ dB}$, tempo $\in [50, 220]\text{ BPM}$). Applying MinMaxScaler maps every numeric column into $[0, 1]$:
$$x'_i = \frac{x_i - x_{\min}}{x_{\max} - x_{\min}} \in [0, 1]$$
Error Example Without Scaling
Let Artist $X = [0.80, 120]$ and Artist $Y = [0.20, 128]$ ([danceability, tempo]):
$$\cos(\theta_{\text{raw}}) = \frac{(0.80 \times 0.20) + (120 \times 128)}{\sqrt{0.80^2 + 120^2}\cdot\sqrt{0.20^2 + 128^2}} = \frac{15360.16}{120.0027 \times 128.0002} = \mathbf{0.999989}$$
Unscaled tempo ($120 \times 128$) overwhelms everything else, making two completely different artists appear $99.999\%$ identical. Scaling restores equal weight.
3. Genre Encoding (One-Hot Encoding, $w_{\text{genre}} = 0.5$) → 123 Dimensions
Two artists from unrelated genres may share similar average energy and tempo. We encode all $114$ genres (track_genre) as binary $\{0, 1\}$ columns weighted by $w_{\text{genre}} = 0.5$:
- Each artist becomes a $9 + 114 = 123$-dimensional vector.
- Sharing a genre adds $0.5 \times 0.5 = \mathbf{+0.25}$ to the dot product — enough to separate genres without masking acoustic differences within a genre.
4. Dynamic Clusters (AgglomerativeClustering) & The Averaging Trap
Real listeners mix diverse tastes. Averaging unrelated genres (e.g., System Of A Down metal + The Prodigy breakbeat, or Queen rock + Eminem hip-hop + Miles Davis jazz) drops the single mean vector into a "dead zone" between genres (Averaging Trap) that represents none of them.
To solve this, cluster_user_artists_content computes pairwise 123-D cosine distance $d = 1 - \cos(\theta)$ (with a $+0.15$ penalty across different genres) and runs AgglomerativeClustering (distance_threshold = 0.08) to partition inputs into $N$ homogeneous clusters, splitting the $5$ final picks fairly across clusters.
5. Sub-Centroids ($\vec{u}_{\text{cluster}}$) & Cosine Similarity Walkthrough
Within each cluster separately, we compute its Sub-Centroid ($\vec{u}_{\text{cluster}}$, the mean vector of that cluster) and rank candidates via Cosine Similarity. Concrete 2D example in $[\text{Energy}, \text{Valence}]$:
Suppose a Rock cluster contains $A_1 = [0.80, 0.70]$ (Queen), $A_2 = [0.60, 0.90]$ (The Beatles), and $A_3 = [0.70, 0.50]$ (Led Zeppelin). Its Sub-Centroid is:
$$u_{\text{cluster, Energy}} = \frac{0.80 + 0.60 + 0.70}{3} = 0.70, \quad u_{\text{cluster, Valence}} = \frac{0.70 + 0.90 + 0.50}{3} = 0.70 \implies \vec{u}_{\text{cluster}} = [0.70, 0.70]$$
Comparing two candidates ($C_1 = [0.65, 0.75]$ aligned direction vs $C_2 = [0.20, 0.90]$ low energy) using $\cos(\theta) = \frac{\vec{u}_{\text{cluster}} \cdot \vec{C}}{\|\vec{u}_{\text{cluster}}\| \|\vec{C}\|}$:
- Candidate $C_1 = [0.65, 0.75]$: Dot product $0.980$, norms $0.9899 \times 0.9925 = 0.9825 \implies \cos(\theta_1) = \mathbf{0.9975}$ ($99.75\%$ similarity).
- Candidate $C_2 = [0.20, 0.90]$: Dot product $0.770$, norms $0.9899 \times 0.9220 = 0.9127 \implies \cos(\theta_2) = \mathbf{0.8436}$ ($84.36\%$ similarity).
In code, this exact calculation runs in $123$ dimensions per cluster using np.mean and cosine_similarity.
6. Empirical Results & Cultural Blindness
Live backend queries clearly highlight both Part A's strengths and its core limitation:
| Seed Artist(s) |
Part A Cluster & Picks |
Genre |
Content Sim (123-D) |
Observation |
| Queen |
Imagine Dragons / Twenty One Pilots / Bastille |
rock |
0.9917 0.9894 |
Matches audio pulse, misses 1970s era ($40$ years newer) |
| Eminem |
Karan Aujla / Sidhu Moose Wala |
hip-hop |
0.9953 0.9950 |
Modern Punjabi hip-hop with similar BPM (cultural mismatch) |
| Miles Davis |
BLOSSOM DEARIE / Chet Baker |
jazz |
0.9951 0.9945 |
Strong acoustic & genre match |
| System Of A Down + The Prodigy |
Cluster 1: Disturbed Cluster 2: Aphrodite |
metal
breakbeat |
0.9971 0.9979 |
Cleanly separates genres and prevents Averaging Trap |
Part A Takeaway (Why We Need Part B)
Content-Based Filtering with Sub-Centroids solves multi-genre mixing, yet suffers from Cultural Blindness: it compares audio numbers blindly without knowing which artists share an era, scene, or real listener base. Learning real listener co-occurrence happens in Part B.