Sort and Average Rank in Fantasy Sports

As a dedicated fantasy hockey sports fan, I try to get an edge on the competition by analyzing player performance statistics for my initial draft and player trades during the season.  This often involves sorting or ranking players in scoring categories (e.g. goals, assists …).  It turns out that there are several ways of sorting and some may be more applicable than others for judging performance statistics.  For example, suppose we’re ranking goals in a league where 10 players have each scored 5 goals. A simple sort may order these players in ranks from 21 – 30. Even though 10 players score the same performance statistic (5 goals) their ranks can differ by 10 places. Not only that but typically a sort operation is non-deterministic i.e. the same 10 players can be ranked in any order, all between 21 – 30. One way to resolve ties (draws) is to assign an average rank (i.e. 25.5)  for all 10 players. Rank averaging maintains “sum-of-ranks” which is important for a defined sample size and fairer when comparing  to other statistics, in my opinion. A rank averaging method is commonly used in Yahoo Fantasy Hockey and other fantasy sports.

I looked far and wide on the Internet for a computer algorithm to automate average ranks.  Unable to find a suitable routine, I made my own. Using the Perl programming language I show how to sort a performance category and calculate the average ranks. No doubt a more elegant solution exists but at least mine works.

The annotated Perl script below should explain the average rank problem. It takes an unsorted hash of player scoring data and outputs a hash with an average rank for each player. The script’s print output shows the (complicated) averaging steps in action. I’ve tested this on sets of ~900 players and it works perfectly. Some lines of code below are truncated, be careful with copy-and-paste.

#!/usr/bin/perl -w
# define sub sr() to sort and calculate (average) ranks a’la Yahoo Fantasy Hockey.
# sorting higher score is better(top rank=0) to lower score.
use strict;
use warnings;
use List::Util qw(sum);

# define hash of real numbers.
my %scrHash = (); # player scores in some category (goals, assists…
my %rnkHash = (); # output for player ranks in this category.

#     Unsorted hash                        # Possible sort                     rank avg_rank
$scrHash{name0}{goals} = 2; # $scrHash{name7}{goals} = 8 0 1.5
$scrHash{name1}{goals} = 3; # $scrHash{name9}{goals} = 8 1 1.5
$scrHash{name2}{goals} = 4; # $scrHash{name8}{goals} = 8 2 1.5
$scrHash{name3}{goals} = 5; # $scrHash{name6}{goals} = 8 3 1.5
$scrHash{name4}{goals} = 5; # $scrHash{nameA}{goals} = 6 4 4
$scrHash{name5}{goals} = 5; # $scrHash{name4}{goals} = 5 5 6
$scrHash{name6}{goals} = 8; # $scrHash{name6}{goals} = 5 6 6
$scrHash{name7}{goals} = 8; # $scrHash{name5}{goals} = 5 7 6
$scrHash{name8}{goals} = 8; # $scrHash{name2}{goals} = 4 8 8
$scrHash{name9}{goals} = 8; # $scrHash{name1}{goals} = 3 9 9
$scrHash{nameA}{goals} = 6; # $scrHash{name0}{goals} = 2 10 10
$scrHash{nameB}{goals} = 1; # $scrHash{nameC}{goals} = 1 11 12
$scrHash{nameC}{goals} = 1; # $scrHash{nameB}{goals} = 1 12 12
$scrHash{nameD}{goals} = 1; # $scrHash{nameD}{goals} = 1 13 12
$scrHash{nameE}{goals} = 0; # $scrHash{nameE}{goals} = 0 14 14
#                                                                                                  sum of ranks 105 105

sr(“goals”);

print “Program End.\n”;
exit(0);

sub sr { # sort rank
my $cat=$_[0]; print “sr: category: $cat\n”;
my @valArray = (); # sorted score values
my @avgArray = (); # variable length array with running average.
my @rnkArray = (); # vartiable length array orders average ranks.
my $cntr=0;
my @sc = reverse sort { $scrHash{$a}{$cat} <=> $scrHash{$b}{$cat} }keys \%scrHash;
foreach my $id (@sc) {
print “sr: $cntr, $id, $scrHash{$id}{$cat}\n”;
$valArray[$cntr++] = $scrHash{$id}{$cat};
}

# Define 2 flags $en: equals-next, $el: equals-last
my $en = 0;
my $el = 0;

# Go through the sorted value array and average the rankings.
# These rankings are zero-based. push $i+1 for 1-based or similar.
for (my $i=0; $i<@valArray; $i++) {
if ($valArray[$i] == $valArray[$i-1]) { $el = 1; } else { $el = 0; }
if (($i <@valArray-1) && ($valArray[$i] == $valArray[$i+1])) { $en = 1; } else { $en = 0; }
if ($en == 0 && $el == 0) {@avgArray = (); push(@avgArray, $i); }
if ($en == 0 && $el == 1) {push(@avgArray, $i); }
if ($en == 1 && $el == 0) {@avgArray = (); push(@avgArray, $i); }
if ($en == 1 && $el == 1) {push(@avgArray, $i); }
my $avg = @avgArray ? sum(@avgArray)/@avgArray : 0;

print “i: $i val: $valArray[$i] el: $el en: $en avg: $avg \tavgArray: (@avgArray)\n”;

if ( $en == 0 ) {
foreach my $j (@avgArray) { push(@rnkArray, $avg); }
print “rnkArray: (@rnkArray)\n”;
}
}

# re-sort %scrHash to insert id and avg rank into new %rnkHash.
$cntr=0;
@sc = reverse sort { $scrHash{$a}{$cat} <=> $scrHash{$b}{$cat} } keys \%scrHash;
foreach my $id (@sc) {
$rnkHash{$id}{$cat}= $rnkArray[$cntr];
print “$cntr, id: $id, val: $scrHash{$id}{$cat}, avg_rank: $rnkHash{$id}{$cat}\n”;
$cntr++;
}
} # end sub

It is my contention that a team composed of the top ranked players in each position would win any league. But in a head-to-head match-up, there may exist combinations of players that could win 6 of 10 scoring categories and thus the match. I’m not a statistician and can’t prove it, but you’re welcome to educate me.

Please visit my fantasy hockey page: Renegade Hockey