I was thinking last night about how an election can fail to have a
Condorcet winner, and I wondered what fraction of all possible
elections had that property.

Rather than spending half an hour looking up the answer on Google and
reading it (finding e.g. http://arxiv.org/abs/math.ST/0511140 for
three candidates) I thought it would be fun to spend three hours
hacking up some code that wouldn't actually give me an answer, but
maybe would give me some feel for the answer.  (It turns out that the
vague answer is that having no Condorcet winner is a common
phenomenon.)

So here it is.  It, or some later version, is also online at
http://pobox.com/~kragen/sw/condorcet.html

<html><head><title>Condorcet cycles</title>
<script type="text/javascript">
/* TODO:
 * D create adjustable parameters for voters and candidates
 * D randomly generate voter prefs with button
 * D display voter prefs
 * D display voter prefs in columns of table
 * D come up with candidate names
 * D come up with candidate colors
 * D display candidate/candidate matrix
 * D compute Condorcet winner
 * (at this point I only had 70 minutes left before kragen-hacks goes out)
 * D compute FPTP and Borda count winners
 * (at this point I had 20 minutes left)
 * - compute dominant cycle if no winner
 * - color-code voters on mouseover of candidate comparisons
 * - support drag-and-drop modification of voter prefs?
 * D generate candidate colors randomly?
 */


/* very basic functions needed even for class Candidate */

function map(func, alist) {
    var rv = []
    for (var ii = 0; ii < alist.length; ii++) 
        rv.push(func(alist[ii]))
    return rv
}

function tag(tagname) {
    return function(content) {
        return '<' + tagname + '>' + content + '</' + tagname + '>'
    }
}
function font(color, content) {
    return tag('font color="' + color + '"')(content)
}
function all(alist) {
    for (var ii = 0; ii < alist.length; ii++)
        if (!alist[ii]) 
            return false
    return true
}            
function sum(alist) {
    var rv = 0
    for (var ii = 0; ii < alist.length; ii++)
        rv += alist[ii]
    return rv
}


/* class Candidate */

function Candidate(name, color) { 
    this.name = name 
    this.color = color
}
Candidate.prototype.as_html = function() { 
    return font(this.color, this.name)
}
Candidate.prototype.compare = function(other_candidate, voters) {
    var wins = 0
    var losses = 0
    for (var ii = 0; ii < voters.length; ii++) {
        if (voters[ii].prefers(this, other_candidate)) {
            wins++
        } else {
            losses++
        }
    }
    return [wins, losses]
}
Candidate.prototype.beats = function(other_candidate, voters) {
    var comparison = this.compare(other_candidate, voters)
    return comparison[0] > comparison[1]
}
Candidate.prototype.beats_all = function(candidates, voters) {
    var self = this
    return all(map(function(candidate) {
        return self.beats(candidate, voters)
    }, candidates))
}
Candidate.prototype.fptp_votes = function(voters) {
    var self = this
    return sum(map(function(voter) { return voter.prefs[0] == self ? 1 : 0 },
                   voters))
}
Candidate.prototype.borda_count = function(voters) {
    var self = this
    return sum(map(function(voter) { return voter.borda_count(self) }, voters))
}

/* end class Candidate */


/* very basic functions needed for class Voter */

function identity(obj) { return obj }
function copylist(alist) { return map(identity, alist) }
function randrange(max) { return parseInt(Math.random() * max) }
function method(methodname) {
    // XXX should take extra parameters somewhere...
    return function(obj) { return obj[methodname]() }
}

function Voter(candidates, voterid) {
    this.voterid = voterid
    this.prefs = copylist(candidates)
    for (var ii = 0; ii < this.prefs.length; ii++) {
        var pos = randrange(ii+1)
        var tmp = this.prefs[pos]
        this.prefs[pos] = this.prefs[ii]
        this.prefs[ii] = tmp
    }
}

Voter.prototype.ballot = function() {
    return ['Voter #' + this.voterid].concat(map(method("as_html"), this.prefs))
}

Voter.prototype.prefers = function(a, b) {
    for (var ii = 0; ii < this.prefs.length; ii++) {
        var cand = this.prefs[ii]
        if (cand == a) return true
        if (cand == b) return false
    }
    throw "candidate not in list"
}
Voter.prototype.borda_count = function(candidate) {
    for (var ii = 0; ii < this.prefs.length; ii++)
        if (this.prefs[ii] == candidate) 
            return this.prefs.length - ii
    throw "candidate not in list"
}
            

/* end class Voter */


/* main program routines, with HTML and so forth */

td = tag('td')
tr = tag('tr')
th = tag('th')

function elem(id) { return document.getElementById(id) }


function table_innerHTML(matrix) {
    var rv = []
    for (var ii = 0; ii < matrix.length; ii++) {
        var row = []
        for (var jj = 0; jj < matrix[ii].length; jj++)
            row.push((ii == 0 ? th : td)(matrix[ii][jj]))
        rv.push(tr(row.join('\n')))
    }
    return rv.join('\n')
}
function transpose(matrix) {
    var rv = []
    // XXX does not work for empty matrix
    for (var jj = 0; jj < matrix[0].length; jj++) rv.push([])
    for (var ii = 0; ii < matrix.length; ii++)
        for (var jj = 0; jj < matrix[0].length; jj++)
            rv[jj].push(matrix[ii][jj])
    return rv
}
function display_voters(dest, voters) {
    elem(dest).innerHTML = table_innerHTML(transpose(
        map(method('ballot'), voters)))
}


function candidate_row(candidates, voters) {
    return function(row_candidate) {
        return tr(th(row_candidate.as_html()) + map(
            function(column_candidate) { 
                if (row_candidate == column_candidate) return td('')
                var comp = row_candidate.compare(column_candidate, voters)
                var color = ((comp[0] > comp[1]) ? row_candidate.color :
                             (comp[1] > comp[0]) ? column_candidate.color :
                             'grey')
                return td(font(color, comp[0] + '-' + comp[1]))
            }, candidates).join('') )
    }
}

function compose(f, g) { return function(obj) { return f(g(obj)) } }
function display_pairwise(dest, candidates, voters) {
    var cand_hdr_top = tr(td('') + map(compose(th, method("as_html")), 
                                       candidates).join(''))
    var cand_rows = map(candidate_row(candidates, voters), candidates)
    elem(dest).innerHTML = cand_hdr_top + cand_rows.join('\n')
}


function display_winner(dest, candidates, voters) {
    var winner = "No candidate"
    for (var ii = 0; ii < candidates.length; ii++) {
        var cand = candidates[ii]
        if (cand.beats_all(candidates, voters)) {
            winner = cand.as_html()
        }
    }
    elem(dest).innerHTML = winner + " is the Condorcet winner."
}

function update_winner(current, name, score) {
    if (score > current[1]) {
        current[0] = name
        current[1] = score
    } else if (score == current[1]) {
        if (current[0])  /* initially null */
            current[0] = "tied"
    }
}

function display_other_winners(table, fptp, borda, candidates, voters) {
    var table_headers = [tr(th('Candidate') + th('FPTP') + th('Borda'))]
    var fptp_winner = [null, 0]
    var borda_winner = [null, 0]
    var table_rows = map(function(candidate) {
        var fptp_votes = candidate.fptp_votes(voters)
        update_winner(fptp_winner, candidate.as_html(), fptp_votes)
        var borda_count = candidate.borda_count(voters)
        update_winner(borda_winner, candidate.as_html(), borda_count)
        return tr(th(candidate.as_html()) + td(fptp_votes) + td(borda_count))
    }, candidates)
    elem(table).innerHTML = table_headers.concat(table_rows).join('\n')
    elem(fptp).innerHTML = fptp_winner[0]
    elem(borda).innerHTML = borda_winner[0]
}



function candidate_name(ii) {
    /* http://www.census.gov/genealogy/names/names_files.html */
    var candidate_names = [
        'Margaret', 'Dorothy', 'Charles', 'Richard', 'Maria', 'Thomas', 
        'Jennifer', 'Elizabeth', 'Joseph', 'David', 'James', 'Mary', 
        'Patricia', 'William', 'John', 'Susan', 'Michael', 'Barbara', 
        'Linda', 'Robert',
    ]
    if (ii < candidate_names.length)
        return candidate_names[ii]
    return 'Candidate #' + ii
}

function randhex() {
    return ['0', '1', '2', '3', '4', '5', '6', '7',
            '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'][randrange(16)]
}
var candidate_color = function(ii) {
    var colors = [ 'black', 'red', 'blue', 'cyan', 'magenta', 'green',
                  'yellow', 'orange', 'purple', 'bluegreen' ]
    if (ii < colors.length) return colors[ii]
    var r = randhex()
    var g = randhex()
    var b = randhex()        
    return '#' + r + r + g + g + b + b
}

function range(n) {
    var rv = []
    for (var ii = 0; ii < n; ii++) 
        rv.push(ii)
    return rv
}

/* main program entry point */

function election(form) {
    var n_voters = form.voters.value
    var n_candidates = form.candidates.value
    var candidates = map(function(ii) { 
        return new Candidate(candidate_name(ii), candidate_color(ii)) 
    }, range(n_candidates))

    var voters = map(function(ii) {
        return new Voter(candidates, ii)
    }, range(n_voters))
    display_voters('voterlist', voters)
    display_pairwise('matrix', candidates, voters)
    display_winner('winner', candidates, voters)
    display_other_winners('othervotingsystems', 'fptp-winner', 'borda-winner',
                          candidates, voters)
}
</script>
</head>
<body onload="election(document.forms[0])">
<h1>Condorcet winners</h1>
<p>The "Condorcet winner" of an election is the candidate that is
preferred by a majority of the voters over any other single candidate.
The basic idea is that each voter has some order of preference of the
available candidates, and you can tell which candidate they prefer out
of any pair by comparing their ranks in that list.</p>
<p>A well-known problem is that, if there are more than two candidates
and more than four voters, there may not be a Condorcet winner in a
particular election.  The case with three candidates ABC and five voters
is as follows: ABC, ABC, CAB, CAB, BCA.  A is preferred to B by four out
of five; C is preferred to A by three out of five; and B is preferred to
C by three out of five.  Such examples demonstrate that the idea of a
Condorcet winner does not solve the problem of collective choice; it's
an interesting question how probable the existence of a Condorcet winner
is.</p>
<p>So here's a little experimental environment.  You can set the number
of voters and the number of candidates and randomly generate the
preferences for each voter, and see the Condorcet results....</p>
<form onsubmit="election(this); return false">
<label for="voters">Voters: </label><input id="voters" 
    name="voters" value="15" size="4" />
<label for="candidates">Candidates: </label><input id="candidates" 
    name="candidates" value="9" size="4" />
<input type="submit" value="Elect!" />
</form>
<h2>Individual voter ballots:</h2>
<table id="voterlist">
<tr><td>No election yet.</td></tr>
</table>
<h2>Pairwise matchups:</h2>
<table id="matrix">
<tr><td>No election yet.</td></tr>
</table>
<h2>Condorcet winner</h2>
<p>A Condorcet winner will appear in the matrix of pairwise matches
with their entire row and their entire column a single solid color:
their color.</p>
<p id="winner">No election yet.</p>
<h2>Other voting systems</h2>
<p>In first-past-the-post (used, for instance, in the US presidential
election and most other US elections) each voter votes for exactly one
candidate, and the candidate with the most votes wins.  If we assume
"sincere voting" --- each voter votes for their first choice --- we
can deduce the FPTP results from the preference orders displayed
above; the winner is <span id="fptp-winner">not yet computed</span>.
Obviously this is a terrible system, and every other voting system
proposed so far has been proposed because its proponents think it is
better.</p>
<p>In the Borda count, your last choice gets 1 point; your
next-to-last choice gets 2 points; and so forth, until your first
choice gets as many points as there are candidates.  The Borda count
winner of this election is <span id="borda-winner">not yet
computed</span>.
</p>
<table id="othervotingsystems">
<tr><td>No election yet.</td></tr>
</table>
</body></html>

Reply via email to