Condorcet Jury Theorem

import random
import pylab
import matplotlib.mlab as mlab
import functools
import itertools
from __future__ import print_function
import math
import numpy as np
import matplotlib as mpl
import matplotlib.pyplot as plt
import seaborn as sns
import numpy as np
import matplotlib.pyplot as plt
from scipy.stats import norm
import math
from functools import reduce
from collections import Counter
from tqdm.notebook import tqdm  

from ipywidgets import interact, interactive, fixed, interact_manual
import ipywidgets as widgets
from IPython.display import display
from IPython.display import display, Math, Latex
sns.set()

Warning

This notebook uses Jupyter widgets that will only work if the notebook is run locally.

Suppose that \(V=\{1, 2, 3, \ldots, n\}\) is a set of voters or experts, and consider a set of two alternatives. E.g., \(\{\mbox{convict}, \mbox{acquit}\}\), \(\{\mbox{abolish}, \mbox{keep}\}\), \(\{0,1\}\), \(\ldots\)

Let \(\mathbf{x}\) be a random variable (called the state) whose values range over the two alternatives.

In addition, let \(\mathbf{v}_1, \mathbf{v}_2, \ldots\) be random variables represeting the votes for individuals \(1, 2, \ldots, n\)

Let \(R_i\) be the event that \(i\) votes correctly: it is the event that \(v_i\) coincides with the state.

Unconditional independence (UI): The correctness events \(R_1, R_2, \ldots, R_n\) are (unconditionally) independent.

Unconditional competence (UC): The (unconditional) correctness probability \(p = Pr(R_i)\), the (unconditional) competence, (i) exceeds \(\frac{1}{2}\) and (ii) is the same for each voter \(i\).

Condorcet Jury Theorem. Assume UI and UC. As the group size increases, the probability of a correct majority (i) increases (growing reliability), and (ii) tends to one (infallibility).

The Condorcet Jury Theorem has two main theses:

The growing-reliability thesis: Larger groups are better truth-trackers. That is, they are more likely to select the correct alternative (by majority) than smaller groups or single individuals.

The infallibility thesis: Huge groups are infallible truth-trackers. That is, the likelihood of a correct (majority) decision tends to full certainty as the group becomes larger and larger.

The probability of at least \(m\) voters being correct is:

\[\sum_{h=m}^n \frac{n!}{h!(n-h)!} * p^h*(1-p)^{n-h}\]
import operator as op
def ncr(n, r):
    r = min(r, n-r)
    if r == 0: return 1
    numer = reduce(op.mul, range(n, n-r, -1))
    denom = reduce(op.mul, range(1, r+1))
    return float(numer//denom)

def probability_majority_is_correct(num_voters=100,prob=0.51):
    return sum([ncr(num_voters,k)*(prob**k)*(1-prob)**(num_voters-k) 
                for k in range(int(num_voters/2+1),num_voters+1)])
def make_maj_prob_graphs():
    probs = np.linspace(0,1,num=100)

    number_of_voters = [ 1, 3,  11, 51,  201, 501, 1001]
    sns.set(rc={'figure.figsize':(10,5)})
    
    plt.subplot(121)
    for num_voters in number_of_voters:
        maj_probs = [probability_majority_is_correct(num_voters=num_voters,prob=p)  for p in probs]
        plt.plot(list(probs),maj_probs, label="$n=" + str(num_voters) + "$")
        plt.legend(bbox_to_anchor=(1.05, 1), loc=2, borderaxespad=0.)
    plt.xlabel('Probability of voting correctly')
    plt.ylabel('Probability the majority is correct')
    
    plt.subplot(122)
    for num_voters in number_of_voters:
        maj_probs = [probability_majority_is_correct(num_voters=num_voters,prob=p) - p  for p in probs]
        plt.plot(list(probs),maj_probs, label="$n=" + str(num_voters) + "$")
        plt.legend(bbox_to_anchor=(1.05, 1), loc=2, borderaxespad=0.)
    plt.xlabel('Probability of voting correctly')
    plt.ylabel('$Pr(M_n) - p$')

    plt.plot([0.0,1.0],[0.0,0.0],color='black',alpha=0.6)
    
    sns.set()
    plt.subplots_adjust(bottom=0.1, right=1.5, top=0.9, wspace = 0.75)

    plt.savefig('cjtplots.png')
make_maj_prob_graphs()
../_images/cjt_9_0.png

Theoreom. For any (odd) number of voters, each with a probability \(p>1/2\) of choosing correctly, then majority rule is preferred to the expert rule.

Theorem. Assume \(p_1\ge p_2>p_3>1/2\), then the simple majority rule is preferred to the expert rule.

def probability_majority_is_correct_diff_probs(p1=0.55, p2=0.6, p3=0.8):
    
    maj_prob = p1*p2*p3 + p1*p2*(1-p3) + + p2*p3*(1-p1) + + p1*p3*(1-p2) 
    expert_prob = 1.0/3.0 * p1 + 1.0/3.0 * p2 + 1.0/3.0 * p3
    print(f"Majority probability: {round(maj_prob,3)}\nExpert Probability: {round(expert_prob,3)}")
    if maj_prob > expert_prob: 
        print(f" Majority rule is better than the expert rule")
    else: 
        print(f"\n The expert rule is better than majority rule")

    
maxprob = interact_manual(probability_majority_is_correct_diff_probs,p1=(0.5,1,0.01),p2=(0.5,1,0.01),p3=(0.5,1,0.01))
evidence = [2,3,4,5,6,7,8,10, 12, 14]

class Agent():
    
    def __init__(self, comp=0.501):
        self.comp = comp
        
    def vote(self, ev):
        #vote on whether the event is true or false
        #need the actual truth value in order to know which direction to be biased
        if ev:
            #ev is true
            return int(random.random() < self.comp)
        else:
            return 1 - int(random.random() < self.comp)


def maj_vote(the_votes):
    votes_true = len([v for v in the_votes if v == 1])
    votes_false = len([v for v in the_votes if v == 0])

    if votes_true > votes_false:
        return 1
    elif votes_false > votes_true:
        return 0
    else:
        return -1  #tied

def generate_competences(n, mu=0.51, sigma=0.2):
    competences = list()
    for i in range(0,n):
        #sample a comp until you find one between 1 and 0
        comp=np.random.normal(mu, sigma)
    
        while comp > 1.0 or comp < 0.0:
            comp=np.random.normal(mu, sigma)
        competences.append(comp)
    return competences
import pandas as pd
NUM_ROUNDS = 500
from tqdm import notebook 

def make_plots(max_voters=201, 
               comp_mu=0.501, 
               comp_sigma=0.1):
    P=True
    max_num_voters = max_voters
    total_num_voters = range(1,max_num_voters)

    competences = generate_competences(max_num_voters,
                                       mu=comp_mu, 
                                       sigma=comp_sigma)
    maj_probs = list()
    expert_probs = list()
    for num_voters in notebook.tqdm(total_num_voters, desc='voting'):
        experts = list()

        experts = [Agent(comp=competences[num-1]) for num in range(0,num_voters)]
    
        maj_votes = list()
        expert_votes = list()
        for r in range(0,NUM_ROUNDS):
            # everyone votes
            votes = [a.vote(P) for a in experts]
            maj_votes.append(maj_vote(votes))
        
            expert_votes.append(random.choice(experts).vote(P))
    
        maj_probs.append(float(float(len([v for v in maj_votes if v==1]))/float(len(maj_votes))))
        expert_probs.append(float(len([v for v in expert_votes if v==1]))/float(len(expert_votes)))
    
    sns.set(rc={'figure.figsize':(11,5)})
    plt.subplot(121)

    data = {" ": range(0,max_num_voters), "competence": competences}
    plt.ylim(0,1.05)
    plt.title("Competences")
    df = pd.DataFrame(data=data)
    sns.regplot(x=" ", y="competence", data=df, color=sns.xkcd_rgb["pale red"])


    plt.subplot(122)
    plt.title("Majority vs. Experts")
    plt.plot(list(total_num_voters), maj_probs, label="majority ")
    plt.plot(list(total_num_voters), expert_probs, label="expert ")
    plt.legend(bbox_to_anchor=(1.05, 1), loc=2, borderaxespad=0.)
    plt.xlabel('Number of experts')
    plt.ylabel('Probability')
    plt.ylim(0,1.05)
    plt.subplots_adjust(bottom=0.1, right=1.5, top=0.9, wspace = 0.75)

    sns.set()
    plt.savefig("cjt_simulation.png")
p = interact_manual(make_plots,max_voters=(1,501,1),comp_mu=(0,1,0.01),comp_sigma=(0,2,0.1))

Further Reading

D. Austen-Smith and J. Banks, Aggregation, Rationality and the Condorcet Jury Theorem, The American Political Science Review, 90, 1, pgs. 34 - 45, 1996

D. Estlund, Opinion Leaders, Independence and Condorcet’s Jury Theorem, Theory and Decision, 36, pgs. 131 - 162, 1994

F. Dietrich, The premises of Condorcet’s Jury Theorem are not simultaneously justified, Episteme, Episteme - a Journal of Social Epistemology 5(1): 56-73, 2008

R. Goodin and K. Spiekermann, An Epistemic Theory of Democracy, Oxford University Press, 2018

What happens if there are more than two options?

C. List and R. Goodin. Epistemic democracy: Generalizing the condorcet jury theorem. Journal of political philosophy, 9(3):277–306, 2001.

def display_probs(cjt_model, num_options):
    '''display the probabilities of the agents'''
    _probs = list()
    for a in cjt_model.schedule.agents: 
        _probs.append(np.array(a.probs))
    probs = np.array(_probs)
    prs = probs.transpose()

    for opt in range(num_options):
        plt.barh(range(num_agents), prs[opt], 1,
                 left=sum([np.array([0.0]*num_agents)] + [prs[i] for i in range(opt)]),
                  lw = 0.01)
    plt.show()
    plt.clf()
from dataclasses import dataclass, field
from typing import List

@dataclass
class Options:
    '''Options is a list with one option identified as the "correct" one '''
    num: int = 2
    correct_idx: int = 0 # index of the correct option
    names:  List[str] = field(default_factory=list) # names of the options
        
    def __post_init__(self):
        self.names = [f"P{p+1}" for p in self.props]
    
    @property
    def props(self) -> List[int]:
        '''the list of all options'''
        return list(range(self.num))
    
    @property
    def C(self) -> int:
        return self.props[self.correct_idx]
    
    @property
    def C_as_list(self) -> List[int]:
        return [self.props[self.correct_idx]]
    
    @property
    def W(self) -> List[int]: 
        return list(self.props[self.correct_idx + 1::])
    
    def name(self, opt): 
        return self.names[opt]
    
    def set_names(self, names):
        assert len(names) == self.num, f"You need {self.num} names, but only provided {len(names)} names: {names}"
        self.names = names
        
    # make options iterable
    def __iter__(self):
        return iter(self.props)
from mesa import Agent, Model
from mesa.time import RandomActivation
from mesa.datacollection import DataCollector


def gen_option_probability_normal(mu,sigma):
    '''return single p'''
    pr=np.random.normal(mu, sigma)
    while pr > 1.0 or pr < 0.0:
        pr=np.random.normal(mu, sigma)
    return [pr, 1-pr]

def gen_option_probability_beta(a,b, num=1):
    pr=np.random.beta(a,b, num)[0]
    return [pr, 1-pr]

def gen_options_probability_dirichlet(params, num=1):
    return np.random.dirichlet(params, num)
    

init_probs = {'1_opt_fixed_probs1': lambda : [0.51, 0.49],
              '1_opt_fixed_probs2': lambda : [0.75, 0.25],
              '1_opt_fixed_probs3': lambda : [0.49, 0.51],
              '4_opt_fixed_probs': lambda : [0.40, 0.20, 0.20, 0.20],
              '7_opt_fixed_probs': lambda : [0.30, 0.10, 0.20, 0.05, 0.05, 0.15, 0.15],
              '2_opt_normal1': lambda : gen_option_probability_normal(0.51, 0.1),
              '2_opt_normal2': lambda : gen_option_probability_normal(0.6, 0.25),
              '2_opt_normal3': lambda : gen_option_probability_normal(0.6, 0.1),
              '2_opt_beta1': lambda : gen_option_probability_beta(20,20),
              '2_opt_beta2': lambda : gen_option_probability_beta(21,20),
              '2_opt_beta3': lambda : gen_option_probability_beta(15,20),
              '4_opt_dirichlet1': lambda : gen_options_probability_dirichlet((2, 1, 1, 1))[0],
              '4_opt_dirichlet2': lambda : gen_options_probability_dirichlet((1.15, 1, 1, 1))[0],
              '4_opt_dirichlet3': lambda : gen_options_probability_dirichlet((4,3,2,1))[0],
             }


def plurality_vote(votes):
    tally  = Counter(votes)
    max_plurality_score = max(tally.values())
    winners = [o for o in tally.keys() if tally[o] == max_plurality_score]
    return winners 

def percent_plurality_vote_correct(model):
    num_correct = 0
    for r in range(model.num_rounds):
        winners = plurality_vote([a.vote() for a in model.schedule.agents])
        if len(winners) == 1 and model.options.C == winners[0]:
            num_correct += 1
    return float(num_correct) / model.num_rounds

def percent_expert_correct(model):
    num_correct = 0
    for r in range(model.num_rounds):
        expert = random.choice(model.schedule.agents)
        if  model.options.C == expert.vote():
            num_correct += 1
    return float(num_correct) / model.num_rounds


class Expert(Agent):
    """Expert to vote on a single proposition.
    competence: float between 0 and 1"""
    def __init__(self, unique_id, model, options, probs):
        super().__init__(unique_id, model)
        self.options = options
        self.probs = probs
        
    def vote(self):
        return np.random.choice(self.options, 1, p=self.probs)[0]
    
    def step(self):
        self.vote()
        #print(self.unique_id, self.selected_option)
    
class CJTModel(Model):
    """A model with some number of experts."""
    def __init__(self, N, num_rounds, gen_prob, num_options=2):
        self.num_experts = N
        self.options = Options(num_options)
        self.schedule = RandomActivation(self)
        self.num_rounds = num_rounds
        
        # Create experts
        for i in range(self.num_experts):
            a = Expert(i, self, self.options.props, gen_prob())
            self.schedule.add(a) 
        
        self.datacollector = DataCollector(
            model_reporters={"PercentPluralityCorrect": percent_plurality_vote_correct,
                             "PercentExpertCorrect": percent_expert_correct})
    
    def run(self):
        '''run simulation.'''
        
        self.schedule.step()
        self.datacollector.collect(self)
def display_plots(plot_type):
    
    max_num_agents = 101
    
    num_options = int(plot_type.split("_")[0]) 
    num_rounds = 500

    fig, (ax1, ax2) = plt.subplots(1, 2)
    fig.set_size_inches(14, 6)
    perc_plurality_correct = list()
    perc_expert_correct = list()
    for num_agents in tqdm(range(1,max_num_agents+1)): 
        cjt_model = CJTModel(num_agents, 
                             num_rounds, 
                             init_probs[plot_type], 
                             num_options=num_options)
        cjt_model.run()
        winners = cjt_model.datacollector.get_model_vars_dataframe()
        perc_plurality_correct.append(winners.PercentPluralityCorrect.values[0])
        perc_expert_correct.append(winners.PercentExpertCorrect.values[0])
        
    _probs = list()
    for a in cjt_model.schedule.agents: 
        _probs.append(np.array(a.probs))
    probs = np.array(_probs)
    prs = probs.transpose()

    for opt in range(num_options):
        ax1.barh(range(num_agents), prs[opt], 1,
                 left=sum([np.array([0.0]*num_agents)] + [prs[i] for i in range(opt)]),
                  lw = 0.01)
    ax2.plot(range(1,max_num_agents+1), perc_plurality_correct, label="Plurality")
    ax2.plot(range(1,max_num_agents+1), perc_expert_correct, label="Expert")
    
    plt.legend(bbox_to_anchor=(1.25,0.5))
    plt.savefig("plurality_example.pdf")
    #plt.show()
    
p=interact_manual(display_plots,plot_type=widgets.Dropdown(
    options=[
        ('4 Options', '4_opt_fixed_probs'), 
        ('7 Options', '7_opt_fixed_probs'), 
        ('4 Options Random Competence 1', '4_opt_dirichlet1'),
        ('4 Options Random Competence 2', '4_opt_dirichlet2'),
        ('4 Options Random Competence 3', '4_opt_dirichlet3'), 
        ('1 Option Random Competence 1', '2_opt_beta1'),
        ('1 Option Random Competence 2', '2_opt_beta2'),
        ('1 Option Random Competence 3', '2_opt_beta3'),
        ('1 Option Random Competence 4', '2_opt_normal1'),
        ('1 Option Random Competence 5', '2_opt_normal2'),
        ('1 Option Random Competence 6', '2_opt_normal3'),

    ],
    value='4_opt_fixed_probs',
    description='Simulation:'))

Further Reading

  1. U. Hahn, M. von Sydow and C. Merdes (2018). How Communication Can Make Voters Choose Less Well, Topics in Cognitive Science

  2. F. Dietrich and K. Spiekermann (2020). Jury Theorems: a review, In: M. Fricker et al. (eds.) The Routledge Handbook of Social Epistemology. New York and Abingdon: Routledge