/[drupal]/contributions/modules/decisions/modes/ranking.module
ViewVC logotype

Contents of /contributions/modules/decisions/modes/ranking.module

Parent Directory Parent Directory | Revision Log Revision Log | View Revision Graph Revision Graph


Revision 1.24 - (show annotations) (download) (as text)
Mon Oct 26 20:49:06 2009 UTC (4 weeks ago) by anarcat
Branch: MAIN
CVS Tags: HEAD
Changes since 1.23: +2 -2 lines
File MIME type: text/x-php
#537804 - fix the explanation of the condorcet calculations
1 <?php
2
3 /**
4 * @file
5 *
6 * This file implements a ranking mode, where n options are ranked
7 * from 1 to n
8 */
9
10 // $Id: ranking.module,v 1.23 2009/10/26 20:41:27 anarcat Exp $
11
12 /**
13 * Implementation of hook_help()
14 */
15 function ranking_help($section) {
16 $help = '';
17 switch ($section) {
18 case 'admin/modules#description':
19 break;
20 default:
21 if ($section == 'node/add#decisions-ranking') {
22 $help = t('Creates a vote where n options are ranked from 1 to n.');
23 }
24 break;
25 }
26 return $help;
27 }
28
29 function ranking_perm() { return decisions_perm(); }
30 function ranking_acces($op, $node, $account) { return decisions_access($op, $node, $account); }
31 function ranking_form(&$node) { return decisions_form($node); }
32
33 function ranking_node_info() {
34 $info = array();
35 $info['decisions_ranking'] = array(
36 'name' => 'Decisions - ranking',
37 'module' => 'decisions',
38 'description' => 'Creates a vote where n options are ranked from 1 to n.',
39 'title_label' => t('Ranking decision'),
40 'body_label' => t('Description'),
41 );
42 return $info;
43 }
44
45 /**
46 * Implementation of the decisions_algorithms() hook
47 */
48 function ranking_decisions_algorithms() {
49 return array('instant runoff' => t('Instant run-off voting, also known as IVR or Alternative Voting, is an algorithm by which the candidate having the least ballots gets its votes redistributed to the other candidates. See <a href="http://en.wikipedia.org/wiki/Instant-runoff_voting">Instant runoff voting on Wikipedia</a> for more information.'),
50 'borda count' => t('Borda count is an algorithm by which each candidate gets a number of points assigned based on the number of candidates standing. See the <a href="http://en.wikipedia.org/wiki/Borda_count">Borda count article on Wikipedia</a> for more information.'),
51 'condorcet' => t('Condorcet finds the one candidate who would beat all other candidates in all possible two-person races. In this implementation, ties are broken using Schulze method. See the <a href="http://en.wikipedia.org/wiki/Schulze_method">Schulze method article on Wikipedia</a> for more information.'));
52 }
53
54 /**
55 * Implementation of the decisions_voting_hook_form() hook for the runoff module.
56 *
57 * This displays a textfield per choice, that should be filled with a
58 * ranking.
59 */
60 function ranking_decisions_voting_form(&$node, $teaser, $page) {
61
62 $weight = 0;
63 $form = array();
64
65 if ($node->choice) {
66 $list = array();
67
68 // Put options in random order if randomize option
69 // selected on node create/edit form.
70 if($node->randomize) {
71 $node->choice = _decisions_randomize_options($node->choice, $node->choices);
72 }
73
74 $num_choices = count($node->choice);
75
76 // Generate the list of possible rankings
77 $choices[0] = '--';
78 for ($i = 1; $i <= $num_choices; $i++) {
79 if ($i == 1) {
80 $val = t('1st');
81 }
82 elseif ($i == 2) {
83 $val = t('2nd');
84 }
85 elseif ($i == 3) {
86 $val = t('3rd');
87 }
88 else {
89 $val = t('!{num}th', array('!{num}' => $i));
90 }
91 $choices[$i] = $val;
92 }
93
94 $form['choice'] = array(
95 '#type' => 'fieldset',
96 '#tree' => TRUE,
97 '#title' => t('Choices'),
98 '#description' => t('Rank the following options in your prefered order, the lower the number the better'),
99 );
100
101 foreach ($node->choice as $key => $choice) {
102 if ($choice['label']) {
103 $form['choice'][$key] = array(
104 '#type' => 'select',
105 '#title' => check_plain($choice['label']),
106 '#options' => $choices,
107 );
108 }
109 }
110 }
111
112 $form['nid'] = array(
113 '#type' => 'hidden',
114 '#value' => $node->nid,
115 '#weight' => $weight++,
116 );
117
118 $form['vote'] = array(
119 '#type' => 'submit',
120 '#value' => t('Vote'),
121 '#weight' => $weight++,
122 );
123
124 $form['#action'] = url('node/'. $node->nid);
125 return $form;
126 }
127
128 /**
129 * implementation of the decisions_view_results() hook for the runoff
130 * module
131 */
132 function ranking_decisions_view_results($node, $teaser, $page) {
133 $results = _ranking_decisions_calculate_results($node);
134
135 $output = '';
136
137 if ($node->algorithm == 'condorcet') { // Use this display method only if using condorcet.
138 $rows[0][] = "";
139 $options = count($node->choice);
140 for ($i = 1; $i<=$options; $i++) {
141 $rows[0][] = array('data' => check_plain($node->choice[$i]['label']), 'header' => 1);
142 }
143 for ($i = 1; $i<=$options; $i++) {
144 $rows[$i][0] = array('data' => check_plain($node->choice[$i]['label']), 'header' => 1);
145 for ($j = 1; $j<=$options; $j++) {
146 if ($i==$j) {
147 $rows[$i][$j] = "N/A";
148 } else {
149 if ($results->matrix[$i][$j]) {
150 $rows[$i][$j] = $results->matrix[$i][$j];
151 } else {
152 $rows[$i][$j] = 0;
153 }
154 }
155 }
156 }
157
158 $output .= theme_table(array(), $rows, array(), "The table below indicates the number of voters who preferred the row to the column.");
159
160 // Display the outcome of the Schulze count.
161 $output .= '<p>Results</p><ul>';
162 for ($i=1; $i<=count($results->ranking); $i++) {
163 $output .= "<li><em>Round ". $i ."</em>: ". $results->ranking[$i][0] .": ";
164 $first_one = TRUE;
165 for ($j=1; $j<count($results->ranking[$i]); $j++) {
166 if (!$first_one) {$output .= ", ";}
167 $output .= check_plain($node->choice[$results->ranking[$i][$j]]['label']);
168 $first_one=FALSE;
169 }
170 $output .= "</li>";
171 }
172 $output .= "</ul>";
173 } else {
174
175 // If no one has voted, $results = array() and thus is empty
176 if (!empty($results)) {
177
178 $output .= t('Results: ') .'<ol>';
179
180 for ($i = 0; $i < count($results->ranking); $i++) {
181 $output .= '<li> ';
182 $first_one = TRUE;
183
184 // Loop through all choices with this ranking
185 foreach ($results->ranking[$i]['choices'] as $choice) {
186 $output .= ($first_one? '' : ', ') . check_plain($node->choice[$choice]['label']);
187 $first_one = FALSE;
188 }
189
190 // Show the ranking's score if it exists (depends on algorithm)
191 if (isset($results->ranking[$i]['viewscore'])) {
192 $output .= ' ('. $results->ranking[$i]['viewscore'] .')';
193 }
194 $output .= '</li>';
195 }
196 $output .= '</ol>';
197
198 if (user_access('inspect all votes') && isset($results->matrix)) {
199 $header[0] = "Rounds";
200 $round = 1;
201 if (count($results->matrix) > 0) {
202 foreach ($results->matrix as $a_round) {
203 $header[$round] = $round;
204 $round++;
205 }
206 }
207
208 $round = 1;
209 $i = 0;
210 if (count($results->matrix) > 0) {
211 foreach ($results->matrix as $a_round) {
212 foreach ($node->choice as $key => $choicename) {
213 $rows[$i][0] = $choicename['label'];
214 $rows[$i][$round] = count($a_round[$key]);
215 $i++;
216 }
217 $i=0;
218 $round++;
219 }
220 }
221 $output .= theme('table', $header, $rows);
222 }
223 }
224 } // end condorcet else.
225 return $output;
226 }
227
228 /**
229 * implementation of the format_votes() hook.
230 *
231 * formats how a user's votes should be displayed.
232 *
233 * @returns a formatted string
234 */
235 function ranking_decisions_format_votes($node, $votes) {
236 $ordered_votes = array();
237 foreach ($votes as $vote) {
238 // Need two dimensional results (if equal rankings are allowed)
239 $ordered_votes[$vote->value][] = check_plain($node->choice[$vote->tag]['label']);
240 }
241 asort($ordered_votes);
242 $rankings = array();
243 foreach ($ordered_votes as $value => $choices) {
244 $rankings[$value] = implode(' = ', $choices);
245 ksort($rankings);
246 }
247 return implode(' > ', $rankings);
248 }
249
250 /**
251 * Implementation of the vote hook for the runoff module.
252 *
253 * This takes care of registering the vote in runoff nodes.
254 */
255 function ranking_decisions_vote($node, $form_values) {
256 $votes = array();
257 foreach ( $form_values['choice'] as $choice => $rank ) {
258 // A zero value indicates they didn't rank that choice
259 if ($rank != 0) {
260 $vote = array('value' => $rank,
261 'content_type' => 'decisions',
262 'content_id' => $node->nid,
263 'value_type' => 'option',
264 'tag' => $choice);
265 $votes[] = $vote;
266 }
267 }
268 votingapi_add_votes($votes);
269 }
270
271 /**
272 * implementation of the vote validation hook for the runoff module.
273 *
274 * This checks if the submitted values are within range, if they are
275 * not empty, and if they are not repeated.
276 *
277 * @returns boolean false on invalid forms, true otherwise.
278 */
279 function ranking_decisions_vote_validate($node, $form_values) {
280 $ok = TRUE;
281 // array used to check which values are set
282 $setvalues = array();
283
284 $numchoices = 0;
285 foreach ($node->choice as $key => $choice) {
286
287 // count the number of choices that are ranked
288 if (!empty($form_values['choice'][$key])) {
289 $numchoices++;
290 }
291 $intvalue = intval($form_values['choice'][$key]);
292 // mark this value as seen
293 if (!array_key_exists($intvalue, $setvalues)) {
294 $setvalues[$intvalue] = 1;
295 }
296 else {
297 $setvalues[$intvalue]++;
298 }
299 // check range
300 if ($intvalue > count($node->choice) || $intvalue < 0) {
301 form_set_error('Choice_'. $key, "illegal rank for choice $key: $intvalue (min: 1, max: ". count($node->choice) .")");
302 $ok = FALSE;
303 }
304
305 }
306
307 // too many choices ranked
308 if ($node->maxchoices != 0 && $numchoices > $node->maxchoices) {
309 form_set_error('choice', t('@num choices were selected but only @max are allowed.', array('@num' => $numchoices, '@max' => $node->maxchoices)));
310 $ok = FALSE;
311 }
312
313 // not enough choices ranked
314 $minchoices = 1;
315 if ($numchoices < $minchoices) {
316 form_set_error('choice', t('At least one choice must be selected.'));
317 $ok = FALSE;
318 }
319
320 // Check that multiple choices are not set to the same value
321 if ($node->algorithm != "condorcet") { // condorcet uses ties.
322 foreach ($setvalues as $val => $count) {
323 if ($val != 0 && $count > 1) {
324 form_set_error('choice', t('Multiple choices given the rank of @val.', array('@val' => $val)));
325 $ok = FALSE;
326 }
327 }
328 } // end condorcet if.
329
330
331 return $ok;
332 }
333
334 /***********************************************************************
335 * INTERNAL FUNCTIONS
336 **********************************************************************/
337
338 /**
339 * Calculate the results for a ranking decision based on the algorithm
340 *
341 * @param $node
342 * The node object for the current decision
343 *
344 * @return
345 * Should return an object that include the following attributes
346 * -results : 2d array listing the aggregate preference, including ties
347 * -rounds : 2d array listing the per-choice vote count for each round and
348 * a status message indicating who was eliminated
349 * -totalVoters : the total number of voters who participated
350 */
351 function _ranking_decisions_calculate_results($node) {
352 if ($node->algorithm == 'borda count') {
353 return _decisions_calculate_bordacount($node);
354 }
355 else if ($node->algorithm == 'instant runoff') {
356 return _decisions_calculate_instantrunoff($node);
357 }
358 else {
359 return _decisions_calculate_condorcet($node);
360 }
361 }
362
363 /**
364 * Calculate the results using borda count
365 *
366 * @param $node
367 * The node object for the current decision
368 *
369 * @return
370 * Should return an object that include the following attributes
371 * -results : 2d array listing the aggregate preference, including ties
372 * -rounds : 2d array listing the per-choice vote count for each round and
373 * a status message indicating who was eliminated
374 * -totalVoters : the total number of voters who participated
375 */
376
377 function _decisions_calculate_bordacount($node) {
378 $votes = _decisions_votes($node);
379
380 if (count($votes) == 0) {
381 // no votes yet
382 return array();
383 }
384
385 // aggregate votes by user (uid if logged in, IP if anonymous)
386 // in ascending order of value
387 $user_votes = array();
388
389 foreach ($votes as $vote) {
390 if ($vote['uid'] == 0) {
391 // anonymous user
392 $key = $vote['vote_source'];
393 }
394 else {
395 // logged-in user
396 $key = $vote['uid'];
397 }
398
399 $user_votes[$key][$vote['value']] = $vote['tag'];
400 }
401
402 $choice_votes = array();
403
404 $total_choices = count($node->choice);
405
406 // Loop through each user's vote
407 foreach ($user_votes as $uid => $user_vote) {
408 foreach ($user_vote as $ranking => $choice) {
409 // Negative values are possible if choices were removed after vote
410 $vote_value = max($total_choices - $ranking, 0);
411 if (!array_key_exists($choice, $choice_votes)) {
412 $choice_votes[$choice] = 0;
413 }
414 $choice_votes[$choice] += $vote_value;
415 }
416 }
417
418 // sort descending (although there may be ties)
419 arsort($choice_votes);
420
421 // Figure out the final ranking
422 $ranking = array();
423 $previous_total = -1;
424 $cur_result = -1;
425
426 foreach ($choice_votes as $choice => $total) {
427 if ($total != $previous_total) {
428 // Didn't tie with the previous score
429 $cur_result++;
430 }
431 $ranking[$cur_result]['choices'][] = $choice;
432 $ranking[$cur_result]['rawscore'] = $total;
433 $ranking[$cur_result]['viewscore'] = $total .' point'. ($total == 1? '' : 's');
434 }
435
436 $total_votes = count($user_votes);
437
438 $result_obj->ranking = $ranking;
439 $result_obj->total_votes = $total_votes;
440 return $result_obj;
441 }
442
443
444 /**
445 * Calculate the results using instant-runoff voting
446 *
447 * @param $node
448 * The node object for the current decision
449 *
450 * @return
451 * Should return an object that include the following attributes
452 * -results : 2d array listing the aggregate preference, including ties
453 * -rounds : 2d array listing the per-choice vote count for each round and
454 * a status message indicating who was eliminated
455 * -totalVoters : the total number of voters who participated
456 */
457
458 function _decisions_calculate_instantrunoff($node) {
459 $votes = _decisions_votes($node);
460
461 if (count($votes) == 0) {
462 // no votes yet
463 return array();
464 }
465
466 // aggregate votes by user (uid if logged in, IP if anonymous)
467 // in ascending order of value
468 $user_votes = array();
469
470 foreach ($votes as $vote) {
471 if ($vote['uid'] == 0) {
472 // anonymous user
473 $key = $vote['vote_source'];
474 }
475 else {
476 // logged-in user
477 $key = $vote['uid'];
478 }
479
480 // Note: relies on ORDER BY value ASC in vote-getting SQL query
481 // Otherwise a later vote might have a lower value
482 $user_votes[$key][] = $vote['tag'];
483 }
484
485 $total_votes = count($user_votes);
486
487 /*
488 if ($vote['value'] == 1) {
489 $cur_round[$vote['tag']]++;
490 // TODO: This method of counting total votes is inaccurate because users
491 // may vote but not choose a 1st-place vote
492 $totalvotes++;
493 }
494 */
495
496
497
498 // log of 1st-place votes per choice in each round
499 $round_log = array();
500
501 //
502 $reverse_ranking = array();
503
504
505
506 // If we eliminate one choice per round and have n choices, we should
507 // not be able to do more than n - 1 rounds
508 $max_rounds = count($node->choice);
509 for ($round = 0; $round < $max_rounds; $round++) {
510
511 // Initialize cur_round
512 $cur_round = array();
513 $total_choices = count($node->choice);
514 foreach ($node->choice as $chi => $temp) {
515 $cur_round[$chi] = array();
516 }
517
518
519 // Loop through each user
520 foreach ($user_votes as $key => $user_vote) {
521 // $user_vote[0] contains the user's first remaining preference
522 $cur_round[$user_vote[0]][] = $key;
523 }
524
525 if ($round == 0) {
526 // This is the first round
527 // Any choices with no first-place votes are considered eliminated
528 foreach ($cur_round as $ch => $choice_votes) {
529 if (count($choice_votes) == 0) {
530 unset($cur_round[$ch]);
531 $reverse_ranking[0]['choices'][] = $ch;
532 }
533 }
534 }
535
536
537 // Add the current round to the matrix
538 $round_log[] = $cur_round;
539
540 //Calculate the min and max number of votes
541 $min_votes = -1;
542 $max_votes = 0;
543
544 // Number of choices that have already been discarded
545 $num_discarded = 0;
546
547 // examine the number of votes each choice received this round
548 foreach ($cur_round as $ch => $choice_votes) {
549 $num_votes = count($choice_votes);
550
551 if ($num_votes > $max_votes) {
552 $max_votes = $num_votes;
553 $cur_winner = $ch; // store current winner in case it has a majority
554 }
555
556 // This choice has already been eliminated (theoretically)
557 // so don't count it as the minimum
558 if ($num_votes == 0) {
559 $num_discarded++; // probably don't need this variable any more
560 }
561 else if ($num_votes != 0 && ($num_votes < $min_votes || $min_votes == -1)) {
562 $min_votes = $num_votes;
563 }
564 }
565
566 // If one choice has a majority of remaining users it wins
567 // Note: we use count($user_votes) because some users may have incomplete
568 // ballots and may have already had all of their choices eliminated
569 if ($max_votes > count($user_votes) / 2) {
570
571 // Prune out the winning choice if it's still in there
572 if (isset($cur_round[$cur_winner])) {
573 unset($cur_round[$cur_winner]);
574 }
575
576 // Keep computing until we figure out all final rankings
577 while (count($cur_round) > 0) {
578 // Loop through non-winning choices
579 $current_place = array();
580 $min = -1;
581 foreach ($cur_round as $ch => $choice_votes) {
582 // Choice has already been eliminated, just unset it
583 if (count($choice_votes) == 0) {
584 unset($cur_round[$ch]);
585 }
586 else if ($min == -1
587 || count($choice_votes) < $min) {
588 // New minimum
589 $current_place = array($ch);
590 $min = count($choice_votes);
591 //drupal_set_message('New minimum: '. $ch .'('
592 //. count($choice_votes) . ')');
593 }
594 else if (count($choice_votes) == $min) {
595 // Tied for minimum
596 $current_place[] = $ch;
597 }
598 }
599
600 // current_place will be empty the first iteration if some
601 // choices had no first-place votes and were eliminated
602 // at the beginning
603 if (count($current_place) > 0) {
604 $reverse_ranking[]['choices'] = $current_place;
605 // Remove all choices that had the minimum
606 foreach ($current_place as $ch_key) {
607 unset($cur_round[$ch_key]);
608 }
609 }
610 }
611
612 // Save a reversed version of the round log to help compute winnerPercent
613 $revmat = array_reverse($round_log);
614
615 // The winner finally gets added
616 $reverse_ranking[]['choices'] = array($cur_winner);
617 $index = count($reverse_ranking) - 1;
618 $reverse_ranking[$index]['rawscore'] = round(count($revmat[0][$cur_winner]) * 100 / count($user_votes), 1);
619 $reverse_ranking[$index]['viewscore'] = $reverse_ranking[$index]['rawscore'] .'%';
620
621 $result_obj->matrix = $round_log;
622 $result_obj->total_votes = $total_votes;
623 $result_obj->ranking = array_reverse($reverse_ranking);
624 return $result_obj;
625 }
626
627 // Since we're still here, no one has won, so eliminate one of the
628 // choices with the lowest number of votes.
629
630 // Find all choices with the minimum number of votes
631 $min_choices = array();
632 foreach ($cur_round as $ch => $choice_votes) {
633 if (count($choice_votes) == $min_votes) {
634 $min_choices[] = $ch;
635 }
636 }
637
638 // Randomly select the choice to eliminate out of the available choices
639 // TODO: due to the randomness, this result must be cached after each vote
640 $round_loser = array_rand($min_choices);
641 //drupal_set_message('Round ' . ($round + 1) . ' eliminated: '
642 //. strval($min_choices[$round_loser])
643 //. ' (min = ' . $min_votes . ') ' . count($cur_round));
644 $reverse_ranking[]['choices'] = array($min_choices[$round_loser]);
645
646 // Loop through the users who voted for the loser and redistribute
647 foreach ($cur_round[$min_choices[$round_loser]] as $user_key) {
648 // Remove their current first preference
649 array_shift($user_votes[$user_key]);
650
651 // Keep eliminating first preference until we run out or find an choice
652 // that hasn't been eliminated
653 while ($cur_round[$user_votes[$user_key][0]] == array()
654 && count($user_votes[$user_key]) > 0) {
655 array_shift($user_votes[$user_key]);
656 }
657
658 // If they have no more preferences, remove from list for simplicity
659 if (count($user_votes[$user_key]) == 0) {
660 unset($user_votes[$user_key]);
661 }
662 }
663 }
664 // loop detected. signal user and record.
665 drupal_set_message("Could not reach a decision within $max_rounds iterations.");
666 $result_obj->matrix = $round_log;
667 $result_obj->total_votes = $total_votes;
668 return $result_obj;
669
670 }
671
672 /**
673 * Calculate the results using condorcet voting.
674 *
675 * @param $node
676 * The node object for the current decision
677 *
678 * @return
679 * Should return an object that include the following attributes
680 * -matrix: 2d array listing the aggregate preferences
681 * -ranking : 2d array listing the results of the Schulze count.
682 * -total_votes : the total number of voters who participated
683 */
684 function _decisions_calculate_condorcet ($node) {
685 $votes = _decisions_votes($node);
686
687 if (count($votes) == 0) {
688 // no votes yet
689 return array();
690 }
691
692 // aggregate votes by user (uid if logged in, IP if anonymous)
693 // in ascending order of value
694 $user_votes = array();
695
696 foreach ($votes as $vote) {
697 if ($vote['uid'] == 0) {
698 // anonymous user
699 $key = $vote['vote_source'];
700 }
701 else {
702 // logged-in user
703 $key = $vote['uid'];
704 }
705
706 $user_votes[$key][$vote['tag']] = $vote['value']; // TAG and Value had to be reversed here to allow for ties.
707 }
708
709 $choice_votes = array();
710
711 $total_choices = count($node->choice);
712 $total_votes = count($user_votes);
713
714 // Loop through each user's vote
715 foreach ($user_votes as $uid => $user_vote) {
716 // Create an array of choice ranks, so we don't look for any of them more than once.
717 $choice_ranks = array();
718 // Go through all the node choices, and find their ranking.
719 for ($choice = 1; $choice <= $total_choices; $choice++) {
720 $ranking = 256; // default value applied to anything that isn't ranked. Tied for last. Problem if there were more than 256 options.
721 if (array_key_exists($choice, $user_vote)) { $ranking = $user_vote[$choice]; } // Pull the ranking from the vote.
722 $choice_ranks[$choice] = 0 - $ranking; // We inverse the numerical values so a high ranking is bad, negatives work fine.
723 }
724 // Loop through to compare every choice.
725 for ($choice_A = 1; $choice_A <= $total_choices-1; $choice_A++) {
726 // Loop through all choices to which choice_A has not been compared, excluding itself.
727 for ($choice_B = $choice_A+1; $choice_B <= $total_choices; $choice_B++) {
728 // Figure out where to put the points.
729 if ($choice_ranks[$choice_A]>$choice_ranks[$choice_B]) {
730 // Indicate A beat B
731 $choice_votes[$choice_A][$choice_B] = $choice_votes[$choice_A][$choice_B] + 1;
732 $choice_votes[$choice_B][$choice_A] = $choice_votes[$choice_B][$choice_A] + 0;
733 } else if ($choice_ranks[$choice_B]>$choice_ranks[$choice_A]) {
734 // Indicate B beat A
735 $choice_votes[$choice_B][$choice_A] = $choice_votes[$choice_B][$choice_A] + 1;
736 $choice_votes[$choice_A][$choice_B] = $choice_votes[$choice_A][$choice_B] + 0;
737 } else {
738 // Indicate a Tie
739 $choice_votes[$choice_B][$choice_A] = $choice_votes[$choice_B][$choice_A] + 0.5;
740 $choice_votes[$choice_A][$choice_B] = $choice_votes[$choice_A][$choice_B] + 0.5;
741 } // end if
742 } // end for
743 } // end for
744 } // end foreach
745
746 //Return the results.
747 $result_obj->matrix = $choice_votes;
748 $result_obj->ranking = _decisions_shultz($choice_votes);
749 $result_obj->total_votes = $total_votes;
750 return $result_obj;
751 }
752
753 function _compare_beatpaths($a, $b) {
754 return strcmp($b[2],$a[2]);
755 }
756
757 /**
758 * Determine the shultz method winner from a pairwise matrix.
759 *
760 * @param $matrix;
761 * A pair-wise matrix of results.
762 *
763 * @return
764 * Should return an array appropriate for the condorcet display method.
765 *
766 * Note: There is another heuristic where strongest
767 * beatpaths are locked in, and weaker beatpaths that conflict with them are ignored.
768 * From my understanding, that would eliminate entirely the need for the second half of the
769 * Floyd-Warshall algorithm.
770 * I think that would be much faster to implement and run, but I would want to satisfy myself that
771 * the results were the same either way, so I went for the hard way first.
772 */
773 function _decisions_shultz($matrix) {
774 $ranking = array();
775 $candidates = count($matrix);
776 // Create a matrix of path strengths. Based on the Floyd-Warshall algorithm.
777 $strongest_paths = array();
778 for ($i = 1; $i<=$candidates; $i++) {
779 for ($j = 1; $j<=$candidates; $j++) {
780 if ($i!=$j) {
781 if ($matrix[$i][$j]>$matrix[$j][$i]) {
782 $strongest_paths[$i][$j] = $matrix[$i][$j];
783 } else {
784 $strongest_paths[$i][$j] = 0;
785 }
786 }
787 }
788 }
789 for ($i = 1; $i<=$candidates; $i++) {
790 for ($j = 1; $j<=$candidates; $j++) {
791 if ($i != $j) {
792 for ($k = 1; $k<=$candidates; $k++) {
793 if (($j!=$k) && ($i != $k)) {
794 $strongest_paths[$j][$k] = max ($strongest_paths[$j][$k], min ($strongest_paths[$j][$i], $strongest_paths[$i][$k]));
795 }
796 }
797 }
798 }
799 }
800 // Create an array of for,against,strength values from the strongest_paths array to allow sorting.
801 $beatpath = array();
802 for ($i = 1; $i<=$candidates; $i++) {
803 for ($j = 1; $j<=$candidates; $j++) {
804 if ($i!=$j && $strongest_paths[$i][$j] != 0) {
805 $beatpath[] = array($i, $j, $strongest_paths[$i][$j]);
806 }
807 }
808 }
809 // sort the array so that we're starting with the strongest links.
810 usort($beatpath,"_compare_beatpaths");
811
812 $round_count = 0;
813 $original_candidates = $candidates;
814 $orig_beatpath = count($beatpath);
815 $pathed_out = array(); // 1 indicates that the candidate has been pathed out. 2 indicates their beatpaths have been removed.
816 while ($candidates>1 && count($beatpath)>0) {
817 // Find if there are any candidates that have been pathed out.
818 // Pathed out means that A has a path to beat B, but B has no path to beat A, so B is eliminated.
819 $round_count++;
820 if ($round_count > $original_candidates) { // We have a problem.
821 $broken = TRUE;
822 break;
823 }
824 for ($i=1; $i<$original_candidates; $i++) {
825 for ($j=$i+1; $j<=$original_candidates; $j++) {
826 $forward = FALSE;
827 $reverse = FALSE;
828 for ($k=0; $k<count($beatpath); $k++){
829 if (($beatpath[$k][0] == $i) && ($beatpath[$k][1] == $j)) {
830 $forward = TRUE;
831 }
832 if (($beatpath[$k][1] == $i) && ($beatpath[$k][0] == $j)) {
833 $reverse = TRUE;
834 }
835 }
836 if ($forward == TRUE && $reverse == FALSE) {
837 $pathed_out[$j] = 1;
838 }
839 if ($forward == FALSE && $reverse == TRUE) {
840 $pathed_out[$i] = 1;
841 }
842 }
843 }
844 $pathed_out_count = 0;
845 for ($i=1; $i<=$original_candidates; $i++) {
846 if ($pathed_out[$i] == 1) {$pathed_out_count++;}
847 }
848 if ($pathed_out_count>0) { // Some candidate was eliminated this round.
849 // Remove all of the beatpaths associated with them.
850 $ranking[$round_count][] = "Eliminated";
851 for ($i=1; $i<=$original_candidates; $i++) {
852 if ($pathed_out[$i] == 1) {
853 $ranking[$round_count][] = $i;
854 $candidates--;
855 for ($j=0; $j<$orig_beatpath; $j++) {
856 if (($beatpath[$j][0]) == $i || ($beatpath[$j][1] == $i)) {
857 unset($beatpath[$j]);
858 }
859 }
860 $pathed_out[$i] = 2;
861 }
862 }
863 } else { // No candidates were eliminated this round. There is a Cyclical Tie.
864 // Drop all the beatpaths tied for weakest.
865 $index = count($beatpath)-1;
866 $value = $beatpath[$index][2];
867 do {
868 unset($beatpath[$index]);
869 $new = $beatpath[$index-1][2];
870 $index--;
871 } while ($new == $value);
872 $ranking[$round_count][] = "Cyclical Tie";
873 }
874 }
875 // Everyone left is tied for the win.
876 $round_count++;
877 $ranking[$round_count][] = "Winner";
878 for ($i=1; $i<=$original_candidates; $i++) {
879 if (!$pathed_out[$i]) {
880 $ranking[$round_count][] = $i;
881 $winner_count++;
882 }
883 }
884 if ($winner_count>1) {
885 $ranking[$round_count][0] = "Tie for Win";
886 }
887 return $ranking;
888 }

  ViewVC Help
Powered by ViewVC 1.1.2