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