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   834266 use strict;
  4         29  
  4         131  
2 4     4   22 use warnings;
  4         9  
  4         117  
3 4     4   85 use 5.024;
  4         14  
4 4     4   22 use feature qw /postderef signatures/;
  4         8  
  4         492  
5              
6             package Vote::Count::Method::CondorcetDropping;
7              
8 4     4   1612 use namespace::autoclean;
  4         57299  
  4         20  
9 4     4   2272 use Moose;
  4         1442020  
  4         30  
10             extends 'Vote::Count';
11              
12             our $VERSION='2.01';
13              
14             =head1 NAME
15              
16             Vote::Count::Method::CondorcetDropping
17              
18             =head1 VERSION 2.01
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   28590 no warnings 'experimental';
  4         11  
  4         190  
88             # use List::Util qw( min max );
89             # use YAML::XS;
90              
91 4     4   1967 use Vote::Count::Matrix;
  4         15  
  4         253  
92 4     4   38 use Carp;
  4         11  
  4         6687  
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 128 sub GetRound ( $self, $active, $roundnum = '' ) {
  30         55  
  30         53  
  30         73  
  30         60  
124 30         1135 my $rule = lc( $self->DropRule() );
125 30 100       205 if ( $rule =~ m/(plurality|topcount)/ ) {
    100          
    100          
    100          
126 17         76 return $self->TopCount($active);
127             }
128             elsif ( $rule eq 'approval' ) {
129 3         26 my $round = $self->Approval($active);
130 3         20 $self->logv( "Round $roundnum Approval Totals ", $round->RankTable() );
131 3         22 return $round;
132             }
133             elsif ( $rule eq 'borda' ) {
134 8         46 my $round = $self->Borda($active);
135 8         54 $self->logv( "Round $roundnum Borda Count ", $round->RankTable() );
136 8         55 return $round;
137             }
138             elsif ( $rule eq 'greatestloss' ) {
139 1         35 return $self->Matrix()->RankGreatestLoss($active);
140             }
141             else {
142 1         25 croak "undefined dropping rule $rule requested";
143             }
144             }
145              
146 9     9 0 22 sub DropChoice ( $self, $round, @jeapardy ) {
  9         16  
  9         16  
  9         29  
  9         14  
147 9         40 my %roundvotes = $round->RawCount()->%*;
148 9         45 my @eliminate = ();
149 9         34 my $lowest = $round->CountVotes();
150 9         33 for my $j (@jeapardy) {
151 28 100       79 $lowest = $roundvotes{$j} if $roundvotes{$j} < $lowest;
152             }
153 9         28 for my $j (@jeapardy) {
154 28 100       72 if ( $roundvotes{$j} == $lowest ) {
155 11         24 push @eliminate, $j;
156             }
157             }
158 9         33 return @eliminate;
159             }
160              
161 2     2   4 sub _newmatrix ($self) {
  2         4  
  2         4  
162 2         52 return Vote::Count::Matrix->new(
163             'BallotSet' => $self->BallotSet(),
164             Active => $self->Active()
165             );
166             }
167              
168 10     10   20 sub _logstart ( $self, $active ) {
  10         19  
  10         19  
  10         21  
169 10         25 my $dropdescription = 'Elimination Rule is Applied to All Active Choices.';
170 10 100       368 if ( $self->DropStyle eq 'leastwins' ) {
171 3         10 $dropdescription =
172             'Elimination Rule is Applied to only Choices with the Fewest Wins.';
173             }
174 10         23 my $rule = '';
175 10 100       318 if ( $self->DropRule() =~ m/(plurality|topcount)/ ) {
    100          
    50          
    0          
176 7         18 $rule = "Drop the Choice With the Lowest TopCount.";
177             }
178             elsif ( $self->DropRule() eq 'approval' ) {
179 1         4 $rule = "Drop the Choice With the Lowest Approval.";
180             }
181             elsif ( $self->DropRule() eq 'borda' ) {
182 2         6 $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         27 'CHOICES:', join( ', ', ( sort keys %{$active} ) ) );
  10         102  
192 10         58 $self->logv( "Elimination Rule: $rule", $dropdescription );
193             }
194              
195 10     10 0 2535 sub RunCondorcetDropping ( $self, $active = undef ) {
  10         25  
  10         23  
  10         18  
196 10 100       36 unless ( defined $active ) { $active = $self->Active() }
  9         324  
197 10         26 my $roundctr = 0;
198 10         21 my $maxround = scalar( keys %{$active} );
  10         29  
199 10         45 $self->_logstart($active);
200 10         71 my $result = { tie => 0, tied => undef, winner => 0 };
201             DROPLOOP:
202 10         21 until (0) {
203 25         59 $roundctr++;
204 25 50       85 die "DROPLOOP infinite stopped at $roundctr" if $roundctr > $maxround;
205 25         127 my $topcount = $self->TopCount($active);
206 25         107 my $round = $self->GetRound( $active, $roundctr );
207 25         158 $self->logv( '---', "Round $roundctr TopCount", $topcount->RankTable() );
208 25         182 my $majority = $self->EvaluateTopCountMajority($topcount);
209 25 100       85 if ( defined $majority->{'winner'} ) {
210 7         24 $result->{'winner'} = $majority->{'winner'};
211 7         64 last DROPLOOP;
212             }
213 18         520 my $matrix = Vote::Count::Matrix->new(
214             'BallotSet' => $self->BallotSet,
215             'Active' => $active
216             );
217 18         105 $self->logv( '---', "Round $roundctr Pairings", $matrix->MatrixTable() );
218 18   100     147 my $cw = $matrix->CondorcetWinner() || 0;
219 18 100       56 if ($cw) {
220 2         7 my $wstr = "* Winner $cw *";
221 2         6 my $rpt = length($wstr);
222 2         16 $self->logt( '*' x $rpt, $wstr, '*' x $rpt );
223 2         7 $result->{'winner'} = $cw;
224 2         62 last DROPLOOP;
225             }
226 16 100       598 my $eliminated =
227             $self->SkipLoserDrop()
228             ? { 'eliminations' => 0 }
229             : $matrix->CondorcetLoser();
230 16 100       67 if ( $eliminated->{'eliminations'} ) {
231             # tracking active between iterations of matrix.
232 7         203 $active = $matrix->Active();
233 7         44 $self->logv( $eliminated->{'verbose'} );
234             # active changed, restart loop
235 7         237 next DROPLOOP;
236             }
237 9         32 my @jeapardy = ();
238 9 100       368 if ( $self->DropStyle eq 'leastwins' ) {
239 3         17 @jeapardy = $matrix->LeastWins();
240             }
241 6         24 else { @jeapardy = keys %{$active} }
  6         23  
242 9         41 for my $goodbye ( $self->DropChoice( $round, @jeapardy ) ) {
243 11         32 delete $active->{$goodbye};
244 11         50 $self->logv("Eliminating $goodbye");
245             }
246 9         33 my @remaining = keys $active->%*;
247 9 50       276 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         3 my $winner = $remaining[0];
256 1         7 $self->logt( "Only 1 choice remains.", "** WINNER : $winner **" );
257 1         5 $result->{'winner'} = $winner;
258 1         32 last DROPLOOP;
259             }
260             }; #infinite DROPLOOP
261 10         59 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