Benutzer:Entropy/Bewertungswahl/EUAV

Aus Piratenwiki Mirror
Zur Navigation springen Zur Suche springen

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