#!/usr/bin/env python # coding: utf-8 # # 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 get_ipython().run_line_magic('matplotlib', 'inline') import seaborn as sns sns.set_style("whitegrid") # In[3]: df = pd.read_csv('levyraati-2016.csv', index_col=0, decimal=',') df # # 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') # ## 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') # 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']) # # 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') # 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 # 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') # ## 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() # 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() # 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") # 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