File Coverage

blib/lib/Vote/Count/Matrix.pm
Criterion Covered Total %
statement 296 297 99.6
branch 79 86 91.8
condition n/a
subroutine 31 31 100.0
pod 13 14 92.8
total 419 428 97.9


line stmt bran cond sub pod time code
1 39     39   298 use strict;
  39         97  
  39         1285  
2 39     39   269 use warnings;
  39         97  
  39         1133  
3 39     39   799 use 5.024;
  39         143  
4 39     39   299 use feature qw /postderef signatures/;
  39         99  
  39         5053  
5              
6             package Vote::Count::Matrix;
7 39     39   314 use Moose;
  39         97  
  39         331  
8              
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 39     39   288929 use Vote::Count::RankCount;
  39         115  
  39         1545  
19              
20 39     39   305 no warnings 'experimental';
  39         82  
  39         1671  
21 39     39   268 use List::Util qw( min max sum );
  39         86  
  39         2668  
22 39     39   251 use Vote::Count::TextTableTiny qw/generate_table/;
  39         79  
  39         1607  
23 39     39   228 use Sort::Hash;
  39         84  
  39         1834  
24 39     39   24909 use Storable 3.15 'dclone';
  39         116406  
  39         2714  
25              
26             # use Try::Tiny;
27             #use Data::Dumper;
28              
29 39     39   13210 use YAML::XS;
  39         74555  
  39         146575  
30              
31             our $VERSION='2.00';
32              
33             =head1 NAME
34              
35             Vote::Count::Matrix
36              
37             =head1 VERSION 2.00
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 193     193   285 sub _untie ( $I, $A, $B ) {
  193         284  
  193         259  
  193         258  
  193         257  
57 193         5638 my @untie = $I->TieBreaker( $I->TieBreakMethod(), $I->Active(), $A, $B );
58 193 100       537 return $untie[0] if ( scalar(@untie) == 1 );
59 158         320 return 0;
60             }
61              
62 1985     1985   2770 sub _pairwinner_rcv ( $ballots, $A, $B ) {
  1985         2653  
  1985         2715  
  1985         2622  
  1985         2497  
63 1985         2723 my $countA = 0;
64 1985         2544 my $countB = 0;
65             FORVOTES:
66 1985         10515 for my $b ( keys $ballots->%* ) {
67 50552         96222 for my $v ( values $ballots->{$b}{'votes'}->@* ) {
68 98355 100       188701 if ( $v eq $A ) {
    100          
69 16737         25664 $countA += $ballots->{$b}{'count'} * $ballots->{$b}{'votevalue'};
70 16737         22931 next FORVOTES;
71             }
72             elsif ( $v eq $B ) {
73 17580         27157 $countB += $ballots->{$b}{'count'} * $ballots->{$b}{'votevalue'};
74 17580         24206 next FORVOTES;
75             }
76             }
77             } # FORVOTES
78 1985         7935 return ( $countA, $countB );
79             }
80              
81 2118     2118   8050 sub _conduct_pair ( $I, $A, $B ) {
  2118         3025  
  2118         3175  
  2118         2826  
  2118         2660  
82 2118         53574 my $ballots = $I->BallotSet()->{'ballots'};
83 2118         3357 my $countA = 0;
84 2118         2825 my $countB = 0;
85 2118         7094 $I->logv("Pairing: $A vs $B");
86 2118 100       51684 if ( $I->BallotSet()->{'options'}{'range'} ) {
87 133         379 ( $countA, $countB ) = $I->RangeBallotPair( $A, $B );
88             }
89             else {
90 1985         4082 ( $countA, $countB ) = _pairwinner_rcv( $ballots, $A, $B );
91             }
92 2118         10106 my %retval = (
93             $A => $countA,
94             $B => $countB,
95             'tie' => 0,
96             'winner' => '',
97             'loser' => '',
98             'margin' => abs( $countA - $countB )
99             );
100 2118         3464 my $diff = $countA - $countB;
101             # 0 : $countA == $countB
102 2118 100       4331 if ( $diff == 0 ) {
103 193         544 my $untied = $I->_untie( $A, $B );
104 193 100       463 if ($untied) {
105 35 100       84 $diff = 1 if $untied eq $A;
106 35 100       94 $diff = -1 if $untied eq $B;
107             }
108             }
109 2118 100       5354 if ( $diff == 0 ) {
    100          
    50          
110 158         293 $retval{'winner'} = '';
111 158         267 $retval{'tie'} = 1;
112             }
113             # $diff > 0 A won or won tiebreaker.
114             elsif ( $diff > 0 ) {
115 981         1621 $retval{'winner'} = $A;
116 981         1467 $retval{'loser'} = $B;
117             }
118             # $diff < 0 B won or won tiebreaker.
119             elsif ( $diff < 0 ) {
120 979         1603 $retval{'winner'} = $B;
121 979         1641 $retval{'loser'} = $A;
122             }
123 2118 100       3829 if ( $retval{'winner'} ) {
124 1960         8640 $I->logv("Winner: $retval{'winner'} ($A: $countA $B: $countB)");
125             }
126 158         619 else { $I->logv("Tie $A: $countA $B: $countB") }
127 2118         5187 return \%retval;
128             }
129              
130             sub BUILD {
131 76     76 0 340276 my $self = shift;
132 76         190 my $results = {};
133 76         2562 my $ballotset = $self->BallotSet();
134 76         2110 my @choices = keys $self->Active()->%*;
135 76         276 while ( scalar(@choices) ) {
136 557         1011 my $A = shift @choices;
137 557         1136 for my $B (@choices) {
138 2115         4458 my $result = $self->_conduct_pair( $A, $B );
139             # Each result has two hash keys so it can be found without
140             # having to try twice or sort the names for a single key.
141 2115         4500 $results->{$A}{$B} = $result;
142 2115         5201 $results->{$B}{$A} = $result;
143             }
144             }
145 76         202 $self->{'Matrix'} = $results;
146 76         289 $self->logt( "# Matrix", $self->MatrixTable() );
147 76         526 $self->logv( "# Pairing Results", $self->PairingVotesTable() );
148             }
149              
150 134     134 1 2633 sub ScoreMatrix ( $self ) {
  134         213  
  134         187  
151 134         257 my $scores = {};
152 134         3820 my %active = $self->Active()->%*;
153 134         492 for my $A ( keys %active ) {
154 808         1059 my $hasties = 0;
155 808         1187 $scores->{$A} = 0;
156 808         1784 for my $B ( keys %active ) {
157 6048 100       9636 next if $B eq $A;
158 5240 100       9784 if ( $A eq $self->{'Matrix'}{$A}{$B}{'winner'} ) { $scores->{$A}++ }
  2434         3032  
159 5240 100       10080 if ( $self->{'Matrix'}{$A}{$B}{'tie'} ) { $hasties = .001 }
  372         519  
160             }
161 808 100       1778 if ( $scores->{$A} == 0 ) { $scores->{$A} += $hasties }
  140         287  
162             }
163 134         393 return $scores;
164             }
165              
166             # return the choice with fewest wins in matrix.
167 3     3 1 5 sub LeastWins ( $matrix ) {
  3         7  
  3         6  
168 3         6 my @lowest = ();
169 3         12 my %scored = $matrix->ScoreMatrix()->%*;
170 3         16 my $lowscore = min( values %scored );
171 3         10 for my $A ( keys %scored ) {
172 10 100       23 if ( $scored{$A} == $lowscore ) {
173 8         15 push @lowest, $A;
174             }
175             }
176 3         13 return @lowest;
177             }
178              
179 17     17 1 2824 sub CondorcetLoser ( $self, $nowins = 0 ) {
  17         139  
  17         40  
  17         29  
180 17         37 my $unfinished = 1;
181 17         31 my $wordy = "Removing Condorcet Losers\n";
182 17         35 my @eliminated = ();
183 183     183   215 my $loser = sub ( $score ) {
  183         221  
  183         240  
184 183 100       284 if ($nowins) { return 1 if $score < 1 }
  41 100       83  
185 142 100       279 else { return 1 if $score == 0 }
186 140         323 return 0;
187 17         90 };
188             CONDORCETLOSERLOOP:
189 17         52 while ($unfinished) {
190 60         90 $unfinished = 0;
191 60         113 my $scores = $self->ScoreMatrix;
192 60         1597 my @alist = ( keys $self->Active()->%* );
193             # Check that tied choices at the top won't be
194             # eliminated. alist is looped over twice because we
195             # don't want to report the scores when the list is
196             # reduced to either a condorcet winner or tied situation.
197 60         119 for my $A (@alist) {
198 313 100       789 unless ( max( values $scores->%* ) ) {
199 2         6 last CONDORCETLOSERLOOP;
200             }
201             }
202 58         2375 $wordy .= YAML::XS::Dump($scores);
203 58         265 for my $A (@alist) {
204 183 100       317 if ( $loser->( $scores->{$A} ) ) {
205 43         123 push @eliminated, ($A);
206 43         104 $wordy .= "Eliminationg Condorcet Loser: *$A*\n";
207 43         87 delete $self->{'Active'}{$A};
208 43         66 $unfinished = 1;
209 43         167 next CONDORCETLOSERLOOP;
210             }
211             }
212             }
213 17 100       76 my $elimstr =
214             scalar(@eliminated)
215             ? "Eliminated Condorcet Losers: " . join( ', ', @eliminated ) . "\n"
216             : "No Condorcet Losers Eliminated\n";
217             return {
218 17         193 verbose => $wordy,
219             terse => $elimstr,
220             eliminated => \@eliminated,
221             eliminations => scalar(@eliminated),
222             };
223             }
224              
225 65     65 1 2874 sub CondorcetWinner( $self ) {
  65         129  
  65         115  
226 65         217 my $scores = $self->ScoreMatrix;
227 65         257 my @choices = keys $scores->%*;
228             # # if there is only one choice left they win.
229             # if ( scalar(@choices) == 1 ) { return $choices[0]}
230 65         173 my $mustwin = scalar(@choices) - 1;
231 65         147 my $winner = '';
232 65         156 for my $c (@choices) {
233 434 100       929 if ( $scores->{$c} == $mustwin ) {
234 32         84 $winner .= $c;
235             }
236             }
237 65         411 return $winner;
238             }
239              
240 26     26 1 40 sub GreatestLoss ( $self, $A ) {
  26         41  
  26         39  
  26         35  
241 26         132 my $bigloss = 0;
242             GREATESTLOSSLOOP:
243 26         82 for my $B ( $self->GetActiveList() ) {
244             # for my $B ( keys $self->Active()->%* ) {
245 290 100       509 next GREATESTLOSSLOOP if $B eq $A;
246 264         945 my %result = $self->{'Matrix'}{$A}{$B}->%*;
247             # warn "$A : $B loser $result{'loser'} : margin $result{'margin'} $A: $result{$A} $B: $result{$B}";
248 264 100       626 if ( $result{'loser'} eq $A ) {
249 111 100       277 $bigloss = $result{'margin'} if $result{'margin'} > $bigloss;
250             }
251             }
252 26         85 return $bigloss;
253             }
254              
255 2     2 1 2990 sub RankGreatestLoss ( $self, $active = undef ) {
  2         6  
  2         6  
  2         6  
256 2         5 my %loss = ();
257 2 100       44 $active = $self->Active() unless defined $active;
258 2         14 for my $A ( keys $active->%* ) {
259 21         47 $loss{$A} = $self->GreatestLoss($A);
260             }
261 2         24 return Vote::Count::RankCount->Rank( \%loss );
262             }
263              
264             # reset active to choices
265 2     2 1 4 sub ResetActive ( $self ) {
  2         4  
  2         2  
266 2         72 $self->{'Active'} = dclone $self->BallotSet->{'choices'};
267             }
268              
269 74     74   4586 sub _getsmithguessforchoice ( $h, $matrix ) {
  74         106  
  74         90  
  74         94  
270 74         125 my @winners = ($h);
271 74         247 for my $P ( keys $matrix->{$h}->%* ) {
272 561 100       1191 if ( $matrix->{$h}{$P}{'winner'} eq $P ) {
    100          
273 258         462 push @winners, ($P);
274             }
275             elsif ( $matrix->{$h}{$P}{'tie'} ) {
276 35         52 push @winners, ($P);
277             }
278             }
279 74         141 return ( map { $_ => 1 } @winners );
  367         663  
280             }
281              
282 5     5 1 2226 sub GetPairResult ( $self, $A, $B ) {
  5         11  
  5         12  
  5         11  
  5         10  
283 5         29 return $self->{'Matrix'}{$A}{$B};
284             }
285              
286 2     2 1 3736 sub GetPairWinner ( $self, $A, $B ) {
  2         5  
  2         4  
  2         4  
  2         2  
287 2         8 my $winner = $self->{'Matrix'}{$A}{$B}{'winner'};
288 2 50       14 return $winner if $winner;
289 0         0 return '';
290             }
291              
292 10     10 1 4896 sub SmithSet ( $self ) {
  10         21  
  10         17  
293 10         25 my $matrix = $self->{'Matrix'};
294 10         321 my @alist = ( keys $self->Active()->%* );
295 10         42 my $sets = {};
296 10         24 my $setcounts = {};
297             # my $shortest = scalar(@list);
298 10         33 for my $h (@alist) {
299 72         132 my %set = Vote::Count::Matrix::_getsmithguessforchoice( $h, $matrix );
300 72         154 $sets->{$h} = \%set;
301             # the keys of setcounts are the counts
302 72         260 $setcounts->{ scalar( keys %set ) }{$h} = 1;
303             }
304 10         24 my $proposal = {};
305 10         95 my $minset = min( keys( $setcounts->%* ) );
306 10         40 for my $h ( keys $setcounts->{$minset}->%* ) {
307 18         42 for my $k ( keys( $sets->{$h}->%* ) ) {
308 34         71 $proposal->{$k} = 1;
309             }
310             }
311 10         22 SMITHLOOP: while (1) {
312 15         35 my $cntchoice = scalar( keys $proposal->%* );
313 15         38 for my $h ( keys $proposal->%* ) {
314 46         61 $proposal = { %{$proposal}, %{ $sets->{$h} } };
  46         89  
  46         163  
315             }
316             # done when no choices get added on a pass through loop
317 15 100       51 if ( scalar( keys $proposal->%* ) == $cntchoice ) {
318 10         30 last SMITHLOOP;
319             }
320             }
321 10         97 return $proposal;
322             }
323              
324             # ScoreMatrix as a table.
325 1     1 1 3272 sub ScoreTable ( $self ) {
  1         3  
  1         3  
326 1         3 my $scores = $self->ScoreMatrix();
327 1         7 my @header = ( 'Choice', 'Score' );
328 1         3 my @rows = ( \@header );
329 1         9 for my $c ( sort_hash( $scores, 'numeric', 'desc' ) ) {
330             # for my $c ( sort ( keys $scores->%* ) ) {
331 12         244 push @rows, [ $c, $scores->{$c} ];
332             }
333 1         7 return generate_table( rows => \@rows );
334             }
335              
336 107     107 1 207 sub MatrixTable ( $self, $options = {} ) {
  107         207  
  107         256  
  107         177  
337 107         396 my @header = ( 'Choice', 'Wins', 'Losses', 'Ties' );
338             # the options option was never fully implemented, it shows what the
339             # structure would be if one were or if I finished the feature.
340             # leaving the code in place even though its useless.
341             my $o_topcount =
342 107 50       371 defined $options->{'topcount'} ? $options->{'topcount'} : 0;
343 107 50       288 push @header, 'Top Count' if $o_topcount;
344 107         3028 my @active = sort ( keys $self->Active()->%* );
345 107         368 my @rows = ( \@header ); # [ 'Rank', 'Choice', 'TopCount']);
346 107         259 for my $A (@active) {
347 733         993 my $wins = 0;
348 733         927 my $ties = 0;
349 733         951 my $losses = 0;
350 733 50       1145 my $topcount = $o_topcount ? $options->{'topcount'} : 0;
351             MTNEWROW:
352 733         1083 for my $B (@active) {
353 5989 100       13105 if ( $A eq $B ) { next MTNEWROW }
  733 100       1106  
    100          
    50          
354             elsif ( $self->{'Matrix'}{$A}{$B}{'winner'} eq $A ) {
355 2441         3191 $wins++;
356             }
357             elsif ( $self->{'Matrix'}{$A}{$B}{'winner'} eq $B ) {
358 2441         3267 $losses++;
359             }
360             elsif ( $self->{'Matrix'}{$A}{$B}{'tie'} ) {
361 374         533 $ties++;
362             }
363             }
364 733         1513 my @newrow = ( $A, $wins, $losses, $ties );
365 733 50       1289 push @newrow, $topcount if $o_topcount;
366 733         1586 push @rows, \@newrow;
367             }
368 107         502 return generate_table( rows => \@rows );
369             }
370              
371 87     87 1 186 sub PairingVotesTable ( $self ) {
  87         164  
  87         153  
372 87         402 my @rows = ( [qw/Choice Choice Votes Opponent Votes/] );
373 87         2345 my @choices = sort ( keys $self->Active()->%* );
374 87         304 for my $Choice (@choices) {
375 627         1189 push @rows, [$Choice];
376 627         981 for my $Opponent (@choices) {
377 5293         7474 my $Cstr = $Choice;
378 5293         6337 my $Ostr = $Opponent;
379 5293 100       8628 next if $Opponent eq $Choice;
380 4666         7359 my $CVote = $self->{'Matrix'}{$Choice}{$Opponent}{$Choice};
381 4666         6975 my $OVote = $self->{'Matrix'}{$Choice}{$Opponent}{$Opponent};
382 4666 100       8173 if ( $self->{'Matrix'}{$Choice}{$Opponent}{'winner'} eq $Choice ) {
383 2149         3507 $Cstr = "**$Cstr**";
384             }
385 4666 100       8304 if ( $self->{'Matrix'}{$Choice}{$Opponent}{'winner'} eq $Opponent ) {
386 2149         3572 $Ostr = "**$Ostr**";
387             }
388 4666         12343 push @rows, [ ' ', $Cstr, $CVote, $Ostr, $OVote ];
389             }
390             }
391 87         357 return generate_table( rows => \@rows );
392             }
393              
394             1;
395              
396             =pod
397              
398             =head1 Win-Loss Matrix
399              
400             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.
401              
402             =head1 SYNOPSIS
403              
404             my $Matrix =
405             Vote::Count::Matrix->new(
406             'BallotSet' => $myVoteCount->BallotSet() );
407             my $Scores = $Matrix->ScoreMatrix();
408             my %DominantSet = $Matrix->SmithSet()->%*;
409             my $CondorcetWinner = $Matrix->CondorcetWinner();
410              
411             =head1 Tie Breakers
412              
413             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.
414              
415             =head2 new
416              
417             Parameters:
418              
419             =head3 BallotSet (required)
420              
421             A Ballot Set reference as generated by ReadBallots, which can be retrieved from a Vote::Count object via the ->BallotSet() method.
422              
423             Both Ranked Choice and Range BallotSets are supported.
424              
425             =head3 Active (optional)
426              
427             A hash reference with active choices as the keys. The default value is all of the choices defined in the BallotSet.
428              
429             =head3 Logging (optional)
430              
431             Has the logging methods of L.
432              
433             =head1 Methods
434              
435             =head2 MatrixTable
436              
437             Returns a MarkDown formatted table with the wins losses and ties for each Active Choice as text.
438              
439             =head2 PairingVotesTable
440              
441             Returns a MarkDown formatted table with the votes for all of the pairings.
442              
443             =head2 GetPairResult ( $A, $B )
444              
445             Returns the results of the pairing of two choices as a hashref.
446              
447             {
448             'FUDGESWIRL' => 6,
449             'loser' => "STRAWBERRY",
450             'margin' => 2,
451             'STRAWBERRY' => 4,
452             'tie' => 0,
453             'winner' => "FUDGESWIRL"
454             }
455              
456             =head2 GetPairWinner ( $A, $B )
457              
458             Returns the winner of the pairing of two choices. If there is no Winner it returns a false value (empty string).
459              
460             =head2 ScoreMatrix
461              
462             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.
463              
464             =head2 ScoreTable
465              
466             Returns the ScoreMatrix as a markdown compatible table.
467              
468             =head2 LeastWins
469              
470             Returns an array of the choice or choices with the fewest wins.
471              
472             =head2 CondorcetLoser
473              
474             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.
475              
476             {
477             verbose => 'verbose message',
478             terse => 'terse message',
479             eliminated => [ eliminated choices ],
480             eliminations => number of eliminated choices,
481             };
482              
483             =head2 CondorcetWinner
484              
485             Returns either the Condorcet Winner or an empty string if there is none.
486              
487             =head2 SmithSet
488              
489             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.
490              
491             Returns a hashref with the keys as the choices of the Smith Set.
492              
493             =head2 ResetActive
494              
495             Reset Active list to the choices list of the BallotSet.
496              
497             =head2 GreatestLoss
498              
499             Returns the greatest loss for a choice C<<< $MyMatrix->GreatestLoss( $A ) >>>.
500              
501             =head2 RankGreatestLoss
502              
503             Returns a RankCount object of the Greatest Loss for each choice.
504              
505             =cut
506              
507             #FOOTER
508              
509             =pod
510              
511             BUG TRACKER
512              
513             L<https://github.com/brainbuz/Vote-Count/issues>
514              
515             AUTHOR
516              
517             John Karr (BRAINBUZ) brainbuz@cpan.org
518              
519             CONTRIBUTORS
520              
521             Copyright 2019-2021 by John Karr (BRAINBUZ) brainbuz@cpan.org.
522              
523             LICENSE
524              
525             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>.
526              
527             SUPPORT
528              
529             This software is provided as is, per the terms of the GNU Public License. Professional support and customisation services are available from the author.
530              
531             =cut
532