Vizualizace průběhu sčítání
parent
5d2d940791
commit
54a0fdb65b
@ -0,0 +1,151 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import matplotlib.pyplot as plt
|
||||
import xml.etree.ElementTree as ET
|
||||
from datetime import date, time, datetime
|
||||
from dataclasses import dataclass
|
||||
from glob import glob
|
||||
from typing import Sequence
|
||||
from functools import reduce
|
||||
|
||||
VOLICU_CELKEM = 8_245_962
|
||||
OKRSKU_CELKEM = 14_857
|
||||
kandidati_jmena = {
|
||||
1: 'Pavel Fischer',
|
||||
2: 'Jaroslav Bašta',
|
||||
4: 'Petr Pavel',
|
||||
5: 'Tomáš Zima',
|
||||
6: 'Danuše Nerudová',
|
||||
7: 'Andrej Babiš',
|
||||
8: 'Karel Diviš',
|
||||
9: 'Marek Hilšer',
|
||||
}
|
||||
|
||||
@dataclass
|
||||
class VysledekOkrsku:
|
||||
poradi: int
|
||||
timestamp: datetime
|
||||
kandidati: dict[str, int]
|
||||
ucast: dict
|
||||
|
||||
def dopln_vysledek(self):
|
||||
assert self.poradi != 0, "Dávka má mít kladné pořadí"
|
||||
self.kandidati['NEPLATNÉ'] = int(self.ucast['ODEVZDANE_OBALKY']) - int(self.ucast['PLATNE_HLASY'])
|
||||
self.kandidati['NEVOLILI'] = int(self.ucast['ZAPSANI_VOLICI']) - int(self.ucast['ODEVZDANE_OBALKY'])
|
||||
# Ztracené (vydané a neodevzdané) obálky neřešíme
|
||||
# Nezapočítané hlasy neexistují na úrovni okrsku
|
||||
|
||||
def nacti_davku(fn) -> dict[tuple[int, int], VysledekOkrsku]:
|
||||
okrsky: dict[tuple[int, int], VysledekOkrsku] = {}
|
||||
|
||||
with open(fn, 'r') as f:
|
||||
root = ET.parse(f).getroot()
|
||||
NS = {'': r'http://www.volby.cz/prezident/'}
|
||||
|
||||
def zpracuj_okrsek(elem) -> tuple[VysledekOkrsku, tuple[int, int]]:
|
||||
idOkrsku = tuple(map(int, (elem.get(x) for x in ['CIS_OBEC', 'CIS_OKRSEK'])))
|
||||
kandidati = {}
|
||||
for kandidat in elem.findall('./HLASY_OKRSEK', NS):
|
||||
cislo = int(kandidat.attrib['PORADOVE_CISLO'])
|
||||
hlasy = int(kandidat.attrib['HLASY'])
|
||||
jmeno = kandidati_jmena[cislo]
|
||||
kandidati[jmeno] = hlasy
|
||||
return VysledekOkrsku(
|
||||
poradi = int(elem.attrib['PORADI_ZPRAC']),
|
||||
timestamp = datetime.fromisoformat(elem.attrib['DATUM_CAS_ZPRAC']),
|
||||
ucast = elem.find('./UCAST_OKRSEK', NS).attrib,
|
||||
kandidati = kandidati,
|
||||
), idOkrsku
|
||||
|
||||
for okrsek in root.findall('./OKRSEK', NS):
|
||||
vysl, id = zpracuj_okrsek(okrsek)
|
||||
vysl.dopln_vysledek()
|
||||
okrsky[id] = vysl
|
||||
|
||||
return okrsky
|
||||
|
||||
def zpracuj_davky(fns: Sequence[str]):
|
||||
# Zpracovávací fáze: načteme dávku, nahradíme předchozí instance pro okrsky
|
||||
# Počítací fáze: sečteme hlasy za všechny okrsky, vyrobíme dump nebo koláč ke každému času
|
||||
# Uložíme pod timestampem do nějaké složky.
|
||||
# Tyto dvě fáze ale musí probíhat zároveň, pokud chceme mít vykreslený graf za každý okamžik předání
|
||||
# Jiný program: vezme dumpy/koláče a naskládá je správně do videa (případně provede jinou analýzu)
|
||||
|
||||
okrsky: dict[tuple[int, int], VysledekOkrsku] = {}
|
||||
celkovy_vysledek: dict[str, int] = {}
|
||||
for fn in fns:
|
||||
print(f'Zpracovávám dávku {fn}')
|
||||
nove = nacti_davku(fn)
|
||||
podle_casu = sorted(nove.items(), key=lambda x: x[1].timestamp)
|
||||
for okrsek, vysledek_okrsku in podle_casu:
|
||||
# Přidat okrsek do dat, přepočítat a exportovat výsledek
|
||||
if okrsek in okrsky:
|
||||
assert vysledek_okrsku.poradi > okrsky[okrsek].poradi
|
||||
# Odečteme předchozí výsledek tohohle okrsku
|
||||
for k in okrsky[okrsek].kandidati:
|
||||
celkovy_vysledek[k] -= okrsky[okrsek].kandidati[k]
|
||||
okrsky[okrsek] = vysledek_okrsku
|
||||
for k in vysledek_okrsku.kandidati:
|
||||
if k not in celkovy_vysledek: celkovy_vysledek[k] = 0
|
||||
celkovy_vysledek[k] += vysledek_okrsku.kandidati[k]
|
||||
# Počítání dat:
|
||||
ts = vysledek_okrsku.timestamp
|
||||
celkovy_vysledek['NEZAPOČÍTANÉ'] = 0 # del který funguje i v první iteraci :-)
|
||||
celkovy_vysledek['NEZAPOČÍTANÉ'] = VOLICU_CELKEM - sum(celkovy_vysledek.values())
|
||||
# Vizualizace / export
|
||||
# FIXME: Zkopírováno, negenerické, fuj. Má používat nějaký dedikovaný spoolečný kód.
|
||||
plt.clf()
|
||||
# Pro srovnání: zpracování okrsků
|
||||
# TODO: WTF: když tohle nakreslím až po kandidátech, tak se
|
||||
# kandidáti vyrenderují mimo bbox.
|
||||
okrsku_secteno = len(okrsky.keys())
|
||||
okrsku_zbyva = OKRSKU_CELKEM - okrsku_secteno
|
||||
plt.pie(
|
||||
[okrsku_secteno, okrsku_zbyva],
|
||||
colors=['#000088', '#cccccc'],
|
||||
center=(1, -1), radius=0.3,
|
||||
)
|
||||
|
||||
# Zastoupení kandidátů
|
||||
order = (
|
||||
'Petr Pavel',
|
||||
'Danuše Nerudová',
|
||||
'Marek Hilšer',
|
||||
'Pavel Fischer',
|
||||
'Karel Diviš',
|
||||
'Tomáš Zima',
|
||||
'Jaroslav Bašta',
|
||||
'Andrej Babiš',
|
||||
'NEPLATNÉ',
|
||||
'NEVOLILI',
|
||||
'NEZAPOČÍTANÉ',
|
||||
)
|
||||
# FIXME: průběžné výsledky nemají některé kandidáty. Tohle bychom neměli opravovat tady.
|
||||
x = [celkovy_vysledek.get(i, 0) for i in order]
|
||||
print(ts, x)
|
||||
labels = order
|
||||
label = str(ts)
|
||||
colors = {
|
||||
'Petr Pavel': '#627210',
|
||||
'Danuše Nerudová': '#811367',
|
||||
'Andrej Babiš': '#262161',
|
||||
'Jaroslav Bašta': '#B51119',
|
||||
'Marek Hilšer': '#CA834E',
|
||||
'Pavel Fischer': '#244C76',
|
||||
'Karel Diviš': '#3B6E5D',
|
||||
'Tomáš Zima': '#E5DE1A',
|
||||
'NEPLATNÉ': '#000000',
|
||||
'NEVOLILI': '#666666',
|
||||
'NEZAPOČÍTANÉ': '#CCCCCC',
|
||||
}
|
||||
plt.pie(x, labels=labels, colors=[colors[i] for i in order], autopct='%1.3f %%')
|
||||
plt.text(0, -1.2, label, ha='center')
|
||||
|
||||
# save
|
||||
fn = f'progress_png/{ts}.png'
|
||||
plt.savefig(fn)
|
||||
|
||||
|
||||
zpracuj_davky(
|
||||
sorted(glob('davky/*.xml'))
|
||||
)
|
@ -0,0 +1,205 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import matplotlib.pyplot as plt
|
||||
import xml.etree.ElementTree as ET
|
||||
from datetime import date, time, datetime, timedelta
|
||||
from dataclasses import dataclass
|
||||
from glob import glob
|
||||
from typing import Sequence
|
||||
from functools import reduce
|
||||
|
||||
VOLICU_CELKEM = 8_245_962
|
||||
OKRSKU_CELKEM = 14_857
|
||||
START = datetime.fromisoformat('2023-01-14T14:00:00')
|
||||
kandidati_jmena = {
|
||||
1: 'Pavel Fischer',
|
||||
2: 'Jaroslav Bašta',
|
||||
4: 'Petr Pavel',
|
||||
5: 'Tomáš Zima',
|
||||
6: 'Danuše Nerudová',
|
||||
7: 'Andrej Babiš',
|
||||
8: 'Karel Diviš',
|
||||
9: 'Marek Hilšer',
|
||||
}
|
||||
|
||||
@dataclass
|
||||
class VysledekOkrsku:
|
||||
poradi: int
|
||||
timestamp: datetime
|
||||
kandidati: dict[str, int]
|
||||
ucast: dict
|
||||
|
||||
def dopln_vysledek(self):
|
||||
assert self.poradi != 0, "Dávka má mít kladné pořadí"
|
||||
self.kandidati['NEPLATNÉ'] = int(self.ucast['ODEVZDANE_OBALKY']) - int(self.ucast['PLATNE_HLASY'])
|
||||
self.kandidati['NEVOLILI'] = int(self.ucast['ZAPSANI_VOLICI']) - int(self.ucast['ODEVZDANE_OBALKY'])
|
||||
# Ztracené (vydané a neodevzdané) obálky neřešíme
|
||||
# Nezapočítané hlasy neexistují na úrovni okrsku
|
||||
|
||||
def nacti_davku(fn) -> dict[tuple[int, int], VysledekOkrsku]:
|
||||
okrsky: dict[tuple[int, int], VysledekOkrsku] = {}
|
||||
|
||||
with open(fn, 'r') as f:
|
||||
root = ET.parse(f).getroot()
|
||||
NS = {'': r'http://www.volby.cz/prezident/'}
|
||||
|
||||
def zpracuj_okrsek(elem) -> tuple[VysledekOkrsku, tuple[int, int]]:
|
||||
idOkrsku = tuple(map(int, (elem.get(x) for x in ['CIS_OBEC', 'CIS_OKRSEK'])))
|
||||
kandidati = {}
|
||||
for kandidat in elem.findall('./HLASY_OKRSEK', NS):
|
||||
cislo = int(kandidat.attrib['PORADOVE_CISLO'])
|
||||
hlasy = int(kandidat.attrib['HLASY'])
|
||||
jmeno = kandidati_jmena[cislo]
|
||||
kandidati[jmeno] = hlasy
|
||||
return VysledekOkrsku(
|
||||
poradi = int(elem.attrib['PORADI_ZPRAC']),
|
||||
timestamp = datetime.fromisoformat(elem.attrib['DATUM_CAS_ZPRAC']),
|
||||
ucast = elem.find('./UCAST_OKRSEK', NS).attrib,
|
||||
kandidati = kandidati,
|
||||
), idOkrsku
|
||||
|
||||
for okrsek in root.findall('./OKRSEK', NS):
|
||||
vysl, id = zpracuj_okrsek(okrsek)
|
||||
vysl.dopln_vysledek()
|
||||
okrsky[id] = vysl
|
||||
|
||||
return okrsky
|
||||
|
||||
@dataclass
|
||||
class KolacovaData:
|
||||
kandidati: dict[str, int]
|
||||
timestamp: datetime
|
||||
secteno_okrsku: int
|
||||
|
||||
def zpracuj_davky(fns: Sequence[str]):
|
||||
# Zpracovávací fáze: načteme dávku, nahradíme předchozí instance pro okrsky
|
||||
# Počítací fáze: sečteme hlasy za všechny okrsky, vyrobíme dump nebo koláč ke každému času
|
||||
# Uložíme pod timestampem do nějaké složky.
|
||||
# Tyto dvě fáze ale musí probíhat zároveň, pokud chceme mít vykreslený graf za každý okamžik předání
|
||||
# Jiný program: vezme dumpy/koláče a naskládá je správně do videa (případně provede jinou analýzu)
|
||||
|
||||
okrsky: dict[tuple[int, int], VysledekOkrsku] = {}
|
||||
celkovy_vysledek: dict[str, int] = {}
|
||||
data: list[KolacovaData] = []
|
||||
for fn in fns:
|
||||
print(f'Zpracovávám dávku {fn}')
|
||||
nove = nacti_davku(fn)
|
||||
podle_casu = sorted(nove.items(), key=lambda x: x[1].timestamp)
|
||||
for okrsek, vysledek_okrsku in podle_casu:
|
||||
# Přidat okrsek do dat, přepočítat a exportovat výsledek
|
||||
if okrsek in okrsky:
|
||||
assert vysledek_okrsku.poradi > okrsky[okrsek].poradi
|
||||
# Odečteme předchozí výsledek tohohle okrsku
|
||||
for k in okrsky[okrsek].kandidati:
|
||||
celkovy_vysledek[k] -= okrsky[okrsek].kandidati[k]
|
||||
okrsky[okrsek] = vysledek_okrsku
|
||||
for k in vysledek_okrsku.kandidati:
|
||||
if k not in celkovy_vysledek: celkovy_vysledek[k] = 0
|
||||
celkovy_vysledek[k] += vysledek_okrsku.kandidati[k]
|
||||
# Počítání dat:
|
||||
ts = vysledek_okrsku.timestamp
|
||||
celkovy_vysledek['NEZAPOČÍTANÉ'] = 0 # del který funguje i v první iteraci :-)
|
||||
celkovy_vysledek['NEZAPOČÍTANÉ'] = VOLICU_CELKEM - sum(celkovy_vysledek.values())
|
||||
# Vizualizace / export
|
||||
data.append(KolacovaData(
|
||||
kandidati = dict(celkovy_vysledek),
|
||||
timestamp = ts,
|
||||
secteno_okrsku = len(okrsky.keys())
|
||||
))
|
||||
return data
|
||||
|
||||
# FIXME: ne globálním datům!
|
||||
data = [KolacovaData(
|
||||
kandidati={'NEZAPOČÍTANÉ': VOLICU_CELKEM},
|
||||
timestamp=START,
|
||||
secteno_okrsku=0,
|
||||
)]
|
||||
data.extend(zpracuj_davky(sorted(glob('davky/*.xml'))))
|
||||
|
||||
# Reálně ale kašleme na jednotlivé sekundy, takže z každé vezmeme jen poslední výsledek
|
||||
po_sekundach = {}
|
||||
for x in data:
|
||||
po_sekundach[x.timestamp] = x
|
||||
casy = sorted(po_sekundach.keys())
|
||||
|
||||
def frametime_to_realtime(t):
|
||||
# Až na úplný konec tak sčítání těchto voleb trvalo asi 3 hodiny. Řekněme,
|
||||
# že cílíme na cca 10minutové video, tedy zrychlení 18x.
|
||||
delta = timedelta(minutes=t.minute, hours=t.hour, seconds=t.second, microseconds=t.microsecond)
|
||||
delta *= 18
|
||||
return START + delta
|
||||
|
||||
from bisect import bisect_right
|
||||
def data_for_timestamp(ts) -> KolacovaData:
|
||||
idx = bisect_right(casy, ts)
|
||||
return po_sekundach[casy[idx-1]] # bisect_right vrací konzistentně hodnotu o jedna větší, než potřebujeme.
|
||||
|
||||
|
||||
def visualize_data(data):
|
||||
# Tady se provede ta vizualizace, kterou děláme pořád dokolečka.
|
||||
# FIXME: zase jsem to zkopíroval :-P
|
||||
plt.clf()
|
||||
# Pro srovnání: zpracování okrsků
|
||||
# TODO: WTF: když tohle nakreslím až po kandidátech, tak se
|
||||
# kandidáti vyrenderují mimo bbox.
|
||||
okrsku_zbyva = OKRSKU_CELKEM - data.secteno_okrsku
|
||||
plt.pie(
|
||||
[data.secteno_okrsku, okrsku_zbyva],
|
||||
colors=['#000088', '#cccccc'],
|
||||
center=(1, -1), radius=0.3,
|
||||
)
|
||||
|
||||
# Zastoupení kandidátů
|
||||
order = (
|
||||
'Petr Pavel',
|
||||
'Danuše Nerudová',
|
||||
'Marek Hilšer',
|
||||
'Pavel Fischer',
|
||||
'Karel Diviš',
|
||||
'Tomáš Zima',
|
||||
'Jaroslav Bašta',
|
||||
'Andrej Babiš',
|
||||
'NEPLATNÉ',
|
||||
'NEVOLILI',
|
||||
'NEZAPOČÍTANÉ',
|
||||
)
|
||||
# FIXME: průběžné výsledky nemají některé kandidáty. Tohle bychom neměli opravovat tady.
|
||||
x = [data.kandidati.get(i, 0) for i in order]
|
||||
labels = order
|
||||
label = str(data.timestamp)
|
||||
colors = {
|
||||
'Petr Pavel': '#627210',
|
||||
'Danuše Nerudová': '#811367',
|
||||
'Andrej Babiš': '#262161',
|
||||
'Jaroslav Bašta': '#B51119',
|
||||
'Marek Hilšer': '#CA834E',
|
||||
'Pavel Fischer': '#244C76',
|
||||
'Karel Diviš': '#3B6E5D',
|
||||
'Tomáš Zima': '#E5DE1A',
|
||||
'NEPLATNÉ': '#000000',
|
||||
'NEVOLILI': '#666666',
|
||||
'NEZAPOČÍTANÉ': '#CCCCCC',
|
||||
}
|
||||
plt.pie(x, labels=labels, colors=[colors[i] for i in order], autopct='%1.3f %%')
|
||||
plt.text(0, -1.2, label, ha='center')
|
||||
|
||||
# save
|
||||
return plt.gcf()
|
||||
|
||||
# https://zulko.github.io/moviepy/getting_started/working_with_matplotlib.html
|
||||
from moviepy.editor import VideoClip
|
||||
from moviepy.video.io.bindings import mplfig_to_npimage
|
||||
|
||||
def make_frame(t):
|
||||
# wtf is t?
|
||||
data: KolacovaData
|
||||
#data = data_for_timestamp(frametime_to_realtime(t))
|
||||
data = data_for_timestamp(START + timedelta(seconds=t)*18)
|
||||
fig = visualize_data(data)
|
||||
return mplfig_to_npimage(fig)
|
||||
|
||||
def make_anim():
|
||||
anim = VideoClip(make_frame, duration=600)
|
||||
anim.write_videofile('progress.mkv', codec='hevc', fps=24)
|
||||
|
||||
make_anim()
|
Loading…
Reference in New Issue