Rank array values with potential duplicate values and skipping some positions if there is a tie
Asked Answered
A

4

7

I am working with database data that manipulates college students exam 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.
Here is an illustration;

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!

Avivah answered 28/5, 2011 at 17:26 Comment(0)
K
7

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
Kearns answered 28/5, 2011 at 17:48 Comment(5)
This works very well until the tie is at the end. In that case the position assigned to the last scores (the ones that tie) will not be equal to the total number of scores. In the above output you will see that the score 37 is positioned at number 10, which is diserable since it is the lowest score and and happens to be the total number of students. If however, we have a tie at 37 (lowest score) and not 38, the last two scores will be assigned position 9 instead of 10. I guess there is need for a conditional that checks if a tie involves the lowest score, I am trying to add such a conditionalAvivah
Lets say we have the following array: $grades = array(92, 84, 84, 84, 83, 65, 41, 38, 37,37); Both 37 show up in the results like 37 - 9, which is correct because the first 37 is actually located at the ninth position. In conclusion: When the tie occurs at the lowest grade, the rank for every lowest grade is equal to the rank of first occurrence of the lowest grade. In this case 9. Right?Kearns
I think your code should solve the problem as it is. The reasoning behing ensuring that the last score be equal to the number of students that sat for the exam is somewhat flawed. If the tie occurs anywhere but at the end, the lowest score will be equal to the total no of students. If it's at the the end that cannot happen, and IU agree with tyou bec ause if the second lowest scorer is positioned at lets say, 9 as in the example, then the next scorer should rightfully get the next position SINCE there is no score after that. Meanwhile thank very very much for this answer you are a Genius!Avivah
Added additional code, hopefully I did understand you correctly :)Kearns
Awesome! Thank you so much. I was almost thinking the one who devised the sytem may have got it wrong so I googled up and found a powerpoint presentation that explained how students are ranked, and it also says the lowest scorer has to have the number of students that sat for the exam as his/ her position regardless of any ties in the scores. So I guess this new solution is universally acceptable. Once again thank you. I wish I had the required reputation to vote for your answers.Avivah
E
2
$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);
Eskilstuna answered 28/5, 2011 at 17:35 Comment(2)
@Avivah As an addition to stuken.yuri code, I would point out, that you can get the data from database already ordered, so you you don't have to sort it via php functions.Kalmick
I am able to get it ordered from mysql by adding some parameters to the query. Are you saying its possible to have the data manipulated to an extent as described in my question? Skipping some positions in case of a tie? That is a requirement that I thought cannot be achieved by adding order by DESC or ASC in a query. I will be happy if you can elaborate how I can achieve this high degree ofranking using a query.Avivah
J
0

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!

Jacquez answered 9/3, 2015 at 23:17 Comment(0)
N
0

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.

In the realm of ranking, there is also "dense ranking". See my time complexity optimized answers at:

Noriega answered 18/1, 2023 at 6:31 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.