Rank array values with potential duplicate values and skipping some positions if there is a tie
I am working with database data that manipulates college students exa开发者_开发技巧m results. Basically, I am pulling the records from a MySQL database and pulling one class at any given time. I want to rank the students with the highest performer given the rank of 1
.
Marks: 37, 92, 84, 83, 84, 65, 41, 38, 38, 84.
I want to capture MySQL data as a single array. Once I have the data in an array, I should then assign each student a position in the class such as 1/10 (number 1, the 92 score), 4/10 etc. Now the problem is that if there is a tie, then the next score skips a position and if there are 3 scores at one position then the next score skips 2 positions. So the scores above would be ranked as follows;
92 - 1
84 - 2,
84 - 2,
84 - 2,
83 - 5,
65 - 6,
41 - 7,
38 - 8,
38 - 8 ,
37 - 10
The grading system requires that the number of positions (ranks, if you will) will be maintained, so we ended up with 10 positions in this class since positions 3, 4, 5 and 9 did not have any occupants. (The alternative of filling every number will have given us only 8 positions!)
Is it possible (humanly/programmatically possible) to use PHP to rank the scores above in such a way that it can handle possible ties such as 4 scores at one position? Sadly, I could not come up with a function to do this. I need a PHP function (or something in PHP) that will take an array and produce a ranking as above.
If it's possible to do this with MySQL query data without having it in an array, then that will also be helpful!
I assume the grades are already sorted by the database, otherwise use sort($grades);
.
Code:
$grades = array(92, 84, 84, 84, 83, 65, 41, 38, 38, 37);
$occurrences = array_count_values($grades);
$grades = array_unique($grades);
foreach($grades as $grade) {
echo str_repeat($grade .' - '.($i+1).'<br>',$occurrences[$grade]);
$i += $occurrences[$grade];
}
Result:
92 - 1
84 - 2
84 - 2
84 - 2
83 - 5
65 - 6
41 - 7
38 - 8
38 - 8
37 - 10
EDIT (Response to discussion below)
Apparently, in case the tie occurs at the lowest score,
the rank of all lowest scores should be equal to the total count of scores.
Code:
$grades = array(92, 84, 84, 84, 83, 65, 41, 38, 37, 37);
$occurrences = array_count_values($grades);
$grades = array_unique($grades);
foreach($grades as $grade) {
if($grade == end($grades))$i += $occurrences[$grade]-1;
echo str_repeat($grade .' - '.($i+1).'<br>',$occurrences[$grade]);
$i += $occurrences[$grade];
}
Result:
92 - 1
84 - 2
84 - 2
84 - 2
83 - 5
65 - 6
41 - 7
38 - 8
37 - 10
37 - 10
$scores = array(92, 84, 84, 84, 83, 65, 41, 38, 38, 37);
$ranks = array(1);
for ($i = 1; $i < count($scores); $i++)
{
if ($scores[$i] != $scores[$i-1])
$ranks[$i] = $i + 1;
else
$ranks[$i] = $ranks[$i-1];
}
print_r($ranks);
I needed to end up with a map of values to rank. This method may be more efficient for the original question too.
public static function getGrades($grades)
{
$occurrences = array_count_values($grades);
krsort($occurrences);
$position = 1;
foreach ($occurrences as $score => $count) {
$occurrences[$score] = $position;
$position += $count;
}
return $occurrences;
}
If you print_r on $occurrences you get
Array
(
[92] => 1
[84] => 2
[83] => 5
[65] => 6
[41] => 7
[38] => 8
[37] => 10
)
Based on the original answer, so thanks!
Using array_count_values()
followed by a foreach()
is doing 2 loops over the input array, but this task can be done will one loop (minimizing/optimizing the time complexity).
Code: (Demo)
// assumed already rsort()ed.
$scores = [92, 84, 84, 84, 83, 65, 41, 38, 38, 37];
$gappedRank = 0;
$result = [];
foreach ($scores as $score) {
++$gappedRank;
$gappedRanks[$score] ??= $gappedRank;
$result[] = [$score => $gappedRanks[$score]];
}
var_export($result);
For a flat, associative lookup array of scores and their rank, unconditionally increment the counter and only push a new element into the lookup array if the key will be new. (Demo)
$gappedRank = 0;
$lookup = [];
foreach ($scores as $score) {
++$gappedRank;
$lookup[$score] ??= $gappedRank;
}
var_export($lookup);
The first snippet provides "gapped ranking". I have another answer which implements a similar approach but with a different input data structure and with the intent of modifying row data while looping.
- Get dense rank and gapped rank for all items in array
In the realm of ranking, there is also "dense ranking". See my time complexity optimized answers at:
- Populate multidimensional array's rank column with dense rank number
- Add order column to array to indicate rank from oldest to youngest
精彩评论