Melancholičnost (smutnost / zádumčivost / tristnost) skladby, ať už je to jakkoliv abstraktní pojem, si zkusím zadefinovat s pomocí https://www.tandfonline.com/doi/abs/10.1080/0929821042000317813.
Rád bych zjistil, jestli se populární hudba stává více melancholickou.¶
Data¶
Použiju data naparsovaná z oficiálních Spotify playlistů 'Top Hits of YYYY' (2008 až 2019) a 'Top Tracks of YYYY' (rok 2020 a 2022) - roky chybí díky tomu, že Top Hits už nevydali a rok 2021 z nějakého důvodu přeskočili.
Parsoval jsem přes službu http://organizeyourmusic.playlistmachinery.com/ a data pak uložil do csv souboru. Služba přímo využívá Spotify API - konkrétně pro svoje účely získává hlavně Track's Audio Features (viz níže a https://developer.spotify.com/documentation/web-api/reference/get-audio-features) - nicméně nezpracovává některé veličiny. Součástí Audio Features bohužel není text, tzn. ztrácíme jeden z důležitých ukazatelů melancholie.
# Spotify's Track's Audio Features
{
"acousticness": float,
"analysis_url": float,
"danceability": float,
"duration_ms": int,
"energy": float,
"id": str,
"instrumentalness": float,
"key": int,
"liveness": float,
"loudness": float,
"mode": int,
"speechiness": float,
"tempo": float,
"time_signature": int,
"track_href": str,
"type": "audio_features",
"uri": str,
"valence": float
};
K definování melancholie v hudbě nám nejlépe poslouží hodnoty (energy, loudness, mode, tempo, valence):
energy: Méně energetické, jednotvárnější skladby mají prý dát prostor rozjímat nad asociacemi vyvolané hudbou.
loudness: Měla by převažovat nižší hlasitost.
mode: V analýze se zaměřím, díky její popularitě, zejména na západní hudbu, ve které jsou skladby v mollové (minor) kompozici, oproti durové (major), považovány za smutnější.
tempo: Nižší, stálé tempo je typické.
valence: Čím nižší valence, tím z definice nižší pozitivita skladby.
import pandas as pd
import numpy as np
import seaborn as sns
path = '.../Statistika/top_hits_2008_2022.csv'
df = pd.read_csv(path)
df.head()
track id | title | artist | top genre | release year | hit year | bpm | nrgy | dnce | dB | live | val | dur | acous | spch | pop | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
0 | 0JXXNGljqupsJaZsgSbMZV | Sure Thing | Miguel | r&b | 2010 | 2011 | 81 | 61 | 68 | -8 | 19 | 50 | 195 | 3 | 10 | 91 |
1 | 6nek1Nin9q48AVZcWs9e9D | Paradise | Coldplay | permanent wave | 2011 | 2011 | 140 | 59 | 45 | -7 | 8 | 21 | 279 | 5 | 3 | 88 |
2 | 3di5hcvxxciiqwMH1jarhY | Set Fire to the Rain | Adele | british soul | 2011 | 2011 | 108 | 67 | 60 | -4 | 11 | 45 | 243 | 0 | 2 | 88 |
3 | 7w87IxuO7BDcJ3YUqCyMTT | Pumped Up Kicks | Foster The People | indietronica | 2011 | 2011 | 128 | 71 | 73 | -6 | 10 | 97 | 240 | 14 | 3 | 87 |
4 | 4QNpBfC0zvjKqPJcyqBy9W | Give Me Everything (feat. Ne-Yo, Afrojack & Na... | Pitbull | dance pop | 2011 | 2011 | 129 | 94 | 67 | -3 | 30 | 53 | 252 | 19 | 16 | 86 |
Jak je vidět, tak jsme bohužel ztratili i, pro nás důležitou, veličinu 'mode'.
df.info()
<class 'pandas.core.frame.DataFrame'> RangeIndex: 1330 entries, 0 to 1329 Data columns (total 16 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 track id 1330 non-null object 1 title 1330 non-null object 2 artist 1330 non-null object 3 top genre 1327 non-null object 4 release year 1330 non-null int64 5 hit year 1330 non-null int64 6 bpm 1330 non-null int64 7 nrgy 1330 non-null int64 8 dnce 1330 non-null int64 9 dB 1330 non-null int64 10 live 1330 non-null int64 11 val 1330 non-null int64 12 dur 1330 non-null int64 13 acous 1330 non-null int64 14 spch 1330 non-null int64 15 pop 1330 non-null int64 dtypes: int64(12), object(4) memory usage: 166.4+ KB
Vidíme, že 'top genre' má u tří záznamů null hodnoty. Řádky odstraním.
df = df.dropna()
Spotify yearly playlisty mají ve (zlo)zvyku obsahovat i skladby, které vyšly až po roce, který playlist nese ve jménu. Zjistím, kolik jich je a pak přepíšu jejich 'hit year' na 'release year'.
Tzn. předpokládám, že když už se zmíněné skladby vyskytly v kterémkoliv z playlistů, lze je považovat za hity i těch let, kdy vyšly.
non_sensical_rows = df[df['release year'] > df['hit year']]
print(f"Počet non-sense záznamů: {len(non_sensical_rows)}")
non_sensical_rows['hit year'].value_counts()
Počet non-sense záznamů: 86
hit year 2016 15 2015 11 2017 10 2011 9 2009 7 2010 6 2013 5 2019 5 2012 4 2014 4 2008 4 2018 3 2020 3 Name: count, dtype: int64
mask = df['release year'] > df['hit year']
df.loc[mask, 'hit year'] = df.loc[mask, 'release year']
non_sensical_rows = df[df['release year'] > df['hit year']]
print(f"Počet non-sense záznamů: {len(non_sensical_rows)}")
Počet non-sense záznamů: 0
import matplotlib.pyplot as plt
hit_years = sorted(df['hit year'].unique())
df['hit year'].hist(bins=len(hit_years))
plt.xlabel('hit year')
plt.ylabel('number of songs')
Text(0, 0.5, 'number of songs')
Odstraním roky s malým vzorkem tak, aby nereprezentovali nepřesně celý rok.
df = df.loc[(df['hit year'] != 2021) & (df['hit year'] != 2023)]
hit_years = sorted(df['hit year'].unique())
Explorační analýza¶
Nejprve se pro přehlednost podívám na korelace mezi jednotlivými veličinami.
plt.figure(figsize=(16, 8))
sns.set(style="whitegrid")
corr = df.select_dtypes(include=[np.number]).corr()
sns.heatmap(corr,annot=True, cmap="YlGnBu")
<Axes: >
Zajímá mě hlavně vývoj veličin za každý hit year.
Podívám se tedy na medián sledovaných veličin skladeb v každém 'hit roku'.
def hit_year_sub_plot_mean(axn, value, xy):
y = df.groupby("hit year")[value].mean()
axn.set_xticks([2010, 2014, 2018, 2022])
axn.plot(hit_years, y, '-o')
axn.annotate(f"mean {value}", xy=xy)
fig, ((ax1, ax2), (ax3, ax4)) = plt.subplots(2, 2, figsize=(10, 7))
hit_year_sub_plot_mean(ax1, 'nrgy', (2014,74))
hit_year_sub_plot_mean(ax2, 'dB', (2014,-5.4))
hit_year_sub_plot_mean(ax3, 'bpm', (2014,126))
hit_year_sub_plot_mean(ax4, 'val', (2014, 54))
Zajímavý náhled by nám mohla poskytnout informace, jestli pop jako žánr postupně z žebříčků ubývá.
plt.figure(figsize=(10, 5))
pop_rows = df[df['top genre'].str.contains("pop")]
y = pop_rows.groupby("hit year").size() / df.groupby("hit year").size() * 100
plt.plot(hit_years, y, '-o')
plt.xlabel('hit year')
plt.ylabel(f"percentage of 'pop' genre")
Text(0, 0.5, "percentage of 'pop' genre")
Konfirmační analýza¶
Nulová hypotéza: Mezi vybranými veličinami a rokem, kdy jsou hitem, neexistuje lineární vztah.¶
Udělám lineární regresi u veličin, které podle mé definice přispívají k melancholii. Spočítám p-values a zjistím, které nulové hypotézy mohu teoreticky zamítnout.
from scipy import stats
import statsmodels.api as sm
def plot_reg_hit_year(axn, value, text_position):
x = df[['hit year']]
y = df[value]
est = sm.OLS(y, sm.add_constant(x))
model = est.fit()
show(axn, value, text_position, model)
def show(axn, value, text_position, model):
intercept = model.params[0]
slope = model.params[1]
print(f"***** hit year - {value} {(10 - len(value))*'*'}")
print(f"Coefficient: {np.round(slope, 3)}")
print(f"Intercept: {np.round(intercept, 3)}")
print(f"R squared: {np.round(model.rsquared, 3)}")
print(f"T-value: {np.round(model.tvalues[1], 4)}")
print(f"P-value: {model.pvalues[1]}")
print(f"{32*'*'}\n")
x = np.linspace(start=2008, stop=2023, num=200)
y = slope * x + intercept
kde = stats.gaussian_kde([df['hit year'], df[value]])
density = kde([df['hit year'], df[value]])
min_size, max_size = 10, 150
normalized_density = (density - np.min(density)) / (np.max(density) - np.min(density))
point_size = min_size + normalized_density * (max_size - min_size)
axn.scatter(df['hit year'], df[value], s=point_size, alpha=0.2)
axn.set_xticks([2010, 2014, 2018, 2022])
axn.plot(x, y, 'r')
axn.annotate(f"{value}", xy=text_position)
fig, ((ax1, ax2), (ax3, ax4)) = plt.subplots(2, 2, figsize=(10, 7))
plot_reg_hit_year(ax1, 'nrgy', (2015,102))
plot_reg_hit_year(ax2, 'dB', (2015,1))
plot_reg_hit_year(ax3, 'bpm', (2015,212))
plot_reg_hit_year(ax4, 'val', (2015, 100))
***** hit year - nrgy ****** Coefficient: -1.04 Intercept: 2163.625 R squared: 0.06 T-value: -9.1599 P-value: 1.9272271634487613e-19 ******************************** ***** hit year - dB ******** Coefficient: -0.08 Intercept: 155.77 R squared: 0.022 T-value: -5.461 P-value: 5.652049794390944e-08 ******************************** ***** hit year - bpm ******* Coefficient: -0.424 Intercept: 974.386 R squared: 0.004 T-value: -2.2274 P-value: 0.02608718204022901 ******************************** ***** hit year - val ******* Coefficient: -0.584 Intercept: 1225.785 R squared: 0.01 T-value: -3.7098 P-value: 0.0002160636627792185 ********************************
Všechny p-values vyšly méně než hladina výnamnosti (0.05), tzn. můžeme zamítnout všechny 4 nulové hypotézy.
Závěr¶
Zpět k úplně původní myšlence - stává se populární hudba v poslední době čím dál tím více melancholickou?
Z pozorování vybraných veličin lze spekulovat, že hlavně energetičnost a hlasitost populární hudby mají klesající trend - u energetičnosti t-value dokonce menší než -9.
U tempa a valence skladeb toho moc říct nelze - nemůžeme ale vyloučit lineární vztah mezi každou z nich a hit rokem.
Tzn. dva parametry ze čtyř, které jsou k dispozici pro měření a poukazují na melancholičnost skladby, měly viditelnější klesající trend. Nelze tedy rostoucí melancholii v populární hudbě vyloučit. Pro potvrzení hypotézy je ale nutné mít větší vzorek, který je určitě nutné doplnit dalšími parametry jako je text a 'mode' skladeb.