Skip to main content

Section 4.1 Percentiles and Percentile Ranks

If you have taken a standardized test, you probably got your results in the form of a raw score and a percentile rank. In this context, the percentile rank is the percentage of people who got the same score as you or lower. So if you are "in the 90th percentile," you did as well as or better than 90% of the people who took the exam.
To understand percentiles and percentile ranks, let’s consider an example based on running speeds. Some years ago I ran the James Joyce Ramble, which is a 10 kilometer road race in Massachusetts. After the race, I downloaded the results to see how my time compared to other runners.
Listing 4.1.1. Python Code
download("https://github.com/AllenDowney/ThinkStats/raw/v3/nb/relay.py")
download(
    "https://github.com/AllenDowney/ThinkStats/raw/v3/data/Apr25_27thAn_set1.shtml"
)
The relay.py module provides a function that reads the results and returns a Pandas DataFrame.
Listing 4.1.2. Python Code
$ from relay import read_results

results = read_results()
results.head()
   Place Div/Tot Division Guntime Nettime Min/Mile        MPH
0      1   1/362    M2039   30:43   30:42     4:57  12.121212
1      2   2/362    M2039   31:36   31:36     5:06  11.764706
2      3   3/362    M2039   31:42   31:42     5:07  11.726384
3      4   4/362    M2039   32:28   32:27     5:14  11.464968
4      5   5/362    M2039   32:52   32:52     5:18  11.320755
results contains one row for each of 1633 runners who finished the race. The column we’ll use to quantify performance is MPH, which contains each runner’s average speed in miles per hour. We’ll select this column and use values to extract the speeds as a NumPy array.
Listing 4.1.3. Python Code
speeds = results["MPH"].values
I finished in 42:44, so we can find my row like this.
Listing 4.1.4. Python Code
$ my_result = results.query("Nettime == '42:44'")
my_result
    Place Div/Tot Division Guntime Nettime Min/Mile       MPH
96     97  26/256    M4049   42:48   42:44     6:53  8.716707
The index of my row is 96, so we can extract my speed like this.
Listing 4.1.5. Python Code
my_speed = speeds[96]
We can use sum to count the number of runners at my speed or slower.
Listing 4.1.6. Python Code
$ (speeds <= my_speed).sum()
np.int64(1537)
And we can use mean to compute the percentage of runners at my speed or slower.
Listing 4.1.7. Python Code
$ (speeds <= my_speed).mean() * 100
np.float64(94.12124923453766)
The result is my percentile rank in the field, which was about 94%.
More generally, the following function computes the percentile rank of a particular value in a sequence of values.
Listing 4.1.8. Python Code
def percentile_rank(x, seq):
    """Percentile rank of x.

    x: value
    seq: sequence of values

    returns: percentile rank 0-100
    """
    return (seq <= x).mean() * 100
In results, the Division column indicates the division each runner was in, identified by gender and age range β€” for example, I was in the M4049 division, which includes male runners aged 40 to 49. We can use the query method to select the rows for people in my division and extract their speeds.
Listing 4.1.9. Python Code
my_division = results.query("Division == 'M4049'")
my_division_speeds = my_division["MPH"].values
Now we can use percentile_rank to compute my percentile rank in my division.
Listing 4.1.10. Python Code
$ percentile_rank(my_speed, my_division_speeds)
np.float64(90.234375)
Going in the other direction, if we are given a percentile rank, the following function finds the corresponding value in a sequence.
Listing 4.1.11. Python Code
def percentile(p, seq):
    n = len(seq)
    i = (1 - p / 100) * (n + 1)
    return seq[round(i)]
n is the number of elements in the sequence; i is the index of the element with the given percentile rank. When we look up a percentile rank, the corresponding value is called a percentile.
Listing 4.1.12. Python Code
$ percentile(90, my_division_speeds)
np.float64(8.591885441527447)
In my division, the 90th percentile was about 8.6 mph.
Now, some years after I ran that race, I am in the M5059 division. So let’s see how fast I would have to run to have the same percentile rank in my new division. We can answer that question by converting my percentile rank in the M4049 division, which is about 90.2%, to a speed in the M5059 division.
Listing 4.1.13. Python Code
$ next_division = results.query("Division == 'M5059'")
next_division_speeds = next_division["MPH"].values

percentile(90.2, next_division_speeds)
np.float64(8.017817371937639)
The person in the M5059 division with the same percentile rank as me ran just over 8 mph. We can use query to find him.
Listing 4.1.14. Python Code
$ next_division.query("MPH > 8.01").tail(1)
     Place Div/Tot Division Guntime Nettime Min/Mile       MPH
222    223  18/171    M5059   46:30   46:25     7:29  8.017817
He finished in 46:25 and came in 18th out of 171 people in his division.
With this introduction to percentile ranks and percentiles, we are ready for cumulative distribution functions.