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.
download("https://github.com/AllenDowney/ThinkStats/raw/v3/nb/relay.py")
download(
"https://github.com/AllenDowney/ThinkStats/raw/v3/data/Apr25_27thAn_set1.shtml"
)
$ 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.
speeds = results["MPH"].values
I finished in 42:44, so we can find my row like this.
$ 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.
my_speed = speeds[96]
We can use
sum to count the number of runners at my speed or slower.
$ (speeds <= my_speed).sum()
np.int64(1537)
And we can use
mean to compute the percentage of runners at my speed or slower.
$ (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.
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.
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.
$ 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.
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.
$ 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.
$ 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.
$ 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.
