Случвало ли ви се е да искате да слушате определени песни в Spotify, но без да се налага да търсите готов плейлист или ръчно да си създавате такъв?
По-лесно би било ако песните автоматично се групират по определени характеристики и след това от тях да се генерират плейлисти. Това е и причината да реша да напиша тази статия.
Чрез практически пример ще ви покажа как можете да групирате песни, които слушате в Spotify, и след това да създадете от тях плейлист. За неговото решение, ще е необходим езикът за програмиране Python и някои негови библиотеки за анализ на данни и машинно обучение.
Преглед на данните
Извадката, с която ще работим, съдържа 1846 произволно избрани песни, които съм слушала в Spotify за периода 28 юни 2015 – 26 януари 2021 година. Данните са предварително изчистени и съдържат 15 числови променливи и 10 категорийни.
Можете да изтеглите файла с първоначалната обработка от тук.
5 случайно избрани реда от данните изглеждат по следния начин:
artist | title | album | genre | year | added | last_listened | listens | bpm | nrgy | dnce | dB | live | val | dur | acous | spch | pop | add_day | add_month | add_year | ll_day | ll_month | ll_year | ll_time | cluster | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
1251 | Fifth Harmony | Squeeze | 7/27 (Deluxe) | dance pop | 2016 | 2016-09-04 | 2016-12-25 22:08:00 | 29 | 100 | 58 | 64 | -8 | 8 | 31 | 213 | 42 | 4 | 43 | 4 | 9 | 2016 | 25 | 12 | 2016 | 22:08:00 | 1 |
636 | Lauv | For Now | ~how i'm feeling~ | electropop | 2020 | 2020-03-08 | 2020-12-29 23:12:00 | 6 | 84 | 23 | 53 | -12 | 11 | 11 | 189 | 78 | 4 | 58 | 8 | 3 | 2020 | 29 | 12 | 2020 | 23:12:00 | 1 |
352 | DallasK | Sometimes | Sometimes | complextro | 2019 | 2020-05-08 | 2020-05-09 09:08:00 | 1 | 122 | 78 | 73 | -7 | 36 | 57 | 183 | 5 | 7 | 50 | 8 | 5 | 2020 | 9 | 5 | 2020 | 09:08:00 | 0 |
981 | Sabrina Carpenter | Smoke and Fire | Smoke and Fire | dance pop | 2016 | 2017-10-28 | 2017-10-30 07:58:00 | 51 | 170 | 87 | 46 | -4 | 7 | 53 | 225 | 1 | 9 | 54 | 28 | 10 | 2017 | 30 | 10 | 2017 | 07:58:00 | 2 |
638 | Lauv | Sweatpants | ~how i'm feeling~ | electropop | 2020 | 2020-03-08 | 2020-12-29 21:22:00 | 4 | 160 | 49 | 72 | -8 | 15 | 73 | 196 | 5 | 8 | 54 | 8 | 3 | 2020 | 29 | 12 | 2020 | 21:22:00 | 0 |
Кои методи за машинно обучение ще използваме?
Ще приложим първо метода на главните компоненти (Principal Component Analysis – PCA), с който можем да редуцираме дименсиите и да визуализираме данните в 2D пространство, запазвайки дисперсията в извадката. След това ще клъстеризираме данните с метода k-средни (k-means clustering), за да открием групи въз основа на определени критерии за подобие на обектите.
PCA и K-means се отнасят към задачите за машинно обучение без контролна извадка и тъй като те изискват входните данни да са числа, е нужно да създадем нов DataFrame само с числови променливи.
Категорийните променливи бихме могли да ги кодираме и използваме също, но нека тях да ги оставим извън данните, за да може като формираме клъстерите, да видим какви песни са попаднали вътре в тях и по какво точно си приличат (жанр, артист или нещо друго).
# Избор само на числови променливи и запазване в нов DataFrame
df_numeric = df.select_dtypes('int64')
5 случайно избрани реда:
year | listens | bpm | nrgy | dnce | dB | live | val | dur | acous | spch | pop | add_day | add_month | add_year | ll_day | ll_month | ll_year | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
837 | 2018 | 8 | 100 | 60 | 53 | -7 | 13 | 32 | 191 | 56 | 12 | 68 | 16 | 7 | 2019 | 17 | 7 | 2019 |
1392 | 2016 | 2 | 103 | 39 | 53 | -9 | 12 | 4 | 267 | 67 | 4 | 53 | 24 | 8 | 2016 | 21 | 5 | 2020 |
785 | 2019 | 36 | 160 | 64 | 20 | -6 | 10 | 34 | 335 | 28 | 4 | 36 | 11 | 12 | 2019 | 31 | 12 | 2019 |
501 | 2018 | 3 | 142 | 36 | 62 | -7 | 28 | 25 | 306 | 78 | 4 | 24 | 16 | 4 | 2020 | 19 | 4 | 2020 |
923 | 2015 | 9 | 85 | 52 | 59 | -9 | 12 | 34 | 260 | 7 | 3 | 87 | 16 | 6 | 2018 | 29 | 1 | 2020 |
Някои от колоните обаче са свързани с дати и не са ни полезни за групиране на песните. Тях ще ги премахнем. Нужни са ни само броя слушания на песните, различните аудио характеристики и популярността според скалата на Spotify (от 0 до 100, като 100 означава, че е много популярна песента, а 0 непопулярна).
# Премахване на ненужни колони
df_numeric.drop(['year','add_day','add_month','add_year','ll_day','ll_month','ll_year'], axis='columns', inplace=True)
Така остават 11 променливи, които ще използваме при прилагане на методите за машинно обучение.
5 случайно избрани реда:
listens | bpm | nrgy | dnce | dB | live | val | dur | acous | spch | pop | |
---|---|---|---|---|---|---|---|---|---|---|---|
1398 | 1 | 102 | 92 | 46 | -6 | 26 | 32 | 202 | 0 | 8 | 19 |
1108 | 62 | 93 | 84 | 63 | -4 | 21 | 66 | 190 | 35 | 6 | 19 |
479 | 20 | 109 | 75 | 70 | -6 | 12 | 75 | 207 | 4 | 12 | 30 |
1056 | 5 | 101 | 70 | 79 | -5 | 8 | 60 | 175 | 16 | 9 | 44 |
1322 | 1 | 138 | 57 | 59 | -7 | 13 | 32 | 227 | 29 | 5 | 19 |
Най-вероятно забелязвате, че стойностите в някои колони са доста по-високи от тези в други колони. Такава е например колоната с продължителност на песните (dur). Това е проблем за методите, които ще използваме, тъй като PCA се влияе много от дисперсията на променливите, а при K-means се изчисляват разстояния между обектите в пространството от характеристики. Поради тази причина е възможно да се определят тези променливи за по-важни. Нужно е данните да бъдат от еднакъв порядък, иначе получените резултати могат да бъдат с голяма грешка.
Мащабиране на променливите
Ще стандартизираме данните с помощта на StandardScaler().
Формулата, която се прилага, е следната:
\frac{X_{i} - \mu}{\sigma}
Формата на разпределението след трансформацията не се променя, стандартното отклонение става равно на 1, а средната стойност на всички променливи става 0.
# Стандартизиране на данните чрез StandartScaler()
X = np.array(df_numeric)
scaler = StandardScaler()
X_scaled = scaler.fit_transform(X)
5 случайно избрани реда:
listens | bpm | nrgy | dnce | dB | live | val | dur | acous | spch | pop | |
---|---|---|---|---|---|---|---|---|---|---|---|
85 | -0.371491 | 0.306033 | 0.460492 | -0.813192 | 0.0699754 | -0.735825 | -1.66148 | 1.61726 | -0.872762 | -0.662881 | 0.210776 |
566 | -0.393265 | -0.289707 | -1.25001 | -2.28932 | -1.3653 | -0.658831 | -1.92799 | 1.46588 | 1.72132 | -0.502965 | -0.062655 |
1262 | -0.458585 | 0.656469 | -0.193523 | 0.66294 | -0.647661 | 0.496083 | -0.328927 | 0.532372 | -0.485586 | -0.343049 | 0.101403 |
305 | -0.436811 | -0.149533 | -0.998464 | 1.25339 | -1.72412 | -0.504842 | -0.595437 | -0.628208 | 0.211332 | 1.0962 | 0.101403 |
794 | 1.78407 | 2.05821 | -0.193523 | -1.62506 | -0.288843 | -0.427848 | 0.115257 | -1.68787 | 1.79876 | 5.89368 | 0.757636 |
Прилагане на PCA
Ще използваме метода на главните компоненти, предоставен от библиотеката Scikit-learn. Идеята, която стои зад него, е да се намали броят на използваните характеристики, но в същото време да се запази възможно най-много информация за данните. Това става като се проектират точките от пространството с характеристики върху такова с по-малка размерност така, че разстоянията да са минимални.
# Прилагане на PCA()
pca = PCA()
pca.fit(X_scaled)
Новосъздадените променливи (компоненти) са комбинация от първоначалните характеристики и са такива, че връзката между тях (мултиколинеарността) е минимална, а по-голямата част от дисперсията е в първите компоненти. В нашия случай те са толкова на брой, колкото променливи има първоначално в извадката – 11. За да преценим оптималния брой компоненти, който ни е необходим, ще създадем 2 визуализации. Една стълбовидна диаграма, на която е представена дисперсията, обяснена от всеки от компонентите, и една линейна диаграма, показващата общата дисперсия при конкретен брой компоненти.
# Създаване на празен списък за имената на компонентите
component_names = []
# Добавяне на имената на компонентите в списъка
for i in range(1, len(pca.components_)+1):
component_names.append('PCA-' + str(i))
# Създаване на фигура
fig = go.Figure()
# Изграждане на стълбовидна диаграма
fig.add_trace(go.Bar(x=component_names, y=pca.explained_variance_ratio_))
# Добавяне на заглавия
fig['layout']['xaxis']['title'] = 'Number of Components'
fig['layout']['yaxis']['title'] = 'Explained Variance Ratio'
fig['layout']['title'] = 'PCA - Explained Variance Ratio for each component'
# Показване на визуализацията
fig.show()
От графиката става ясно, че по-голямата част от дисперсията в данните е обяснена от 1-вия компонент.
# Създаване на фигура
fig = go.Figure()
# Изграждане на линейната диаграма
fig.add_trace(go.Scatter(y=np.cumsum(pca.explained_variance_ratio_)))
# Добавяне на заглавия
fig['layout']['xaxis']['title'] = 'Number of Components'
fig['layout']['yaxis']['title'] = 'Explained Variance Ratio'
fig['layout']['title'] = 'Principal Component Analysis'
# Показване на визуализацията
fig.show()
Нека поставим за критерий компонентите да съдържат над 80% от данните. Тогава оптималния брой ще бъде 7, тъй като първите 7 компонента съдържат в себе си информация за 84.36% от данните.
След това можем да преминем към прилагане на PCA.
# Задаване на брой компоненти
pca.n_components = 7
# Прилагане на PCA
X_reduced = pca.fit_transform(X_scaled)
df_X_reduced = pd.DataFrame(X_reduced, index=df.index)
5 случайно избрани реда от данните след PCA изглеждат по следния начин:
0 | 1 | 2 | 3 | 4 | 5 | 6 | |
---|---|---|---|---|---|---|---|
371 | 0.95619 | -1.20414 | 1.88634 | 0.0874564 | 0.341647 | -1.03741 | 0.418123 |
557 | 3.99529 | -0.461566 | -0.690672 | 0.462283 | -0.551567 | 0.372696 | 1.17407 |
251 | 0.0580481 | 0.717426 | 0.553869 | -1.13996 | -0.258895 | -0.492269 | -0.165832 |
7 | 1.4627 | 0.892714 | -0.815698 | -1.23431 | 0.145045 | 0.456694 | -0.738249 |
42 | -0.67613 | 1.00259 | 0.959748 | 0.297643 | -0.520085 | -0.540276 | -1.92545 |
Клъстеризация на данните с метода K-means
K-means е един често използван метод за групиране на данни в клъстери. При него се изчисляват разстояния между всеки обект до центъра на клъстера и се организират данните така, че да има голяма прилика между обектите в клъстера и малка прилика между обектите от съседни клъстери. Този метод изисква предварително да се зададе броя клъстери.
Основни стъпки на алгоритъма:
- инициализация на центровете на клъстерите
- определяне на точките с минимално разстояние до центъра на клъстера
- преизчисляване на центровете на клъстера
- точки 2-3 се изпълняват докато центровете не спрат да се изместват значително
Съществуват множество метрики за измерване на разстояние между обектите, но най-често използваните са разстоянията на:
- Евклид:
dist_{euc} = \sqrt{\sum^N_{j=1}(x_{aj} - x_{bj})^2}
- Манхатън:
dist = \sum^N_{j=1}|x_{aj} - x_{bj}|
Какъв е оптималният брой клъстери?
За да отговорим на този въпрос ще създадем една линейна диаграма, която показва изменението на отдалечеността на точките вътре в клъстерите (inertia) в зависимост от броя клъстери.
# Създаване на празен списък за стойностите на intertia
inertias = []
# Добавяне на стойностите на inertia при различен брой клъстери към списъка
for i in range(1, 12):
kmeans = KMeans(n_clusters=i)
kmeans.fit(X_reduced)
kmeans_pred = kmeans.predict(X_reduced)
inertias.append(kmeans.inertia_)
# Създаване на фигура
fig = go.Figure()
# Изграждане на линейна диаграма
fig.add_trace(go.Scatter(x=np.array(range(1, 12)),
y=inertias[1:]))
# Настройки на визуализацията
fig.update_layout(xaxis={'dtick':1, 'title':'Number of Clusters'}, yaxis={'title':'Intertia'})
# Добавяне на заглавие
fig['layout']['title'] = 'Spotify songs - optimal number of clusters '
# Показване на визуализацията
fig.show()
Оптималният брой клъстери според визуализацията е 3 или 4, защото след това наклонът на кривата става по-плавен и при по-големите бройки клъстери разликата в стойностите за отдалечеността на точките вътре в клъстерите е по-малка. Ние ще групираме данните в 3 клъстера обаче, тъй като по време на тест на алгоритъма с единия и след това с другия брой, при 4 клъстера не се получиха толкова добри резултати.
След като сме определили на колко групи да се разделят данните, можем да продължим нататък с прилагане на метода за клъстеризация. Ще използваме KMeans(), предоставен от библиотеката Scikit-learn.
# Прилагане на KMeans()
kmeans = KMeans(n_clusters=3, random_state=3442)
kmeans.fit(X_reduced)
kmeans_pred = kmeans.predict(X_reduced)
След прилагане на KMeans(), всяка от песните в извадката е разпределена в един от 3 клъстера в зависимост от нейните характеристики.
Резултати и създаване на плейлист
За да представим получените резултати от групирането на песните, ще създадем 2 визуализации.
# Създаване на речник с имената на компонентите и процентът дисперсия
labels = {
str(i): f'PC {i+1} ({var:.1f}%)'
for i, var in enumerate(pca.explained_variance_ratio_ * 100)
}
# Изчисляване на общата дисперсия
total_var = pca.explained_variance_ratio_.sum() * 100
# Създаване на визуализацията
fig = px.scatter_matrix(
X_reduced,
labels=labels,
dimensions=range(6),
color=kmeans.labels_,
width=900,
height=800,
title=f'Total Explained Variance: {total_var:.2f}%'
)
# Настройки на визуализацията - скриване на диагонала
fig.update_traces(diagonal_visible=False)
# Показване на визуализацията
fig.show()
Първата включва множество диаграми на разсейването, показващи връзката между отделните компоненти. На нея можем да видим с различни цветове клъстерите и да изберем тези 2 компонента, при които те са разделени най-ясно. В този случай това са 1 и 3.
# Създаване на фигура
fig = go.Figure()
# Визуализиране на точките в клъстерите
fig.add_trace(go.Scatter(x=df_X_reduced[0],
y=df_X_reduced[1],
text='Song: ' + df['artist'] + ' - ' + df['title'],
name='',
mode='markers',
marker=pgo.scatter.Marker(
size=df_numeric['pop'],
sizemode='diameter',
sizeref=df_numeric['pop'].max()/20,
opacity=0.8,
color=kmeans.labels_),
showlegend=False))
# Визуализиране на центровете на клъстера
fig.add_trace(go.Scatter(x=kmeans.cluster_centers_[:, 0],
y=kmeans.cluster_centers_[:, 1],
name='',
mode='markers',
marker=pgo.scatter.Marker(symbol='x',
size=12,
color='red'),
showlegend=False))
# Добавяне на заглавия
fig['layout']['xaxis']['title'] = 'Principal Component 1'
fig['layout']['yaxis']['title'] = 'Principal Component 2'
fig['layout']['title'] = 'Visualization of Clusters'
# Показване на визуализацията
fig.show()
На тази графика се вижда по-ясно как са разделени песните. Размера на точките е на базата на популярността на конкретната песен. Колкото по-големи са, толкова песента е по-популярна.
Създадените клъстери са следните:
- Клъстер 0 (Син клъстер) е със 716 песни, предимно такива, които са енергични, бързи и весели, най-вече от жанра k-pop.
- Клъстер 1 (Розов клъстер) включва 417 песни, които са най-вече от жанра инди и са бавни и тъжни.
- Клъстер 2 (Жълт клъстер) има 713 песни, предимно такива, на които може да се танцува, предназначени за клубове. Най-много песни в този клъстер са от жанр dance pop и electropop.
От тук нататък можем от всеки клъстер да извадим определен брой песни и да ги запазим във файл с разширение .csv, след което да използваме онлайн платформа като Soundiiz, за да импортираме плейлист в Spotify.
# Запазване на 10 произволни песни от Клъстер 0 в нов DataFrame
playlist_c0 = df[df['cluster'] == 0][['title', 'artist', 'album']].sample(10)
# Запазване на данните в нов csv файл
playlist_c0.to_csv('./Data/playlist.csv', index=False, sep=';')
10 случайно избрани песни от Клъстер 0:
title | artist | album | |
---|---|---|---|
1222 | Don't Wanna Dance Alone | Fifth Harmony | Better Together |
1630 | VVITH | NU'EST | Q IS. |
472 | Drove You Away | Fly By Midnight | Happy About Everything Else… |
1115 | RUMOR | KARD | K.A.R.D Project Vol.3 "RUMOR" |
1098 | Your Song | Rita Ora | Your Song |
1248 | The Life | Fifth Harmony | 7/27 (Deluxe) |
1749 | The Art Of Breaking | Thousand Foot Krutch | Deja Vu: The TFK Anthology |
1176 | Oh NaNa (Hidden. HUR YOUNG JI) | KARD | K.A.R.D Project Vol.1 "Oh NaNa" |
1650 | Broken Heart | After School | PLAYGIRLZ |
1582 | Will You Be Alright | Beast | Hard To Love, How To Love |
За да е успешно импортирането на плейлиста в Soundiiz, е необходимо csv файлът ни да съдържа само колоните заглавие (title), изпълнител (artist) и албум (album), а разделителят да е точка и запетая (semicolon).
Като извод можем да кажем, че групирането на песните се е получило доста добре, макар да има и такива, които се различават от останалите в съответния клъстер, но в повечето случаи наистина си приличат. Резултатите от клъстеризацията могат да бъдат подобрени като се правят множество експерименти с различни стойности на параметрите на KMeans().
Цялостния пример можете да изтеглите от тук.
Искате да научите повече за Python?
Включете се в курса по програмиране с Python.
Автор: Десислава Христова