File Coverage

blib/lib/Vote/Count/Matrix.pm
Criterion Covered Total %
statement 300 300 100.0
branch 80 86 93.0
condition n/a
subroutine 32 32 100.0
pod 13 14 92.8
total 425 432 98.3


line stmt bran cond sub pod time code
1 39     39   382 use strict;
  39         116  
  39         1181  
2 39     39   187 use warnings;
  39         77  
  39         1023  
3 39     39   811 use 5.024;
  39         128  
4 39     39   212 use feature qw /postderef signatures/;
  39         87  
  39         4399  
5              
6             package Vote::Count::Matrix;
7 39     39   264 use Moose;
  39         76  
  39         323  
8 39     39   248303 use MooseX::StrictConstructor;
  39         65125  
  39         274  
9              
10             with
11             'Vote::Count::Common',
12             'Vote::Count::Approval',
13             'Vote::Count::Borda',
14             'Vote::Count::Log',
15             'Vote::Count::Score',
16             'Vote::Count::TieBreaker',
17             ;
18              
19 39     39   157795 use Vote::Count::RankCount;
  39         113  
  39         1420  
20              
21 39     39   288 no warnings 'experimental';
  39         79  
  39         1621  
22 39     39   225 use List::Util qw( min max sum );
  39         81  
  39         2470  
23 39     39   225 use Vote::Count::TextTableTiny qw/generate_table/;
  39         78  
  39         1362  
24 39     39   200 use Sort::Hash;
  39         84  
  39         1594  
25 39     39   22267 use Storable 3.15 'dclone';
  39         104001  
  39         2570  
26              
27             # use Try::Tiny;
28             #use Data::Dumper;
29              
30 39     39   12528 use YAML::XS;
  39         67709  
  39         131504  
31              
32             our $VERSION='2.01';
33              
34             =head1 NAME
35              
36             Vote::Count::Matrix
37              
38             =head1 VERSION 2.01
39              
40             =cut
41              
42             # ABSTRACT: Condorcet Win Loss Matrix
43              
44             has BallotSet => (
45             is => 'ro',
46             required => 1,
47             isa => 'HashRef',
48             );
49              
50             has TieBreakMethod => (
51             is => 'rw',
52             isa => 'Str',
53             required => 0,
54             default => 'none',
55             );
56              
57 313     313   433 sub _untie ( $I, $A, $B ) {
  313         423  
  313         430  
  313         375  
  313         462  
58 313         8323 my @untie = $I->TieBreaker( $I->TieBreakMethod(), $I->Active(), $A, $B );
59 313 100       925 return $untie[0] if ( scalar(@untie) == 1 );
60 172         353 return 0;
61             }
62              
63 2485     2485   3174 sub _pairwinner_rcv ( $ballots, $A, $B ) {
  2485         3206  
  2485         3133  
  2485         3053  
  2485         2842  
64 2485         3028 my $countA = 0;
65 2485         3116 my $countB = 0;
66             FORVOTES:
67 2485         11691 for my $b ( keys $ballots->%* ) {
68 55852         101932 for my $v ( values $ballots->{$b}{'votes'}->@* ) {
69 107725 100       199885 if ( $v eq $A ) {
    100          
70 17520         26607 $countA += $ballots->{$b}{'count'} * $ballots->{$b}{'votevalue'};
71 17520         24121 next FORVOTES;
72             }
73             elsif ( $v eq $B ) {
74 18498         27993 $countB += $ballots->{$b}{'count'} * $ballots->{$b}{'votevalue'};
75 18498         25596 next FORVOTES;
76             }
77             }
78             } # FORVOTES
79 2485         8822 return ( $countA, $countB );
80             }
81              
82 2624     2624   8137 sub _conduct_pair ( $I, $A, $B ) {
  2624         3412  
  2624         3356  
  2624         3487  
  2624         3113  
83 2624         62940 my $ballots = $I->BallotSet()->{'ballots'};
84 2624         3785 my $countA = 0;
85 2624         3315 my $countB = 0;
86 2624         8571 $I->logv("Pairing: $A vs $B");
87 2624 100       59642 if ( $I->BallotSet()->{'options'}{'range'} ) {
88 139         324 ( $countA, $countB ) = $I->RangeBallotPair( $A, $B );
89             }
90             else {
91 2485         4851 ( $countA, $countB ) = _pairwinner_rcv( $ballots, $A, $B );
92             }
93 2624         12057 my %retval = (
94             $A => $countA,
95             $B => $countB,
96             'tie' => 0,
97             'winner' => '',
98             'loser' => '',
99             'margin' => abs( $countA - $countB )
100             );
101 2624         4119 my $diff = $countA - $countB;
102             # 0 : $countA == $countB
103 2624 100       5040 if ( $diff == 0 ) {
104 313         769 my $untied = $I->_untie( $A, $B );
105 313 100       650 if ($untied) {
106 141 100       288 $diff = 1 if $untied eq $A;
107 141 100       360 $diff = -1 if $untied eq $B;
108             }
109             }
110 2624 100       6142 if ( $diff == 0 ) {
    100          
    50          
111 172         277 $retval{'winner'} = '';
112 172         292 $retval{'tie'} = 1;
113             }
114             # $diff > 0 A won or won tiebreaker.
115             elsif ( $diff > 0 ) {
116 1268         2027 $retval{'winner'} = $A;
117 1268         1884 $retval{'loser'} = $B;
118             }
119             # $diff < 0 B won or won tiebreaker.
120             elsif ( $diff < 0 ) {
121 1184         1828 $retval{'winner'} = $B;
122 1184         1799 $retval{'loser'} = $A;
123             }
124 2624 100       4423 if ( $retval{'winner'} ) {
125 2452         9963 $I->logv("Winner: $retval{'winner'} ($A: $countA $B: $countB)");
126             }
127 172         706 else { $I->logv("Tie $A: $countA $B: $countB") }
128 2624         6519 return \%retval;
129             }
130              
131             sub BUILD {
132 86     86 0 193 my $self = shift;
133 86         202 my $results = {};
134 86         2273 my $ballotset = $self->BallotSet();
135 86         2190 my @choices = keys $self->Active()->%*;
136 86         310 while ( scalar(@choices) ) {
137 658         1166 my $A = shift @choices;
138 658         1256 for my $B (@choices) {
139 2621         5252 my $result = $self->_conduct_pair( $A, $B );
140             # Each result has two hash keys so it can be found without
141             # having to try twice or sort the names for a single key.
142 2621         5277 $results->{$A}{$B} = $result;
143 2621         6097 $results->{$B}{$A} = $result;
144             }
145             }
146 86         253 $self->{'Matrix'} = $results;
147 86         434 $self->logt( "# Matrix", $self->MatrixTable() );
148 86         632 $self->logv( "# Pairing Results", $self->PairingVotesTable() );
149             }
150              
151 134     134 1 2694 sub ScoreMatrix ( $self ) {
  134         261  
  134         185  
152 134         297 my $scores = {};
153 134         3679 my %active = $self->Active()->%*;
154 134         502 for my $A ( keys %active ) {
155 808         1092 my $hasties = 0;
156 808         1207 $scores->{$A} = 0;
157 808         1645 for my $B ( keys %active ) {
158 6048 100       9557 next if $B eq $A;
159 5240 100       9866 if ( $A eq $self->{'Matrix'}{$A}{$B}{'winner'} ) { $scores->{$A}++ }
  2437         2942  
160 5240 100       9339 if ( $self->{'Matrix'}{$A}{$B}{'tie'} ) { $hasties = .001 }
  366         447  
161             }
162 808 100       1770 if ( $scores->{$A} == 0 ) { $scores->{$A} += $hasties }
  139         277  
163             }
164 134         428 return $scores;
165             }
166              
167             # return the choice with fewest wins in matrix.
168 3     3 1 4 sub LeastWins ( $matrix ) {
  3         7  
  3         5  
169 3         7 my @lowest = ();
170 3         11 my %scored = $matrix->ScoreMatrix()->%*;
171 3         21 my $lowscore = min( values %scored );
172 3         9 for my $A ( keys %scored ) {
173 10 100       24 if ( $scored{$A} == $lowscore ) {
174 8         16 push @lowest, $A;
175             }
176             }
177 3         15 return @lowest;
178             }
179              
180 17     17 1 2648 sub CondorcetLoser ( $self, $nowins = 0 ) {
  17         39  
  17         31  
  17         30  
181 17         39 my $unfinished = 1;
182 17         41 my $wordy = "Removing Condorcet Losers\n";
183 17         41 my @eliminated = ();
184 194     194   233 my $loser = sub ( $score ) {
  194         246  
  194         252  
185 194 100       312 if ($nowins) { return 1 if $score < 1 }
  49 100       80  
186 145 100       285 else { return 1 if $score == 0 }
187 151         345 return 0;
188 17         119 };
189             CONDORCETLOSERLOOP:
190 17         52 while ($unfinished) {
191 60         98 $unfinished = 0;
192 60         137 my $scores = $self->ScoreMatrix;
193 60         1531 my @alist = ( keys $self->Active()->%* );
194             # Check that tied choices at the top won't be
195             # eliminated. alist is looped over twice because we
196             # don't want to report the scores when the list is
197             # reduced to either a condorcet winner or tied situation.
198 60         144 for my $A (@alist) {
199 313 100       839 unless ( max( values $scores->%* ) ) {
200 2         7 last CONDORCETLOSERLOOP;
201             }
202             }
203 58         2458 $wordy .= YAML::XS::Dump($scores);
204 58         263 for my $A (@alist) {
205 194 100       351 if ( $loser->( $scores->{$A} ) ) {
206 43         87 push @eliminated, ($A);
207 43         107 $wordy .= "Eliminationg Condorcet Loser: *$A*\n";
208 43         89 delete $self->{'Active'}{$A};
209 43         59 $unfinished = 1;
210 43         162 next CONDORCETLOSERLOOP;
211             }
212             }
213             }
214 17 100       102 my $elimstr =
215             scalar(@eliminated)
216             ? "Eliminated Condorcet Losers: " . join( ', ', @eliminated ) . "\n"
217             : "No Condorcet Losers Eliminated\n";
218             return {
219 17         211 verbose => $wordy,
220             terse => $elimstr,
221             eliminated => \@eliminated,
222             eliminations => scalar(@eliminated),
223             };
224             }
225              
226 65     65 1 2707 sub CondorcetWinner( $self ) {
  65         127  
  65         122  
227 65         268 my $scores = $self->ScoreMatrix;
228 65         266 my @choices = keys $scores->%*;
229             # # if there is only one choice left they win.
230             # if ( scalar(@choices) == 1 ) { return $choices[0]}
231 65         170 my $mustwin = scalar(@choices) - 1;
232 65         133 my $winner = '';
233 65         155 for my $c (@choices) {
234 434 100       862 if ( $scores->{$c} == $mustwin ) {
235 32         90 $winner .= $c;
236             }
237             }
238 65         410 return $winner;
239             }
240              
241 26     26 1 38 sub GreatestLoss ( $self, $A ) {
  26         33  
  26         34  
  26         31  
242 26         33 my $bigloss = 0;
243             GREATESTLOSSLOOP:
244 26         62 for my $B ( $self->GetActiveList() ) {
245             # for my $B ( keys $self->Active()->%* ) {
246 290 100       445 next GREATESTLOSSLOOP if $B eq $A;
247 264         802 my %result = $self->{'Matrix'}{$A}{$B}->%*;
248             # warn "$A : $B loser $result{'loser'} : margin $result{'margin'} $A: $result{$A} $B: $result{$B}";
249 264 100       556 if ( $result{'loser'} eq $A ) {
250 111 100       240 $bigloss = $result{'margin'} if $result{'margin'} > $bigloss;
251             }
252             }
253 26         74 return $bigloss;
254             }
255              
256 2     2 1 2816 sub RankGreatestLoss ( $self, $active = undef ) {
  2         4  
  2         4  
  2         5  
257 2         11 my %loss = ();
258 2 100       37 $active = $self->Active() unless defined $active;
259 2         12 for my $A ( keys $active->%* ) {
260 21         43 $loss{$A} = $self->GreatestLoss($A);
261             }
262 2         21 return Vote::Count::RankCount->Rank( \%loss );
263             }
264              
265             # reset active to choices
266 2     2 1 5 sub ResetActive ( $self ) {
  2         3  
  2         3  
267 2         59 $self->{'Active'} = dclone $self->BallotSet->{'choices'};
268             }
269              
270 74     74   4387 sub _getsmithguessforchoice ( $h, $matrix ) {
  74         98  
  74         85  
  74         82  
271 74         117 my @winners = ($h);
272 74         213 for my $P ( keys $matrix->{$h}->%* ) {
273 561 100       1144 if ( $matrix->{$h}{$P}{'winner'} eq $P ) {
    100          
274 258         382 push @winners, ($P);
275             }
276             elsif ( $matrix->{$h}{$P}{'tie'} ) {
277 35         54 push @winners, ($P);
278             }
279             }
280 74         138 return ( map { $_ => 1 } @winners );
  367         708  
281             }
282              
283 43     43 1 2272 sub GetPairResult ( $self, $A, $B ) {
  43         92  
  43         103  
  43         69  
  43         70  
284 43         256 return $self->{'Matrix'}{$A}{$B};
285             }
286              
287 8     8 1 3504 sub GetPairWinner ( $self, $A, $B ) {
  8         15  
  8         15  
  8         17  
  8         14  
288 8         25 my $winner = $self->{'Matrix'}{$A}{$B}{'winner'};
289 8 100       48 return $winner if $winner;
290 2         33 return '';
291             }
292              
293 10     10 1 4445 sub SmithSet ( $self ) {
  10         22  
  10         17  
294 10         28 my $matrix = $self->{'Matrix'};
295 10         285 my @alist = ( keys $self->Active()->%* );
296 10         24 my $sets = {};
297 10         23 my $setcounts = {};
298             # my $shortest = scalar(@list);
299 10         30 for my $h (@alist) {
300 72         137 my %set = Vote::Count::Matrix::_getsmithguessforchoice( $h, $matrix );
301 72         153 $sets->{$h} = \%set;
302             # the keys of setcounts are the counts
303 72         215 $setcounts->{ scalar( keys %set ) }{$h} = 1;
304             }
305 10         22 my $proposal = {};
306 10         102 my $minset = min( keys( $setcounts->%* ) );
307 10         45 for my $h ( keys $setcounts->{$minset}->%* ) {
308 18         44 for my $k ( keys( $sets->{$h}->%* ) ) {
309 34         59 $proposal->{$k} = 1;
310             }
311             }
312 10         21 SMITHLOOP: while (1) {
313 15         35 my $cntchoice = scalar( keys $proposal->%* );
314 15         35 for my $h ( keys $proposal->%* ) {
315 46         63 $proposal = { %{$proposal}, %{ $sets->{$h} } };
  46         86  
  46         151  
316             }
317             # done when no choices get added on a pass through loop
318 15 100       51 if ( scalar( keys $proposal->%* ) == $cntchoice ) {
319 10         28 last SMITHLOOP;
320             }
321             }
322 10         92 return $proposal;
323             }
324              
325             # ScoreMatrix as a table.
326 1     1 1 2088 sub ScoreTable ( $self ) {
  1         3  
  1         2  
327 1         3 my $scores = $self->ScoreMatrix();
328 1         5 my @header = ( 'Choice', 'Score' );
329 1         3 my @rows = ( \@header );
330 1         6 for my $c ( sort_hash( $scores, 'numeric', 'desc' ) ) {
331             # for my $c ( sort ( keys $scores->%* ) ) {
332 12         229 push @rows, [ $c, $scores->{$c} ];
333             }
334 1         6 return generate_table( rows => \@rows );
335             }
336              
337 117     117 1 250 sub MatrixTable ( $self, $options = {} ) {
  117         223  
  117         323  
  117         210  
338 117         469 my @header = ( 'Choice', 'Wins', 'Losses', 'Ties' );
339             # the options option was never fully implemented, it shows what the
340             # structure would be if one were or if I finished the feature.
341             # leaving the code in place even though its useless.
342             my $o_topcount =
343 117 50       416 defined $options->{'topcount'} ? $options->{'topcount'} : 0;
344 117 50       339 push @header, 'Top Count' if $o_topcount;
345 117         3176 my @active = sort ( keys $self->Active()->%* );
346 117         401 my @rows = ( \@header ); # [ 'Rank', 'Choice', 'TopCount']);
347 117         334 for my $A (@active) {
348 834         1117 my $wins = 0;
349 834         1041 my $ties = 0;
350 834         1010 my $losses = 0;
351 834 50       1338 my $topcount = $o_topcount ? $options->{'topcount'} : 0;
352             MTNEWROW:
353 834         1187 for my $B (@active) {
354 7102 100       15069 if ( $A eq $B ) { next MTNEWROW }
  834 100       1276  
    100          
    50          
355             elsif ( $self->{'Matrix'}{$A}{$B}{'winner'} eq $A ) {
356 2933         3743 $wins++;
357             }
358             elsif ( $self->{'Matrix'}{$A}{$B}{'winner'} eq $B ) {
359 2933         3736 $losses++;
360             }
361             elsif ( $self->{'Matrix'}{$A}{$B}{'tie'} ) {
362 402         519 $ties++;
363             }
364             }
365 834         1697 my @newrow = ( $A, $wins, $losses, $ties );
366 834 50       1422 push @newrow, $topcount if $o_topcount;
367 834         1714 push @rows, \@newrow;
368             }
369 117         671 return generate_table( rows => \@rows );
370             }
371              
372 97     97 1 214 sub PairingVotesTable ( $self ) {
  97         198  
  97         147  
373 97         466 my @rows = ( [qw/Choice Choice Votes Opponent Votes/] );
374 97         2769 my @choices = sort ( keys $self->Active()->%* );
375 97         363 for my $Choice (@choices) {
376 728         1394 push @rows, [$Choice];
377 728         1079 for my $Opponent (@choices) {
378 6406         8696 my $Cstr = $Choice;
379 6406         7255 my $Ostr = $Opponent;
380 6406 100       10037 next if $Opponent eq $Choice;
381 5678         8888 my $CVote = $self->{'Matrix'}{$Choice}{$Opponent}{$Choice};
382 5678         7697 my $OVote = $self->{'Matrix'}{$Choice}{$Opponent}{$Opponent};
383 5678 100       9769 if ( $self->{'Matrix'}{$Choice}{$Opponent}{'winner'} eq $Choice ) {
384 2641         4162 $Cstr = "**$Cstr**";
385             }
386 5678 100       9572 if ( $self->{'Matrix'}{$Choice}{$Opponent}{'winner'} eq $Opponent ) {
387 2641         4154 $Ostr = "**$Ostr**";
388             }
389 5678         14636 push @rows, [ ' ', $Cstr, $CVote, $Ostr, $OVote ];
390             }
391             }
392 97         492 return generate_table( rows => \@rows );
393             }
394              
395             __PACKAGE__->meta->make_immutable;
396             1;
397              
398             =pod
399              
400             =head1 Win-Loss Matrix
401              
402             Condorcet Pairwise Methods require a Win-Loss Matrix. This object takes an RCV BallotSet with an optional Active list and returns the Matrix as an object. The object is capable of Scoring itself, Calculating a Smith Set, and identifying Condorcet Winners and Losers.
403              
404             =head1 SYNOPSIS
405              
406             my $Matrix =
407             Vote::Count::Matrix->new(
408             'BallotSet' => $myVoteCount->BallotSet() );
409             my $Scores = $Matrix->ScoreMatrix();
410             my %DominantSet = $Matrix->SmithSet()->%*;
411             my $CondorcetWinner = $Matrix->CondorcetWinner();
412              
413             =head1 Tie Breakers
414              
415             A tie breaker may be specified by setting the Tie::Breaker attribute, see the Tie::Breaker module for more information. If using Range Ballots 'none' and 'approval' are the only currently supported options.
416              
417             =head2 new
418              
419             Parameters:
420              
421             =head3 BallotSet (required)
422              
423             A Ballot Set reference as generated by ReadBallots, which can be retrieved from a Vote::Count object via the ->BallotSet() method.
424              
425             Both Ranked Choice and Range BallotSets are supported.
426              
427             =head3 Active (optional)
428              
429             A hash reference with active choices as the keys. The default value is all of the choices defined in the BallotSet.
430              
431             =head3 Logging (optional)
432              
433             Has the logging methods of L.
434              
435             =head1 Methods
436              
437             =head2 MatrixTable
438              
439             Returns a MarkDown formatted table with the wins losses and ties for each Active Choice as text.
440              
441             =head2 PairingVotesTable
442              
443             Returns a MarkDown formatted table with the votes for all of the pairings.
444              
445             =head2 GetPairResult ( $A, $B )
446              
447             Returns the results of the pairing of two choices as a hashref.
448              
449             {
450             'FUDGESWIRL' => 6,
451             'loser' => "STRAWBERRY",
452             'margin' => 2,
453             'STRAWBERRY' => 4,
454             'tie' => 0,
455             'winner' => "FUDGESWIRL"
456             }
457              
458             =head2 GetPairWinner ( $A, $B )
459              
460             Returns the winner of the pairing of two choices. If there is no Winner it returns a false value (empty string).
461              
462             =head2 ScoreMatrix
463              
464             Returns a HashRef of the choices and their Matrix Scores. The scoring is 1 for each win, 0 for losses and ties. In the event a choice has ties but no wins their score will be .001. Where N is the number of choices, a Condorcet Winner will have a score of N-1, a Condorcet Loser will have a score of 0. Since a choice with at least one tie but no wins is not defeated by all other choices they are not a Condorcet Loser, and thus those cases are scored with a near to zero value instead of 0. Methods that wish to treat no wins but tie case as a Condorcet Loser may test for a score less than 1.
465              
466             =head2 ScoreTable
467              
468             Returns the ScoreMatrix as a markdown compatible table.
469              
470             =head2 LeastWins
471              
472             Returns an array of the choice or choices with the fewest wins.
473              
474             =head2 CondorcetLoser
475              
476             Eliminates all Condorcet Losers from the Matrix Object's Active list. Returns a hashref. Takes an optional true false argument (default is false) to include choices that have tied but not won in the elimination.
477              
478             {
479             verbose => 'verbose message',
480             terse => 'terse message',
481             eliminated => [ eliminated choices ],
482             eliminations => number of eliminated choices,
483             };
484              
485             =head2 CondorcetWinner
486              
487             Returns either the Condorcet Winner or an empty string if there is none.
488              
489             =head2 SmithSet
490              
491             Finds the innermost Smith Set (Dominant Set). [ assistance in finding proof of the algorithm used would be appreciated so it could be correctly referenced in this documentation ]. A Dominant Set is a set which defeats all choices outside of that set. The inner Smith Set is the smallest possible Dominant Set.
492              
493             Returns a hashref with the keys as the choices of the Smith Set.
494              
495             =head2 ResetActive
496              
497             Reset Active list to the choices list of the BallotSet.
498              
499             =head2 GreatestLoss
500              
501             Returns the greatest loss for a choice C<<< $MyMatrix->GreatestLoss( $A ) >>>.
502              
503             =head2 RankGreatestLoss
504              
505             Returns a RankCount object of the Greatest Loss for each choice.
506              
507             =cut
508              
509             #FOOTER
510              
511             =pod
512              
513             BUG TRACKER
514              
515             L<https://github.com/brainbuz/Vote-Count/issues>
516              
517             AUTHOR
518              
519             John Karr (BRAINBUZ) brainbuz@cpan.org
520              
521             CONTRIBUTORS
522              
523             Copyright 2019-2021 by John Karr (BRAINBUZ) brainbuz@cpan.org.
524              
525             LICENSE
526              
527             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>.
528              
529             SUPPORT
530              
531             This software is provided as is, per the terms of the GNU Public License. Professional support and customisation services are available from the author.
532              
533             =cut
534