MusicMap
I built an artist-discovery prototype that groups artists by their track features and ranks candidates within those groups. A Flask interface connects the results with available previews and Spotify track information.
The problem
Finding a related artist requires a representation of musical similarity. Track-level attributes offer one way to explore that relationship: energy, acousticness, tempo, and other features can describe patterns across an artist’s catalog.
MusicMap turns those attributes into an artist-level discovery workflow. A listener supplies a known artist, and the application returns candidates drawn from the same feature cluster.
My contribution
I built the clustering workflow, Flask service, artist lookup, Spotify integration, and asynchronous web experience. I connected the feature pipeline to an interface where recommendations could be inspected through track information and available previews.
Architecture
The server constructs the clusterer when it starts. It loads the dataset, builds artist representations, and fits K-Means. A request to the prediction endpoint lowercases the artist name, retrieves candidates, and assembles the interface response.
The pinned server calls Spotify’s HTTP endpoints with the Requests library. It obtains a client-credentials token, searches for tracks by artist, and selects one of the returned tracks. Dataset preview URLs are handled through a separate lookup.
Artist representations
The preprocessing drops rows with missing required features and normalizes artist names to lowercase. It label-encodes playlist genre, groups tracks by artist, and averages the selected numeric fields. The encoded genre value is included in that aggregation.
StandardScaler then transforms the artist vectors for K-Means. The number of observed playlist genres determines the number of clusters, with a fixed random seed of 42. This produces a cluster assignment for each artist in the dataset.
def load_and_preprocess_data(self):
df = pd.read_csv(self.data_path)
df.dropna(subset=self.feature_columns, inplace=True)
df = df[self.feature_columns]
df['track_artist'] = df['track_artist'].apply(lambda x: str(x).lower())
df["playlist_genre"] = self.encoder.fit_transform(df["playlist_genre"])
self.num_genres = df["playlist_genre"].nunique()
self.artist_features = df.groupby('track_artist').mean()
self.scaled_features = self.scaler.fit_transform(self.artist_features)
def fit_clusters(self):
self.kmeans = KMeans(n_clusters=self.num_genres, random_state=self.random_state)
self.cluster_labels = self.kmeans.fit_predict(self.scaled_features)
return self.cluster_labelsThe figure uses fixed two-dimensional vectors and illustrative memberships to expose the ranking calculation. The original project also uses t-SNE to project the standardized artist features into two dimensions for its cluster visualization.
Similarity and discovery
The lookup transforms the query artist for cluster prediction, selects artists with the same cluster label, and removes the query itself. It then computes cosine similarity using the original averaged feature vectors.
Within that candidate set, min-max scaling maps the smallest similarity to zero and the largest to one. Sorting those relative scores produces the recommendation order. The request handler returns the first five candidate names.
def calculate_artists_probability(self, cluster_index):
print("cluster #: ", cluster_index)
if not self.cluster_artists:
print("No similar artists found in the cluster")
return None
cluster_features = self.artist_features.loc[self.cluster_artists].values
input_features = self.artist_features.loc[self.input_artist].values.reshape(1, -1)
similarities = cosine_similarity(input_features, cluster_features)[0]
probabilities = (similarities - np.min(similarities)) / (np.max(similarities) - np.min(similarities)) if len(similarities) > 1 else [1.0]
artist_probabilities = dict(zip(self.cluster_artists, probabilities))
sorted_artists = sorted(artist_probabilities.items(), key=lambda x: x[1], reverse=True)
return sorted_artistsA single candidate receives a score of one. Multiple candidates with identical similarities reach a zero denominator in the source formula, producing nonfinite scores. The offline fixtures preserve this observed edge case in the source records.
Preview availability depends on the dataset and external track data. The interface can still receive artist names and track identifiers when a preview URL is unavailable.
Results
MusicMap produced an artist-discovery prototype that connects an unsupervised model to browsable track cards. The saved screenshot shows the interface’s query artist and similar-artist sections.
The case-study fixtures exercise candidate exclusion, ranking, unknown artists, singleton clusters, equal similarities, and missing previews. Spotify and media playback remain outside the page’s execution path.
Interface
