Benutzer:Entropy/Bewertungswahl/EUAV
Dies ist der Quellcode zur Implementation der Wahlverfahren a) und b) der Wahlordnung zur Europawahl-Listenaufstellung 2014.
Als Eingabe-Datei wird das von Michael Ebner vorgebene Format für die Stimmen genutzt ("ja/enthaltung/nein" pro Zeile, wobei ja/nein jeweils aus zehn mit ; getrennten Rängen bestehen. Bein Rängen und Enthaltung sind die Kandidatennummern jeweils durch , getrennt).
Das Programm erzeugt zusätzlich eine Datei "input.csv", bei der die Stimmen in ein einfache CSV-Tabelle konvertiert sind. Damit kann jeder die Auswertung auch in einem anderen Programm nachvollziehen (z.B. Tabellenkalkulation). Die erste Zeile enthält die Kandidatennummern. Enthaltungen sind durch 0 ersetzt.
Auswertung eines "Range voting" Wahlgangs von a)
python range.py ballots.txt
Auswertung von b) "Reweighted range voting":
python range.py -p ballots.txt
Optionale Parameter jeweils
- Liste der Namen der Kandidaten (Kandidat 1=erste Zeile usw)
-n names.txt
- begrenzte Liste: es werden nur die ersten N Plätze ermittelt
-l N
- zu ignorierende Kandidaten (die ausgeschieden oder bereits gewählt sind": Kandidatennummer (gezählt ab 1) durch Kommata getrennt (ohne Leerzeichen!)
-i c1,c2,c3
- Zufallszahlen für das Losen bei Gleichstand (sehr unwahrscheinlich, zur Sicherheit 5 Stück ermitteln). Diese sollten unabhängig und vor der Wahl z.B. durch Münzwurf ermittelt und deren Liste von 0 oder 1 wird zeilenweise in der Datei gespeichert werden. Sollten keine ausreichenden Zufallszahlen vorhanden sein, fragt das Programm automatisch beim Losen nach einer Eingabe.
-r random.txt
- Hilfe
-h
Und hier der Python (>=2.7) Source code (erfordert numpy). Das Wahlverfahren selbst findet sich in am Ende (hinter "Elect the winners").
#!/usr/bin/env python # -*- coding: iso-8859-15 -*- # version 1.1 # (reweighted) range voting # copyleft 2014 by entropy@heterarchy.net # license: http://www.gnu.org/licenses/gpl.html # requires Python >=2.7, numpy>=1.6 from __future__ import division, print_function import sys, os, argparse, itertools import numpy as np parser = argparse.ArgumentParser('range voting') parser.add_argument("input",help="input file") parser.add_argument("-i", "--ignore", metavar='IGNORE', default=, help="candidate numbers to ignore (comma-separated)") parser.add_argument("-l", "--length", metavar='LENGTH', type=int,default=0, help="maximum list length") parser.add_argument("-n", "--names", metavar='NAMES',help='file with candidate names') parser.add_argument("-p", "--proportional", action="store_true", default=False, help="proportional variant (reweighted range voting)") parser.add_argument("-r", "--random", metavar='COINS',help='file with a list of 0/1 coin tosses (linewise)') args = parser.parse_args() # read predetermined random numbers (0/1) for ties (optional) random = [] if args.random: for coin in open(args.random,'rt').readlines(): random.append((int(coin)>0)*1) # read candidate names (optional) names = [] if args.names: for cand in open(args.names,'rt').readlines(): cand=cand.rstrip() if '#' in cand: cand=cand[cand.index('#')+2:] names.append(cand) # read ballots into a matrix (nballots,ncands) of scores (0..9) # input line per ballot: ;9...1/0/0;9*empty candidate numbers separated by comma # abstention and -1 are counted as 0 score ncands, votes = 0, [] for ib,ballot in enumerate(open(args.input,'rt').readlines()): ballot = ballot.rstrip() if '#' in ballot: ballot=ballot[:ballot.index(' #')-1] approv=ballot.split('/') if not len(approv[0])+len(approv[2]): continue # complete abstention vote, bad = [], False for ia,ranks in enumerate(approv): # yes,abstention,no ranks = ranks.split(';') if (ia==1 and len(ranks)!=1) or (ia!=1 and len(ranks)!=10): print('invalid number of ranks in ballot', ib+1) bad = True break for ir,cands in enumerate(ranks): if (not ia and not ir) or (ia==2 and ir): # only -1..9 allowed bad = len(cands) if bad: print('score outside of range in ballot', ib+1) break continue if cands: cands = np.array(eval('['+cands+']'),'i')-1 # candidate numbers start at 0 ncands = max(ncands,max(cands)+1) # find max candidate number else: cands = np.array([],'i') if ia==2: vote[-1] = np.concatenate((vote[-1],cands)) # append -1 to abstentions = 0 else: vote.append(cands) if bad: break if not bad: votes.append(vote) # ignore invalid ballots ballots = np.zeros((len(votes),ncands),'i') allcands= set(range(ncands)) for ib,vote in enumerate(votes): cands = itertools.chain.from_iterable(vote) rem = allcands.difference(cands) if rem: print('missing candidates',list(rem),'in ballot', ib+1) for score in range(10): # reverse order: 9,8,...,0 ballots[ib,vote[9-score]] = score nballots = len(ballots) print(nballots, 'ballots read') assert not names or len(names)==ncands, "mismatch of candidate names" candlut = -1*np.ones(ncands,'i') # candidate lookup table selection = list(range(ncands)) ignore = eval('['+args.ignore+']') selection = [cand for cand in selection if not cand in ignore] ncands = len(selection) print(ncands, 'candidates') if not args.length: args.length = ncands candlut[selection] = list(range(ncands)) ballots = ballots[:,selection] # remove candidates # save converted input as csv, first line candidate numbers np.savetxt("input.csv", np.vstack((np.array(selection,'i')+1,ballots)), delimiter=",", fmt='%i') def getnumber(n): # read a number between 0 and n while True: x = input('Please enter a number between 0 and %i:' % n) if not x: continue try: x=int(x) except ValueError: continue if x<=n and x>=0: return x # elect the winners done, elected = np.zeros((1,nballots)), np.ones(ncands) for place in range(1,ncands+1): if args.proportional: weights = 9/(9+2*done) # major fractions (Webster, Sainte-Lague) method total = np.sum(weights.T * ballots,axis=0) else: total = np.sum(ballots,axis=0) total *= elected # reverse sign for already elected candidates best = total==max(total) winners = np.nonzero(best)[0] if not args.proportional and len(winners)>1: nyes = np.sum(ballots>0,axis=0) # count no of >0 votes nyes[~best] = -1 # only ties winners = np.nonzero(nyes==max(nyes))[0] if len(winners)>1: # tie if len(winners)>2 or not random: winner = getnumber(len(winners)-1) else: winner = random.pop() winner = winners[winner] else: winner = winners[0] icand = selection[winner] cand = names[icand] if names else icand+1 tie = '(tie)' if len(winners)>1 else "" print("%i. %s = %.2f %s" % (place, cand, total[winner],tie)) if args.proportional: done += ballots[:,winner] elected[winner] = -1 # negative weight if args.length and place==args.length: break