Skip to main content

Section 14.1 Normal Probability Plots

Many analytic methods are based on the properties of the normal distribution, for two reasons: distributions of many measurements in the real world are well-approximated by normal distributions, and normal distributions have mathematical properties that make them useful for analysis.
To demonstrate the first point, weโ€™ll look at some of the measurements in the penguin dataset. Then weโ€™ll explore the mathematical properties of the normal distribution.
The following cell downloads the data from a repository created by Allison Horst.
Horst AM, Hill AP, Gorman KB (2020). palmerpenguins: Palmer Archipelago (Antarctica) penguin data. R package version 0.1.0. https://allisonhorst.github.io/palmerpenguins/. doi: 10.5281/zenodo.3960218.
The data was collected as part of the research that led to this paper: Gorman KB, Williams TD, Fraser WR (2014). Ecological sexual dimorphism and environmental variability within a community of Antarctic penguins (genus Pygoscelis). PLoS ONE 9(3):e90081. https://doi.org/10.1371/journal.pone.0090081
Listing 14.1.1. Python Code
download(
    "https://raw.githubusercontent.com/allisonhorst/palmerpenguins/c19a904462482430170bfe2c718775ddb7dbb885/inst/extdata/penguins_raw.csv"
)
We can read the data like this.
Listing 14.1.2. Python Code
$ penguins = pd.read_csv("penguins_raw.csv")
penguins.shape
(344, 17)
The dataset contains measurements from three penguin species. For this example, weโ€™ll select the Adรฉlie penguins.
Listing 14.1.3. Python Code
$ adelie = penguins.query('Species.str.startswith("Adelie")').copy()
len(adelie)
152
To see if penguin weights follow a normal distribution, weโ€™ll compute the empirical CDF of the data.
Listing 14.1.4. Python Code
from empiricaldist import Cdf

weights = adelie["Body Mass (g)"].dropna()
cdf_weights = Cdf.from_seq(weights)
And weโ€™ll compute the analytic CDF of a normal distribution with the same mean and standard deviation.
Listing 14.1.5. Python Code
$ m, s = weights.mean(), weights.std()
m, s
(np.float64(3700.662251655629), np.float64(458.5661259101348))
Listing 14.1.6. Python Code
from scipy.stats import norm

dist = norm(m, s)
qs = np.linspace(m - 3.5 * s, m + 3.5 * s)
ps = dist.cdf(qs)
Hereโ€™s what the CDF of the data looks like compared to the normal model.
Listing 14.1.7. Python Code
model_options = dict(color="gray", alpha=0.5, label="model")
plt.plot(qs, ps, **model_options)
cdf_weights.plot(label="data")

decorate(ylabel="CDF")
Figure 14.1.8.
The normal distribution might be a good enough model of this data, but itโ€™s certainly not a perfect fit.
In general, plotting the CDF of the data and the CDF of a model is a good way to evaluate how well the model fits the data. But one drawback of this method is that it depends on how well we estimate the parameters of the modelโ€”in this example, the mean and standard deviation.
An alternative is a normal probability plot, which does not depend on our ability to estimate parameters. In a normal probability plot the \(y\) values are the sorted measurements.
Listing 14.1.9. Python Code
ys = np.sort(weights)
And the \(x\) values are the corresponding percentiles of a normal distribution, computed using the ppf method of the norm object, which computes the โ€œpercent point functionโ€, which is the inverse CDF.
Listing 14.1.10. Python Code
n = len(weights)
ps = (np.arange(n) + 0.5) / n
xs = norm.ppf(ps)
If the measurements are actually drawn from a normal distribution, the \(y\) and \(x\) values should fall on a straight line. To see how well they do, we can use linregress to fit a line.
Listing 14.1.11. Python Code
from scipy.stats import linregress

results = linregress(xs, ys)
intercept, slope = results.intercept, results.slope

fit_xs = np.linspace(-3, 3)
fit_ys = intercept + slope * fit_xs
The following figure shows the \(x\) and \(y\) values along with the fitted line.
Listing 14.1.12. Python Code
plt.plot(fit_xs, fit_ys, **model_options)
plt.plot(xs, ys, label="data")

decorate(xlabel="Standard normal", ylabel="Body mass (g)")
Figure 14.1.13.
The normal probability plot is not a perfectly straight line, which indicates that the normal distribution is not a perfect model for this data.
One reason is that the dataset includes male and female penguins, and the two groups have different meansโ€”letโ€™s see what happens if we plot the groups separately. The following function encapsulates the steps we used to make a normal probability plot.
Listing 14.1.14. Python Code
def normal_probability_plot(sample, **options):
    """Makes a normal probability plot with a fitted line."""
    n = len(sample)
    ps = (np.arange(n) + 0.5) / n
    xs = norm.ppf(ps)
    ys = np.sort(sample)

    results = linregress(xs, ys)
    intercept, slope = results.intercept, results.slope

    fit_xs = np.linspace(-3, 3)
    fit_ys = intercept + slope * fit_xs

    plt.plot(fit_xs, fit_ys, color="gray", alpha=0.5)
    plt.plot(xs, ys, **options)
    decorate(xlabel="Standard normal")
Hereโ€™s what the results look like for male and female penguins separately.
Listing 14.1.15. Python Code
grouped = adelie.groupby("Sex")

weights_male = grouped.get_group("MALE")["Body Mass (g)"]
normal_probability_plot(weights_male, ls="--", label="Male")

weights_female = grouped.get_group("FEMALE")["Body Mass (g)"]
normal_probability_plot(weights_female, label="Female")

decorate(ylabel="Weight (g)")
Figure 14.1.16.
The normal probability plots for both groups are close to a straight line, which indicates that the distributions of weight follow normal distributions. When we put the groups together, the distribution of their weights is a mixture of two normal distributions with different meansโ€”and a mixture like that is not always well modeled by a normal distribution.
Now letโ€™s consider some of the mathematical properties of normal distributions that make them so useful for analysis.