File Coverage

blib/lib/Vote/Count/Method/CondorcetDropping.pm
Criterion Covered Total %
statement 125 131 95.4
branch 35 40 87.5
condition 2 2 100.0
subroutine 14 14 100.0
pod 0 3 0.0
total 176 190 92.6


line stmt bran cond sub pod time code
1 4     4   864972 use strict;
  4         30  
  4         127  
2 4     4   24 use warnings;
  4         7  
  4         108  
3 4     4   83 use 5.024;
  4         13  
4 4     4   20 use feature qw /postderef signatures/;
  4         9  
  4         440  
5              
6             package Vote::Count::Method::CondorcetDropping;
7              
8 4     4   1740 use namespace::autoclean;
  4         57802  
  4         20  
9 4     4   2275 use Moose;
  4         1475951  
  4         34  
10             extends 'Vote::Count';
11              
12             our $VERSION='2.00';
13              
14             =head1 NAME
15              
16             Vote::Count::Method::CondorcetDropping
17              
18             =head1 VERSION 2.00
19              
20             =cut
21              
22             # ABSTRACT: Methods which use simple dropping rules to resolve a Winner-less Condorcet Matrix.
23              
24             =pod
25              
26             =head1 Method Description for Simple Dropping
27              
28             Simple Dropping eliminates the I<weakest> choice until there is a Condorcet Winner. This method is simple and widely used.
29              
30             =head1 SYNOPSIS
31              
32             my $CondorcetElection =
33             Vote::Count::Method::CondorcetDropping->new(
34             'BallotSet' => $ballotset ,
35             'DropStyle' => 'all', # default = leastwins
36             'DropRule' => 'topcount', # default
37             'TieBreakMethod' => 'none', # default
38             );
39              
40             my $Winner = $CondorcetElection->RunCondorcetDropping( $ActiveSet )->{'winner'};
41              
42             =head1 RunCondorcetDropping
43              
44             Takes an optional parameter of an Active Set as a HashRef. Returns a HashRef with the standard result keys: winner, tie, tied. Writes details to the Vote::Count logs.
45              
46             =head1 Dropping Options
47              
48             =head2 DropStyle
49              
50             Set DropStyle to 'all' for dropping against all choices or 'leastwins' to only consider those choices.
51              
52             Default is leastwins.
53              
54             =head2 DropRule
55              
56             Determines the rule by which choices will be eliminated when there is no Condorcet Winner. Supported Dropping Rules are: 'borda' count (with all the attendant weighting issues), 'approval', 'topcount' ('plurality'), and 'greatestloss'.
57              
58             default is plurality (topcount)
59              
60             =head2 SkipLoserDrop
61              
62             Normally RunCondorcetDropping eliminates Condorcet Losers whenever they are discovered. Dropping Condorcet Losers will be skipped if set to 1.
63              
64             =head1 Benham
65              
66             This method modifies IRV by checking for a Condorcet Winner each round, and then drops the low choice as regular IRV. It is probably the most widely used Condorcet Method for Hand Counting because it does not require a full matrix. For each choice it is only required to determine if they lose to any of the other active choices. By Counting Approval at the beginning, it is often possible to determine that a choice will lose at least one pairing without conducting any pairings, then it is only necessary to check choices that possibly have no losses.
67              
68             This method is fairly simple, and meets Condorcet Winner/Loser, but fails LNH, and inherits IRV's failings on consistency. BTR-IRV is even easier to Hand Count and is Smith Compliant, Benham has no obvious advantage to it, other than having been used more widely in the past.
69              
70             The original method specified Random for Tie Breaker, which can be done in a reproducable manner with L<Precedence|Vote::Count::TieBreaker/Precedence>.
71              
72             The following example implements Benham, resolving ties with a precedence file generated using the number of ballots cast as the random seed.
73              
74             my $Benham = Vote::Count::Method::CondorcetDropping->new(
75             'BallotSet' => $someballotset,
76             'DropStyle' => 'all',
77             'DropRule' => 'topcount',
78             'SkipLoserDrop' => 1,
79             'TieBreakMethod' => 'precedence',
80             'PrecedenceFile' => '/tmp/benhamties.txt',
81             );
82             $Benham->CreatePrecedenceRandom( '/tmp/benhamties.txt' );
83             my $Result = $Benham->RunCondorcetDropping();
84              
85             =cut
86              
87 4     4   30501 no warnings 'experimental';
  4         12  
  4         218  
88             # use List::Util qw( min max );
89             # use YAML::XS;
90              
91 4     4   1984 use Vote::Count::Matrix;
  4         15  
  4         202  
92 4     4   31 use Carp;
  4         7  
  4         7011  
93             # use Try::Tiny;
94             # use Data::Dumper;
95              
96             has 'Matrix' => (
97             isa => 'Object',
98             is => 'ro',
99             lazy => 1,
100             builder => '_newmatrix',
101             );
102              
103             # DropStyle: whether to apply drop rule against
104             # all choices ('all') or the least winning ('leastwins').
105             has 'DropStyle' => (
106             isa => 'Str',
107             is => 'ro',
108             default => 'leastwins',
109             );
110              
111             has 'DropRule' => (
112             isa => 'Str',
113             is => 'ro',
114             default => 'plurality',
115             );
116              
117             has 'SkipLoserDrop' => (
118             isa => 'Int',
119             is => 'ro',
120             default => 0,
121             );
122              
123 30     30 0 118 sub GetRound ( $self, $active, $roundnum = '' ) {
  30         48  
  30         54  
  30         55  
  30         44  
124 30         1086 my $rule = lc( $self->DropRule() );
125 30 100       188 if ( $rule =~ m/(plurality|topcount)/ ) {
    100          
    100          
    100          
126 17         79 return $self->TopCount($active);
127             }
128             elsif ( $rule eq 'approval' ) {
129 3         27 my $round = $self->Approval($active);
130 3         23 $self->logv( "Round $roundnum Approval Totals ", $round->RankTable() );
131 3         22 return $round;
132             }
133             elsif ( $rule eq 'borda' ) {
134 8         47 my $round = $self->Borda($active);
135 8         47 $self->logv( "Round $roundnum Borda Count ", $round->RankTable() );
136 8         49 return $round;
137             }
138             elsif ( $rule eq 'greatestloss' ) {
139 1         36 return $self->Matrix()->RankGreatestLoss($active);
140             }
141             else {
142 1         27 croak "undefined dropping rule $rule requested";
143             }
144             }
145              
146 9     9 0 15 sub DropChoice ( $self, $round, @jeapardy ) {
  9         20  
  9         12  
  9         23  
  9         18  
147 9         33 my %roundvotes = $round->RawCount()->%*;
148 9         23 my @eliminate = ();
149 9         26 my $lowest = $round->CountVotes();
150 9         26 for my $j (@jeapardy) {
151 28 100       69 $lowest = $roundvotes{$j} if $roundvotes{$j} < $lowest;
152             }
153 9         22 for my $j (@jeapardy) {
154 28 100       61 if ( $roundvotes{$j} == $lowest ) {
155 11         29 push @eliminate, $j;
156             }
157             }
158 9         31 return @eliminate;
159             }
160              
161 2     2   5 sub _newmatrix ($self) {
  2         4  
  2         6  
162 2         48 return Vote::Count::Matrix->new(
163             'BallotSet' => $self->BallotSet(),
164             Active => $self->Active()
165             );
166             }
167              
168 10     10   23 sub _logstart ( $self, $active ) {
  10         17  
  10         18  
  10         18  
169 10         20 my $dropdescription = 'Elimination Rule is Applied to All Active Choices.';
170 10 100       352 if ( $self->DropStyle eq 'leastwins' ) {
171 3         10 $dropdescription =
172             'Elimination Rule is Applied to only Choices with the Fewest Wins.';
173             }
174 10         27 my $rule = '';
175 10 100       307 if ( $self->DropRule() =~ m/(plurality|topcount)/ ) {
    100          
    50          
    0          
176 7         19 $rule = "Drop the Choice With the Lowest TopCount.";
177             }
178             elsif ( $self->DropRule() eq 'approval' ) {
179 1         3 $rule = "Drop the Choice With the Lowest Approval.";
180             }
181             elsif ( $self->DropRule() eq 'borda' ) {
182 2         7 $rule = "Drop the Choice With the Lowest Borda Score.";
183             }
184             elsif ( $self->DropRule() eq 'greatestloss' ) {
185 0         0 $rule = "Drop the Choice With the Greatest Loss.";
186             }
187             else {
188 0         0 croak "undefined dropping rule $rule requested";
189             }
190             $self->logt( 'CONDORCET SEQUENTIAL DROPPING METHOD',
191 10         37 'CHOICES:', join( ', ', ( sort keys %{$active} ) ) );
  10         111  
192 10         57 $self->logv( "Elimination Rule: $rule", $dropdescription );
193             }
194              
195 10     10 0 798 sub RunCondorcetDropping ( $self, $active = undef ) {
  10         29  
  10         23  
  10         64  
196 10 100       38 unless ( defined $active ) { $active = $self->Active() }
  9         315  
197 10         21 my $roundctr = 0;
198 10         21 my $maxround = scalar( keys %{$active} );
  10         35  
199 10         42 $self->_logstart($active);
200 10         56 my $result = { tie => 0, tied => undef, winner => 0 };
201             DROPLOOP:
202 10         24 until (0) {
203 25         2776 $roundctr++;
204 25 50       75 die "DROPLOOP infinite stopped at $roundctr" if $roundctr > $maxround;
205 25         140 my $topcount = $self->TopCount($active);
206 25         98 my $round = $self->GetRound( $active, $roundctr );
207 25         125 $self->logv( '---', "Round $roundctr TopCount", $topcount->RankTable() );
208 25         173 my $majority = $self->EvaluateTopCountMajority($topcount);
209 25 100       84 if ( defined $majority->{'winner'} ) {
210 7         22 $result->{'winner'} = $majority->{'winner'};
211 7         65 last DROPLOOP;
212             }
213 18         483 my $matrix = Vote::Count::Matrix->new(
214             'BallotSet' => $self->BallotSet,
215             'Active' => $active
216             );
217 18         244 $self->logv( '---', "Round $roundctr Pairings", $matrix->MatrixTable() );
218 18   100     110 my $cw = $matrix->CondorcetWinner() || 0;
219 18 100       53 if ($cw) {
220 2         7 my $wstr = "* Winner $cw *";
221 2         4 my $rpt = length($wstr);
222 2         14 $self->logt( '*' x $rpt, $wstr, '*' x $rpt );
223 2         7 $result->{'winner'} = $cw;
224 2         18 last DROPLOOP;
225             }
226 16 100       621 my $eliminated =
227             $self->SkipLoserDrop()
228             ? { 'eliminations' => 0 }
229             : $matrix->CondorcetLoser();
230 16 100       59 if ( $eliminated->{'eliminations'} ) {
231             # tracking active between iterations of matrix.
232 7         200 $active = $matrix->Active();
233 7         38 $self->logv( $eliminated->{'verbose'} );
234             # active changed, restart loop
235 7         90 next DROPLOOP;
236             }
237 9         20 my @jeapardy = ();
238 9 100       347 if ( $self->DropStyle eq 'leastwins' ) {
239 3         16 @jeapardy = $matrix->LeastWins();
240             }
241 6         13 else { @jeapardy = keys %{$active} }
  6         20  
242 9         38 for my $goodbye ( $self->DropChoice( $round, @jeapardy ) ) {
243 11         27 delete $active->{$goodbye};
244 11         47 $self->logv("Eliminating $goodbye");
245             }
246 9         36 my @remaining = keys $active->%*;
247 9 50       72 if ( @remaining == 0 ) {
    100          
248 0         0 $self->logt(
249             "All remaining Choices would be eliminated, Tie between @jeapardy");
250 0         0 $result->{'tie'} = 1;
251 0         0 $result->{'tied'} = \@jeapardy;
252 0         0 last DROPLOOP;
253             }
254             elsif ( @remaining == 1 ) {
255 1         4 my $winner = $remaining[0];
256 1         7 $self->logt( "Only 1 choice remains.", "** WINNER : $winner **" );
257 1         5 $result->{'winner'} = $winner;
258 1         9 last DROPLOOP;
259             }
260             }; #infinite DROPLOOP
261 10         636 return $result;
262             }
263              
264             1;
265              
266             #FOOTER
267              
268             =pod
269              
270             BUG TRACKER
271              
272             L<https://github.com/brainbuz/Vote-Count/issues>
273              
274             AUTHOR
275              
276             John Karr (BRAINBUZ) brainbuz@cpan.org
277              
278             CONTRIBUTORS
279              
280             Copyright 2019-2021 by John Karr (BRAINBUZ) brainbuz@cpan.org.
281              
282             LICENSE
283              
284             This module is released under the GNU Public License Version 3. See license file for details. For more information on this license visit L<http://fsf.org>.
285              
286             SUPPORT
287              
288             This software is provided as is, per the terms of the GNU Public License. Professional support and customisation services are available from the author.
289              
290             =cut
291