Ylen avointa vaalikone-dataa on jo tutkittu jonkin verran, esimerkiksi klusteroimalla ehdokkaita vastausten perusteella (linkit tässä ja tässä). Löytämissäni analyysiessä oltiin ennen kaikkea keskitytty ordinaalisella asteikolla arvosteltujen kysymysten analyysiin. Tässä notebookissa tarkoituksenani on tutkia Ylen vaalikoneen avoimien kysymyksien vastauksia, ja tehdä tilastollista analyysiä sen pohjalta. Samalla tarkoituksena on kokeilla Pythonin text mining-ominaisuuksia suomen kielen analysoinnissa. Lisäksi päästään kokeilemaan Githubin uutta Jupyter-notebook-renderingiä.
In [1]:
import pandas as pd
import numpy as np
import re
from langdetect import detect
import string
from nltk import word_tokenize
from nltk.stem.snowball import SnowballStemmer
from nltk.corpus import stopwords
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.linear_model import LogisticRegression
from sklearn.cross_validation import cross_val_score
import seaborn as sns
import matplotlib as mpl
import matplotlib.pyplot as plt
import matplotlib.pylab as pylab
from scipy import sparse
#notebook-asetukset
%matplotlib inline
%config InlineBackend.figure_format = 'svg'
Data ladattiin .csv-muodossa täältä. Datan alkuperäinen julkaisija ja kerääjä on Yle.
Lataamaani .csv-tiedostoon oli yhteen vastaukseen eksynyt merkki jota ei pystynyt enkoodaamaan utf-8-muotoon (tarkemmat tiedot: merkki = 伋, vastaaja Raimo Piirainen, vastausid 4798, rivi 1358). Tämä merkki poistettiin datasta käsin. Tiedostonimellä data/vaalikone_data_fix.csv viitataan jatkossa korjattuun versioon tiedostosta.
Alkuun poimittiin tiedostosta avoimia vastauksia sisältävät sarakkeet. Tätä varten luettiin kaksi ensimmäistä riviä datasta, eli header sekä ensimmäinen datarivi:
In [2]:
df_cols = pd.read_csv("data/vaalikone_data_fix.csv", sep = ";", encoding = "utf-8", nrows = 2)
#df_cols = pd.read_csv("data/vastauksetavoimenadatana.csv", sep = ";", encoding = "utf-8", nrows = 2) #korjaamaton data
columns = list(df_cols.columns)
Regexeillä poimittiin sarakkeista ne jotka sisältävät vastauksia avoimiin kysymyksiin. Avoimia kysymyksiä on kahdenlaisia: ehdokkaan ydinsanoman sisältävät kysymykset (Miksi juuri sinut kannattaisi valita/Mitä asioita haluat/Vaalilupaukset) sekä ordinaaliasteikolla vastattaviin kysymyksiin liittyvät avoimet kommentit. Lisäksi poimittiin mukaan ehdokkaiden perustiedot (id, sukunimi, etunimi, puolue).
In [3]:
#regex for the text fields without pipe characters
regex_text = re.compile(("Miksi juuri sinut kannattaisi valita|"
"Mitä asioita haluat edistää|"
"Vaalilupaus"))
text = filter(regex_text.match, columns)
comments = filter(re.compile("kommentti").search, columns)
text_columns = text + comments
basic_columns = ["id", "sukunimi", "etunimi", "puolue"]
Data luettiin kahteen dataframeen. Yhteen poimittiin tekstidata (df_text) ja toiseen perustiedot (df):
In [4]:
df_text = pd.read_csv("data/vaalikone_data_fix.csv", sep = ";", encoding = "utf-8", usecols = text_columns)
df = pd.read_csv("data/vaalikone_data_fix.csv", sep = ";", encoding = "utf-8", usecols = basic_columns)
Huom! Mikäli halutaan käyttää suoraan avoindata.fi-sivulta ladattua dataa (eikä korjattua versiota), pitää jättää tuo ongelmallinen rivi pois, muuten parseri heittää errorin:
In [5]:
#df_text = pd.read_csv("data/vastauksetavoimenadatana1.csv", sep = ";", encoding = "utf-8", usecols = text_columns, skiprows = [1357])
#df = pd.read_csv("data/vastauksetavoimenadatana1.csv", sep = ";", encoding = "utf-8", usecols = basic_columns, skiprows = [1357])
Tekstikentät yhdistettiin kaikki yhteen pötköön. Loppupeleissä käyttämämme tekstin representaatio on bag-of-words-tyylinen (tai tarkemmin sanottuna bag-of-ngrams), joten kaikki tekstikentät voinee huoletta koota yhdeksi tajunnanvirraksi. Tekstipötköjä tehtiin kuitenkin kahdenlaisia, yhteen otettiin vain edellämainitut "ydinsanomaan" liittyvät avoimet kysymykset, ja toiseen lisättiin vielä perään kaikki muutkin avoimet kommentit
In [6]:
for i, col in enumerate(text):
if i == 0:
series_lesstext = df_text[col].fillna("")
else:
series_lesstext = series_lesstext + " " + df_text[col].fillna("")
series_moretext = series_lesstext.copy()
for col in comments:
series_moretext = series_moretext + " " + df_text[col].fillna("")
series_lesstext = series_lesstext.str.strip()
series_moretext = series_moretext.str.strip()
Lyhyelle tekstinpätkälle annettiin sarakenimeksi Short_text ja pidemmälle tekstille nimeksi Long_text.
In [7]:
df["Short_text"] = series_lesstext
df["Long_text"] = series_moretext
df.head()
Out[7]:
Nyt datassamme on kuusi saraketta: id, sukunimi, etunimi, puolue, Short_text ja Long_text.
Tässä vaiheessa huomasin että datasta löytyy jokunen ruotsiksi vastannut ehdokas. Tällaiset tapaukset ovat oletettavasti RKP:n ehdokkaita, ja tekevät mallinnustehtävän vähän turhan helpoksi. Käyttämällä Pythonin langdetect-kirjaston detect-funktiota, etsimme jokaiselle ei-tyhjälle Long_text-havainnolle kielen.
In [8]:
Lang = []
for i in range(len(df.index)):
if df["Long_text"][i] == "":
Lang.append(u"Missing")
else:
Lang.append(detect(df["Long_text"][i]))
df["Lang"] = Lang
df.Lang.value_counts()
Out[8]:
Ruotsinkielisiä vastauksia oli siis 28, ja täysin tyhjiä vastauksia 156. Otetaan mukaan ainoastaan suomenkieliset vastaukset.
In [9]:
df = df[df.Lang == u"fi"]
Viimeisenä esikäsittelyaskeleena jätettiin datasta pikkupuolueet pois. Ajatuksena oli, että näin tilastollisilla malleilla olisi tarpeeksi havaintoja jonkinlaisen rakenteen löytämiseksi. Havaintojen etsimiseksi käytettiin regexejä ja pandas-kirjaston str.contains-funktiota.
In [10]:
Suuret = ("Perussuomalaiset|Vihre|Vasemmistoliitto|Kristillisdemokraatit|"
"Suomen ruotsalainen kansanpuolue|Kansallinen Kokoomus|Suomen Sosialidemokraattinen Puolue|"
"Suomen Keskusta")
df = df[df.puolue.str.contains(Suuret)]
pylab.rcParams['figure.figsize'] = 5, 5
df.puolue.value_counts().plot(kind = "barh", fontsize = 13);
RKP:tä lukuunottamatta puolueilla on suunnilleen 200 ehdokasta per puolue (KD-ehdokkaita hitusen vähemmän). Kun ruotsinkieliset vastaukset poistettiin aineistosta, jäi sen sijaan jäljelle vain kuutisenkymmentä RKP:n vastausta (mikä taitaa olla vähemmän kuin joidenkin "pikkupuolueiden" havaintojen määrät). Pidetään nyt kuitenkin tämä puoluevalikoima, vaikkakin RKP:n ehdokkaita on aineistossa selvästi vähemmän.
Datan esikäsittelyn jälkeen pitää vielä muuntaa tekstidata muotoon jota tilastollinen algoritmi voi "ymmärtää". Tähän tarkoitukseen käytettiin Pythonin nltk- ja scikit-learn-kirjastoja.
Ilahduttavasti nltk-kirjastosta löytyi suomenkielinen tokenizer, stemmer ja myös täytesanojen lista. Tokenizer määrää sen miten tekstinpätkä jaetaan osasanoihin kun taas stemmer muuntaa eri käännösmuodoissa olevat sanat samaan muotoon. Esimerkiksi sekä edeltäjiinsä että edeltäjistään stemmataan muotoon edeltäj. Tarkempi kuvaus nltk-kirjaston suomenkielisestä stemmeristä löytyy täältä. Täytesanalista taas määrittelee sellaiset täytesanat jotka eivät oikeastaan kerro sisällöstä mitään ja jotka on syytä jättää analyyseistä pois (esim. sanat ja, sekä).
Scikit-learn-kirjastosta käytettiin n.s. TfidfVectorizer-luokkaa. Tfidf-metodi perustuu eri sanojen (tai n-grammien) esiintymistiheyksien laskemiseen. Eri sanoja painotetaan sitten niin, että harvemmin esiintyvillä sanoilla on korkeampi painoarvo kuin lähes joka dokumentissa esiintyvillä sanoilla. Tarkempi kuvaus löytyy täältä.
nltk- ja scikit-learn-kirjastojen toimintojen yhdistämisessä auttoi tämä Stack Overflow-postaus. Määriteltiin tokenize-funktio joka kutsuu nltk:n tokenizeria ja stem_tokens-funktiota joka taas kutsuu nltk:n suomenkielistä stemmeria. Täten määritelty tokenize-funktio voidaan sitten syöttää parametrina TfidfVectorizer-luokan konstruktorille, kuten myös täytesanojen lista.
Ngrammien pituuden vaihteluväliksi määrättiin (1,3), eli pisimmillään bag-of-words-representaatioon otetaan mukaan kolmen sanan mittaisia ngrammeja. Parametrin min_df arvoksi asetettiin 5, mikä tarkoittaa että jonkun ngrammin pitää löytyä aineistosta vähintään viisi kertaa jotta se tulee mukaan analyysimatriisiin. Parametrin max_df arvoksi taas asetettiin 0.9, mikä tarkoittaa sitä, että sanat jotkat esiintyvät useammassa kuin 90%:ssa aineistosta eivät tule mukaan analyysimatriisiin.
In [11]:
#http://stackoverflow.com/questions/26126442/combining-text-stemming-and-removal-of-punctuation-in-nltk-and-scikit-learn
stemmer = SnowballStemmer("finnish")
def stem_tokens(tokens, stemmer):
stemmed = []
for item in tokens:
stemmed.append(stemmer.stem(item))
return stemmed
def tokenize(text):
text = "".join([ch for ch in text if ch not in string.punctuation])
tokens = word_tokenize(text, language = "finnish")
stems = stem_tokens(tokens, stemmer)
return stems
TfidfVectorizeria käyttämällä pitkälle ja lyhyelle tekstipätkälle laskettiin omat sparse-matriisit (x_tfidf_short ja x_tfidf_long). Tokenizer/stemmer-yhdistelmämme ei vaikuta olevan kovin skaalautuva, joten alla olevan koodinpätkän ajamiseen menee minuutti/pari.
In [12]:
vect_long = TfidfVectorizer(ngram_range = (1, 3), min_df = 5, max_df = 0.9,
tokenizer=tokenize, stop_words=stopwords.words("finnish"))
x_tfidf_long = df["Long_text"].values
x_tfidf_long = vect_long.fit_transform(x_tfidf_long)
names_long = vect_long.get_feature_names()
vect_short = TfidfVectorizer(ngram_range = (1, 3), min_df = 5, max_df = 0.9,
tokenizer=tokenize, stop_words=stopwords.words("finnish"))
x_tfidf_short = df["Short_text"].values
x_tfidf_short = vect_short.fit_transform(x_tfidf_short)
names_short = vect_short.get_feature_names()
Tilastolliseksi malliksi valitsimme mahdollisimman yksinkertaisen luokittelumallin eli logistisen regression. Syitä ovat scikit-learnin LogisticRegression-implementaation tuki sparse-matriiseille (joita x_tfidf_short ja x_tfidf_long ovat), mallin tulkinnan helppous regressiokertoimien avulla ja se, että mallista saa suoraan irti luokittelutodennäköisyyksiä.
Päätimme määritellä luokitteluongelman "one-vs-all"-tyylisesti. Määrittelemme siis jokaiselle puolueelle oman luokitteluongelman jossa tavoitteena on luokitella havainnot juuri tuohon kyseiseen puolueeseen kuuluviin sekä muihin puolueisiin kuuluviin. Valitsimme tämänlaisen formulaation sen takia, että voitaisiin vertailla eri luokittelumallien laatua, ja jotta saataisiin regressiokertoimet laskettua puoluekohtaisesti.
Seuraavassa tutkitaan miten hyvin logistinen regressiomalli suoriutuu havaintojen luokittelusta eri puolueille. Lisäksi tutkitaan sitä, miten paljon mallin suorituskyky muuttuu kun siirrytään lyhyestä, ydinsanoman sisältävästä tekstistä pitempään tekstiin jossa on vapaat kommentitkin mukana.
Mallin suorituskykymittarina käytetään n.s. receiver operating characteristic-käyrän (ROC) alle jäävää pinta-alaa (ROC-AUC). Tämä mitta on hyvinkin yleisessä käytössä kun arvioidaan binäärisen luokittelumallin luokittelutodennäköisyyksien laatua. Tarkempi selitys mittarista löytyy täältä. Tulkinnan kannalta oleellisinta on tietää että surempi ROC-AUC on parempi, ja että ROC-AUC-arvo 0.5 vastaa täysin satunnaista arvausta.
Seuraavassa koodinpätkässä lasketaan jokaiselle puolueelle ja molemmille tekstityypeille ristikkäisvalidoidut ROC-AUC-arvot ja tallennetaan ne dataframeen.
In [13]:
clf = LogisticRegression(random_state = 42)
suuret = df.puolue.unique()
df_models = pd.DataFrame(columns = ("puolue", "roc_auc_short_text", "roc_auc_long_text"))
i = 0
for puolue in suuret:
y = (df.puolue.values == puolue).astype(int)
roc_short = cross_val_score(clf, x_tfidf_short, y, scoring = "roc_auc", cv = 4).mean()
roc_long = cross_val_score(clf, x_tfidf_long, y, scoring = "roc_auc", cv = 4).mean()
df_models.loc[i] = [puolue, roc_short, roc_long]
i += 1
df_models.sort(["roc_auc_long_text"], ascending = False)
Out[13]:
Huomataan, että lyhyen ja pitkän tekstin välillä on suuret erot pitkän tekstin hyväksi, paitsi Vihreiden tapauksessa. Pelkän ydinsanoman perusteella ei siis mitenkään virheettömästi pystytä ennustamaan ehdokkaan puoluetta.
Vasemmistoliiton ehdokkaiden tunnistaminen on tämän mittarin perusteella helpointa, kun taas RKP:n malli on kaikista heikoin. Tämä vaikuttaa ihan järkevältä ottaen huomioon RKP:n aineiston vähäisen määrän ja puolueen muutenkin vapaahkon puolueohjelman.
ROC-AUC-arvojen valossa tilastomallit näyttävät aika hyviltä. Jopa 0.7:n ylitystä voi joskus pitää jo ihan hyvänä, ja nyt päästään jo parhaimmillaan yli 0.9:n. Tosin puolueen ennustaminen voi joissain tilanteissa olla todellakin helppoa jos useampi puolueen ehdokas itse mainitsee vapaissa kentissä oman puolueensa tai vaikka puolueenjohtajansa nimen.
Lopussa tulemme tekemään jonkinlaisen "puoluejohtajatentin" jossa laitetaan puoluejohtajien vastaukset jokaiselle puolueelle sovitetun mallin läpi, ja tutkitaan sitten ennustettuja todennäköisyyksiä. Tätä varten poimimme nyt puoluejohtajat erilliseen testi-aineistoon.
Etsimme puheenjohtajien vastaajaidt regexiä käyttämällä:
In [14]:
Pjt = (u"Stubb|Räsänen|Soini|Haglund|Niinist|Rinne|Sipil|Arhinmäk")
df[df.sukunimi.str.contains(Pjt)]
Out[14]:
Listasta voidaan lukea puheenjohtajien idt: Arhinmäki = 5720, Haglund = 5908, Niinistö = 5525, Rinne = 5073, Räsänen = 5063, Sipilä = 5791, Soini = 5857 ja Stubb = 5369. Näiden perusteella voidaan nyt jakaa x_tfidf_long-matriisi train- ja test-aineistoihin.
In [15]:
pj_idt = [5720, 5908, 5525, 5073, 5063, 5791, 5857, 5369]
mask_pjt = df.id.isin(pj_idt).values
xtrain = x_tfidf_long[~mask_pjt, ]
puolue_train = df.puolue[~mask_pjt]
xtest = x_tfidf_long[mask_pjt, ]
pj_test = df.sukunimi[mask_pjt]
Puolueiden avainsanat arvioitiin sovittamalla jokaiselle puolueelle luokittelumalli ja poimimalla regressiomallin itseisarvoiltaan 20 suurinta kerrointa avainsanoiksi. Nämä kertoimet yhdistettiin sanoihin tai n-grammeihin jotka meillä on tallessa tekstinkäsittelyvaiheesta (names_long-vektori, kats ipython-chunk nro 12). Avainsanat koottiin yhteen dataframeen joka plotataan myöhemmin. Lisäksi sovitetut luokittelumallit poimitaan talteen myöhempää käyttöä varten.
In [16]:
n_keep = 20
clfs_fin = {}
for j, puolue in enumerate(suuret):
y = (puolue_train == puolue).astype(int)
clf = LogisticRegression(random_state = 42)
clf.fit(xtrain, y)
clfs_fin[puolue] = clf
coefs = pd.DataFrame({'muuttuja':names_long, 'kerroin':np.ravel(clf.coef_)})
coefs['absval'] = coefs.kerroin.abs()
coefs.sort(columns = 'absval', ascending = False, inplace = True)
coefs = coefs[0:n_keep]
coefs['puolue'] = puolue
coefs.sort(columns = 'kerroin', ascending = False, inplace = True)
coefs['sort_idx'] = list(string.ascii_lowercase[0:n_keep])
if j == 0:
df_out = coefs.copy()
else:
df_out = df_out.append(coefs.copy(), ignore_index = True)
df_out['suunta'] = np.where(df_out.kerroin > 0, 'positiivinen', 'negatiivinen')
df_out.sort(['puolue', 'kerroin'], ascending = [True, False], inplace = True)
df_out['sana'] = df_out['sort_idx'].map(str) + ". " + df_out.muuttuja
df_out.head()
Out[16]:
Plottaamista varten aineistoon on lisätty suunta-sarake joka ilmaisee avainsanan kertoimen merkin. Data tullaan piirtämään pylväskuvaajana jossa kertoimen itseisarvo määrää pylvään koon ja suunta määrää pylvään värin. Positiivinen suunta tarkoittaa sitä, että avainsanan esiintyminen kasvattaa kyseisen puolueen luokittelutodennäköisyyttä. Negatiivinen suunta taas merkitsee sitä, että avainsanan esiintyminen vähentää kyseisen puolueen luokittelutodennäköisyyttä.
Datan piirtämiseksi käytettiin R:n ggplot2-kirjastoa jota voi kutsua Jupyter-notebookista käsin. Sarakkeet sort_idx ja sana lisättiin teknisistä syistä jotta pylväät saataisiin kivuttomasti järjestettyä oikeaan järjestykseen. Tämä on ruma ratkaisu siihen ettei ggplotissa (ainakaan ihan helposti) voi järjestellä faktoreita eri järjestykseen eri faceteissa.
In [17]:
%load_ext rpy2.ipython
%R require(ggplot2);
%Rdevice svg
In [18]:
%%R -w 10 -h 20 -u px -i df_out
p <- ggplot(df_out, aes(x = sana, y = absval, fill = suunta)) +
geom_bar(stat = "identity") +
facet_wrap(~puolue, ncol = 2, scales = "free") +
theme(axis.text.x = element_text(size = 14, color = "black", angle = 60, hjust = 1)) +
xlab("") + ylab("") + theme(legend.position = "top")
print(p)
Löydetyt avainsanat vaikuttavat ihan järkeviltä. Esimerkiksi Vihreiltä nousee esiin kärkiteemana perustulo, Vasemmistoliitolta harmaa talous/veroparatiisit ja Kokoomukselta sääntely. RKP:n avainsanoista huomataan, että ruotsinkielisten havaintojen poistamisesta huolimatta jäljelle on jäänyt ruotsinkielisiä vastauksia (esim. det, är). Ehkä jotkut ovat vastanneet vaalikoneeseen molemmmilla kotimaisilla ja langdetect on mennyt tästä sekaisin.
Tässä osiossa tutkitaan mihin puolueisiin luokittelumallimme laittaisivat puolueiden puheenjohtajat. Alla olevassa koodinpätkässä käydään yksitellen läpi puolueiden luokittelumallit ja ennustetaan puheenjohtajille mallin mukaiset osumatodennäköisyydet (muistetaaan, että puoluejohtajien vastaukset poimittiin aiemmin sparse-matriisiin xtest). Todennäköisyydet kerätään numpy-matriisiin mat_preds.
In [19]:
mat_preds = np.zeros((len(pj_test), len(clfs_fin.keys())))
for i, puolue in enumerate(clfs_fin.keys()):
clf = clfs_fin[puolue]
preds = clf.predict_proba(xtest)
mat_preds[:, i] = preds[:, 1]
Todennäköisyysmatriisi piirretään Pythonin seaborn-kirjaston heatmap-funktiolla siten, että puolueet laitetaan riveille ja puheenjohtajat sarakkeille.
In [20]:
sns.set_context("notebook", font_scale=1.2, rc={"lines.linewidth": 2.5})
pylab.rcParams['figure.figsize'] = 8, 8
sns.heatmap(mat_preds.T, yticklabels = clfs_fin.keys(), xticklabels = pj_test.values, annot = True, cbar = False);
Huomataan, että puheenjohtajat soveltuvat omiin puolueisiinsa sen verran hyvin, että jokaisella puoluejohtajalla on oman puolueensa korkein osumatodennäköisyys. Lisäksi Carl Haglundia lukuunottamatta jokaisen puoluejohtajan osuvin puolue on myös heidän oma puolueensa. RKP:n lukuja on vähän vaikea verrata muihin kun osumatodennäköisyydet ovat kauttaaltaan niin pieniä.
Osat matriisin todennäköisyyksistä ovat ihan järkeviä. Esimerkiksi Alexander Stubb sopisi lähes yhtä hyvin Keskustaan kuin Kokoomukseen (tod. 0.17 vs. 0.19), ja puolueet ajoivatkin mielestäni osittain aika samanlaisia teemoja tämän vuoden eduskuntavaaleissa. Paavo Arhinmäki soveltuisi melko hyvin sekä Vihreisiin (0.17) että SDP:hen (0.14). Myös Ville Niinistön erittäin korkea osumatodennäköisyys Vihreille (0.36) vaikuttaa järkevältä.
Matriisista löytyy myös vähän outoja havaintoja. Esimerkiksi Carl Haglundin korkea soveltuvuus SDP:n kanssa (0.17) yllättää. Samoin Päivi Räsäsen korkea osumatodennäköisyys SDP:lle hämmentää hieman. SDP:n osumaluvuissa on yleisesti merkillepantavaa se, että kaikki kahdeksan puheenjohtajaa ovat osumatodennäköisyyksissä aika lähellä toisiaan.
Lievä pettymys on se miten alhaisiksi osumatodennäköisyydet yleisesti jäävät. Korkein yksittäinen todennäköisyys on Ville Niinistön Vihreiden osumatodennäköisyys (0.36), mutta tämäkin on aika kaukana positiivisesta luokittelusta (oletusarvona vain 0.5:n todennäköisyyden ylittävät ennustetaan positiivisiksi havainnoiksi). Vaikka mallit ovat ROC-AUC-lukujen perusteella hyviä ennustamaan osumatodennäköisyyksiä ja erottamaan positiivisia ja negatiivisia havaintoja toisistaan, ne vaatisivat kuitenkin kalibrointia jotta niistä tulisi toimivia luokittelumalleja.
Jotta voitaisiin ymmärtää mallin painotuksia vielä vähän paremmin, voidaan tutkia niitä avainsanoja jotka vahvimmin vaikuttavat jonkun puheenjohtajan luokitteluun muuhun kuin omaan puolueeseen. Ensin tutkitaan Stubbin vastauksia ja Keskustan luokittelumallin painotuksia.
In [21]:
pylab.rcParams['figure.figsize'] = 6, 6
vastaukset_stubb = np.ravel(xtest[7, :].todense())
malli_keskusta = clfs_fin[u'Suomen Keskusta']
loadings = np.ravel(malli_keskusta.coef_)*vastaukset_stubb
df_loadings = pd.DataFrame({'sana':names_long, 'paino':loadings})
df_loadings.sort(['paino'], ascending = False, inplace = True)
df_loadings = df_loadings[0:20]
df_loadings = df_loadings.set_index(df_loadings.sana)
df_loadings.plot(kind = 'barh', fontsize = 13);
Ainakin työn kannattavuus vaikuttaa nousevan esille avainsanojen työn, kannat ja kan myötä. Kaiken kaikkiaan suome on kuitenkin merkittävin yksittäinen sana joka tekee Stubbista todennäköisemmän Keskusta-ehdokkaan. Keskustan ehdokkaiden tapauksessa suome-sana esiintyy useimmiten sanayhteydessä koko suomen tai koko suomi, mutta myös yksittäinen suome nousee tilastollisesti merkitseväksi avainsanaksi. Täten on mahdollista (tai jopa todennäköistä kun vastauksia tarkemmin katsoo), että Stubb on käyttänyt suome-sanaa jokseenkin eri yhteydessä kuin Keskusta-ehdokkaat yleensä, eikä malli ole tätä ymmärtänyt.
Tehdään vielä vastaava tarkastelu Carl Haglundin vastauksille ja hänen soveltuvuudelle SDP:n ehdokkaaksi.
In [22]:
vastaukset_haglund = np.ravel(xtest[1, :].todense())
malli_sdp = clfs_fin[u'Suomen Sosialidemokraattinen Puolue']
loadings = np.ravel(malli_sdp.coef_)*vastaukset_haglund
df_loadings = pd.DataFrame({'sana':names_long, 'paino':loadings})
df_loadings.sort(['paino'], ascending = False, inplace = True)
df_loadings = df_loadings[0:20]
df_loadings = df_loadings.set_index(df_loadings.sana)
df_loadings.plot(kind = 'barh', fontsize = 13);
Haglund nosti avoimissa vastauksissaan esille muun muassa hyvinvointiyhteiskunnan, työllisyyden, ja sen että "hyvä hoiva kuuluu kaikille". Nämä kaikki nousivat mallinnuksessa SDP:n osumatodennäköisyyttä kasvattaviksi avainsanoiksi ja selittävät sen että Haglundin todennäköisyys olla SDP:n ehdokas on verrattain korkea.
Viimeiseksi selvitetään mitkä ehdokkaat ovat kaikista soveltuvimpia eri puolueisiin avoimien vastaustensa perusteella. Sovitetaan jokaiselle puolueelle vielä uudet luokittelumallit siten että myös puheenjohtajat ovat mukana, ja poimitaan aina mukaan ne 15 ehdokasta joilla on korkein soveltuvuustodennäköisyys kyseiselle puolueelle. Tässä "huijataan" vähän siinä mielessä että tehdään analyysi ja mallin sovitus samalla datalla, mutta ei anneta sen nyt häiritä.
Otetaan nyt myös puheenjohtajat mukaan perustieto-dataframeen df_all ja tekstimatriisiin x_all.
In [23]:
df_all = pd.concat((df.loc[~mask_pjt, ['sukunimi', 'etunimi', 'puolue']],
df.loc[mask_pjt, ['sukunimi', 'etunimi', 'puolue']]), ignore_index = True)
df_all['kokonimi'] = df_all.etunimi + ' ' + df_all.sukunimi
x_all = sparse.vstack((xtrain, xtest))
Ajetaan sitten jokaiselle suurelle puolueelle vielä kertaalleen logistinen regressio ja poimitaan viisitoista soveltuvuustodennäköisyyden perusteella ominaisinta ehdokasta kullekin puolueelle.
In [24]:
n_keep = 15
for j, puolue in enumerate(suuret):
y = (df_all.puolue == puolue).astype(int)
clf = LogisticRegression(random_state = 42)
clf.fit(x_all, y)
df_tmp = df_all.copy()
df_tmp['malli'] = puolue
df_tmp['tod'] = clf.predict_proba(x_all)[:, 1]
df_tmp.sort(columns = 'tod', ascending = False, inplace = True)
df_tmp = df_tmp[0:n_keep]
if j == 0:
df_out2 = df_tmp.copy()
else:
df_out2 = df_out2.append(df_tmp.copy(), ignore_index = True)
Plotataan kuva ominaisehdokkaista R:n ggplotilla:
In [25]:
%%R -w 10 -h 20 -u px -i df_out2
p <- ggplot(df_out2, aes(x = reorder(kokonimi, -tod), y = tod, fill = puolue)) +
geom_bar(stat = "identity") +
facet_wrap(~malli, ncol = 2, scales = "free_x") +
theme(axis.text.x = element_text(angle = 60, size = 12, color = "black", hjust = 1)) +
xlab("") + ylab("") + theme(legend.position = "top") +
guides(fill=guide_legend(nrow=2,byrow=TRUE))
print(p)
Kuvaaja ei tarjoa hirveästi yllättäviä ehdokkaita ominaisiksi ehdokkaiksi (esimerkiksi yli puoluerajojen). Ainut poikkeus on Vasemmistoliiton Daniel Nyman, joka on RKP:n mallin mukaan 10:nneksi todennäköisiin RKP:n ehdokas. Tämä selittyy sillä, että Nyman vastasi Vaalikoneen kysymyksiin osittain ruotsiksi. Se, ettei ominaisia ehdokkaita löydy hirveästi yli puoluerajojen johtunee osittain siitä että teimme nyt analyysit ja sovituksen samalle datalle.
Ominaisia ehdokkaita tarkastellessa nähdään että monet esiin nousevat ehdokkaat ovat hieman tuntemattomampia. Puoluejohtajista 15:n ominaisimman ehdokkaan listalle pääsivät vain Paavo Arhinmäki (sija 13) ja Juha Sipilä (sija 3).