Lee Traynor's PHP Tutorials - A Simple Method for Distributing ECTS Grades

Contents

Return to Tutorial Index

According to Wikipedia, the ECTS grading scale distributes grades corresponding to the relative achievement of students within a class/institution. Thus, of the students who pass an examination or course, their grades should be distributed as follows:

PercentilePercentageGrade
91-10010A
66-9025B
36-6530C
11-3525D
1-1010E
Table 1: The relationship between percentile ranking and grade proposed by ECTS

The percentile should not be confused with a percentage score (e.g. in a test) but corresponds to the percentage rank of the students' achievement. Another way of looking at this is to say that the top 10% of students are graded with an "A", the next 25% with a "B", and so on.

Uncertainty also prevails about how to distribute ECTS grades when small numbers of students are involved, when grading is relatively insensitive and results in cohorts larger than the quotas proposed by the ECTS grades, or when an incommensurate grading system is used (see Tables 2-4). In this sense, "cohort" refers to a group of students with identical ranking and "quota" to the portion of a class reserved for a particular grade.

GradeNo of StudentsGradeNo of StudentsGradeNo of Students
1,311st251,01
2,022nd301,32
2,343rd301,74
2,754th202,06
3,32Table 3. Insensitive grade structure2,39
Table 2. Small number of students2,712
3,010
3,37
3,75
4,02
Table 4. Incommensurate grade structure

In all of these cases, the ECTS grading system can be applied if commonsense mathematical principles are adhered to.

The purpose of the program ECTS_Form.php is enable ECTS grades to be determined for any grading system and any number of students. You are free to use the above working example for 10, 20 or 50 ranking levels, which can be freely named. Or you can download the source code and adapt it to your own preferences, in accordance with the GNU General Public Licence.

It is important to realise that if two grading systems do not seem to match—for whatever reason—that impartially applying a standardised procedure is generally the solution. As long as the rules are promulgated in advance and everybody agrees to play by them, then any arbitrariness inherent in their application is part of the game.

Let's start first with considering what problems might arise and how they might be remedied.

  1. An original cohort might be too small for the quota it is mean to be mapped onto;
  2. A cohort might exceed a quota and span two grades;
  3. A cohort might span more than two grades.
Note that none of this requires the original marking system to be normed to any requirements (in respect to sample size, cohort size, or cohort distribution).

Problem #1 only really becomes a problem of how to distribute the remainder of the quota (see 1 in the graphic on the left), and this would particularly be the case when a following ranking level becomes spread over two grades (problem #2). If a cohort were to span three grades or more, then the question is justified as to which of the three grades should be selected (problem #3).

The solution to problem #2 can be described as follows: If the majority of a split cohort falls within one grade, then that grade is given (2a); if the split is exactly 50:50, then the higher grade is chosen (2b); otherwise the lower grade is applied (2c).


Solving problem #3 involves making a decision as to which of the grades is most prominently represented in the span covered by the cohort. If this were an election, the method would be "First past the post". Any cohort which completely covers the quota for the "C" grade would therefore, by inspection, be graded with "C", since this is the largest of all the quotas. However, if the C-quota is only partially occupied (to less than, or less than or equal to 25%) then fully covering the B- or D-quotas would result in a B (3a) or D (3d): As a tie-breaker, we could then say that if the B-quota is fully covered and the C-quota is covered to less than or equal to 25%, then the B-grade is given (3b); on the other hand, if the D-quota is fully covered and the C-quota is covered by more than or equal to 25%, then the C-grade is given (3c).


[Images created with InterDraw.]

Now let's move on to the implementation. As a first stage we will need a form which calls itself on being submitted; if the form contains data, then the data have to be crunched and the results presented. Notice how the side-by-side placement of two tables is achieved by using "tables within a table", i.e. a table with one line and two cells is created, and within each of the two cells two further tables—one with the results and the other with the form fields for new data— are placed. The framework, then, looks like this:

<?php
$b = "\r\n";
extract ($_POST);
if (!isset ($imax)) $imax = 10;
$grades = array ("A", "B", "C", "D", "E");
$limits = array (0.1, 0.35, 0.65, 0.9);
$distributed_grades = array ();
if (isset ($submit))
{
   $total_students = array_sum ($group_size);
   $start_j = $progress = 0;
   for ($i = 0; $i < $imax; $i++) if (isset ($group_size[$i]) and $group_size[$i] > 0)
   {
# Work through the ranks from first to last.
   }
}
# Here a table with the results
# And here a table for entering new data
?>

Before we leave this, just a word of warning about initialising the array for the distributed grades, which here is implemented with $distributed_grades = array (): this is necessary syntax from PHP 5.3, but will cause an error in PHP <5.2. In that case simply comment out the line and the rest should work as intended.

And now the details of the decision process, step by step.

Our variable $start_j tells us the grade that the start of the cohort will fall in (0 = A, etc), and to determine where the end of the cohort falls we simply have to work through the array $limits until we go over the limit:

# At the start of the loop:
      $portion_students = $group_size[$i] / $total_students;
      $j = $start_j;
      $subtotal = $progress + $portion_students;
      while ($j < 4 and $subtotal > $limits[$j]) $j++;

Now the variable $j has been set to the end quota. The difference between the starting and ending values will give us the span that the cohort covers. Depending on the number of quotas covered by the cohort, different rules will have to be applied, so further action can be split up with a switch ($j - $start_j) control. In the current case, this will be zero, and the result will be that the grade corresponding to the start/end quota is given:

      switch ($j - $start_j)
      {
         case 0: $grade_j = $j; break;
# Other values will be added here
      }

Finally, the actual grade has to be transferred to the array for the distributed array, the portion of students dealt with has to be added to the $progress variable, and $start_j has to be set to the value reached by $j:

      $distributed_grade[$i] = $grades[$grade_j];
      $progress += $portion_students;
      $start_j = $j;

The case in which a cohort covers all 5 or any 4 grades is also fairly trivial, since, as we have seen, the only possible grade is "C". Thus we only need to add one line to the switch control. Note how this line will deal with both the values 3 and 4:

         case 3: case 4: $grade_j = 2; break;

A little more serious is the case of 3 grades being spanned because we have to determine which of the three represents the majority of the span. This decision can be broken down into three parts:

  1. If the start is in the quota for "A" then the decision is between "B" and "C" and falls to "C", if the portion of the cohort within "C" is greater than 25%;
  2. Starting with "B" the only result can be "C" because as it is entirely spanned and the largest of the quotas anyway, the other quotas cannot form a majority;
  3. Similar to case #1 is a starting point within the quota for "C": A grade of "D" can only be given, if the proportion remaining in "C" is less than 25%.
And the implementation:

         case 2:
            switch ($start_j)
            {
               case 0:
                  if ($subtotal - 0.25 > 0) $grade_j = 1; else $grade_j = 2;
               break;
               case 1: $grade_j = 2; break;
               case 2:
                  if (round ($progress, 4) <= 0.4) $grade_j = 2; else $grade_j = 3;
               break;
            }
         break;

Note how in case #3 the starting point ($progress) has to be rounded in a peculiar way. Any starting point at less than or equal to 40% then the result should be a "C". Due to errors in the way floating point numbers are calculated, represented and added, this can lead to discrepancies in the actual values obtained. Note that we would want to award a "C" even if the starting point was equal to 40%, not just less than it. Although in this case the rounding is probably of little consequence, it will be crucial in the following section.

Now that we have solved the question of how to deal with comparing floating point numbers reliably by rounding, let's move on to the final stage. Here we have to determine whether the overhang which extends into the subsequent quota exceeds the portion that remains in the higher grade.

         case 1:
            $remainder[$i] = $limits[$j - 1] - $progress;
            if (round ($portion_students * $total_students / 2, 4) <= round ($remainder[$i] * $total_students, 4)) $grade_j = $j - 1;
            else $grade_j = $j;
         break;

Here now the complete data processing loop, with all the parts in order:

   for ($i = 0; $i < $imax; $i++) if (isset ($group_size[$i]) and $group_size[$i] > 0)
   {
      $portion_students = $group_size[$i] / $total_students;
      $j = $start_j;
      $subtotal = $progress + $portion_students;
      while ($j < 4 and $subtotal > $limits[$j]) $j++;
      switch ($j - $start_j)
      {
         case 0: $grade_j = $j; break;
         case 1:
            $remainder[$i] = $limits[$j - 1] - $progress;
            if (round ($portion_students * $total_students / 2, 4) <= round ($remainder[$i] * $total_students, 4)) $grade_j = $j - 1;
            else $grade_j = $j;
         break;
         case 2:
            switch ($start_j)
            {
               case 0:
                  if ($subtotal - 0.25 > 0) $grade_j = 1; else $grade_j = 2;
               break;
               case 1: $grade_j = 2; break;
               case 2:
                  if (round ($progress, 4) <= 0.4) $grade_j = 2; else $grade_j = 3;
               break;
            }
         break;
         case 3: case 4: $grade_j = 2; break;
      }
      $distributed_grade[$i] = $grades[$grade_j];
      $progress += $portion_students;
      $start_j = $j;
   }

After the processing loop is done, it is time to represent the results and make a new data entry table available. This is done with "tables within tables", which is an elegant way of placing two tables side by side without advanced string theory.
Imagine a simple table with one row and two cells:

<table>
   <tr>
      <td>One cell</td>
      <td>Another</td>
   </tr>
</table>

Now instead of just having "ordinary" content in the cells, they contain tables in their own right, with rows and cells:

<table>
   <tr>
      <td><table><tr><td></td><td></td></tr></table></td>
      <td><table><tr><td></td><td></td></tr></table></td>
   </tr>
</table>

And here how it's done. Note the Javascript in the second last line for the "Clear" button, which clears all the fields in the new data entry form.

echo "<table border=\"5\" rules=\"cols\">$b<tr valign=\"top\">$b";
if (isset ($group_size) and $total_students > 0)
{
   echo "<td>$b<table>$b";
   echo "<tr><th colspan=\"4\">Your Results</th></tr>$b";
   echo "<tr><th colspan=\"2\">Ranked Group</th><th>Number of Students</th><th>ECTS Grade</th></tr>$b";
   for ($i = 0; $i < count ($group_size); $i++)
   {
      if ((isset ($group_size[$i]) and $group_size[$i] > 0) or $rank_description[$i] != "")
      {
         echo "<tr><td>$i</td>";
         if (isset ($rank_description[$i]) and $rank_description[$i] == "") echo "<td>&nbsp;</td>"; else echo "<td>$rank_description[$i]</td>";
         echo "<td>$group_size[$i]</td><td>";
         if (isset ($distributed_grade[$i])) echo "$distributed_grade[$i]"; else echo "-";
         echo "</td></tr>$b";
      }
   }
   echo "<tr><th colspan=\"2\">Total</th><th colspan=\"2\">$total_students</th></tr>$b</table>$b</td>$b";
}
echo "<td>$b<table>$b";
echo "<form method=\"post\" name=\"ects_form\" action=\"{$_SERVER["SCRIPT_NAME"]}\">$b";
echo "<tr><th colspan=\"3\">New Distribution</th></tr>$b";
echo "<tr><th colspan=\"2\">Ranked Group</th><th>Number of Students</th></tr>$b";
for ($i = 0; $i < $imax; $i++)
{
   echo "<tr><td align=\"right\">$i:</td><td><input name=\"rank_description[]\" tape=\"text\" size=\"12\"";
   if (isset ($rank_description[$i])) echo " value=\"$rank_description[$i]\"";
   echo "></td><td align=\"center\"><input name=\"group_size[]\" type=\"text\" size=\"6\"";
   if (isset ($group_size[$i])) echo " value=\"$group_size[$i]\"";
   echo "></td></tr>$b";
}
echo "<tr><td align=\"center\"><input type=\"submit\" name=\"submit\" value=\"Calculate!\"></td><th colspan=\"2\">No of ranking levels:<select name=\"imax\" size=\"1\">";
foreach ($size_options as $option)
{
   echo "<option";
   if ($imax == $option) echo " selected";
   echo ">$option</option>";
}
echo "</select></td></tr>$b</form>$b";
echo "<tr><td align=\"center\" colspan=\"3\"><button onclick=\"for (i = 0; i < document.forms[0].length - 2; i++) document.forms[0].elements[i].value = ''\">Clear</button></td></tr>$b";
echo "</table>$b</td>$b</tr>$b</table>$b";

The complete code for a working PHP script can be downloaded here.

Artefacts are most likely to arise in small groups of students. Table 5 summarises the distribution of students from 1-20, with the assumption that each student has been ranked uniquely (i.e. all cohorts have n = 1). Of course, having cohorts of different sizes would lead to different results.

Grade/Number1234567891011121314151617181920
A----1111111111222222
B-1111112233344344455
C1-121232333434545656
D-1112112223344444455
E-----111111111122222
Table 5. Examples of grade distributions among small numbers of uniquely ranked students

In particular the distributions for n = 13 and n = 15 appear a little unusual, but this is to be expected from the application of a procedure to small numbers. In any case, the procedure was found to be operating as intended. The group for n = 20 is perfectly matched, and both n = 5 and n = 10 show the effects of rounding to the next highest grade, as expected.

Comments and Questions

Can be directed to me here.
Thanks and have fun!

Lee Traynor