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   380 use strict;
  39         141  
  39         1187  
2 39     39   216 use warnings;
  39         88  
  39         1019  
3 39     39   780 use 5.024;
  39         141  
4 39     39   266 use feature qw /postderef signatures/;
  39         93  
  39         4734  
5              
6             use Moose;
7 39     39   288 use MooseX::StrictConstructor;
  39         105  
  39         290  
8 39     39   268566  
  39         66720  
  39         290  
9             with
10             'Vote::Count::Common',
11             'Vote::Count::Approval',
12             'Vote::Count::Borda',
13             'Vote::Count::Log',
14             'Vote::Count::Score',
15             'Vote::Count::TieBreaker',
16             ;
17              
18             use Vote::Count::RankCount;
19 39     39   162559  
  39         120  
  39         1497  
20             no warnings 'experimental';
21 39     39   293 use List::Util qw( min max sum );
  39         85  
  39         1614  
22 39     39   247 use Vote::Count::TextTableTiny qw/generate_table/;
  39         83  
  39         2454  
23 39     39   245 use Sort::Hash;
  39         87  
  39         1431  
24 39     39   218 use Storable 3.15 'dclone';
  39         83  
  39         1632  
25 39     39   17408  
  39         115210  
  39         2657  
26             # use Try::Tiny;
27             #use Data::Dumper;
28              
29             use YAML::XS;
30 39     39   11393  
  39         71657  
  39         142045  
31             our $VERSION='2.02';
32              
33             =head1 NAME
34              
35             Vote::Count::Matrix
36              
37             =head1 VERSION 2.02
38              
39             =cut
40              
41             # ABSTRACT: Condorcet Win Loss Matrix
42              
43             has BallotSet => (
44             is => 'ro',
45             required => 1,
46             isa => 'HashRef',
47             );
48              
49             has TieBreakMethod => (
50             is => 'rw',
51             isa => 'Str',
52             required => 0,
53             default => 'none',
54             );
55              
56             my @untie = $I->TieBreaker( $I->TieBreakMethod(), $I->Active(), $A, $B );
57 349     349   448 return $untie[0] if ( scalar(@untie) == 1 );
  349         459  
  349         480  
  349         456  
  349         458  
58 349         9729 return 0;
59 349 100       1085 }
60 172         683  
61             my $countA = 0;
62             my $countB = 0;
63 2617     2617   3261 FORVOTES:
  2617         3362  
  2617         3372  
  2617         3147  
  2617         3099  
64 2617         3379 for my $b ( keys $ballots->%* ) {
65 2617         3143 for my $v ( values $ballots->{$b}{'votes'}->@* ) {
66             if ( $v eq $A ) {
67 2617         11073 $countA += $ballots->{$b}{'count'} * $ballots->{$b}{'votevalue'};
68 57304         95307 next FORVOTES;
69 110289 100       205533 }
    100          
70 17961         26505 elsif ( $v eq $B ) {
71 17961         25424 $countB += $ballots->{$b}{'count'} * $ballots->{$b}{'votevalue'};
72             next FORVOTES;
73             }
74 18509         27565 }
75 18509         26131 } # FORVOTES
76             return ( $countA, $countB );
77             }
78              
79 2617         8485 my $ballots = $I->BallotSet()->{'ballots'};
80             my $countA = 0;
81             my $countB = 0;
82 2756     2756   8060 $I->logv("Pairing: $A vs $B");
  2756         3727  
  2756         3509  
  2756         3333  
  2756         3219  
83 2756         67032 if ( $I->BallotSet()->{'options'}{'range'} ) {
84 2756         4025 ( $countA, $countB ) = $I->RangeBallotPair( $A, $B );
85 2756         3375 }
86 2756         8803 else {
87 2756 100       64867 ( $countA, $countB ) = _pairwinner_rcv( $ballots, $A, $B );
88 139         301 }
89             my %retval = (
90             $A => $countA,
91 2617         4905 $B => $countB,
92             'tie' => 0,
93 2756         11840 'winner' => '',
94             'loser' => '',
95             'margin' => abs( $countA - $countB )
96             );
97             my $diff = $countA - $countB;
98             # 0 : $countA == $countB
99             if ( $diff == 0 ) {
100             my $untied = $I->_untie( $A, $B );
101 2756         4210 if ($untied) {
102             $diff = 1 if $untied eq $A;
103 2756 100       5275 $diff = -1 if $untied eq $B;
104 349         856 }
105 349 100       735 }
106 177 100       439 if ( $diff == 0 ) {
107 177 100       383 $retval{'winner'} = '';
108             $retval{'tie'} = 1;
109             }
110 2756 100       6442 # $diff > 0 A won or won tiebreaker.
    100          
    50          
111 172         311 elsif ( $diff > 0 ) {
112 172         263 $retval{'winner'} = $A;
113             $retval{'loser'} = $B;
114             }
115             # $diff < 0 B won or won tiebreaker.
116 1284         2040 elsif ( $diff < 0 ) {
117 1284         1886 $retval{'winner'} = $B;
118             $retval{'loser'} = $A;
119             }
120             if ( $retval{'winner'} ) {
121 1300         2038 $I->logv("Winner: $retval{'winner'} ($A: $countA $B: $countB)");
122 1300         1861 }
123             else { $I->logv("Tie $A: $countA $B: $countB") }
124 2756 100       4488 return \%retval;
125 2584         10046 }
126              
127 172         680 my $self = shift;
128 2756         7488 my $results = {};
129             my $ballotset = $self->BallotSet();
130             my @choices = keys $self->Active()->%*;
131             while ( scalar(@choices) ) {
132 88     88 0 195 my $A = shift @choices;
133 88         190 for my $B (@choices) {
134 88         2396 my $result = $self->_conduct_pair( $A, $B );
135 88         2385 # Each result has two hash keys so it can be found without
136 88         328 # having to try twice or sort the names for a single key.
137 682         1222 $results->{$A}{$B} = $result;
138 682         1296 $results->{$B}{$A} = $result;
139 2753         5101 }
140             }
141             $self->{'Matrix'} = $results;
142 2753         5609 $self->logt( "# Matrix", $self->MatrixTable() );
143 2753         6373 $self->logv( "# Pairing Results", $self->PairingVotesTable() );
144             }
145              
146 88         288 my $scores = {};
147 88         426 my %active = $self->Active()->%*;
148 88         682 for my $A ( keys %active ) {
149             my $hasties = 0;
150             $scores->{$A} = 0;
151 134     134 1 2163 for my $B ( keys %active ) {
  134         198  
  134         175  
152 134         253 next if $B eq $A;
153 134         3645 if ( $A eq $self->{'Matrix'}{$A}{$B}{'winner'} ) { $scores->{$A}++ }
154 134         506 if ( $self->{'Matrix'}{$A}{$B}{'tie'} ) { $hasties = .001 }
155 808         1064 }
156 808         1223 if ( $scores->{$A} == 0 ) { $scores->{$A} += $hasties }
157 808         1592 }
158 6048 100       9333 return $scores;
159 5240 100       9741 }
  2433         2959  
160 5240 100       9190  
  374         495  
161             # return the choice with fewest wins in matrix.
162 808 100       1778 my @lowest = ();
  139         291  
163             my %scored = $matrix->ScoreMatrix()->%*;
164 134         423 my $lowscore = min( values %scored );
165             for my $A ( keys %scored ) {
166             if ( $scored{$A} == $lowscore ) {
167             push @lowest, $A;
168 3     3 1 9 }
  3         5  
  3         5  
169 3         9 }
170 3         8 return @lowest;
171 3         19 }
172 3         12  
173 10 100       116 my $unfinished = 1;
174 8         19 my $wordy = "Removing Condorcet Losers\n";
175             my @eliminated = ();
176             my $loser = sub ( $score ) {
177 3         17 if ($nowins) { return 1 if $score < 1 }
178             else { return 1 if $score == 0 }
179             return 0;
180 17     17 1 2257 };
  17         30  
  17         35  
  17         27  
181 17         32 CONDORCETLOSERLOOP:
182 17         40 while ($unfinished) {
183 17         32 $unfinished = 0;
184 199     199   227 my $scores = $self->ScoreMatrix;
  199         253  
  199         227  
185 199 100       307 my @alist = ( keys $self->Active()->%* );
  39 100       68  
186 160 100       312 # Check that tied choices at the top won't be
187 156         337 # eliminated. alist is looped over twice because we
188 17         83 # don't want to report the scores when the list is
189             # reduced to either a condorcet winner or tied situation.
190 17         47 for my $A (@alist) {
191 60         83 unless ( max( values $scores->%* ) ) {
192 60         129 last CONDORCETLOSERLOOP;
193 60         1534 }
194             }
195             $wordy .= YAML::XS::Dump($scores);
196             for my $A (@alist) {
197             if ( $loser->( $scores->{$A} ) ) {
198 60         127 push @eliminated, ($A);
199 313 100       754 $wordy .= "Eliminationg Condorcet Loser: *$A*\n";
200 2         7 delete $self->{'Active'}{$A};
201             $unfinished = 1;
202             next CONDORCETLOSERLOOP;
203 58         2398 }
204 58         248 }
205 199 100       332 }
206 43         433 my $elimstr =
207 43         105 scalar(@eliminated)
208 43         90 ? "Eliminated Condorcet Losers: " . join( ', ', @eliminated ) . "\n"
209 43         58 : "No Condorcet Losers Eliminated\n";
210 43         157 return {
211             verbose => $wordy,
212             terse => $elimstr,
213             eliminated => \@eliminated,
214 17 100       80 eliminations => scalar(@eliminated),
215             };
216             }
217              
218             my $scores = $self->ScoreMatrix;
219 17         235 my @choices = keys $scores->%*;
220             # # if there is only one choice left they win.
221             # if ( scalar(@choices) == 1 ) { return $choices[0]}
222             my $mustwin = scalar(@choices) - 1;
223             my $winner = '';
224             for my $c (@choices) {
225             if ( $scores->{$c} == $mustwin ) {
226 65     65 1 2415 $winner .= $c;
  65         119  
  65         99  
227 65         211 }
228 65         256 }
229             return $winner;
230             }
231 65         160  
232 65         141 my $bigloss = 0;
233 65         152 GREATESTLOSSLOOP:
234 434 100       823 for my $B ( $self->GetActiveList() ) {
235 32         92 # for my $B ( keys $self->Active()->%* ) {
236             next GREATESTLOSSLOOP if $B eq $A;
237             my %result = $self->{'Matrix'}{$A}{$B}->%*;
238 65         405 # warn "$A : $B loser $result{'loser'} : margin $result{'margin'} $A: $result{$A} $B: $result{$B}";
239             if ( $result{'loser'} eq $A ) {
240             $bigloss = $result{'margin'} if $result{'margin'} > $bigloss;
241 26     26 1 33 }
  26         37  
  26         41  
  26         33  
242 26         34 }
243             return $bigloss;
244 26         65 }
245              
246 290 100       469 my %loss = ();
247 264         862 $active = $self->Active() unless defined $active;
248             for my $A ( keys $active->%* ) {
249 264 100       580 $loss{$A} = $self->GreatestLoss($A);
250 111 100       267 }
251             return Vote::Count::RankCount->Rank( \%loss );
252             }
253 26         77  
254             # reset active to choices
255             $self->{'Active'} = dclone $self->BallotSet->{'choices'};
256 2     2 1 2424 }
  2         5  
  2         5  
  2         4  
257 2         6  
258 2 100       38 my @winners = ($h);
259 2         13 for my $P ( keys $matrix->{$h}->%* ) {
260 21         43 if ( $matrix->{$h}{$P}{'winner'} eq $P ) {
261             push @winners, ($P);
262 2         24 }
263             elsif ( $matrix->{$h}{$P}{'tie'} ) {
264             push @winners, ($P);
265             }
266 2     2 1 4 }
  2         2  
  2         5  
267 2         62 return ( map { $_ => 1 } @winners );
268             }
269              
270 74     74   3719 return $self->{'Matrix'}{$A}{$B};
  74         98  
  74         90  
  74         86  
271 74         118 }
272 74         215  
273 561 100       1153 my $winner = $self->{'Matrix'}{$A}{$B}{'winner'};
    100          
274 258         384 return $winner if $winner;
275             return '';
276             }
277 35         59  
278             my $matrix = $self->{'Matrix'};
279             my @alist = ( keys $self->Active()->%* );
280 74         137 my $sets = {};
  367         656  
281             my $setcounts = {};
282             # my $shortest = scalar(@list);
283 43     43 1 1857 for my $h (@alist) {
  43         70  
  43         68  
  43         63  
  43         56  
284 43         160 my %set = Vote::Count::Matrix::_getsmithguessforchoice( $h, $matrix );
285             $sets->{$h} = \%set;
286             # the keys of setcounts are the counts
287 14     14 1 3040 $setcounts->{ scalar( keys %set ) }{$h} = 1;
  14         26  
  14         27  
  14         24  
  14         21  
288 14         51 }
289 14 100       103 my $proposal = {};
290 2         42 my $minset = min( keys( $setcounts->%* ) );
291             for my $h ( keys $setcounts->{$minset}->%* ) {
292             for my $k ( keys( $sets->{$h}->%* ) ) {
293 10     10 1 3892 $proposal->{$k} = 1;
  10         27  
  10         18  
294 10         25 }
295 10         297 }
296 10         29 SMITHLOOP: while (1) {
297 10         23 my $cntchoice = scalar( keys $proposal->%* );
298             for my $h ( keys $proposal->%* ) {
299 10         27 $proposal = { %{$proposal}, %{ $sets->{$h} } };
300 72         142 }
301 72         141 # done when no choices get added on a pass through loop
302             if ( scalar( keys $proposal->%* ) == $cntchoice ) {
303 72         251 last SMITHLOOP;
304             }
305 10         23 }
306 10         89 return $proposal;
307 10         41 }
308 18         43  
309 34         63 # ScoreMatrix as a table.
310             my $scores = $self->ScoreMatrix();
311             my @header = ( 'Choice', 'Score' );
312 10         21 my @rows = ( \@header );
313 15         32 for my $c ( sort_hash( $scores, 'numeric', 'desc' ) ) {
314 15         34 # for my $c ( sort ( keys $scores->%* ) ) {
315 46         60 push @rows, [ $c, $scores->{$c} ];
  46         88  
  46         159  
316             }
317             return generate_table( rows => \@rows );
318 15 100       57 }
319 10         26  
320             my @header = ( 'Choice', 'Wins', 'Losses', 'Ties' );
321             # the options option was never fully implemented, it shows what the
322 10         92 # structure would be if one were or if I finished the feature.
323             # leaving the code in place even though its useless.
324             my $o_topcount =
325             defined $options->{'topcount'} ? $options->{'topcount'} : 0;
326 1     1 1 1760 push @header, 'Top Count' if $o_topcount;
  1         2  
  1         1  
327 1         4 my @active = sort ( keys $self->Active()->%* );
328 1         4 my @rows = ( \@header ); # [ 'Rank', 'Choice', 'TopCount']);
329 1         3 for my $A (@active) {
330 1         6 my $wins = 0;
331             my $ties = 0;
332 12         249 my $losses = 0;
333             my $topcount = $o_topcount ? $options->{'topcount'} : 0;
334 1         6 MTNEWROW:
335             for my $B (@active) {
336             if ( $A eq $B ) { next MTNEWROW }
337 119     119 1 218 elsif ( $self->{'Matrix'}{$A}{$B}{'winner'} eq $A ) {
  119         212  
  119         276  
  119         205  
338 119         435 $wins++;
339             }
340             elsif ( $self->{'Matrix'}{$A}{$B}{'winner'} eq $B ) {
341             $losses++;
342             }
343 119 50       396 elsif ( $self->{'Matrix'}{$A}{$B}{'tie'} ) {
344 119 50       308 $ties++;
345 119         3241 }
346 119         416 }
347 119         308 my @newrow = ( $A, $wins, $losses, $ties );
348 858         1090 push @newrow, $topcount if $o_topcount;
349 858         1031 push @rows, \@newrow;
350 858         1027 }
351 858 50       1344 return generate_table( rows => \@rows );
352             }
353 858         1175  
354 7390 100       15534 my @rows = ( [qw/Choice Choice Votes Opponent Votes/] );
  858 100       1276  
    100          
    50          
355             my @choices = sort ( keys $self->Active()->%* );
356 3065         3863 for my $Choice (@choices) {
357             push @rows, [$Choice];
358             for my $Opponent (@choices) {
359 3065         4042 my $Cstr = $Choice;
360             my $Ostr = $Opponent;
361             next if $Opponent eq $Choice;
362 402         573 my $CVote = $self->{'Matrix'}{$Choice}{$Opponent}{$Choice};
363             my $OVote = $self->{'Matrix'}{$Choice}{$Opponent}{$Opponent};
364             if ( $self->{'Matrix'}{$Choice}{$Opponent}{'winner'} eq $Choice ) {
365 858         1782 $Cstr = "**$Cstr**";
366 858 50       1457 }
367 858         1771 if ( $self->{'Matrix'}{$Choice}{$Opponent}{'winner'} eq $Opponent ) {
368             $Ostr = "**$Ostr**";
369 119         605 }
370             push @rows, [ ' ', $Cstr, $CVote, $Ostr, $OVote ];
371             }
372 99     99 1 202 }
  99         190  
  99         149  
373 99         444 return generate_table( rows => \@rows );
374 99         2710 }
375 99         416  
376 752         1400 __PACKAGE__->meta->make_immutable;
377 752         1174 1;
378 6694         8539  
379 6694         7623 =pod
380 6694 100       10727  
381 5942         9035 =head1 Win-Loss Matrix
382 5942         8088  
383 5942 100       10223 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.
384 2773         4372  
385             =head1 SYNOPSIS
386 5942 100       10355  
387 2773         4760 my $Matrix =
388             Vote::Count::Matrix->new(
389 5942         15405 'BallotSet' => $myVoteCount->BallotSet() );
390             my $Scores = $Matrix->ScoreMatrix();
391             my %DominantSet = $Matrix->SmithSet()->%*;
392 99         415 my $CondorcetWinner = $Matrix->CondorcetWinner();
393              
394             =head1 Tie Breakers
395              
396             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.
397              
398             =head2 new
399              
400             Parameters:
401              
402             =head3 BallotSet (required)
403              
404             A Ballot Set reference as generated by ReadBallots, which can be retrieved from a Vote::Count object via the ->BallotSet() method.
405              
406             Both Ranked Choice and Range BallotSets are supported.
407              
408             =head3 Active (optional)
409              
410             A hash reference with active choices as the keys. The default value is all of the choices defined in the BallotSet.
411              
412             =head3 Logging (optional)
413              
414             Has the logging methods of L.
415              
416             =head1 Methods
417              
418             =head2 MatrixTable
419              
420             Returns a MarkDown formatted table with the wins losses and ties for each Active Choice as text.
421              
422             =head2 PairingVotesTable
423              
424             Returns a MarkDown formatted table with the votes for all of the pairings.
425              
426             =head2 GetPairResult ( $A, $B )
427              
428             Returns the results of the pairing of two choices as a hashref.
429              
430             {
431             'FUDGESWIRL' => 6,
432             'loser' => "STRAWBERRY",
433             'margin' => 2,
434             'STRAWBERRY' => 4,
435             'tie' => 0,
436             'winner' => "FUDGESWIRL"
437             }
438              
439             =head2 GetPairWinner ( $A, $B )
440              
441             Returns the winner of the pairing of two choices. If there is no Winner it returns a false value (empty string).
442              
443             =head2 ScoreMatrix
444              
445             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.
446              
447             =head2 ScoreTable
448              
449             Returns the ScoreMatrix as a markdown compatible table.
450              
451             =head2 LeastWins
452              
453             Returns an array of the choice or choices with the fewest wins.
454              
455             =head2 CondorcetLoser
456              
457             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.
458              
459             {
460             verbose => 'verbose message',
461             terse => 'terse message',
462             eliminated => [ eliminated choices ],
463             eliminations => number of eliminated choices,
464             };
465              
466             =head2 CondorcetWinner
467              
468             Returns either the Condorcet Winner or an empty string if there is none.
469              
470             =head2 SmithSet
471              
472             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.
473              
474             Returns a hashref with the keys as the choices of the Smith Set.
475              
476             =head2 ResetActive
477              
478             Reset Active list to the choices list of the BallotSet.
479              
480             =head2 GreatestLoss
481              
482             Returns the greatest loss for a choice C<<< $MyMatrix->GreatestLoss( $A ) >>>.
483              
484             =head2 RankGreatestLoss
485              
486             Returns a RankCount object of the Greatest Loss for each choice.
487              
488             =cut
489              
490             #FOOTER
491              
492             =pod
493              
494             BUG TRACKER
495              
496             L<https://github.com/brainbuz/Vote-Count/issues>
497              
498             AUTHOR
499              
500             John Karr (BRAINBUZ) brainbuz@cpan.org
501              
502             CONTRIBUTORS
503              
504             Copyright 2019-2021 by John Karr (BRAINBUZ) brainbuz@cpan.org.
505              
506             LICENSE
507              
508             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>.
509              
510             SUPPORT
511              
512             This software is provided as is, per the terms of the GNU Public License. Professional support and customisation services are available from the author.
513              
514             =cut
515