- HTML 53%
- Python 46.1%
- Makefile 0.9%
|
|
||
|---|---|---|
| .github/workflows | ||
| data | ||
| docs | ||
| scripts | ||
| .gitignore | ||
| Makefile | ||
| README.md | ||
Migracija podataka o biračkim mestima
Prelazak sa starog, normalizovanog izvora (ps_regions.json) na noviji i detaljniji
(polling_stations.json) — sa dokazom da je spajanje ispravno, tabelama preslikavanja
i interaktivnim pregledom za ručnu proveru.
Repozitorijum je napisan kao primer kako se radi migracija između dva šifarnika koji nemaju zajedničke ID vrednosti na svim nivoima, a ne samo kao rešenje za ove konkretne fajlove.
Problem u jednoj slici
STARO (normalizovano) NOVO (denormalizovano)
polling_stations results
id: "4246" ─── isto ───► id: "4246"
name: "Precizni Liv" name_lat: "Precizni Liv - Ada, Molski put 4"
address: "Ada, Molski put 4" locality:
municipality: 21 ──┐ id: "1" ◄── isti grad, drugi broj
│ county: "severnobanatski"
municipalities │ region: "vojvodina"
21: "Ada" ◄────────┘
Biračko mesto 4246 ima isti id u oba fajla. Ali grad Ada je u starom fajlu vođen
pod brojem 21, a u novom pod "1". Brojevi opština, okruga i regiona nemaju
nikakve veze jedni sa drugima, a ni nazivi nisu uvek isti
(Zvezdara → Beograd-Zvezdara).
Rešenje u jednoj rečenici
Migracija se radi na nivou biračkog mesta, preko polja id, a preslikavanje šifarnika
se izvodi iz samih biračkih mesta — svako mesto već nosi i staru opštinu i novi
lokalitet, pa veza ispada sama.
Brzi start
Bez ijedne spoljne biblioteke — samo Python 3.9+ iz standardne instalacije.
# 1. stavi ulazne fajlove u data/
cp ps_regions.json polling_stations.json data/
# 2. dokaži da je ključ ispravan (izlazni kod 0/1, pogodno za CI)
python3 scripts/verify_join.py
# 3. generiši tabele preslikavanja
python3 scripts/build_crosswalk.py # -> out/crosswalk.json
# 4. prebaci podatke u ciljnu šemu
python3 scripts/migrate.py # -> out/polling_stations_migrated.json
# 5. napravi interaktivni pregled
python3 scripts/build_dashboard.py # -> out/dashboard.html
Ili sve odjednom:
make all
Otvori out/dashboard.html u pregledaču — radi bez interneta, podaci su ugrađeni u fajl.
Šta se nalazi u repozitorijumu
scripts/
common.py učitavanje, normalizacija teksta, otisak, geo
verify_join.py 5 provera da `id` označava isto biračko mesto
build_crosswalk.py izvodi tabele preslikavanja iz uparenih mesta
migrate.py prebacuje podatke u ciljnu šemu
build_dashboard.py pravi samostalni HTML pregled
dashboard_template.html šablon za HTML (podaci se ubacuju na mesto __PAYLOAD__)
docs/
struktura-podataka.md oblik oba izvora, poređenje obima
dokaz-kljuca.md zašto se sme verovati polju `id`
mapiranje-polja.md tabela: koje staro polje ide u koje novo
id-prostori.md pet ID prostora i zamka sa brojevima
hijerarhija.md opština → lokalitet, okrug → county, regioni
crosswalk.md kako je nastao crosswalk.json i šta je u njemu
rucna-provera.md kako proveriti rezultat rukom
plan-migracije.md kontrolna lista i ciljna šema
anomalije.md rizici, zamke, stvari koje pucaju tiho
data/ ulazni fajlovi (nisu u gitu — vidi .gitignore)
out/ sve generisano (nije u gitu)
Ključni rezultati
| Provera | Rezultat |
|---|---|
Biračkih mesta spojeno preko id |
8.309 / 8.309 (0 izgubljenih, 1 dodato) |
voter_count identičan |
8.309 / 8.309 — 100,00% |
name + " - " + address = name_lat |
8.307 / 8.309 — 99,98% |
| Medijana pomeraja koordinata | 70 m |
| Lokaliteta koji mešaju više starih opština | 0 od 193 |
Nezavisna provera otiskom (bez id) |
8.307 / 8.307 — 100,00% slaganja |
Poređenja radi, kod nasumičnog uparivanja istih zapisa voter_count se poklapa u 0,02%
slučajeva, a medijana rastojanja je 137 km.
Šta je najvažnije razumeti
1. Samo id se prenosi direktno. Sve ostale ID vrednosti su različiti brojevni
prostori. Detaljno u docs/id-prostori.md.
2. Preslikavanje se izvodi, ne pogađa. Ne poredimo opštine po nazivu ni po broju —
prolazimo kroz biračka mesta i brojimo glasove. Detaljno u
docs/crosswalk.md.
3. Dokaz nije kružan. Postoji druga, potpuno nezavisna provera koja ne čita id ni sa
jedne strane. Detaljno u docs/dokaz-kljuca.md.
4. Jedan podatak se gubi. control_region ne postoji u novom izvoru i moramo ga sami
održavati.
5. Adresa se ne gubi, ali se ne sme izvoditi deljenjem. Novi fajl ima naziv i adresu
spojene; deljenje po " - " promašuje na oko 10% zapisa jer i sami nazivi objekata
sadrže crticu.
Ulazni podaci
Fajlovi u data/ nisu deo repozitorijuma (vidi .gitignore) jer su oko 10 MB i
predstavljaju izvorne podatke. Očekivani oblik:
data/ps_regions.json
{
"regions": [{ "id": 1, "name": "Vojvodina" }],
"control_regions": [{ "id": 1, "name": "Univerzitet u Novom Pazaru" }],
"districts": [{ "id": 1, "name": "Južnobanatski okrug", "region": 1 }],
"municipalities": [{ "id": 1, "name": "Alibunar", "district": 1, "control_region": 4 }],
"polling_stations": [{ "id": "4246", "name": "Precizni Liv", "station_number": 1,
"address": "Ada, Molski put 4", "geo": { "lat": 45.78, "lon": 20.13 },
"voter_count": 747, "municipality": 21 }]
}
data/polling_stations.json
{
"count": 8310,
"results": [{
"id": "4246", "number": 1,
"name_cyr": "…", "name_lat": "Precizni Liv - Ada, Molski put 4",
"locality": { "id": "1", "name_lat": "Ada",
"region": { "id": "vojvodina", "name_lat": "Region Vojvodine" },
"county": { "id": "severnobanatski", "name_lat": "Severnobanatski okrug" } },
"geo": { "lat": 45.78524, "lon": 20.1355 },
"geo_approx": false, "geo_source": "volunteer",
"voter_count": 747, "turnout": 376,
"network": { "mts": 2, "yettel": 2, "a1": 2 }, "image": null
}]
}
Primena na druge podatke
Postupak nije vezan za ove fajlove. Da bi se primenio na drugi par izvora, dovoljno je promeniti:
common.py— funkcijefingerprint_old/fingerprint_new. Otisak treba da bude sastavljen od polja koja postoje u oba izvora, a nemaju veze sa ključem koji proveravamo. Ovde je to naziv, adresa i broj birača.build_crosswalk.py— koji parovi šifarnika se izvlače u koraku 3.
Ostalo radi bez izmena: provere u koracima 1–2, brojanje glasova, otkrivanje podela i dvosmislenosti.
Licenca
Kod je slobodan za korišćenje. Ulazni podaci nisu deo repozitorijuma i podležu uslovima svog izvora.