Kandidatų duomenų analizė remiasi A. Tapino pasiūlytu metodu.
Rinkimų dieną reikės išsirinkti:
Vienmandatės apygardos kandidatą.
Daugiamandatės apygardos partiją (sąrašą).
Surašyti 5 pirmumo balsus iš pasirinktos partijos.
Tam, kad geriau suprastumėte partijų sudėtį, žemiau rasite visų vienmandačių apygardų ir daugiamandačių partijų sudėčių vizualizaciją sudarytą pagal A. Tapino pasiūlytus vertinimo kriterijus.
Duomenys analizei surinkti iš vrk.lt svetainės.
Jei jus domina, tik rezultatai, galite peršokti tiesiai prie rezultatų dalies. Jei taip pat domina kaip buvo surinkti duomenys ir kaip atlikti skaičiavimai, galite skaityti nuo pradžių.
Jei pastebėjote klaidų, praneškite https://gist.github.com/sirex/e63f65fed33c81a45f6f9dc908afc560 komentaruose.
Perbraukti punktai nebuvo įtraukti į vertinimą, dėl sunkiai prieinamų duomenų.
Vyriausioji rinkimų komisija nepateikia „žalių“ duomenų apie kandidatus, todėl juos tenka susirinkti sunkiuoju būdu iš vrk.lt svetainės. Atliekant analizę, duomenų surinkimas pareikalavo apie 80% viso laiko.
Kadangi duomenys iš vrk.lt svetainės jau surinkti, kad kitiems nereikėtų to kartoti, visus „žalius“ duomenis galite rasti adresu http://atviriduomenys.lt/data/vrk/rinkimai/2016/
%matplotlib inline
import databot
import urllib.parse
import base64
import imghdr
import pandas as pd
import matplotlib as mpl
import matplotlib.pyplot as plt
import numpy as np
import datetime
import re
import textwrap
from IPython.display import display, HTML
mpl.rc('font', family='Ubuntu', size=16)
mpl.rc('figure', figsize=(16, 10))
bot = databot.Bot('vrk_kadidatai.db')
table_page = bot.define('kandidatų-lentelės-puslapis')
table = bot.define('kandidatų-lentelė')
questions_page = bot.define('anketos-puslapis')
questions = bot.define('anketa')
degree = bot.define('mokslo-laipsnis')
photo = bot.define('nuotrauka')
experience = bot.define('patirtis')
education = bot.define('išsilavinimas')
profile_links = bot.define('profilio-nuorodos')
bio_page = bot.define('biografijos-puslapis')
bio = bot.define('biografija')
assets_page = bot.define('turto-deklaracijos-puslapis')
assets = bot.define('turto-deklaracijos')
interests_page = bot.define('interesų-deklaracijos-puslapis')
interests = bot.define('interesų-dekraracijos')
campaign_page = bot.define('kampanijos-lėšų-puslapis')
other_page = bot.define('kita-puslapis')
Atsisiunčiame pirmą puslapį su visų kandidatų sąrašu. vrk.lt svetainėje, nuorodos rodomos naršyklėje užkrauna tik puslapio rėmus, o turinys siunčiamas atskirai, JavaScript pagalba, todėl su nuorodomis reikia tam tikro „masažavimo“.
table_page.download('http://vrk.lt/pub/webcontent/kandidatai/lrsKandidataiPilnasSarasasHtml?rnkId=426')
Iš pirmo puslapio išsirenkame nuorodas į kandidatų profilius ir duomenis iš kandidatų lentelės.
@databot.func(skipna=True)
def fixlink(link):
url = dict(urllib.parse.parse_qsl(urllib.parse.urlparse(link).query))['srcUrl']
return 'http://vrk.lt/pub/webcontent/' + url
@databot.func()
def vardas(name):
return ' '.join([w for w in name.split() if w[-1].islower()])
@databot.func()
def pavarde(name):
return ' '.join([w for w in name.split() if w[-1].isupper()])
with table_page:
table.select([
'table.partydata tbody tr', (
fixlink('td[1] [email protected]'), {
'vardas': vardas('td[1] a:text'),
'pavardė': pavarde('td[1] a:text'),
'sąrašas': {
'pavadinimas': 'td[2] a:text?',
'nuoroda': 'td[2] [email protected]?',
},
'numeris sąraše': 'td[3]:text',
'vienmadatė apygarda': {
'pavadinimas': 'td[5] a:text?',
'nuoroda': 'td[5] [email protected]?',
},
'iškėlė vienmandatėje': {
'pavadinimas': databot.first('td[6] a:text?', 'td[6]:text'),
'nuoroda': 'td[6] [email protected]?',
},
}
)
])
with table:
questions_page.download()
@databot.func(skipna=True)
def join(value):
return ' '.join(value)
with questions_page:
questions.select([
'body > div[3] > table > tr', (
databot.row.key, {
'klausimas': join(['xpath:./td/text()']),
'atsakymas': 'xpath:./td/b/text()?',
}
)
])
with questions_page:
degree.select(databot.row.key, 'xpath://td[contains(text(), "Jei turite, nurodykite pedagoginį vardą, mokslo laipsnį")]/b/text()')
@databot.func(skipna=True)
def decode_photo(value):
return base64.b64decode(value[len('data:;base64,'):])
@databot.func(skipna=True)
def detect_image_type(blob):
return imghdr.what(None, blob)
with questions_page:
photo.clean().reset().select(databot.row.key, {
'body': decode_photo('body > div[1] [email protected]?'),
'type': detect_image_type(decode_photo('body > div[1] [email protected]?')),
})
with questions_page:
profile_links.clean().reset().select(
databot.row.key, {
'biografija': fixlink('xpath://body/ul[1]/li/a[contains(text(), "Biografija")]/@href'),
'turtas': fixlink('xpath://body/ul[1]/li/a[contains(text(), "Turto ir pajamų")]/@href'),
'interesai': fixlink('xpath://body/ul[1]/li/a[contains(text(), "Privačių interesų")]/@href'),
'kampanija': fixlink('xpath://body/ul[1]/li/a[contains(text(), "Politinės kampanijos")]/@href?'),
'kita': fixlink('xpath://body/ul[1]/li/a[contains(text(), "Kita")]/@href'),
},
)
Atsisiunčiame visus kandidato profilio puslapius iš profilio meniu.
with profile_links:
bio_page.download(databot.row.value.biografija)
assets_page.download(databot.row.value.turtas)
interests_page.download(databot.row.value.interesai)
campaign_page.download(databot.row.value.kampanija)
other_page.download(databot.row.value.kita)
with questions_page:
experience.select([
'xpath://table[contains(thead/tr/th/text(), "Institucijos pavadinimas, pareigos")]/tr', (
databot.row.key, {
'institucija': split('td[1]:text', 1, ',', 0),
'pareigos': split('td[1]:text', 1, ',', 1),
'laikotarpis': split('td[2]:text', 1, ',', 0),
})
])
with questions_page:
education.select([
'xpath://table[contains(thead/tr/th/text(), "Išsilavinimas")]/tr', (
databot.row.key, {
'išsilavinimas': 'td[1]:text',
'mokymo įstaiga': 'td[2]:text',
'specialybė': 'td[3]:text',
'baigimo metai': 'td[4]:text',
})
])
@databot.func(skipna=True)
def euro(value):
return int(value.replace('Eur', '').strip())
with assets_page:
assets.select(databot.row.key, {
'turtas': euro('xpath://tr[contains(td/text(), "Privalomas registruoti turtas")]/td[2]/b/text()'),
'vertybės': euro('xpath://tr[contains(td/text(), "Vertybiniai popieriai")]/td[2]/b/text()'),
'pinigai': euro('xpath://tr[contains(td/text(), "Piniginės lėšos")]/td[2]/b/text()'),
'suteiktos paskolos': euro('xpath://tr[contains(td/text(), "Suteiktos paskolos")]/td[2]/b/text()'),
'gautos paskolos': euro('xpath://tr[contains(td/text(), "Gautos paskolos")]/td[2]/b/text()'),
})
with bio_page:
bio.select(databot.row.key, databot.text('body > div[3]'))
Patikriname ar visi puslapiai sėkmingai atsisiuntė ir ar duomenų išrinkimas praėjo be klaidų.
profilio-nuorodos -> (biografijos-puslapis, kampanijos-lėšų-puslapis
turi klaidų, bet tos klaidos susiję su tuo, kad ne visi kandidatai turi biografijos puslapius ir ne visi asmeniškai administruoja savo kampanijos lėšas, todėl tų puslapių tiesiog nėra.
bot.main(argv=['status'])
Eksportuojame, tai kas buvo išrinkta į csv
formatą, tam, kad būtų galima duomenis importuoti į Pandas įrankį.
questions.export('anketa.csv')
experience.export('patirtis.csv')
education.export('išsilavinimas.csv')
bio.export('bio.csv')
table.export('kandidatai.csv')
degree.export('mokslo-laipsnis.csv')
Importuojam visus išrinktus duomenis į Pandas.
anketa = pd.read_csv('anketa.csv').pivot(values='atsakymas', index='key', columns='klausimas')
patirtis = pd.read_csv('patirtis.csv')
išsilavinimas = pd.read_csv('išsilavinimas.csv')
biografija = pd.read_csv('bio.csv')
biografija['key'] = biografija.key.str.replace('KandidatasBiografijaHtml', 'KandidatasAnketaHtml')
biografija = biografija.set_index('key')
kandidatai = pd.read_csv('kandidatai.csv', index_col='key')
mokslolaipsnis = pd.read_csv('mokslo-laipsnis.csv', index_col='key', na_values='Nenurodė').dropna()
Panašu, kad to, kas suvesta į kandidatų anketas, niekas netikrina. Dalis duomenų surašyti laisvu tekstu, be jokios tvarkos, kaip kam patinka. Tai labai apsunkina duomenų surinkimą. Geras pavyzdys yra mokymo įstaigos pavadinimas, kur vieni pavadinimą rašo sutrumpintai, kiti pateikia pilną pavadinimą, kiti pavadinimą nurodo su gramatinėmis klaidomis, treti nurodo ne oficialų pavadinimą, o kokį nors alternatyvų, trumpesnį pavadinimą.
lietuvos = [
'lietuvos', 'vilni?ai?us', 'vilnius', 'vilniuas', 'kau[nt]o', 'klaipėdos', 'šiaulių', 'utenos',
'marijampolės', 'kelmės', 'vilkijos', 'švenčioni?ų', 'rietavo',
'joniškėlio', 'alytaus', 'smalininkų', 'švenčionėlių', 'šalčininkų', 'tauragės', 'plungės',
'vytauto', 'ri?omerio', 'stulginskio',
'žemės ūkio', 'kolegija', 'gimnazija', 'mokykla', 'valstybinė', 'technikumas',
'^teisės universitetas$', '^teisės magistras$', '^vadybos ir ekonomikos universitetas$',
'^psichologijos akademija$',
'policijos', 'medicinos', 'nenurodė', r'Elekt\.tech',
r'^.{1,3}[UAIMRĮ](\b|$)', '^.{1,5}$', '^LT ', '^kmaik$', '^važum$', '^mruni$',
]
tarptautinis_išsilavinimas = ~(
išsilavinimas['mokymo įstaiga'].str.contains(re.compile('|'.join(lietuvos), flags=re.IGNORECASE))
)
išsilavinimas[tarptautinis_išsilavinimas]['mokymo įstaiga'].value_counts().head()
specialybės = [
'teisė', 'viešasis administravimas', 'teisinink(as|ė)', 'politikos mokslai', 'politologija', 'vadyb(a|os)'
]
tinkamas_išsilavinimas = (
išsilavinimas.specialybė.str.contains(re.compile('|'.join(specialybės), flags=re.IGNORECASE))
)
išsilavinimas[tinkamas_išsilavinimas].specialybė.value_counts().head()
magistras = mokslolaipsnis.value.str.contains('magistr', case=False)
daktaras = mokslolaipsnis.value.str.contains('daktar', case=False)
komunistas = ['komjaunimo', 'komunistų partijos', 'tskp narys']
komunistas = biografija.value.fillna('').str.contains(re.compile('|'.join(komunistas), flags=re.IGNORECASE))
nekomunistas = ['išaiškinant buvusių komjaunimo veikėjų', 'baigė kauno komjaunimo']
nekomunistas = biografija.value.fillna('').str.contains(re.compile('|'.join(nekomunistas), flags=re.IGNORECASE))
komunistas = komunistas & ~nekomunistas
display(biografija[komunistas].head())
biografija[komunistas].shape
Galiausiai išvalius visus duomenis, sudedame juos į vieną didelė lentelę, ir transformuojame duomenis į tokį pavidalį, kurį bus patogu nadoti tolesniuose skaičiavimuose.
now = datetime.datetime.now()
aukštasis = išsilavinimas.išsilavinimas == 'Aukštasis universitetinis'
kitos_kalbos = lambda v: sum(1 for x in v.split(',') if x.lower().strip() not in ('anglų', 'rusų', 'nenurodė'))
data = pd.DataFrame({
'amžius': (now - pd.to_datetime(anketa['5. Gimimo data '])).dt.days // 365,
'patirtis': patirtis[patirtis.pareigos == 'Seimo narys'].groupby('key').size(),
'anglų kalba': anketa['13. Kokias užsienio kalbas mokate '].str.contains('anglų', case=False),
'kitos kalbos': anketa['13. Kokias užsienio kalbas mokate '].apply(kitos_kalbos),
'aukštasis išsilavinimas': išsilavinimas[aukštasis].groupby('key').size() > 0,
'tarptautinis išsilavinimas': išsilavinimas[tarptautinis_išsilavinimas].groupby('key').size() > 0,
'tinkamas išsilavinimas': išsilavinimas[tinkamas_išsilavinimas].groupby('key').size() > 0,
'magistras': magistras,
'daktaras': daktaras,
'komunistas': komunistas,
})
data['patirtis'] = data['patirtis'].fillna(0).astype(int)
data['anglų kalba'] = data['anglų kalba'].fillna(False).astype(int)
data['kitos kalbos'] = data['kitos kalbos'].fillna(0).astype(int)
data['daktaras'] = data.daktaras.fillna(False).astype(int)
data['magistras'] = data.magistras.fillna(False).astype(int) | data.daktaras
data['aukštasis išsilavinimas'] = data['aukštasis išsilavinimas'].fillna(False).astype(int) | data.magistras
data['tarptautinis išsilavinimas'] = data['tarptautinis išsilavinimas'].fillna(False).astype(int)
data['tinkamas išsilavinimas'] = data['tinkamas išsilavinimas'].fillna(False).astype(int)
data['komunistas'] = data.komunistas.astype(int)
data['amžius'] = (data.amžius > 65).fillna(False).astype(int)
data.head(2).T
Žemiau panaudojus išvalytus duomenis sudedami visi balai ir gaunama viena didelė rezultatų lentelė. Balai paimti iš A. Tapino pasiūlytos formulės.
Deje, iš vrk pateiktų duomenų ne viską pavyko išgauti, todėl, ne visi kriterijai yra įvertinti.
Taip pat, tokie kriterijai, kaip išsilavinimas, nėra 100% patikimi, kadangi kai kurie kandidatai nenurodė išsilavinimo, arba išsilavinimą nurodį tik biografijoje, bet ne anketoje.
Atpažinimas ar kandidatas ėjo aukštas pareigas sovietiniame režime, taip pat nėra 100% patikimas, kadangi tokio požymio nustatymui buvo tiesiog ieškoma tam tikrų raktinių žodžių biografijoje. Jei pastebėjote, klaidų rašykite komentaruose.
def tapino_formule(res):
# - Keitęs partiją: -3 balai
#
# + Nemoka anglų kalbos: -4 balai
res['Nemoka anglų kalbos'] = (data['anglų kalba'] - 1) * 4
# + Moka daugiau kalbų (be anglų ir rusų): +2 balai už kiekvieną kalbą
res['Moka daugiau kalbų (be anglų ir rusų)'] = data['kitos kalbos'] * 2
# + Neturi aukštojo išsilavinimo: -6 balai
res['Neturi aukštojo išsilavinimo'] = (data['aukštasis išsilavinimas'] - 1) * 6
# + Turi magistro aukštąjį išsilavinimą: +2 balai
res['Turi magistro aukštąjį išsilavinimą'] = data.magistras * 2
# + Turi mokslų daktaro laipsnį: +4 balai
res['Turi mokslų daktaro laipsnį'] = data.daktaras * 4
# - Turi tik sovietinį aukštąjį išsilavinimą: -3 balai
#
# + Turi aukštąjį išsilavinimą užsienio universitete: +3 balai
res['Turi aukštąjį išsilavinimą užsienio universitete'] = data['tarptautinis išsilavinimas'] * 3
# + Išsilavinimas nesusijęs su teise, politika ar vadyba: -1 balas
res['Išsilavinimas nesusijęs su teise, politika ar vadyba'] = (data['tinkamas išsilavinimas'] - 1) * 1
# + Ėjo aukštas pareigas sovietiniame režime: -3 balai
res['Ėjo aukštas pareigas sovietiniame režime'] = data.komunistas * -3
# - Turi patirties valstybės tarnyboje ar viešajame sektoriuje užsienyje: nuo -3
# iki +3 jūsų nuožiūra (buhalterė kolūkyje šiek tiek skiriasi nuo Europos audito
# rūmų seniūno)
#
# - Visą gyvenimą užsiėmė veikla, niekuo nesusijusia nei su valstybės valdymu,
# nei su viešuoju administravimu: -4 balai
#
# - Daugiau kaip penkias kadencijas Seime: -3 balai
#
# - Daugiau kaip tris kadencijas Seime: -1 balas
#
# - Neturi patirties Seime: patys nuspręskite, kaip vertinti - pliusu ar minusu
#
# + Daugiau nei 65 metai: -2 balai (jeigu norite, kad į valdžią ateitų jaunesni,
# jeigu manote, kad valdžioje turi būti pensinio amžiaus žmonės, tada skirkite
# pliusinius balus )
res['Daugiau nei 65 metai'] = data.amžius * -2
# - Pajamų ir interesų deklaracija jums kelia įtarimų: -2 balai.
#
# Skaitykite daugiau:
# http://www.delfi.lt/news/ringas/lit/a-tapinas-lemiamas-ir-trumpas-gidas-uz-ka-balsuoti.d?id=72463294
return res
vienmadačiai = kandidatai[~kandidatai['vienmadatė apygarda.pavadinimas'].isnull()][[
'vardas',
'pavardė',
'vienmadatė apygarda.pavadinimas',
]]
vienmadačiai['kandidatas'] = vienmadačiai.vardas + ' ' + vienmadačiai.pavardė
apygardos = vienmadačiai['vienmadatė apygarda.pavadinimas'].value_counts().index
apygardos = sorted(apygardos, key=lambda x: int(x.split(None, 1)[0].rstrip('.')))
vienmadačiai['apygarda'] = pd.Categorical(vienmadačiai['vienmadatė apygarda.pavadinimas'], categories=apygardos)
vienmadačiai = vienmadačiai[['kandidatas', 'apygarda']]
res = tapino_formule(pd.DataFrame(vienmadačiai))
res = res.sort_values('apygarda').set_index(['apygarda', 'kandidatas'])
Grafikas žemiau parodo kiekvienos viemandatės kandidatų pasiskirstimą, pagal įvertinimo balą. Kaip interpretuoti šį grafiką galite sužinoti pasiskaitę Vikipedijoje.
Mėlina dėžutė rodo kandiatus nuo 25 iki 75 procentų, raudona juostelė rodo medianą, raudonas kvardatėlis rodo vidurkį, juodos juostelės iš šonų rodo ribas kur patenka mažiausią ir didžiausią balus turintys kandidatai, pluso ženklai rodo pavienius kandidatus, kurie stipriai skiriasi nuo visų kitų kandidatų.
Bendras šio grafiko vertinimas yra toks, kad visų kandidatų vidurkis yra minusinis ir tik retais atvejais kandidatai turi teigiamą įvertinimą.
columns = (res.sum(axis=1).median(axis=0, level=0)).sort_values().index.get_values()
ax = res.sum(axis=1).unstack().T[columns].plot.box(vert=False, showmeans=True, grid=True, figsize=(16, 42))
ax.set_yticklabels(['\n'.join(textwrap.wrap(x, 30)[:3]) for x in columns])
None
frame = pd.DataFrame({'balas': res.sum(axis=1)})
frame = frame.reset_index(level=1).sort_index()
for i in range(len(frame.groupby(level=0).groups)):
label = apygardos[i]
group = frame.ix[label].set_index('kandidatas').sort_values('balas')
fig = plt.figure(figsize=(16, group.shape[0] // 3))
ax = fig.add_axes([0, 0, 0.5, 1])
x = group.balas.values
y = np.arange(len(x))
categories = [
((x >= 0), 'green'),
((x < 0), 'red'),
]
for category, color in categories:
if len(category) > 0:
cx = x[category]
cy = y[category]
ax.hlines(cy, 0, cx, color=color)
ax.plot(cx, cy, 'o', color=color)
ax.plot([0, 0], [-1, len(y)], '--', color='blue')
display(HTML('<h3>%s</h3>' % label))
ax.set_yticks(y)
ax.set_yticklabels(['%30s' % group.index[0]] + list(group.index)[1:], family='Ubuntu Mono')
ax.set_xlabel('Balas')
ax.set_ylabel('Kandidatas')
plt.xlim(-20, 15)
plt.ylim(-1, len(y))
plt.grid()
plt.show()