File: 2016-05-26-exploring-voting-with-hypothesis.md

package info (click to toggle)
python-hypothesis 6.138.0-1
  • links: PTS, VCS
  • area: main
  • in suites: sid
  • size: 15,272 kB
  • sloc: python: 62,853; ruby: 1,107; sh: 253; makefile: 41; javascript: 6
file content (178 lines) | stat: -rw-r--r-- 6,159 bytes parent folder | download | duplicates (2)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
---
tags: python, technical, example
date: 2016-05-26 11:00
title: Exploring Voting Systems with Hypothesis
author: drmaciver
---

Hypothesis is, of course, a library for writing tests.

But from an *implementation* point of view this is hardly noticeable.
Really it's a library for constructing and exploring data and using it
to prove or disprove hypotheses about it. It then has a small testing
library built on top of it.

It's far more widely used as a testing library, and that's really where
the focus of its development lies, but with the *find* function you can
use it just as well to explore your data interactively.

In this article we'll go through an example of doing this, by using it
to take a brief look at one of my other favourite subjects: Voting
systems.

<!--more-->

We're going to focus entirely on single winner preferential voting
systems: You have a set of candidates, and every voter gives a complete
ordering of the candidates from their favourite to their least
favourite. The voting system then tries to select a single candidate and
declare them the winner.

The general Python interface for a voting system we'll use is things
that look like the following:

```python
def plurality_winner(election):
    counts = Counter(vote[0] for vote in election)
    alternatives = candidates_for_election(election)
    winning_score = max(counts.values())
    winners = [c for c, v in counts.items() if v == winning_score]
    if len(winners) > 1:
        return None
    else:
        return winners[0]
```

That is, they take a list of individual votes, each expressed
as a list putting the candidates in order, and return a candidate that
is an unambiguous winner or None in the event of a tie.

The above implements plurality voting, what most people might think of
as "normal voting": The candidate with the most first preference votes
wins.

The other main voting system we'll consider is Instant Runoff Voting (
which you might know under the name "Alternative Vote" if you follow
British politics):

```python
def irv_winner(election):
    candidates = candidates_for_election(election)
    while len(candidates) > 1:
        scores = Counter()
        for vote in election:
            for c in vote:
                if c in candidates:
                    scores[c] += 1
                    break
        losing_score = min(scores[c] for c in candidates)
        candidates = [c for c in candidates if scores[c] > losing_score]
    if not candidates:
        return None
    else:
        return candidates[0]
```

In IRV, we run the vote in multiple rounds until we've eliminated all
but one candidate. In each round, we give each candidate a score which
is the number of voters who have ranked that candidate highest amongst
all the ones remaining. The candidates with the joint lowest score
drop out.

At the end, we'll either have either zero or one candidates remaining (
we can have zero if all candidates are tied for joint lowest score at
some point). If we have zero, that's a draw. If we have one, that's a
victory.

It seems pretty plausible that these would not produce the same answer
all the time (it would be surprising if they did!), but it's maybe not
obvious how you would go about constructing an example that shows it.

Fortunately, we don't have to because Hypothesis can do it for us!

We first create a strategy which generates elections, using Hypothesis's
composite decorator:

```python
import hypothesis.strategies as st


@st.composite
def election(draw):
    candidates = list(range(draw(st.integers(2, 10))))
    return draw(st.lists(st.permutations(candidates), min_size=1))
```

This first draws the set of candidates as a list of integers of size
between 2 and 10 (it doesn't really matter what our candidates are as
long as they're distinct, so we use integers for simplicity). It then
draws an election as lists of permutations of those candidates, as we
defined it above.

We now write a condition to look for:

```python
def differing_without_ties(election):
    irv = irv_winner(election)
    if irv is None:
        return False
    plurality = plurality_winner(election)
    if plurality is None:
        return False
    return irv != plurality
```

That is, we're interested in elections where neither plurality nor IRV
resulted in a tie, but they resulted in distinct candidates winning.

We can now run this in the console:

```
>>> from hypothesis import find
>>> import voting as v
>>> distinct = find(v.election(), v.differing_without_ties)
>>> distinct
[[0, 1, 2],
 [0, 1, 2],
 [1, 0, 2],
 [2, 1, 0],
 [0, 1, 2],
 [0, 1, 2],
 [1, 0, 2],
 [1, 0, 2],
 [2, 1, 0]]
```

The example is a bit large, mostly because we insisted on there being
no ties: If we'd broken ties arbitrarily (e.g. preferring the lower
numbered candidates) we could have found a smaller one. Also, in some
runs Hypothesis ends up finding a slightly smaller election but with
four candidates instead of three.

We can check to make sure that these really do give different results:

```
>>> v.irv_winner(distinct)
1

>>> v.plurality_winner(distinct)
0
```

There are a lot of other interesting properties of voting systems to
explore, but this is an article about Hypothesis rather than one about
voting, so I'll stop here. However the interested reader might want to
try to build on this to:

1. Find an election which has a [Condorcet Cycle](https://en.wikipedia.org/wiki/Voting_paradox)
2. Find elections in which the majority prefers the plurality winner to
   the IRV winner and vice versa.
3. Use @given rather than find and write some tests verifying some of
   [the classic properties of election systems](https://en.wikipedia.org/wiki/Voting_system#Evaluating_voting_systems_using_criteria).

And the reader who isn't that interested in voting systems might still
want to think about how this could be useful in other areas: Development
is often a constant series of small experiments and, while testing is
often a good way to perform them, sometimes you just have a more
exploratory "I wonder if...?" question to answer, and it can be
extremely helpful to be able to bring Hypothesis to bear there too.