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.