Olennaiset taidot kuvailevaan analyysiin

Tämän artikkelin ohjelmakoodin ja tulosteet löydät GitHubista:

https://github.com/taanila/tilastoapu/blob/master/olennaiset.ipynb

Jos kopioit koodia itsellesi, niin kannattaa käyttää GitHubia. Tästä artikkelista kopioidut koodit eivät välttämättä toimi oikein.

Oletan, että lukijalla on asennettuna Anaconda ja sen mukana tuleva Jupyter notebook.

Tämän artikkelin esimerkeissä käytän dataa http://taanila.fi/data1.xlsx

Johdanto

Kuvailevan analyysiin voin vaiheistaa esimerkiksi seuraavasti:

  • Ohjelmakirjastojen tuonti
  • Datojen avaaminen
  • Dataan tutustuminen
  • Datan valmistelu
  • Datan kuvailu (lukumäärät, prosentit, tunnusluvut, korrelaatiot)
  • Tulosten viimeistely
  • Tulosten siirtäminen raporttiin

Käyn läpi kuvailevan analyysin vaiheiden olennaisimmat taidot omissa luvuissaan. Tulosten viimeistelylle en varaa omaa lukuaan, vaan käsittelen aihetta sekä Datan kuvailu -luvussa että Tulosten siirtäminen raporttiin -luvussa.

Ohjelmakirjastojen tuonti

Olennaiset kirjastot datojen analysointiin ovat numpy, pandas ja matplotlib.pyplot. Vakiintuneen tavan mukaisesti käytän niille lyhenteitä np, pd ja plt.

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt

%matplotlib inline

Viimeisen rivin (%matplotlib inline) ansiosta kuviot tulostuvat Jupyter-notebookiin ilman erillistä tulostuskomentoa.

Datojen avaaminen

Datan luen dataframe-tyyppisen muuttujan arvoksi. Jos data-tiedosto on netissä, niin käytän nettiosoitetta; jos data on työasemalla, niin käytän tiedostopolkua. Jos data on samassa kansiossa ohjelmakoodin kanssa, niin pelkkä tiedostonimi riittää.

Tekstitiedosto: pd.read_csv()

Jos data on csv-muotoisena tekstiedostona, niin avaaminen sujuu pd.read_csv() -funktiolla:

df = pd.read_csv('http://taanila.fi/data1.csv', 
    sep = ';', decimal = ',')

Suomalaisilla asetuksilla tallennetuissa csv-datoissa on erottimena puolipiste ja desimaalierottimena pilkku. Jos erottimena on pilkku ja desimaalierottimena piste, niin sep– ja decimal-parametreja ei tarvitse erikseen määrittää.

Lisätietoa pd.read_csv -funktion lisäparametreista:

https://pandas.pydata.org/pandas-docs/stable/generated/pandas.read_csv.html

Excel-tiedosto: pd.read_excel()

Jos data on Excel-muodossa kuten tämän artikkelin esimerkkidata, niin avaaminen sujuu pd.read_excel() -funktiolla:

df = pd.read_excel('http://taanila.fi/data1.xlsx', 
   sheet_name = 'Data')

Vanhemmissa pandas-versioissa sheet_name sijasta täytyy käyttää sheetname. Taulukkovälilehteä (sheet_name) ei tarvitse määritellä, jos data on ensimmäisellä taulukkovälilehdellä. Jos datan yläpuolella on dataan kuulumattomia rivejä, niin tarvitaan lisäparametria skiprows.

Lisätietoa pd.read_excel -funktion lisäparametreista:

https://pandas.pydata.org/pandas-docs/stable/generated/pandas.read_excel.html

Tietokanta tai html: pd.read_sql(), pd.read_html()

Tietokantoja varten pandas sisältää pd.read_sql() -funktion ja html-sivuilta lukemista varten pd.read_html() -funktion.

Dataan tutustuminen

Datan voin tulostaa näytölle suoraan dataframen nimellä, esimerkiksi df. Pitkistä datoista vain datan alku- ja loppuosa tulostuu näkyviin.

head(), tail()

Ensimmäiset viisi riviä voin tulostaa funktiolla df.head() ja viisi viimeistä riviä funktiolla df.tail(). Jos haluan nähdä enemmän kuin 5 riviä, niin kirjoitan haluamani rivimäärän sulkeiden sisään, esimerkiksi df.head(10).

columns

Datan sarakkeiden (muuttujien) nimet näen funktiolla df.columns.

count()

Sarakkeiden ei-tyhjien arvojen lukumäärät näen funktiolla df.count().

olennaiset1

np.unique()

Sarakkeiden ainutkertaiset arvot näen numpyn unique-funktiolla:

for var in df:
   print(var, np.unique(df[var]))

Esimerkkiaineistolleni tulosteen alku näyttää seuraavalta:

olennaiset2

Datan valmistelu

replace()

Voin koodata muuttujan arvot uudella tavalla replace()-funktiolla. Esimerkiksi seuraavassa luon uuden sukup2-muuttujan, jossa sukupuolet ovat tekstimuodossa:

df['sukup2']=df['sukup'].replace({1 : 'Mies', 2 : 'Nainen'})
df.head(6)

pd.cut()

Voin luokitella määrällisen muuttujan arvot pd.cut()-funktiolla. Seuraavassa on ensiksi määritelty luokkarajat bins-listaan.

bins = [18, 28, 38, 48, 58, 68]
df['ikä2'] = pd.cut(df['ikä'], bins = bins)
df.head()

rename()

Voin vaihtaa muuttujien nimiä:

df.rename(columns = {'sukup2': 'sukup_teksti', 
   'ikä2': 'ikäluokka'}, inplace = True)
df.head()

Muuttujien tekstimuotoiset arvot listana

Analyysit sujuvat parhaiten, jos tiedot ovat numeromuodossa. Analyysin tuloksiin haluan kategoristen ja mielipidemuuttujien tekstimuotoiset arvot. Tätä ajatellen määrittelen listat muuttujien tekstiarvoille:

koulutus = ['Peruskoulu', '2. aste', 'Korkeakoulu', 'Ylempi korkeakoulu']
perhe = ['Perheetön', 'Perheellinen']
sukup = ['Mies', 'Nainen']
tyytyväisyys = ['Erittäin tyytymätön', 'Jokseenkin tyytymätön', 
   'Ei tyytymätön eikä tyytyväinen', 'Jokseenkin tyytyväinen', 
   'Erittäin tyytyväinen']

Datan kuvailu

Yhteenveto lukumäärinä: pd.crosstab()

Lukumäärä-yhteenvedot saan pd.crosstab()-funktiolla. Funktion toinen parametri (’n’) määrittää otsikon lukumäärille.

df1 = pd.crosstab(df['koulutus'], 'n')
df1.index = koulutus
df1.columns.name = ''
df1

Lisään koulutuksen tekstimuotoiset arvot df1-dataframen rivi-indeksiksi (df1.index = koulutus). Rivi df1.columns.name = ” (viimeisenä on kaksi hipsua) korvaa df1-dataframen vasemman yläkulman otsikon tyhjällä.

olennaiset3

Lisään lukumäärien viereen %-sarakkeen:

df1['%'] = df1/df1.sum()
df1.style.format({'%': '{:.1%}'})

Jos haluan tulostaulukot viimeistellyssä muodossa, niin tarvitsen style.format()-funktiota. Edellä määritän df1-dataframen %-sarakkeelle prosenttimuotoilun yhdellä desimaalilla.

olennaiset4

Tästä ei ole pitkä matka pylväskuvioon. df1-dataframen %-sarakkeesta saan pylväskuvion plot.barh()-funktiolla. Värimäärityksen (color = ’C0’) teen, jotta kaikki pylväät ovat saman värisiä (väri C0 tarkoittaa käytössä olevan teeman ensimmäistä väriä).

ax = df1['%'].plot.barh(color = 'C0')
vals = ax.get_xticks()
ax.set_xticklabels(['{:.0%}'.format(x) for x in vals])

olennaiset5

Arvoakselin asteikon esittämiseksi prosentteina luen ensin ax-muuttujasta asteikon arvot ja sen jälkeen muotoilen jokaisen arvon prosenteiksi ilman desimaaleja.

Monivalinta: count()

Monivalintakysymys on kysymys, jonka vaihtoehdoista vastaaja voi valita useampiakin. Monivalinnan jokainen vaihtoehto tallennataan dataan omana sarakkeenaan. Valintaa merkitään ykkösellä (tai joskus jollain muulla numerolla) ja valitsematta jättämistä tyhjällä.

Esimerkkiaineistossani työterv, lomaosa, kuntosa ja hieroja ovat olleet vaihtoehtoja, joista vastaajat ovat valinneet ne työantajan tarjoamat etuisuudet, joita ovat hyödyntäneet. Valintojen lukumäärät voin laskea count()-funktiolla:

df2=df[['työterv', 'lomaosa', 'kuntosa', 'hieroja']].count()
df2 = df2.to_frame('n').sort_values(by = 'n', ascending = False)
df2.style.format('{:.0f}')

Laskemisen tulos ei ole dataframe, vaan series. Toisella rivillä vaihdan series-tyyppisen tuloksen dataframeksi, määritän valintojen lukumääräsarakkeen nimeksi n ja järjestän dataframen valintojen mukaiseen laskevaan järjestykseen.

Muotoilen valintojen lukumäärät ilman desimaaleja esitetyiksi liukuluvuiksi (liukuluvun symboli on f; liukuluku tarkoittaa käytännössä desimaalilukua).

olennaiset6

Tämän voin esittää pylväskaaviona:

df2.sort_values(by = 'n').plot.barh(legend = False, color = 'C0')
plt.xlabel('Käyttäjien lukumäärä')

Ilman legend = False parametria kuvioon tulee tarpeeton pylväsvärin selite. Arvoakselille lisäsin otsikoksi ’Käyttäjien lukumäärä’.

olennaiset7

Jos haluan ristiintaulukoida monivalinnan valintojen lukumäärät sukupuolen kanssa, niin voin hyödyntää groupby()-funktiota:

df3 = df.groupby('sukup')['työterv', 'lomaosa', 
   'kuntosa', 'hieroja'].count()
df3.index = sukup
df3.style.format('{:.0f}')

Rivi-indeksin tekstimuotoiset arvot (mies, nainen) saan aiemmin määritellystä sukup-listasta. Lukumäärät esitän liukulukuna ilman desimaaleja.

olennaiset8

Tästä syntyy vaivatta pylväskuvio:

df3.plot.barh()
plt.xlabel('Käyttäjien lukumäärä')

olennaiset9

Ristiintaulukointi: pd.crosstab()

Edellä tarkastelin monivalinnan valintojen lukumäärän ristiintaulukointia sukupuolen kanssa. Muissa tapauksissa ristiintaulukointi sujuu pd.crosstab()-funktiolla. Ensimmäisen argumentin muuttuja sijoittuu ristiintaulukoinnin riveille ja toisen argumentin muuttuja sarakkeisin. Lisäparametrillä normalize voin määrittää laskettavaksi prosentit lukumäärien sijaan. Seuraavassa lasken prosentit sarakkeista eli sukupuolesta (columns):

df4 = pd.crosstab(df['koulutus'], df['sukup'], normalize = 'columns')
df4.index = koulutus
df4.columns = sukup
df4.style.format('{:.1%}')

Rivi-indeksin tekstimuotoiset arvot otan aiemmin määritellystä koulutus-listasta ja sarakkeen arvot sukup-listasta. Tulokset muotoilen prosenteiksi yhdellä desimaalilla.

olennaiset10

Tämän voin esittää pylväinä:

ax = df4.plot.barh()
plt.xlabel('Prosenttia sukupuolesta')
vals = ax.get_xticks()
ax.set_xticklabels(['{:.0%}'.format(x) for x in vals])

olennaiset11

Vaihtoehtoisesti voin käyttää 100% pinottuja vaakapylväitä. Tätä varten minun pitää ensin vaihtaa dataframen rivit ja sarakkeet päittäin transpose()-funktiolla:

ax = df4.transpose().plot.barh(stacked = True)
plt.xlabel('Prosenttia sukupuolesta')
vals = ax.get_xticks()
ax.set_xticklabels(['{:.0%}'.format(x) for x in vals])

olennaiset12

Useiden muuttujan yhteenvedot samaan taulukkoon: value_counts()

Esimerkkiaineistossani on muuttujina muiden muassa ’tyytyväisyys johtoon’, ’tyytyväisyys työtovereihin’, ’tyytyväisyys työympäristöön’, ’tyytyväisyys palkkaan’ ja ’tyytyväisyys työtehtäviin’. Näitä kaikkia on mitattu 5-portaisella tyytyväisyysasteikolla. Jotta voin vertailla tyytyväisyyksiä eri asioihin, haluan ne kaikki samaan taulukkoon vierekkäin. Tämän voin toteuttaa monellakin tavalla, mutta mielestäni kätevin tapa on käyttää value_counts()-funktiota:

df5 = df['johto'].value_counts(sort = False, normalize = True).to_frame()
df5['työtov'] = df['työtov'].value_counts(sort = False, normalize = True)
df5['työymp'] = df['työymp'].value_counts(sort = False, normalize = True)
df5['palkkat'] = df['palkkat'].value_counts(sort = False, normalize = True)
df5['työteht'] = df['työteht'].value_counts(sort = False, normalize = True)
df5.index = tyytyväisyys
df5.style.format('{:.1%}')

Ensimmäisellä rivillä lasken ensimmäiselle tyytyväisyys-muuttujalle vastausvaihtoehtojen 1-5 lukumäärät value_counts()-funktiolla. Lisäparametreina määritän

  • järjestämisen muuttujan arvojen mukaan (sort = False; ilman tätä järjestäminen tapahtuisi lukumäärien mukaiseen järjestykeen)
  • prosenttien esittämisen lukumäärien sijasta (normalize = True).

Lisäksi muunnan tuloksena syntyvät series-tyyppisen muuttujan dataframeksi (to_frame()).

Seuraavilla riveillä lisään dataframeen muiden tyytyväisyys-muuttujien lukumäärät.

Lopuksi lisään tekstimuotoiset rivi-indeksin arvot aiemmin määritellystä tyytyväisyys-listasta. Tulokset näytän prosenttimuodossa yhden desimaalin  tarkkuudella.

olennaiset13

Tämän voin esittää myös pinottuna pylväskuviona:

ax = df5.transpose().plot.barh(stacked = True, 
   color=['#C44E52','#D65F5F','grey','#4878CF','#4C72B0'])
vals = ax.get_xticks()
ax.set_xticklabels(['{:.0%}'.format(x) for x in vals])

Määritän itse värit oletusvärien sijaan.

olennaiset14

Luokiteltu jakauma: pd.cut()

Määrälliset muuttujat kannattaa yleensä luokitella ennen lukumäärien ja prosenttien laskemista. Esimerkkiaineistossani palkka on tällainen muuttuja.

Seuraavassa määritän luokkarajat (bins) ja luon uuden sarakkeen palkkaluokkaa varten. Varsinaisen luokittelun teen pd.cut()-funktiolla.

bins = [1000, 2000, 3000, 4000, 7000]
df['palkkaluokka'] = pd.cut(df['palkka'], bins = bins)
df6 = pd.crosstab(df['palkkaluokka'], 'n')
df6.columns.name = ''
df6

olennaiset15

Huomaa sulkumerkkien merkitys: kaarisulku tarkoittaa, että arvo ei kuulu luokkaan ja hakasulku tarkoittaa, että arvo kuuluu luokkaan. Esimerkiksi toisessa luokassa 2000 ei sisälly luokkaan, mutta 3000 sisältyy.

Palkkajakauman voin esittää histogrammina:

df['palkka'].plot.hist(bins)
plt.xlabel('Palkka')
plt.ylabel('Lukumäärä')

olennaiset16

Tunnuslukuja: describe()

Keskeisimmät tilastolliset tunnusluvut (lukumäärä, keskiarvo, keskihajonta, viiden luvun yhteenveto) lasken describe()-funktiolla. Seuraavassa lasken tunnuslukuja usealle muuttujalle ja muotoilen tunnusluvut näytettäväksi kahden desimaalin tarkkuudella:

df[['ikä', 'palveluv', 'palkka', 'johto', 'työtov', 
   'työymp', 'palkkat', 'työteht']].describe().
   style.format('{:.2f}')

olennaiset17

Pivot-taulukoita pivot_table(values = ”, index = ”, columns = ”)

Pivot-taulukoilla voin laatia monipuolisia raportteja eri tunnuslukuja käyttäen. Oletuksena pivot_table() -funktio laskee keskiarvoja. pivot_table()-funktion parametreinä ovat values (muuttuja, josta lasketaan), index (muuttujat, joiden arvot otetaan riveille) ja columns (muuttujat, joiden arvot otetaan sarakkeisiin). Esimerkiksi palkkakeskiarvot sukupuolen, perhesuhteen ja koulutuksen mukaan ryhmiteltyinä saan seuraavasti:

df7 = df.pivot_table(values = 'palkka', index = 
   ['sukup', 'perhe'], columns = 'koulutus')
df7.style.format('{:.0f}')

olennaiset18

Haluan tietysti täydentää taulukkoa sukupuolen, perhesuhteen ja koulutuksen tekstimuotoisilla arvoilla. Rivi-indeksin arvot ovat nyt kahdella tasolla ja tämän vuoksi tekstimuotoiset arvot määritellään set_levels-funktiolla:

df7.index = df7.index.set_levels(sukup, level=0)
df7.index = df7.index.set_levels(perhe, level=1)
df7.columns = koulutus
df7.style.format('{:.0f}')

olennaiset19

Voin laskea pivot-taulukkoon myös muita tunnuslukuja aggfunc-parametrin avulla. Seuraavassa lasken palkalle minimin, mediaanin, keskarvon ja maksimin eri koulutustasoille. Huomaa, että funktioina käytän numpy-kirjaston (np) funktioita.

df8 = df.pivot_table(values = 'palkka', index = 'koulutus',  
   aggfunc = [np.min, np.median, np.mean, np.max])
df8.index = koulutus
df8.columns = ['pienin', 'mediaani', 'keskiarvo', 'suurin']
df8.style.format('{:.0f}')

olennaiset20

Ruutu- ja janakaavio:  boxplot()

Ruutu- ja janakaavion (laatikko- ja viiksikaavio, boxplot, box & whisker) avulla voin visualisoida viiden luvun yhteenvedon. Seuraavassa esitän palkalle viiden luvun yhteenvedon eri koulutustasoilla:

ax = df.boxplot('palkka', by = 'koulutus')
plt.title('')
plt.suptitle('')
plt.xlabel('Koulutus')
plt.ylabel('Palkka')
ax.set_xticklabels(koulutus)

Tässä käytän pandas-kirjaston boxplot()-funktiota, joka tuottaa oletuksena kaksi otsikkoa (title, suptitle). Koodissa poistin otsikot näkyviltä. Vaaka-akselille lisäsin koulutukselle tekstimuotoiset arvot.

olennaiset21

Korrelaatio: corr()

Korrelaatiokertoimet lasken corr()-funktiolla. Seuraavassa lasken iän, palveluvuosien ja palkan väliset korrelaatiokertoimet ja esitän ne kahden desimaalin tarkkuudella:

df[['ikä', 'palveluv', 'palkka']].corr().style.format('{:.2f}')

olennaiset22

Hajontakaaviona voin esittää kahden muuttujan välisen korrelaation visuaalisesti:

df.plot.scatter('ikä', 'palkka')

olennaiset23

Tulosten siirtäminen raporttiin

Taulukot

Taulukoiden ulkoasu riippuu jonkin verran käyttämästäsi selaimesta (Jupyter notebook toimii oletusselaimessasi). Tämän artikkelin esimerkeissä olen käyttänyt Microsoftin Edge-selainta.

Julkaistavaksi tarkoitetun taulukon voin valita, kopioida ja liittää (copy – paste) esimerkiksi PowerPointiin, Wordiin tai Exceliin. Liitetty taulukko on myös taulukko ja voin muokata sitä taulukkotyökaluilla.

Python käyttää desimaalipilkkuna pistettä. PowerPointissa ja Wordissä voin helposti korvata pisteet pilkulla. Excelissä osa desimaalipisteellä varustetuista luvuista muuttuu päivämääriksi. Tämän voin estää kierrättämällä taulukko Wordin kautta ja korvaamalla pisteet pilkuilla Wordissä.

Voin myös tallentaa taulukot csv-tiedostoon. Tällöin desimaalipilkkujen kanssa ei tule ongelmia. Katso esimerkki artikkelista Frekvenssitaulukot Exceliin.

Jos taulukko kelpaa sellaisenaan julkaistavaksi, niin voin toki leikata sen ja liittää kuvana raporttiin.

Kuviot

Kuvion voin tallentaa plt.gcf().savefig -komennolla (gcf = get current figure). Tallennusformaatti määrittyy  kuviolle annettavan nimen tarkentimesta (esimerkiksi png):

plt.gcf().savefig('kuva.png')

Järjestelmäsi tukemat kuvaformaatit saat selville komennolla

plt.gcf().canvas.get_supported_filetypes()

Jos tarvitset tietyn kokoisia kuvia tai haluat käyttää tietynlaista tai tietynkokoista fonttia, niin voit tehdä määritykset heti ohjelmakirjastojen tuonnin jälkeen (ei kuitenkaan samassa Jupyter notebookin solussa kuin ohjelmakirjastojen tuonti, koska se ei jostain syystä toimi). Esimerkkejä mahdollisista määrityksistä:

plt.rc('figure', figsize = (8, 6))
plt.rc('font', family = 'sans-serif', size = 8)
plt.rc('axes', titlesize = 8 )
plt.rc('axes', labelsize = 8)
plt.rc('xtick', labelsize = 8)
plt.rc('ytick', labelsize = 8)
plt.rc('legend', fontsize = 8)

Kuvailevasta analyysistä merkitsevyystestaukseen

Jos kuvailet otosta ja haluat edetä testaamaan havaittujen erojen ja/tai riippuvuuksie tilastollista merkitsevyyttä, niin lue artikkelini

Merkitsevyyden testaus Pythonilla

Mainokset