Desenvolvendo uma aplicação de Recomendação do Spotify

O que está sendo feito
Resolvi escrever esse texto para explicar melhor como que foi construída a aplicação web que avalia qual música das 50 mais tocadas no Spotify se parece com uma música que seja testada pelo usuário (disponível aqui).
Esse texto está bastante resumido, a ideia é apenas comentar como foi construído. Disponibilizei o código apenas da parte que calcula a recomendação. Abaixo o resultado final.
Em linhas gerais tentarei explicar tudo em 4 partes:
- Descrição das ferramentas na aplicação
- Como funciona a API do Spotify
- Como é calculado a semelhança entre as músicas
- O cálculo na prática
Descrição das ferramentas na aplicação
Eu resolvi fazer essa aplicação utilizando o back-end em Python e o front-end utilizando React. Ambas ferramentas me dão um certo conforto em trabalhar.
O back-end em Python foi uma escolha quase que essencial, pois pude utilizar várias bibliotecas muito boas para quem é cientista de dados, como Pandas, Numpy, Scikitlearn.
Em parceria com essas bibliotecas, que foram responsáveis pelos cálculos e estimativas, utilizei o framework Django, que ficou responsável pela criação dos end-points.
Neste caso, o back-end havia apenas três end-points:
- Requisitar a música que será comparada,
- Requisitar as 50 músicas mais tocadas atualmente no Spotify,
- Realizar os cálculos e devolver os resultados.
Não utilizei nada de banco de dados nesta aplicação, por não tinha interesse em armazenar nenhuma informação, ao menos até este momento.
Como funciona a API do Spotify
O Spotify disponibiliza um serviço no qual diversas métricas do seu conteúdo são disponibilizadas.
Com um cadastro e em posse de suas credenciais você tem acesso. Como suporte utilizei a biblioteca Spotify do Python.
Quando coletei dados de uma música, diversas informações são retornadas, inclusive características das músicas hospedadas neste aplicativo.
Apenas baseado nas métricas por eles geradas, consiste essa aplicação.
Utilizei uma transformação, de modo a ficar entre zero e um, isso facilita em aspecto de interpretação e cálculos.
Como é calculado a semelhança entre as músicas
Para buscar quais músicas parecem com a requisitada pelo usuário fiz uso de um conceito bastante simples que foi nada mais do que a distância euclidiana entre pontos.
De uma forma sucinta explicarei, com um caso de apenas três variáveis, como seria este cálculo.
Quem já tiver clareza deste conceito pode seguir a próxima seção.
Primeiramente, eu partir de duas variáveis muito comuns: Peso e Altura.
De forma muito fácil gerei um conjunto de dados com 500 observações.
Assumindo que peso e altura são grandezas que aderem a uma distribuição normal multivariada, com médias 60kg e 1.66m, respectivamente, e uma matriz de covariância:
import pandas as pd import numpy as np import warningsfrom scipy.stats import multivariate_normal df = pd.DataFrame(np.random.multivariate_normal([60, 1.66], [[100, 0.8], [0.8, 0.0025]],500)) df.columns = ["Peso","Altura"] df.head(5)
Daí criei duas variáveis a mais: IMC e Estado. A primeira, é referente a um indicador que pode ser utilizado na avaliação nutricional.
A segunda é apenas uma categorização da primeira, subdividindo-a em: Desnutrido, Peso normal e Sobrepeso.
df['IMC'] = df['Peso']/(df['Altura']**2) df['Estado'] = pd.cut(df['IMC'], [0,18.5,25,np.inf], right=False, labels= ["Desnutrido","Peso normal","Sobrepeso"])df = df.drop(["IMC"],axis=1)
Com um gráfico de pontos, pode-se ver claramente que os grupos se separam de forma bem evidente.
import seaborn as sns; sns.set()
import matplotlib.pyplot as plt
sns.scatterplot(x="Peso", y="Altura", hue="Estado", data=df)
Desta forma vamos supor um novo indivíduo com 1.60m e 68kg. Após transformar os dados de forma que todas as variáveis fiquem entre zero e um, calculei a distância euclidiana deste novo indivíduo em comparação com todos os demais gerados anteriormente.
E simplesmente coletei o que mais se aproxima dele. Como todas as variáveis estão apenas variando entre zero e um, a distância máxima possível é raiz de 2.
Dividindo esta distância por raiz de dois temos uma distância entre zero e um. Utilizei este indicador para avaliar similaridade. Quanto mais próximo de zero mais similar são.
from sklearn.preprocessing import MinMaxScaler
scala = MinMaxScaler()
scala.fit(df.drop(['Estado'],axis=1))
df_escalonado = pd.DataFrame(scala.transform(df.drop(['Estado'],axis=1)))
df_escalonado.columns = df.drop(['Estado'],axis=1).columns
df_escalonado.head(5)
sujeito = { "Peso":[68], "Altura":[1.60] } sujeito = pd.DataFrame(sujeito)minimos = {} largura = {} for id,nome in enumerate(df_escalonado.columns): minimos[nome] = scala.data_min_[id] largura[nome] = scala.data_range_[id]for n in sujeito.columns: sujeito[n] = (sujeito[n] - minimos[n])/largura[n]dists
O cálculo na prática
Primeiramente um objeto foi criado com funções úteis para realizar todo o processo.
Em seguida iniciamos o objeto Funcoes
. Nesta etapa é feita a requisição de acesso a API. Aqui ficam incluídas as credenciais.
Utilizando o id da música escolhida, tive acesso aos dados que queria. Exclui as variáveis que não me interessava e transformei as únicas duas variáveis que não possuíam domínio entre zero e um.
O mesmo processo foi realizado para requisitar as informações das músicas mais tocadas. Para este caso, o método em questão, primeiramente coleta os id de todas as músicas que pertencem as mais tocadas e em seguida coleta as informações dos áudios em questão. Realizei a mesma transformação feita com a música solicitada na etapa anterior.
De agora em diante fica fácil. Apenas calculei as distâncias euclidianas e verifiquei qual a música do top 50 que mais se aproxima da solicitada.
import spotipy import spotipy.util as util from spotipy.oauth2 import SpotifyClientCredentials class Funcoes: def __init__(self): self.client_id = 'client_id' self.client_secret = 'client_secret' self.client_credentials_manager = SpotifyClientCredentials(client_id=self.client_id, client_secret=self.client_secret) self.sp = spotipy.Spotify(client_credentials_manager=self.client_credentials_manager) def get_musica(self,music_id): sp = self.sp saida = { 'features':sp.audio_features(music_id), 'track':sp.track(music_id) } return(saida) def get_playlist_audio_features(self,username, playlist_id): sp = self.sp offset = 0 songs = [] items = [] ids = [] while True: content = sp.user_playlist_tracks(username, playlist_id, fields=None, limit=100, offset=offset, market=None) songs += content['items'] if content['next'] is not None: offset += 100 else: break for i in songs: ids.append(i['track']['id']) index = 0 audio_features = [] while index < len(ids): try: audio_features += sp.audio_features(ids[index:index + 50]) index += 50 except: pass top_null = [] for i in audio_features: if not i is None: top_null.append(i) return (top_null)funcoes = Funcoes() musicas = funcoes.get_musica(music_id = "2XU0oxnq2qxCpomAAuJY8K") musicas = pd.DataFrame(musicas['features']) links_musica = musicas['uri'] musicas = musicas.drop(['time_signature','key','mode','type','uri','id','track_href','analysis_url','duration_ms'],axis=1) musicas['loudness'] = (musicas['loudness']+60)/(60) musicas['tempo'] = (musicas['tempo'] - 25)/200 musicas
top = funcoes.get_playlist_audio_features('renan_bispo',"37i9dQZEVXbMDoHDwVN2tF")top = pd.DataFrame(top) top.head(5)
top = top.drop(['time_signature','key','mode','type','uri','id','track_href','analysis_url','duration_ms'],axis=1) nomes = top.columns top['loudness'] = (top['loudness'] + 60)/(60) top['tempo'] = (top['tempo'] - 25)/200 top.head(5)for id,col in enumerate(top.columns): top.rename(columns={col:nomes[id]}, inplace=True)minimos = {} maximos = {} largura = {} saida = [] for id,nome in enumerate(top.columns): if nome == "loudness": minimos[nome] = -60 maximos[nome] = 0 largura[nome] = 60 elif nome == "tempo": minimos[nome] = 25 maximos[nome] = 225 largura[nome] = 200 else: minimos[nome] = 0 maximos[nome] = 1 largura[nome] = 1dists = [] for i in range(0,np.shape(top)[0]): dists.append(np.sqrt(np.sum((top.iloc[i,:].values - musicas.values)**2))) dists = dists/np.sqrt(2)posicao = np.where(dists == np.amin(dists))[0][0] parecida = top.iloc[posicao,:]np.where(dists == np.amin(dists))dist = np.absolute(top.iloc[posicao,:].values - musicas.values) distcaracteristica = nomes[np.where(dist == np.amin(dist))[1][0]] caracteristicadist_parecidas = 1 - dists[posicao] saida = { "parecida":parecida, "dist_parecidas": dist_parecidas } saida
Como resumir esta aplicação
O objetivo aqui foi de tentar ilustrar o uso de técnicas simples para se propor soluções de problemas reais. Mostrar também uma aplicação de forma simples de uso também é importante, pois não limita-se a um único relatório e mostra como isso pode ser encorporado em produtos digitais.
Gostou desse artigo? Tem alguma sugestão? Deixe um comentário.
0 Comentários