Data-analyysi levyraadin tuloksista

Esittelen tässä Jupyter-muistiossa Python-ohjelmointikielen data-analyysityökaluja. Muistion esimerkit pohjautuvat Facebook-ryhmässä järjestettyyn musiikkikilpailuun, joka on mukailtu televisio-ohjelmasta Levyraati.

Analyysissä käytetyt menetelmät:

  • laatikkodiagrammi, joka sisältää kvartiilit ja mediaanin
  • keskihajonta
  • aineiston normalisointi
  • euklidisen etäisyyden mittaaminen
  • pääkomponenttianalyysi

Aineisto

Aineisto on kerätty Facebook-ryhmästä keväällä 2016. Mukana olleet raatilaiset ovat arvostelleet jokaisen kappaleen, kirjoittaneen sanallisen arvioinnin sekä antaneet numeerisen arvosanan. Tässä analyysissä hyödynnän vain arvosanoja.

  • 10 kappaletta
  • 7 arvostelijaa
  • arvosanat asteikolla 0,5 - 5

Hae aineisto

In [1]:
# Data-analyysin perustyökalut
import numpy as np
import pandas as pd
In [2]:
# Kuvaajat
%matplotlib inline
import seaborn as sns
sns.set_style("whitegrid")
In [3]:
df = pd.read_csv('levyraati-2016.csv', index_col=0, decimal=',')
df
Out[3]:
Sami Joe Mikko Taneli Arttu Ville Kirmo
Kappale
Tungevaag & Raaban – Parade 4.50 0.50 3 2 2.0 2.00 4.50
Asa – Mä haluun olla hippi 1.00 2.25 4 1 2.0 2.00 2.50
Chubby Wolf – You are the Description that brings me out of Myself 0.50 1.00 1 3 4.0 1.00 2.00
Supperheads – Easy 2.00 2.75 5 3 2.0 5.00 3.50
Dream Theater – The Silent Man 2.25 2.25 1 3 2.5 4.50 3.75
Tony Bennett & Lady Gaga – It Don't Mean a Thing 3.00 1.25 3 3 4.0 1.50 4.00
Elina Born – Miss Calculation 2.00 2.50 4 4 1.0 3.00 3.75
Caravan Palace – Lone Digger 0.50 2.50 5 5 4.0 1.75 4.75
Avril Lavigne – When You're Gone 4.50 2.75 2 3 1.0 3.75 4.25
Halestorm – Here's to US 3.50 4.00 4 4 0.5 3.50 4.00

Analysoi musiikkikappaleet

Mikä kappale voitti levyraadin?

Levyraadin voittaa eniten pisteitä kerännyt kilpailija. Raadissamme oli kaksi tasavertaista kappaletta: Here's to US ja Lone Digger.

In [4]:
df.sum(axis=1).sort_values().plot(kind = 'barh', title = 'Yhteenlasketut pisteet suurimmasta pienimpään')
Out[4]:
<matplotlib.axes._subplots.AxesSubplot at 0x7f2af6cc3da0>

Oliko raati arvosteluissaan yksimielinen?

Seuraavaksi selvitän, kuinka yksimielisesti raati äänesti. Aloitan piirtämällä laatikkodiagrammin, joka paljastaa kappaleiden yksittäiset arvosanat, vaihteluvälin, mediaanin sekä ylä- ja alaneljänneksen.

Voin nähdä kuvioista, että kappaleiden pisteytyksessä on suurta vaihtelua. Tämä viittaa siihen, että raati ei ole arvostelussaan yksimielinen. Lisäksi huomaan, että tiettyjen kappaleiden laatikot ovat muita pidempiä, eli nämä kappaleet saavat ristiriitaisia arvosteluja.

In [5]:
ax = sns.boxplot(data=df.T, orient='h')
ax = sns.swarmplot(data=df.T, orient='h', color='black')
ax.set_title('Kappaleiden arvosanajakauma')
Out[5]:
<matplotlib.text.Text at 0x7f2af4946748>

Pystyn jo kuvaa katsomalla arvailla, mikä kappaleista on saanut ristiriitaisimman vastaanoton, mutta haluan todeta asian laskennallisesti. Käytän tarkoitukseeni standardipoikkeamaa eli keskimääräistä poikkeamaa odotusarvosta. $$D(X)=\sigma_x = \sqrt{\sigma^{2}_x}$$

Kappaleen Lone Digger arvosanat poikkeavat 1,8 pistettä odotusarvosta. Levyraadin ääniharava on myös kierroksen ristiriitaisin kappale. Pienin keskihajonta on Asan kappaleella Mä haluun olla hippi, eli tuomaristo on yksimielisesti pitänyt sita raadin heikoimpiin kuuluvana esityksenä.

In [6]:
pd.DataFrame(df.std(axis=1).sort_values(ascending=False), columns=['Keskihajonta'])
Out[6]:
Keskihajonta
Kappale
Caravan Palace – Lone Digger 1.790351
Tungevaag & Raaban – Parade 1.463850
Chubby Wolf – You are the Description that brings me out of Myself 1.286375
Halestorm – Here's to US 1.281740
Supperheads – Easy 1.264205
Avril Lavigne – When You're Gone 1.253566
Dream Theater – The Silent Man 1.136515
Elina Born – Miss Calculation 1.135205
Tony Bennett & Lady Gaga – It Don't Mean a Thing 1.086990
Asa – Mä haluun olla hippi 1.019162

Analysoi arvostelijat

Kuka on avokätisin raatilainen?

Edellisessä tarkastelussa huomasin, että sama kappale saa vaihtelevia pisteitä eri arvostelijoilta. Seuraavaksi haluan selvittää, miten raadit jäsenet eroavat toisistaan. Aloitan laskemalla, mitä raatilaiset antavat keskimäärin arvosanaksi kappaleille.

In [7]:
df.mean().sort_values().plot(kind='barh')
Out[7]:
<matplotlib.axes._subplots.AxesSubplot at 0x7f2af482b710>

Keskiarvoista näkee, että Kirmo on antaa keskimäärin parhaimmat pisteet ja Joe huonoimmat. Ero voi johtua siitä, että Kirmo piti tämän kierroksen kappaleista enemmän kuin Joe tai sitten he käyttävät arvosteluasteikkoa eri tavalla.

Normalisoi arvosanat

Arvosana 3 voi olla yhdelle jäsenelle hyvä ja toiselle huono. Tuomaristo käyttää arvosteluasteikkoa eri tavoin, joten pisteet eivät välttämättä ole vertailukelpoisia. Haluan, että lukuja on helppo vertailla keskenään, joten normalisoin luvut. Normalisoinnin jälkeen jälkeen arvosanat ovat nollan ja yhden välillä.

$$X' = \frac{X - X_{min}}{X_{max} - X_{min}}$$

In [8]:
# Määritellään kaavan mukainen normalisointifunktio
def normalize_values(x):
    return( (x - x.min()) / (x.max() - x.min()) )
In [9]:
df_norm = df.apply(normalize_values)
df_norm
Out[9]:
Sami Joe Mikko Taneli Arttu Ville Kirmo
Kappale
Tungevaag & Raaban – Parade 1.0000 0.000000 0.50 0.25 0.428571 0.2500 0.909091
Asa – Mä haluun olla hippi 0.1250 0.500000 0.75 0.00 0.428571 0.2500 0.181818
Chubby Wolf – You are the Description that brings me out of Myself 0.0000 0.142857 0.00 0.50 1.000000 0.0000 0.000000
Supperheads – Easy 0.3750 0.642857 1.00 0.50 0.428571 1.0000 0.545455
Dream Theater – The Silent Man 0.4375 0.500000 0.00 0.50 0.571429 0.8750 0.636364
Tony Bennett & Lady Gaga – It Don't Mean a Thing 0.6250 0.214286 0.50 0.50 1.000000 0.1250 0.727273
Elina Born – Miss Calculation 0.3750 0.571429 0.75 0.75 0.142857 0.5000 0.636364
Caravan Palace – Lone Digger 0.0000 0.571429 1.00 1.00 1.000000 0.1875 1.000000
Avril Lavigne – When You're Gone 1.0000 0.642857 0.25 0.50 0.142857 0.6875 0.818182
Halestorm – Here's to US 0.7500 1.000000 0.75 0.75 0.000000 0.6250 0.727273

Normalisoinnin jälkeen laskemme voittajan uudelleen. Jos kaikki raatilaiset olisivat käyttäneet arvosteluasteikkoa yhtä laajasti, niin kilpailun olisi voittanut kappale Lone Digger.

In [10]:
df_norm.T.sum().sort_values().plot(kind='barh')
Out[10]:
<matplotlib.axes._subplots.AxesSubplot at 0x7f2af47aff28>

Ketkä arvostelijat ovat samankaltaisia?

Levyraadin pistetaulukko kuvastaa raatilaisten mieltymyksiä, joten voin selvittää kenen musiikkimaku on lähimpänä omaani. Aion verrata yksitellen jokaisen raatilaisen antamia pisteitä omiini ja katsoa, kuinka paljon pisteissä on eroa. Raatilainen, jonka pisteissä on vähiten eroa omiini, on mahdollisesti musiikkimaultaan samantyylinen kuin minä.

Kahden tasolla olevan pisteen etäisyyden voi laskea Pythagoraan lauseella. Samaa menettelyä voi soveltaa myös moniulotteisemmissa avaruuksissa. Menetelmän nimi on euklidinen mittaus:

$$ \mathrm{d}(\mathbf{p},\mathbf{q}) = \sqrt{\sum_{i=1}^n (q_i-p_i)^2} $$

Raatimme arvosteli 10 kappaletta, joten arvioin yhden raatilaisen etäisyyden toiseen raatilaiseen 10 kappaleen perusteella. Sovellamme siis euklidista mittausta 10-ulotteisessa avaruudessa.

In [11]:
# Numpy-moodulin funktio linalg.norm, laskee euklidisen etäisyyden kahden vektorin välillä.
# Haluan verrata datataulun jokaista saraketta jokaiseen sarakkeeseen, joten luon silmukka-
# rakenteen kahdella sisäkkäisellä lamda-funktiolla.

D = df_norm.apply(lambda c1: df_norm.apply(lambda c2: np.linalg.norm(c1 - c2)))

# Ei verrata raatilaisen musiikkimakua häneen itseensä <-- arvoksi NaN
np.fill_diagonal(D.values, np.nan)
D.style.background_gradient()
Out[11]:
Sami Joe Mikko Taneli Arttu Ville Kirmo
Sami nan 1.40019 1.71505 1.5013 1.98273 1.25468 1.09199
Joe 1.40019 nan 1.10657 0.916821 1.79142 0.846568 1.23788
Mikko 1.71505 1.10657 nan 1.19896 1.71874 1.46575 1.22052
Taneli 1.5013 0.916821 1.19896 nan 1.33583 1.27169 0.951022
Arttu 1.98273 1.79142 1.71874 1.33583 nan 1.92952 1.61635
Ville 1.25468 0.846568 1.46575 1.27169 1.92952 nan 1.33103
Kirmo 1.09199 1.23788 1.22052 0.951022 1.61635 1.33103 nan

Kahden nimen risteyskohta kertoo raatilaisten etäisyyden toisistaan. Suuri luku tarkoittaa suurta eroa annetuissa arvosanoissa. Huomaan, että nimeni sarakkeella pienin arvo on Joen kohdalla (1,10657), eli hänen musiikkimakunsa eroaa vähiten omastani. Vähiten samanlainen musiikkimaku minulla on Artun kanssa (1,71874).

Yritän tehdä taulukosta helppolukuisemman korvaamalla euklidisen etäisyyden sijoitusnumerolla. Toisin sanoen haluan nähdä helposti, kenen musiikkimaku on minua ensimmäiseksi, toiseksi ja kolmanneksi lähimpänä.

In [12]:
D.rank().style.background_gradient()
Out[12]:
Sami Joe Mikko Taneli Arttu Ville Kirmo
Sami nan 5 5 6 6 2 2
Joe 3 nan 1 1 4 1 4
Mikko 5 3 nan 3 3 5 3
Taneli 4 2 2 nan 1 3 1
Arttu 6 6 6 5 nan 6 6
Ville 2 1 4 4 5 nan 5
Kirmo 1 4 3 2 2 4 nan

Nyt voin etsiä nimeni pystysarakkeesta ja näen riveistä, kenen musiikkimaku on lähimpänä minua. Rivitasolla näen, kuinka mones olen muiden raatilaisten sijoituslistalla. Parhaimmillaan musiikkimakuni on kolmanneksi lähimpänä toisia raatilaisia.

Taulukosta nähdään, että Joen musiikkimaku on lähimpänä useimpia raatilaisia, sillä hänellä on eniten 1-sijoituksia. Poikkeuksellisin musiikkimaku on Artulla, joka on kauimpana useimpia raatilaisia.

Tasapaksuja arvosanoja jakava Joe on lähempänä muita raatilaisia kuin ääripään arvosanoja jakava Arttu. Tämä johtuu euklidisen mittaustavasta, jossa erotusten summa korotetaan neliöön: yksi iso ero voi rangaista enemmän kuin monta pientä.

Samankaltaisuuden arviointi kuvaajasta

Kokeilen vielä euklidisen mittauksen lisäksi toista tapaa etsiä musiikkimaultaan lähin raatilainen. Sijoitan raatilaiset pisteinä kolmiulotteiseen kuvaajaan, josta etäisyyden voi arvioida silmämääräisesti. Tällä hetkellä yhdellä raatilaisella on 10 kappaletta tai ominaisuutta, jotka joudun typistämään kolmeksi. Tarkoitukseni on niputtaa kymmenen tasoa kolmeksi tasoksi siten, että mahdollisimman vähän tietoa menisi hukkaan.

Pääkomponenttianalyysi

Vähennän dimensioita pääkomponenttianalyysillä, joka etsii tasoja, joiden varianssi on mahdollisimman suuri. Toisin sanoen pääkomponenttianalyysi pyrkii keksimään ne tasot, jotka ovat kaikkein ilmaisuvoimaisimpia.

In [13]:
from IPython.display import YouTubeVideo
YouTubeVideo("_UVHneBUBW0")
Out[13]:
In [14]:
# Käytän SciKit Learnin funktiota PCA, joka osaa redusoida dimensioita
from sklearn.decomposition import PCA
pca = PCA(n_components=3)
X = pca.fit_transform(df_norm.T)
In [15]:
# Piirrän kolmiulotteisen kuvaajan Matplotlib-kirjastolla
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D

fig = plt.figure(figsize=(12,10))
ax = fig.add_subplot(111, projection='3d')

# Lisään kuvaajan redusoidut dimensiot
ax.scatter(X[:,0], X[:,1], X[:,2])

# Nimeän pisteet raatilaisten mukaan
for i, name in enumerate(df_norm.columns):
    ax.text(X[i,0], X[i,1], X[i,2], name)

# Hyvä kuvakulma
ax.azim = 135