Interest in alternatives to the most common type of voting (plurality), and in particular Ranked-Choice Voting (RCV), has periodically waxed and waned over the current era of modern democracy. Right now in parts of the US, it has been increasing for a decade or two, and has reached a local peak with the victory of Zohran Mamdani in the Democratic primary for mayor of New York City, which used RCV.
Much of the research I found (though I didn’t dig too hard) was either empirical studies of RCV, or proofs of theoretical aspects of RCV. So, out of curiosity, I decided to do some simulations. Coincidentally, one of the few simulation studies I found was that of a Wolfram summer school student, Sofia Wong (https://community.wolfram.com/groups/-/m/t/2964799). I encourage the reader to look at that post as well and probably first, if you are unfamiliar with voting systems.
This work is both deeper and simpler than Wong’s, which investigates multiple voting methods, and uses a multidimensional preference space to determine the “distance” of a voter to a candidate. Here, we focus on RCV, an analyze how it compares to plurality over many elections in a variety of cases: many or few candidates, many or few voters, and different relative “popularities” of candidates (defined below).
The main results are:
◼
RCV wins in every case, but the marginal increase in utility (happiness) is very small.
◼
For a few cases, I could not get all tests to show statistical significance.
◼
RCV does best when there is more candidates and fewer voters.
My conclusion is that collecting more information from voters (a ranked choice), can often lead to slightly greater levels of satisfaction.
Details:
Generating ballots and computing the winners, utilities
Generating ballots and computing the winners, utilities
In[]:=
numVoters=10;numCands = 5;
ballots=(*5candidates,4randomdivisionsof"utility"(happiness)(eachvoterhasmaxutility1),eachlabeledbyacandidate*)Table[Sort[Join[RandomReal[1,numCands-1],{1}]]->RandomSample[Range[numCands]],numVoters]
Out[]=
{{0.172299,0.754636,0.899329,0.972762,1}{1,2,5,3,4},{0.253154,0.593623,0.649826,0.939197,1}{5,1,2,3,4},{0.0546519,0.631077,0.779897,0.885189,1}{2,3,4,1,5},{0.0263662,0.376277,0.771167,0.89077,1}{2,3,5,1,4},{0.0987935,0.770706,0.771867,0.90828,1}{3,4,1,5,2},{0.0429284,0.336529,0.53372,0.98014,1}{2,4,3,1,5},{0.0733739,0.191093,0.248492,0.353643,1}{2,5,4,3,1},{0.0463696,0.234226,0.313308,0.92331,1}{4,1,3,2,5},{0.178769,0.50784,0.66413,0.915381,1}{3,1,2,5,4},{0.586813,0.605976,0.743765,0.928661,1}{4,3,5,1,2}}
One “problem” with these ballots is that all candidates are equally popular given a large number of voters, which is not usually the case empirically.
To fix that, we give RandomSample random weights. These “relative popularities” are uniformly random numbers from 0 to 1, which RandomSample scales to use as its weights in deciding which candidate a voter prefers first, second, and so on (using the same weights for every voter).
To fix that, we give RandomSample random weights. These “relative popularities” are uniformly random numbers from 0 to 1, which RandomSample scales to use as its weights in deciding which candidate a voter prefers first, second, and so on (using the same weights for every voter).
relPopularities = RandomReal[1,numCands]&;(*should actually use a better distribution. find empirically?)Or: use a hypercube "overton window" and distribute voters in a normal dist, and candidates in a uniform dist (because there is always a candidate for every position (maybe need more candidates than 5 in the model, then)), and take the distribution of distances from a random voter to a random candidate -- which is simple in 1D ("the political spectrum") *)
Generating a set of ballots
Generating a set of ballots
In[]:=
generateBallots[numCands_,numVoters_,relPopularities_] := Table Sort[Join[relPopularities[],{1}]]-> RandomSample[(*relPopularities->*)Range[numCands]], numVoters
In[]:=
ballts=generateBallots[numCands,10,relPopularities]
Out[]=
{{0.36993,0.503957,0.509451,0.79216,0.899573,1}{5,2,3,1,4},{0.311168,0.54775,0.646789,0.670387,0.719707,1}{1,3,4,2,5},{0.0207576,0.212059,0.255622,0.430864,0.900904,1}{5,3,4,1,2},{0.228114,0.36994,0.556457,0.613476,0.613687,1}{5,1,3,2,4},{0.334707,0.57704,0.583082,0.876722,0.932988,1}{4,5,2,1,3},{0.0746916,0.279887,0.532412,0.773332,0.828952,1}{1,4,5,2,3},{0.294312,0.469508,0.514269,0.70682,0.970832,1}{1,5,3,4,2},{0.0984522,0.693708,0.706301,0.920041,0.968598,1}{1,5,4,3,2},{0.0702401,0.199929,0.301897,0.582752,0.877103,1}{2,4,3,5,1},{0.0418573,0.295673,0.443853,0.634535,0.82936,1}{4,1,2,3,5}}
RCV on ballots
RCV on ballots
doRCV[ballots_] := Module{numVoters = Length[ballots], winner, balts = ballots, morethan2Cands = (Length[ballots[[1,2]]]>2)}, While(*Echo@*)morethan2Cands && Last[Sort@Counts[balts[[All,-1,-1]]]] <= numVoters2,(*Print[3433232];*) balts = (*Echo@*)removeWorst[balts] ; (*winner*)winner = Sort@Counts[balts[[All,-1,-1]]]//Keys//Last, (*total utility*)(*Echo@*)Total[getUtil[#, winner]&/@balts]numVoters ;removeWorst[ballots_] := Withworst = (*Echo@*)First Keys@(*Echo@*)Sort@Join (*0's for cands not already elim'd*)AssociationMap[0&,ballots[[1,2]]], Counts[ballots[[All,-1,-1]]] , remove[worst]/@ballotsremove[worst_] := Functionballot, With{pos =(* Echo@*)First@First@Position[ballot[[2]],worst]}, Delete[ballot[[1]],pos] -> Delete[ballot[[2]],pos]getUtil[bal_, winner_] := With{pos = First@First@Position[bal[[2]],winner]}, bal[[1,pos]]
In[]:=
doRCV[ballts]
Out[]=
{5,0.671226}
For this set of ballots, RCV chooses candidate “5”, with average voter satisfaction .671.
Plurality voting
Plurality voting
In[]:=
doPlurality[ballots_] := Module{cnts = (*Echo@*)Sort@Counts[ballots[[All,-1,-1]]], winner, numVoters = Length[ballots]}, winner = Keys[cnts][[-1]]; {winner, Total[getUtil[#, winner]&/@ballots]/numVoters}
In[]:=
doPlurality[ballts]
Out[]=
{3,0.638917}
On the same set of ballots, plurality chooses candidate “3”, with average voter satisfaction .639.
functions to compare plurality to RCV over many elections
functions to compare plurality to RCV over many elections
(*compare and summarize the difference between the two methods, for fixed numbers of candidates and voters, and one popularity distribution*)compare[numElections_,numCands_,numVoters_,relPopularities_]:=Withres = Table With{b=generateBallots[numCands,numVoters,relPopularities]}, {doPlurality[b],doRCV[b]} , numElections(*numElections*) , "plurality"->Total[res[[All,1,2]]], "RCV"->Total[res[[All,2,2]]], "% agreement"->N[100 Total[Boole[#[[1,1]]===#[[2,1]]]&/@res]/numElections] ;(*a complete report of the above, for each election in the test*)multiElectionTable[numElections_,numCands_,numVoters_,relPopularities_]:=Withres = Table With{b=(*Echo@*)generateBallots[numCands,numVoters,relPopularities]}, {doPlurality[b],doRCV[b]} , numElections(*numElections*) , res
results for various voter/candidate amounts
results for various voter/candidate amounts
uniformly random candidate popularities, “uniformly random” voter utilities
uniformly random candidate popularities, “uniformly random” voter utilities
Grid[Join[{{"# cands \\ # voters",7,15,50,150,500}},#]&[Table[If[numVoters===0,numCands,Column[compare[300,numCands,numVoters,RandomReal[1,numCands]&],Alignment->Right]],{numCands,{(*nodifferenceuntil:*)3,4,5,7,13}},{numVoters,{0,7,15,50,150,500}}]],Frame->All,Spacings->{2,2}]
First, note the fairly obvious facts that agreement decreases with the number of candidates, that the total average happiness of voters is always over half of 300, the number of elections (but not much over!).
The major result seems to be that RCV wins every type of contest, but only by a little, and that little is least when the number of voters is high and the number of candidates is low, which might be the most important/realistic case.
Therefore, for this one case, we will increase the number of elections to see the difference is statistically significant:
The major result seems to be that RCV wins every type of contest, but only by a little, and that little is least when the number of voters is high and the number of candidates is low, which might be the most important/realistic case.
Therefore, for this one case, we will increase the number of elections to see the difference is statistically significant:
Difference of two means tests, elections at the most contentious parameters: 3 candidates, 500 voters
Difference of two means tests, elections at the most contentious parameters: 3 candidates, 500 voters
It takes about 2000 elections to see that the difference is statistically significant:
right-skewed beta popularity distribution, “uniformly random” voter utilities
right-skewed beta popularity distribution, “uniformly random” voter utilities
difference of two means (3 candidates, 500 voters)
difference of two means (3 candidates, 500 voters)
Here we get less robust statistical significance, even with 3000 elections:
left-skewed beta popularity distribution, “uniformly random” voter utilities
left-skewed beta popularity distribution, “uniformly random” voter utilities
difference of two means (3 candidates, 500 voters)
difference of two means (3 candidates, 500 voters)
Here we get very robust statistical significance over all tests:
CITE THIS NOTEBOOK
CITE THIS NOTEBOOK
A (slightly) deeper dive into ranked-choice voting
by Peter Barendse
Wolfram Community, STAFF PICKS, August 4, 2025
https://community.wolfram.com/groups/-/m/t/3524606
by Peter Barendse
Wolfram Community, STAFF PICKS, August 4, 2025
https://community.wolfram.com/groups/-/m/t/3524606