Projeto de Clustering

bunner

Objetivo

Criar um sistema de recomendação utilizando o algoritmo não-supervisionado KMeans do Scikit-learn.
Neste projeto será utilizado uma base de dados com músicas do serviço de streaming de áudio Spotify.

Spotify

É um serviço de streaming de áudio, disponível em praticamente todas as plataformas e está presente no mundo todo. Foi lançado oficialmente em 2008, na Suécia. Possui acordos com a Universal Music, Sony BGM, EMI Music e Warner Music Group.

O usuário pode encontrar playlists e rádios, checar quais músicas estão fazendo sucesso entre os assinantes, criar coleções ou seguir as coleções de amigos e artistas. A plataforma conta com mais de 170 milhões de usuários e veio para o Brasil em 2014.

Importando as bibliotecas

In [1]:
# para identificar os arquivos em uma pasta
import glob

# para manipulação dos dados
import pandas as pd
import numpy as np

# para visualizações
import seaborn as sns
import matplotlib.pyplot as plt

# para pré-processamento
from sklearn.preprocessing import MinMaxScaler

# para machine learning do agrupamento
from sklearn.cluster import KMeans

# para ignorar eventuais warnings
import warnings
warnings.filterwarnings("ignore")

# algumas configurações do notebook
%matplotlib inline
pd.options.display.max_columns = None
plt.style.use('ggplot')

importando a base de dados

Vamos importar nossa base de dados, utilizando o método glob do framework de mesmo nome para buscar os arquivos ".csv" que contem na pasta, no nosso caso temos 2 arquivos referentes aos anos de 2018 e 2019. Em seguida utilizaremos um list comprehension para fazer a leitura dos dados com o método do pandas, com isso podemos já inserir dentro de uma lista vazia criada, concatenando esses dois elementos da lista, pelas linhas, formando um arquivo único e por fim, visualizamos o resultado com as primeiras 5 linhas com o método head().

Temos 20 colunas ou features em nosso conjunto, vamos conhecer cada uma delas com o nosso Dicionário de dados:

  • name: nome da faixa
  • album: album que contém a faixa
  • artist: nome do artista
  • release_date: ano de lançamento da faixa
  • length: tempo de duração da música
  • popularity: quanto mais alto o valor, maior a popularidade
  • track_id: ID
  • mode: Modo indica a modalidade (maior ou menor) de uma faixa, o tipo de escala da qual seu conteúdo melódico é derivado
  • acousticness: quanto mais alto o valor, mais acústico é a música
  • danceability: quanto mais alto o valor, mais dançante é a música com base nos elementos musicais
  • energy: A energia da música, quanto mais alto o valor, mais enérgica é
  • instrumentalness: o valor de nível instrumental, se mais cantada ou sem vocal
  • liveness: presença de audiência, é a probabilidade da música ter tido público ou não
  • valence: quanto maior o valor, mais positiva (feliz, alegre, eufórico) a música é
  • loudness: quanto maior o valor, mais alto é a música
  • speechiness: quanto mais alto o valor, mais palavras cantadas a música possui
  • tempo: o tempo estimado em BPM (batida por minuto), relaciona com o ritmo derivando a duração média do tempo
  • duration_ms: a duração da faixa em milisegundos
  • time_signature: Uma estimativa de fórmula de compasso geral de uma faixa
  • genre: Genero do artista
In [2]:
# identificando os arquivos .csv contidos na pasta
all_files = glob.glob("dados/*.csv")

# criando uma lista vazia para receber os dois arquivos
df_list = []

# fazendo um loop e armazenando os arquivos na pasta df_list
[df_list.append(pd.read_csv(filename, parse_dates = ['release_date'])) for filename in all_files]

# concatenando os dois arquivos para formar um
df = pd.concat(df_list, axis=0, ignore_index=True).drop('Unnamed: 0', axis=1)

# visualizando as primeiras 5 linhas
df.head()
Out[2]:
name album artist release_date length popularity track_id mode acousticness danceability energy instrumentalness liveness valence loudness speechiness tempo duration_ms time_signature genre
0 goosebumps Birds In The Trap Sing McKnight Travis Scott 2016-09-16 243836 89 6gBFPUFcJLzWGx4lenP6h2 1 0.0847 0.841 0.728 0.000000 0.1490 0.430 -3.370 0.0484 130.049 243837 4 ['rap']
1 Trevo (Tu) ANAVITÓRIA ANAVITÓRIA 2016-08-18 205467 69 2vRBYKWOyHtFMtiK60qRz7 1 0.8770 0.676 0.374 0.000132 0.1650 0.200 -8.201 0.0266 99.825 205468 4 ['folk brasileiro', 'nova mpb', 'pop lgbtq+ br...
2 Hear Me Now Hear Me Now Alok 2016-10-21 194840 73 39cmB3ZoTOLwOTq7tMNqKa 1 0.5460 0.778 0.463 0.002890 0.0731 0.496 -7.603 0.0389 121.999 194840 4 ['brazilian edm', 'edm', 'electro house', 'pop...
3 Refém O Cara Certo Dilsinho 2016-06-10 223546 67 4muZ1PUxmss4cVmYnpLrvs 1 0.6920 0.648 0.542 0.000000 0.0923 0.835 -6.292 0.0360 147.861 223547 4 ['pagode', 'pagode novo']
4 Ela Só Quer Paz Ela Só Quer Paz Projota 2016-01-27 174382 66 1RPrzKZdkzCNV0Ux0xEHg2 0 0.0207 0.655 0.664 0.000000 0.0771 0.934 -4.506 0.1790 172.005 174382 4 ['brazilian edm', 'brazilian hip hop', 'pop na...

Como extraimos também os IDs das músicas, que é a parte final da url, única para cada música, vamos juntar e formar a url completa.

In [3]:
# definindo a url padrão
url_standard = 'https://open.spotify.com/track/'

# unindo com os IDs de cada música
df['url'] = url_standard + df['track_id']

# removendo a coluna dos IDs
df.drop(['track_id'], axis=1, inplace=True)

# visualizando as primeiras duas linhas
df.head(2)
Out[3]:
name album artist release_date length popularity mode acousticness danceability energy instrumentalness liveness valence loudness speechiness tempo duration_ms time_signature genre url
0 goosebumps Birds In The Trap Sing McKnight Travis Scott 2016-09-16 243836 89 1 0.0847 0.841 0.728 0.000000 0.149 0.43 -3.370 0.0484 130.049 243837 4 ['rap'] https://open.spotify.com/track/6gBFPUFcJLzWGx4...
1 Trevo (Tu) ANAVITÓRIA ANAVITÓRIA 2016-08-18 205467 69 1 0.8770 0.676 0.374 0.000132 0.165 0.20 -8.201 0.0266 99.825 205468 4 ['folk brasileiro', 'nova mpb', 'pop lgbtq+ br... https://open.spotify.com/track/2vRBYKWOyHtFMti...

Assim como as primeiras 5 linhas, vamos também visualizar as 5 últimas, com o método tail()

In [4]:
# visualizando as últimas 5 linhas
df.tail()
Out[4]:
name album artist release_date length popularity mode acousticness danceability energy instrumentalness liveness valence loudness speechiness tempo duration_ms time_signature genre url
7995 Phases Phases PRETTYMUCH 2019-04-26 215759 76 0 0.1690 0.770 0.636 0.0 0.0676 0.492 -6.724 0.1310 105.930 215760 4 ['boy band', 'dance pop', 'pop', 'post-teen po... https://open.spotify.com/track/3je88Q4OvTqIx7B...
7996 Vizinho Chato - Ao Vivo Pé na Areia (Ao Vivo) Gustavo Mioto 2019-03-22 161333 48 0 0.2640 0.618 0.705 0.0 0.3960 0.607 -2.676 0.0581 111.765 161333 4 ['sertanejo', 'sertanejo pop', 'sertanejo univ... https://open.spotify.com/track/6GGPR1U5uN31XtX...
7997 Bacc Seat (feat. Ty Dolla $ign) Please Excuse Me For Being Antisocial Roddy Ricch 2019-12-06 172320 73 1 0.0404 0.783 0.524 0.0 0.0870 0.153 -6.828 0.1150 144.989 172320 4 ['north carolina hip hop', 'rap'] https://open.spotify.com/track/6In6SkveIw26thd...
7998 Missão - Ao vivo Chão de estrelas (Ao vivo) Ferrugem 2019-07-19 179038 48 0 0.5480 0.493 0.785 0.0 0.4840 0.732 -5.818 0.0610 155.994 179039 4 ['pagode'] https://open.spotify.com/track/2v63tyymGKLvpdI...
7999 Dream Glow (BTS World Original Soundtrack) - P... Dream Glow (BTS World Original Soundtrack) [Pt... BTS 2019-06-07 187457 68 0 0.0967 0.735 0.740 0.0 0.1010 0.541 -3.837 0.0519 141.948 187458 4 ['k-pop', 'k-pop boy group'] https://open.spotify.com/track/4c1WgUnHXq2LEnc...

Quais são as dimensões desse conjunto? O método shape pode nos ajudar a descobrir.

In [5]:
print(f"Quantidade de linhas: {df.shape[0]}")
print(f"Quantidade de colunas: {df.shape[1]}")
Quantidade de linhas: 8000
Quantidade de colunas: 20

Análise Exploratória dos dados

Dados faltantes ou nulos, é uma característica normal para um conjunto de dados extraido no mundo real. Vamos dar uma olhada se esse conjunto possui algum.

In [6]:
# checando dados nulos
df.isnull().sum()
Out[6]:
name                0
album               0
artist              0
release_date        0
length              0
popularity          0
mode                0
acousticness        0
danceability        0
energy              0
instrumentalness    0
liveness            0
valence             0
loudness            0
speechiness         0
tempo               0
duration_ms         0
time_signature      0
genre               0
url                 0
dtype: int64

Puxando os dados diretamente desta API, notamos que não foram trazidos nenhum dado faltante.

Podemos notar que a maioria são dados numéricos, mas será que realmente estão nesse formato, vamos fazer um check utilizando o método *dtypes.

In [7]:
# checando os tipos de dados
df.dtypes
Out[7]:
name                        object
album                       object
artist                      object
release_date        datetime64[ns]
length                       int64
popularity                   int64
mode                         int64
acousticness               float64
danceability               float64
energy                     float64
instrumentalness           float64
liveness                   float64
valence                    float64
loudness                   float64
speechiness                float64
tempo                      float64
duration_ms                  int64
time_signature               int64
genre                       object
url                         object
dtype: object

Podemos ver que os formatos estão de acordo com o que deveriam ser.

Vamos agora checar se há dados, ou melhor, linhas iguais ou duplicadas com o método duplicated(), caso encontramos vamos remover com o método drop_duplicates e checar as novas dimensões.

In [8]:
# checando dados duplicados
df.duplicated().sum()
Out[8]:
0
In [9]:
# removendo linhas duplicadas
df.drop_duplicates(inplace=True)

df = df.reset_index()
In [10]:
# checando as novas dimensões
df.shape
Out[10]:
(8000, 21)
In [11]:
# garantindo que não há mais dados duplicados
df.duplicated().sum()
Out[11]:
0

Vamos dar uma olhada em algumas estatísticas, para conhecermos mais nossos dados, e pra isso fazemos uso do método describe().

In [12]:
# analisando estatísticas descritivas (numéricas)
df.describe()
Out[12]:
index length popularity mode acousticness danceability energy instrumentalness liveness valence loudness speechiness tempo duration_ms time_signature
count 8000.00000 8.000000e+03 8000.000000 8000.000000 8000.000000 8000.000000 8000.000000 8000.000000 8000.000000 8000.000000 8000.000000 8000.000000 8000.000000 8.000000e+03 8000.000000
mean 3999.50000 2.125530e+05 57.335875 0.615625 0.352589 0.624380 0.634561 0.055460 0.270518 0.508747 -7.441924 0.098780 122.234154 2.125534e+05 3.941875
std 2309.54541 7.170669e+04 11.412421 0.486478 0.283883 0.172115 0.211528 0.204293 0.254686 0.250479 4.996436 0.099974 29.378527 7.170670e+04 0.398455
min 0.00000 3.305600e+04 38.000000 0.000000 0.000000 0.000000 0.000020 0.000000 0.000000 0.000000 -42.825000 0.000000 0.000000 3.305600e+04 0.000000
25% 1999.75000 1.737755e+05 48.000000 0.000000 0.096500 0.528000 0.499000 0.000000 0.101000 0.318000 -8.389250 0.038800 98.988250 1.737762e+05 4.000000
50% 3999.50000 2.016580e+05 56.000000 1.000000 0.297000 0.647000 0.664000 0.000000 0.143000 0.516000 -6.323000 0.057400 123.829000 2.016585e+05 4.000000
75% 5999.25000 2.347822e+05 66.000000 1.000000 0.579000 0.743000 0.800000 0.000067 0.352000 0.706000 -4.717750 0.112000 140.036000 2.347822e+05 4.000000
max 7999.00000 1.147406e+06 95.000000 1.000000 0.996000 0.979000 1.000000 1.000000 1.000000 0.980000 0.710000 0.841000 211.974000 1.147406e+06 5.000000

Como argumentos padrões do próprio método, foram retornadas somente das variáveis numéricas, mas também podemos fazer isso para as variáveis categóricas passando o tipo como argumento.

In [13]:
# analisando estatísticas descritivas (categóricas)
df.describe(include='O')
Out[13]:
name album artist genre url
count 8000 8000 8000 8000 8000
unique 7460 4715 2062 1147 8000
top Alone Barulho de Chuva para Dormir (Com Trovões) Various Artists [] https://open.spotify.com/track/0OGw7V8wAI6OsZI...
freq 5 40 196 292 1

Olhando somente para os números não são muito intuitivos, vamos então analisar graficamente, utilizado a biblioteca seaborn.
Primeiro, vamos criar um dataframe filtrando somente as variáveis de tipo numéricas, depois criaremos um loop para plotar os dados.

In [14]:
# separando o conjunto de dados em tipos numéricos
df_num = df.select_dtypes(['float64', 'int64'])

# definindo a área de plotagem
plt.figure(figsize=(14,8))

# plotando os gráficos
for i in range(1, len(df_num.columns)):
    ax = plt.subplot(3, 5, i)
    sns.distplot(df_num[df_num.columns[i]])
    ax.set_title(f'{df_num.columns[i]}')
    ax.set_xlabel('')

# otimizando o espaçamento entre os gráficos
plt.tight_layout()

Podemos notar vários tipos de distribuição entre as variáveis e escalas diferentes. A princípio fizemos isso mais para saber como são as distribuições, não vamos analisar nada muito a fundo nesse momento.

Vamos analisar também a correção entre essas variáveis, utilizando um heatmap.

In [15]:
# definindo a área de plotagem
plt.figure(figsize=(14,8))

# plotando o gráfico
sns.heatmap(df_num.iloc[:, 1:].corr(), vmin=-1, vmax=1, annot=True).set_title('Correlação entre as variáveis');

Analisando as correlações, podemos notar que algumas variáveis se correlacionam fortemente, então não há problema em remover pelo menos uma delas, dependendo o caso o modelo pode ficar enviesado, vou optar em remover pelo menos uma, de cada duas correlactionadas fortemente.

In [16]:
df_num.drop(['length', 'loudness'], axis=1, inplace=True)

Pré-processamento dos dados

Para o pré-processamento dos dados, vamos utilizar o MinMaxScaler do scikit-learn. Vamos normalizar porque como vimos acima, nas distribuições dos dados, as escalas são muito diferentes o framework é sensível à isso, se não fizermos isso poderá dar um resultado bem diferente do esperado.

O formato das distribuições continuarão as mesmas, porém estarão na mesma escala.

In [17]:
# instanciando o tranformador
scaler = MinMaxScaler()

# treinando e transformando os dados
scaled = scaler.fit_transform(df_num)

# colocando os dados transformados em um dataframe
df_scaled = pd.DataFrame(scaled, columns=df_num.columns)

# olhando o resultado
df_scaled.head()
Out[17]:
index popularity mode acousticness danceability energy instrumentalness liveness valence speechiness tempo duration_ms time_signature
0 0.000000 0.894737 1.0 0.085040 0.859040 0.727995 0.000000 0.1490 0.438776 0.057551 0.613514 0.189152 0.8
1 0.000125 0.543860 1.0 0.880522 0.690501 0.373988 0.000132 0.1650 0.204082 0.031629 0.470930 0.154720 0.8
2 0.000250 0.614035 1.0 0.548193 0.794688 0.462989 0.002890 0.0731 0.506122 0.046254 0.575538 0.145182 0.8
3 0.000375 0.508772 1.0 0.694779 0.661900 0.541991 0.000000 0.0923 0.852041 0.042806 0.697543 0.170944 0.8
4 0.000500 0.491228 0.0 0.020783 0.669050 0.663993 0.000000 0.0771 0.953061 0.212842 0.811444 0.126824 0.8

Vamos confirmar o que foi dito, sobre as distribuições e escalas.

In [18]:
# definindo a área de plotagem
plt.figure(figsize=(14,8))

# plotando os gráficos
for i in range(1, len(df_scaled.columns)):
    ax = plt.subplot(3, 5, i)
    sns.distplot(df_scaled[df_scaled.columns[i]])
    ax.set_title(f'{df_scaled.columns[i]}')
    ax.set_xlabel('')

# otimizando o espaçamento entre os gráficos
plt.tight_layout()

Todas as escalas estão de 0 à 1.

Machine Learning

Agora vamos partir para o aprendizado do modelo, antes um breve introdução do algoritmo que será usado e como é seu funcionamento.

Clustering

Clustering é o conjunto de técnicas para análise de agrupamento de dados, que visa fazer agrupamentos automáticos de dados segundo o grau de semelhança. O algoritmo que vamos utilizar é o K-Means.

K-Means

Como funciona o algoritmo K-Means?
O K-Means agrupa os dados tentando separar as amostras em grupos de variancias iguais, minimizando um criterio conhecido como inertia ou wcss (within-cluster sum-of-squares), em português, soma dos quadrados dentro do cluster, ou seja, minimizar essa soma dentro do cluster, quanto menor, melhor o agrupamento.

Como definir a quantidade de grupos?
Uma técnica à se usar é a do cotovelo, com base na inertia ou wcss, onde definimos, basicamente, quando a diferença da inertia parar de ser significativa. Esse método compara a distância média de cada ponto até o centro do cluster para diferentes números de cluster.

Além do método do cotovelo, para identificar o melhor número de clusters para nossos dados, podemos também utilizar inspeção visual, conhecimento prévio dos dados e do negócio e as vezes já temos até um número pré-definido, dependendo do objetivo.

Exemplo de como funciona a técnica do cotovelo. gif

Vamos começar!

In [19]:
# criando uma lista vazia para inertia
wcss_sc = []

# criando o loop
for i in range(1, 50):
    
    # instanciando o modelo
    kmeans = KMeans(n_clusters=i, random_state=42)
    
    # treinando o modelo
    kmeans.fit(df_scaled.iloc[:, 1:])
    
    # salvando os resultados
    wcss_sc.append(kmeans.inertia_)
In [20]:
# plotando o Elbow Method
plt.figure(figsize=(12,6))
plt.plot(range(1, 50), wcss_sc, 'o')
plt.plot(range(1, 50) , wcss_sc , '-' , alpha = 0.5)
plt.title('Elbow Method')
plt.xlabel('Number of Clusters')
plt.ylabel('WCSS')
# plt.savefig('Elbow_Method.png')
plt.show()

Vamos fazer um teste com 20 grupos, pois podemos notar que a diferença não será mais muito significativa a partir desse valor. Claros que podemos testar com outros quantidades depois.

Então, vamos instanciar o modelo que vamos usar, definindo o número correto de clusters, e vamos ver como os dados foram separados.

In [21]:
# instanciando o modelo
kmeans = KMeans(n_clusters=20, random_state=42)

# treinando, excluindo a primeira linha que é o index
kmeans.fit(df_scaled.iloc[:, 1:])

# fazendo previsões
y_pred = kmeans.predict(df_scaled.iloc[:, 1:])
In [22]:
# Visualizando os clusters em um dataframe
cluster_df = pd.DataFrame(y_pred, columns=['cluster'])

# visualizando a dimensão
cluster_df.shape
Out[22]:
(8000, 1)

Vamos unir o dataframe original com os grupos que o algoritmo definiu para cada linha.

In [23]:
# concatenando com o dataset original
df_new = pd.concat([df, cluster_df], axis=1)

# checando o dataframe
df_new.head()
Out[23]:
index name album artist release_date length popularity mode acousticness danceability energy instrumentalness liveness valence loudness speechiness tempo duration_ms time_signature genre url cluster
0 0 goosebumps Birds In The Trap Sing McKnight Travis Scott 2016-09-16 243836 89 1 0.0847 0.841 0.728 0.000000 0.1490 0.430 -3.370 0.0484 130.049 243837 4 ['rap'] https://open.spotify.com/track/6gBFPUFcJLzWGx4... 9
1 1 Trevo (Tu) ANAVITÓRIA ANAVITÓRIA 2016-08-18 205467 69 1 0.8770 0.676 0.374 0.000132 0.1650 0.200 -8.201 0.0266 99.825 205468 4 ['folk brasileiro', 'nova mpb', 'pop lgbtq+ br... https://open.spotify.com/track/2vRBYKWOyHtFMti... 4
2 2 Hear Me Now Hear Me Now Alok 2016-10-21 194840 73 1 0.5460 0.778 0.463 0.002890 0.0731 0.496 -7.603 0.0389 121.999 194840 4 ['brazilian edm', 'edm', 'electro house', 'pop... https://open.spotify.com/track/39cmB3ZoTOLwOTq... 10
3 3 Refém O Cara Certo Dilsinho 2016-06-10 223546 67 1 0.6920 0.648 0.542 0.000000 0.0923 0.835 -6.292 0.0360 147.861 223547 4 ['pagode', 'pagode novo'] https://open.spotify.com/track/4muZ1PUxmss4cVm... 0
4 4 Ela Só Quer Paz Ela Só Quer Paz Projota 2016-01-27 174382 66 0 0.0207 0.655 0.664 0.000000 0.0771 0.934 -4.506 0.1790 172.005 174382 4 ['brazilian edm', 'brazilian hip hop', 'pop na... https://open.spotify.com/track/1RPrzKZdkzCNV0U... 5
In [24]:
# olhando a nova dimensão
df_new.shape
Out[24]:
(8000, 22)

Analisando os clusters

Vamos responder algumas perguntas, relacionadas ao resultado.

Qual a média, desvio padrão, min e max dos elementos que compõe os clusters?

In [25]:
# agrupando por cluster e calculando a média
df_new.groupby('cluster')['name'].count().describe()
Out[25]:
count     20.000000
mean     400.000000
std      197.822621
min       46.000000
25%      290.750000
50%      417.500000
75%      531.750000
max      709.000000
Name: name, dtype: float64
In [26]:
print(f"Temos em média {df_new.groupby('cluster')['name'].count().describe()['mean']:.2f} elementos por cluster, \
com um desvio padrão de {df_new.groupby('cluster')['name'].count().describe()['std']:.2f}.")
print(f"O Cluster com a menor quantidade de elementos possui {df_new.groupby('cluster')['name'].count().describe()['min']:.0f}\
 e o maior possui {df_new.groupby('cluster')['name'].count().describe()['max']:.0f} elementos.")
Temos em média 400.00 elementos por cluster, com um desvio padrão de 197.82.
O Cluster com a menor quantidade de elementos possui 46 e o maior possui 709 elementos.

Podemos notar uma certa dispersão nos dados, isso pode ser devido à coleta de dados não balanceadas.

In [27]:
sns.distplot(df_new.groupby('cluster')['name'].count())
Out[27]:
<matplotlib.axes._subplots.AxesSubplot at 0x263e044e508>

Podemos notar que os grupos estão desbalanceados, precisaríamos levar em consideração as regras de negócios, para sabermos se os grupos criados estão adequados com base nas músicas, ritmos entre outros.

Vamos dar uma olhada nos números de outros atributos.

A popularidade entre os grupos seguem uma distribuição normal.

In [28]:
# agrupando por cluster e calculando a média
sns.distplot(df_new.groupby(['cluster'])['popularity'].mean())
Out[28]:
<matplotlib.axes._subplots.AxesSubplot at 0x263dfe9c588>
In [29]:
df_new.groupby(['cluster'])['popularity'].mean().skew()
Out[29]:
0.7931223632486717
In [30]:
df_new.groupby(['cluster'])['popularity'].mean().kurtosis()
Out[30]:
-0.7661320783566028

Com os resultados de skewness e kurtosis, vemos que os dados não obedecem a uma distribuição normal, isso pode ser caracterizado por amostras de diferentes populações.

Qual é o grupo com mais e menos elementos?

In [31]:
# checando o cluster com mais músicas
pd.DataFrame(df_new.cluster.value_counts()).reset_index()[:1].rename(columns={'index': 'cluster', 'cluster': 'qtd'})
Out[31]:
cluster qtd
0 6 709
In [32]:
# checando o cluster com menos músicas
pd.DataFrame(df_new.cluster.value_counts()).reset_index()[-1:].rename(columns={'index': 'cluster', 'cluster': 'qtd'})
Out[32]:
cluster qtd
19 12 46

Vamos dar uma olhada nos dados dos grupos que contém o menor conjunto de elementos.

In [33]:
df_new[df_new.cluster == 12]
Out[33]:
index name album artist release_date length popularity mode acousticness danceability energy instrumentalness liveness valence loudness speechiness tempo duration_ms time_signature genre url cluster
7 7 Devil Eyes Providence Hippie Sabotage 2016-02-05 131271 80 0 0.702 0.3910 0.396000 0.405 0.3150 0.19900 -8.621 0.1890 99.112 131272 5 ['edm'] https://open.spotify.com/track/7MiZjKawmXTsTNe... 12
309 309 Quick Musical Doodles First Steps Two Feet 2016-07-29 144000 66 0 0.241 0.6880 0.349000 0.685 0.3740 0.43300 -7.182 0.2700 169.773 144000 4 ['indie poptimism', 'modern rock'] https://open.spotify.com/track/7tZdkPtebOG29Tz... 12
427 427 5:32pm Vibes 2 The Deli 2016-10-17 136914 72 0 0.686 0.6910 0.186000 0.934 0.0762 0.54800 -13.672 0.0487 86.383 136914 4 ['chillhop', 'lo-fi beats', 'lo-fi house'] https://open.spotify.com/track/7qrBYrivpvfXUPB... 12
789 789 Canto de Ossanha Os Primeiros Anos Toquinho 2016-03-18 122760 46 0 0.947 0.4570 0.308000 0.874 0.1080 0.47000 -11.795 0.0491 196.065 122760 4 ['bossa nova', 'mpb', 'samba', 'violao'] https://open.spotify.com/track/5N8wA5SKIlFk2gt... 12
830 830 Só de Tú Nada Sem Ela Academia da Berlinda 2016-07-01 240333 45 0 0.765 0.7000 0.659000 0.886 0.3650 0.94100 -8.270 0.0305 122.165 240333 4 ['brazilian indie', 'manguebeat', 'mpb', 'nova... https://open.spotify.com/track/0Sl8XcWYCcCZs7b... 12
1168 1168 French Inhale 444 [bsd.u] 2016-12-02 108078 68 0 0.499 0.8700 0.146000 0.941 0.1050 0.88900 -11.877 0.2200 80.087 108078 4 ['chillhop', 'japanese chillhop', 'lo-fi beats... https://open.spotify.com/track/4JLI9AgtAXutJvj... 12
1431 1431 You're so Cold First Steps Two Feet 2016-07-29 196128 58 0 0.156 0.8940 0.334000 0.782 0.1050 0.34600 -7.852 0.0838 93.025 196128 4 ['indie poptimism', 'modern rock'] https://open.spotify.com/track/6ytwjgwBRxYTUxY... 12
1432 1432 Fear of the Water Hurt for Me SYML 2016-09-09 245813 60 0 0.954 0.4750 0.208000 0.883 0.1180 0.15200 -17.635 0.0299 93.029 245813 4 ['pop'] https://open.spotify.com/track/3xn7cwof2Srt3CR... 12
1457 1457 Only Dawn RY X 2016-05-06 268455 60 0 0.903 0.2930 0.175000 0.528 0.1090 0.08900 -15.543 0.0308 83.182 268456 3 ['indie folk', 'vapor soul'] https://open.spotify.com/track/1hrar0wbUsvgSUp... 12
1486 1486 Jovial Fresh Squeezed Limes 2016-11-20 142727 62 0 0.918 0.7640 0.185000 0.926 0.1180 0.37800 -14.408 0.0489 69.972 142728 4 ['indie pop', 'indie poptimism', 'indie rock',... https://open.spotify.com/track/2COBMrpsGXEagCG... 12
1535 1535 Light of the Seven Game of Thrones (Music from the HBO® Series - ... Ramin Djawadi 2016-06-24 589094 62 0 0.903 0.2730 0.102000 0.721 0.2680 0.05830 -16.843 0.0494 120.378 589095 4 ['german soundtrack', 'scorecore', 'soundtrack... https://open.spotify.com/track/6iLzFJhs4ATwJn7... 12
1682 1682 The Watchtower The Watchtower Sigimund 2016-07-20 205577 58 0 0.991 0.2480 0.027400 0.922 0.1530 0.15900 -28.768 0.0333 75.124 205578 1 ['background music', 'focus'] https://open.spotify.com/track/504NLPDUBRylbZU... 12
1815 1815 Walls To Build - Mall Grab Remix Walls To Build (Mall Grab Remix) Kllo 2016-01-01 376079 54 0 0.136 0.8300 0.403000 0.816 0.1090 0.40000 -11.360 0.0451 124.001 376079 4 ['alternative r&b', 'aussietronica', 'electrop... https://open.spotify.com/track/6wmWC3kGcFvumqB... 12
2280 2280 Som do Útero Sons da Natureza para Relaxar e Dormir Para Dormir 2017-06-20 98992 55 0 0.908 0.1590 0.097000 0.971 0.1030 0.23700 -42.363 0.0455 78.615 98993 4 ['musica para ninos'] https://open.spotify.com/track/4PQ2yeFmkWrhIVy... 12
2482 2482 Mystery of Love Call Me By Your Name (Original Motion Picture ... Various Artists 2017-11-03 248964 70 0 0.940 0.3650 0.273000 0.431 0.1090 0.23800 -16.526 0.0380 132.285 248965 5 ['reggaeton', 'reggaeton flow'] https://open.spotify.com/track/4HbeGjbt7u3pvwD... 12
2493 2493 Losing Interest Missing itssvd 2017-02-12 120546 74 0 0.480 0.7530 0.355000 0.865 0.1220 0.50600 -13.295 0.0898 111.863 120547 4 ['lo-fi chill', 'sad rap'] https://open.spotify.com/track/1LGFhRGycG5VI9c... 12
2547 2547 Mystery of Love Mystery of Love Sufjan Stevens 2017-12-01 248960 71 0 0.940 0.3650 0.273000 0.431 0.1090 0.23800 -16.526 0.0380 132.285 248960 5 ['baroque pop', 'chamber pop', 'freak folk', '... https://open.spotify.com/track/5GbVzc6Ex5LYlLJ... 12
2579 2579 i'm closing my eyes i'm closing my eyes potsu 2017-05-17 118302 74 0 0.534 0.8950 0.109000 0.549 0.1060 0.54600 -13.853 0.0996 134.067 118302 4 ['japanese chillhop', 'lo-fi beats'] https://open.spotify.com/track/3NsuucK8qXpIJf7... 12
2672 2672 White Noise - 500 hz Unison Granular 2017-10-13 147096 79 0 0.923 0.0000 0.000050 0.297 0.1100 0.00000 -32.354 0.0000 0.000 147097 0 ['sleep', 'white noise'] https://open.spotify.com/track/65rkHetZXO6DQmB... 12
2772 2772 Visions of Gideon Call Me By Your Name (Original Motion Picture ... Various Artists 2017-11-03 247808 68 0 0.942 0.2520 0.314000 0.676 0.0892 0.20000 -18.197 0.0407 75.056 247808 4 ['reggaeton', 'reggaeton flow'] https://open.spotify.com/track/0SUDiaR0qm30fXV... 12
2813 2813 O Peso do Meu Coração Sintoma Castello Branco 2017-10-01 380390 49 0 0.815 0.5620 0.284000 0.510 0.1250 0.04740 -10.881 0.0305 76.912 380390 4 ['ecuadorian indie', 'ethnotronica', 'latintro... https://open.spotify.com/track/2DmEvNxGJOLyuQB... 12
2934 2934 Som do Útero Materno, Pt. 1 Som do Útero Sons da Natureza Projeto ECO Brasil 2017-09-13 72078 48 0 0.916 0.0919 0.073000 0.967 0.1040 0.34800 -41.215 0.0365 72.686 72079 4 [] https://open.spotify.com/track/5ndD8oxF6VsXoet... 12
3325 3325 No Moon At All Maude De Geyndt Maude De Geyndt 2017-06-27 242102 45 0 0.995 0.3410 0.033400 0.914 0.0920 0.18200 -26.968 0.0483 84.963 242103 4 [] https://open.spotify.com/track/4qyrWuIir1FTYhr... 12
3395 3395 Drifter Drifter Hippie Sabotage 2017-05-19 210307 63 0 0.853 0.3890 0.308000 0.937 0.1720 0.13000 -14.460 0.0751 129.138 210308 4 ['edm'] https://open.spotify.com/track/1DehtVbbTjUQjxG... 12
3563 3563 White Noise - 145 hz Unison Granular 2017-10-13 135483 78 0 0.944 0.0000 0.000020 0.869 0.1120 0.00000 -40.449 0.0000 0.000 135484 0 ['sleep', 'white noise'] https://open.spotify.com/track/6H4B9gJD6eQlNoE... 12
3830 3830 Sonido del Utero Sonidos de la Naturaleza para Relajarse y Dormir Para Dormir 2017-06-20 104878 43 0 0.922 0.1790 0.116000 0.973 0.1050 0.25200 -42.825 0.0479 76.078 104878 4 ['musica para ninos'] https://open.spotify.com/track/33HhCL1tnRyyEY4... 12
3895 3895 sincerely, yours osho Nohidea 2017-02-04 139684 67 0 0.800 0.6050 0.396000 0.872 0.1410 0.62200 -11.301 0.0509 80.957 139684 4 ['chillhop', 'lo-fi beats'] https://open.spotify.com/track/3gV0E3N4uOcoNsb... 12
3964 3964 m i s t ep seeds eevee 2017-03-16 118182 65 0 0.354 0.6240 0.229000 0.470 0.1160 0.39500 -15.698 0.0460 130.069 118183 4 ['chillhop', 'japanese chillhop', 'lo-fi beats'] https://open.spotify.com/track/5j6vt3vHU22A9nu... 12
3991 3991 Som de Tempestades Som de Chuva e Trovoadas Som De Chuva E Tempestades 2017-12-14 63660 42 0 0.644 0.1750 0.581000 0.993 0.2050 0.00499 -29.475 0.0522 96.352 63660 4 [] https://open.spotify.com/track/1t9VffD5xxs1rZW... 12
4786 4786 Reasons For Being Reasons For Being Deep Watch 2018-12-14 176390 64 0 0.980 0.1410 0.028200 0.906 0.1040 0.06580 -30.003 0.0454 145.220 176391 3 ['background music', 'focus'] https://open.spotify.com/track/5tY0sWgi6v0UEib... 12
4866 4866 Piloto Piloto Flora Matos 2018-06-12 197538 51 0 0.970 0.7890 0.642000 0.803 0.1060 0.89400 -8.631 0.0379 130.021 197538 4 ['afrofuturismo brasileiro', 'brazilian hip ho... https://open.spotify.com/track/07DskqQ1NbTiotX... 12
4930 4930 Call me Collection 90sFlav 2018-02-09 126250 70 0 0.952 0.7480 0.265000 0.917 0.0866 0.14000 -12.074 0.2290 77.009 126250 4 ['lo-fi beats'] https://open.spotify.com/track/34A2accnIDPOhkR... 12
5779 5779 Nobody Cares Nobody Cares Kina 2018-03-05 175229 66 0 0.725 0.6570 0.159000 0.730 0.2790 0.08330 -15.618 0.0395 100.018 175229 4 ['pop', 'sad rap'] https://open.spotify.com/track/4CALLtGlZBwdpfQ... 12
5792 5792 whoa (mind in awe) SKINS XXXTENTACION 2018-12-07 157776 73 0 0.653 0.7350 0.525000 0.918 0.1010 0.36900 -2.939 0.0441 160.147 157777 4 ['emo rap', 'miami hip hop'] https://open.spotify.com/track/7pdF27mSDuPWhpp... 12
5848 5848 Gelborn Heights Soft Slow Sleep Guyara 2018-03-02 193349 55 0 0.968 0.1510 0.023700 0.894 0.1070 0.03370 -29.067 0.0550 139.790 193349 4 ['background music'] https://open.spotify.com/track/5D1t5wq4NGB1Z1r... 12
6168 6168 everything i wanted everything i wanted Billie Eilish 2019-11-13 245425 87 0 0.902 0.7040 0.225000 0.657 0.1060 0.24300 -14.454 0.0994 120.006 245426 4 ['electropop', 'pop'] https://open.spotify.com/track/3ZCTVFBt2Brf31R... 12
6732 6732 Forever Euphoria (Original Score from the HBO Series) Labrinth 2019-10-04 202536 76 0 0.920 0.5630 0.459000 0.724 0.1100 0.19700 -7.781 0.0292 79.983 202536 4 ['pop'] https://open.spotify.com/track/6potEImiklXkwD9... 12
7017 7017 Cinnamon Girl Norman Fucking Rockwell! Lana Del Rey 2019-08-30 300683 71 0 0.839 0.2920 0.334000 0.345 0.1510 0.14800 -10.679 0.0378 67.836 300683 4 ['art pop', 'pop'] https://open.spotify.com/track/2mdEsXPu8ZmkHRR... 12
7119 7119 Tipo Gta Tipo Gta MC Caverinha 2019-05-18 221570 53 0 0.213 0.8280 0.412000 0.749 0.0883 0.45900 -12.160 0.0835 130.034 221571 4 ['funk carioca', 'funk ostentacao', 'trap bras... https://open.spotify.com/track/3hCzQ8c7PCcPfa9... 12
7211 7211 Snowman 1 Am. Study Session Various Artists 2019-12-08 195136 72 0 0.802 0.7170 0.193000 0.893 0.0653 0.04470 -16.141 0.0459 109.992 195136 4 ['reggaeton', 'reggaeton flow'] https://open.spotify.com/track/5ehVOwEZ1Q7Ckkd... 12
7239 7239 Quiet Rain in River Rain White Noise Stereo Outdoor Sampling 2019-10-10 120083 72 0 0.125 0.1660 0.001330 0.846 0.2330 0.02180 -23.291 0.2150 119.669 120083 4 ['sound'] https://open.spotify.com/track/2dz9Mx1pZKF2OLA... 12
7480 7480 When I am with thee When I am with thee IGRAINE 2019-05-25 175124 59 0 0.986 0.2520 0.003200 0.931 0.1140 0.03860 -30.568 0.0371 71.635 175125 3 [] https://open.spotify.com/track/3bBGo3Le7RDVWYq... 12
7545 7545 goodbye WHEN WE ALL FALL ASLEEP, WHERE DO WE GO? Billie Eilish 2019-03-29 119409 72 0 0.837 0.1530 0.138000 0.550 0.2540 0.05030 -21.877 0.0503 74.318 119410 3 ['electropop', 'pop'] https://open.spotify.com/track/3LgWsmilsrWXiPY... 12
7877 7877 White Noise Crashing Waves Soothing Synth Noise Bruce Brus 2019-08-09 168000 76 0 0.862 0.0000 0.000431 0.977 0.1620 0.00000 -14.105 0.0000 0.000 168000 0 ['white noise'] https://open.spotify.com/track/3AFEx7f9qxc6U59... 12
7903 7903 In Bloom In Bloom Zoé de Vera 2019-02-10 179086 57 0 0.990 0.6370 0.081200 0.908 0.0870 0.21300 -23.060 0.0374 109.965 179087 4 [] https://open.spotify.com/track/60nhoUdtsUq4FOw... 12
7918 7918 Breathing Star Breathing Star Allana Johnson 2019-02-20 176091 55 0 0.867 0.1570 0.163000 0.888 0.0846 0.10400 -23.560 0.0377 86.352 176092 3 ['background music'] https://open.spotify.com/track/2TcvA5WlWyZdLMd... 12

Agora vamos salvar o dataframe com os respectivos grupos e para podemos analisar ou criar outras visualizações, com outros aplicativos como o Power BI ou Tableau, por exemplo.

Além do dataframe, vamos salvar o nosso modelo com treinado para utilizarmos em outra aplicação.

In [34]:
# # biblioteca que salvam os modelos
# import pickle

# # nome do modelo
# filename = 'model.pkl'

# # persistindo em disco local
# pickle.dump(kmeans, open(filename, 'wb'))

# # fazendo o download do modelo
# model_load = pickle.load(open("model.pkl", "rb"))

# # realizando as predições com o modelo treinado do disco
# model_load.predict(df_scaled.iloc[:, 1:])
In [35]:
# Create a Pandas Excel writer using XlsxWriter as the engine.
writer = pd.ExcelWriter('grupo_musicas.xlsx', engine='xlsxwriter')

# Convert the dataframe to an XlsxWriter Excel object.
df_new.to_excel(writer, sheet_name='Sheet1', index=False)

# Close the Pandas Excel writer and output the Excel file.
writer.save()

Conclusão

Criamos um framework para coleta dos dados direto da API do spotify e salvamos os dados em um arquivo .csv, fizemos algumas análises e pré-processamento dos dados.
Após criarmos um modelo de clusterização, o modelo fez as previsões para os grupos relacionados, colocamos todos em um dataframe e salvamos em um arquivo em excel.
Para os próximos passos, vamos criar uma POC (Proof of Concept) com o framework streamlit e publicar um dashboard criado com os dados em Power BI.

link para dashboard do Power BI: https://bit.ly/3j2KCiU

Há outros tipos de modelos para clusterização, mas utilizamos o KMeans por ser um dos mais utilizados e conseguir ter um bom resultado de forma fácil.

Referencias