Levene's Test for Equality of Variances with Python

Levene's test is a statistical procedure for testing equality of variances (also sometimes called homoscedasticity or homogeneity of variances) between two or more sample populations. Several commonly used statistical routines such as the t-test and analysis of variance assume the populations have equal variances. Therefore Levene's test is often employed to test this assumption before performing these tests. Levene's test is an alternative to Bartlett's test, another procedure for testing equality of variances between $k$ sample populations. Bartlett's test is sensitive to departures from normality, and thus Levene's test is preferred when the population samples are generally not distributed.

The null and alternative hypotheses of Levene's test can be generally stated as follows:

  • $H_0$: All of the $k$ sample populations have equal variances.
  • $H_A$: At least one of the $k$ sample population variances are not equal.

Test Procedure

The test statistic, $W$ used in Levene's test is defined as:

$$ W = \frac{(N - k)}{(k - 1)} \frac{\sum^k_{i=1} n_i (Z_{i.} - Z_{..})^2}{\sum^k_{i=1} \sum^{n_i}_{j=1} (Z_{ij} - Z_{i.})^2} $$

where,

  • $k$ is the number of groups
  • $n_i$ is the number of samples belonging to the $i$-th group.
  • $N$ is the total number of samples.
  • $Y_{ij}$ is the $j$-th observation from the $i$-th group.

and,

$$ Z_{i.} = \frac{1}{n_i} \sum^{n_i}_{j=1} Z_{ij} $$$$ Z_{..} = \frac{1}{N} \sum^k_{i=1} \sum^{n_i}_{j=1} Z_{ij} $$

are the mean of the calculated $Z_{ij}$ for group $i$ and mean of all $Z_{ij}$, respectively.

In Levene's Test, $Z_{ij}$ can have one of the following three definitions:

  • $|Y_{ij} - \tilde{Y}_i|$, where $\tilde{Y}_i$ is the median of the $i$-th group.
  • $|Y_{ij} - \bar{Y}_i|$, where $\bar{Y}_i$ is the mean of the $i$-th group.
  • $|Y_{ij} - \bar{Y}^{\prime}_i|$, where $\bar{Y}^{\prime}_i$ is the trimmed mean of the $i$-th group.

Levene originally proposed using the mean, but a paper by Brown and Forsythe in 1974 extended Levene's test to use the median or the trimmed mean. The median is generally recommended as it more robust against non-normal data, but if the underlying distribution of the sample is known, an alternative metric may provide better performance.

The $W$ test statistic is approximately $F$-distributed with $k - 1$ and $n - k$ degrees of freedom, $F(\alpha, k-1, n-k)$

Levene's Test in Python

In [1]:
import numpy as np
import pandas as pd
from scipy.stats import f
import numpy_indexed as npi

The PlantGrowth dataset is available in R as part of its standard datasets and can also be downloaded here. After downloading the data, we load it into memory with pandas' read_csv function. Once the data is loaded, we transform the resulting DataFrame into a numpy array with the .to_numpy method. The first three rows of the dataset are then printed to get a sense of what the data contains.

In [2]:
plants = pd.read_csv('../../data/PlantGrowth.csv')
plants = plants.to_numpy()
plants[:3]
Out[2]:
array([[1, 4.17, 'ctrl'],
       [2, 5.58, 'ctrl'],
       [3, 5.18, 'ctrl']], dtype=object)

As the dataset description stated, there are two columns (three including the index column), one containing the plant weight of the sample and the sample in which the group belongs. There are three sample groups in the dataset, which we can confirm using numpy's unique function.

In [3]:
list(np.unique(plants[:,2]))
Out[3]:
['ctrl', 'trt1', 'trt2']

The first step in computing Levene's test is to find the $Z_{ij}$ for each observation. To do this, we first take the median of all three sample groups and append the medians array to the original plants array with numpy's column_stack function. With the medians of each group computed, we can then find the $Z_{ij}$ by subtracting the row observation with its respective group median and appending the array to the plants array. Lastly, we display the first five rows of the newly appended plants array to verify our operations proceeded correctly.

In [4]:
group_obs = np.array([i for _, i in npi.group_by(plants[:, 2], plants[:, 2], len)])

group_medians = []

for i in plants[:, 2]:
    group_medians.append(np.median(plants[np.where(plants[:, 2] == i)][:, 1]))

plants = np.column_stack([plants, np.array(group_medians)])

zij = np.abs(np.array(plants[:, 1] - plants[:, 3]))
plants = np.column_stack([plants, zij])

plants[:5]
Out[4]:
array([[1, 4.17, 'ctrl', 5.154999999999999, 0.9849999999999994],
       [2, 5.58, 'ctrl', 5.154999999999999, 0.4250000000000007],
       [3, 5.18, 'ctrl', 5.154999999999999, 0.025000000000000355],
       [4, 6.11, 'ctrl', 5.154999999999999, 0.955000000000001],
       [5, 4.5, 'ctrl', 5.154999999999999, 0.6549999999999994]],
      dtype=object)

As expected, the newly created plants array has the group median and the corresponding $Z_{ij}$ value for each observation row. Now that we have the $Z_{ij}$ for each observation and group medians, we can proceed to the next step.

The next operation in performing Levene's test is to find the group medians of the recently calculated $Z_{ij}$ values. This operation can be done as before when we found the group medians of the sample data, except we replace the sample data with the respective $Z_{ij}$ value.

In [5]:
zij_group_means = []

for i in plants[:, 2]:
    zij_group_means.append(np.mean(plants[np.where(plants[:, 2] == i)][:, 4]))
    
plants = np.column_stack([plants, np.array(zij_group_means)])
plants[:5]
Out[5]:
array([[1, 4.17, 'ctrl', 5.154999999999999, 0.9849999999999994, 0.442],
       [2, 5.58, 'ctrl', 5.154999999999999, 0.4250000000000007, 0.442],
       [3, 5.18, 'ctrl', 5.154999999999999, 0.025000000000000355, 0.442],
       [4, 6.11, 'ctrl', 5.154999999999999, 0.955000000000001, 0.442],
       [5, 4.5, 'ctrl', 5.154999999999999, 0.6549999999999994, 0.442]],
      dtype=object)

With the group $Z_{ij}$ medians found, we can proceed to compute the $W$ statistic. To help keep the computations simple, we split the $W$ statistic numerator and denominator into their own variables.

In [8]:
n = plants.shape[0]
k = len(np.unique(plants[:,2]))

zij_group_means = np.array([i for _, i in npi.group_by(plants[:, 2], plants[:, 4], np.mean)])

total_mean = np.mean(plants[:, 4])

numerator = np.sum(group_obs * (zij_group_means - total_mean) ** 2)

denominator = np.sum((plants[:, 4] - plants[:, 5]) ** 2)

w = (n - k) / (k - 1) * (numerator / denominator)

w
Out[8]:
1.1191856948703904

Levene's test reported a $W$ statistic of approximately $1.12$. To find the corresponding p-value, we use the cumulative distribution function from scipy.stats.f variable with $k-1$ and $n-k$ degrees of freedom.

In [7]:
1 - f.cdf(w, k - 1, n - k)
Out[7]:
0.3412266241254738

The reported p-value exceeds $0.05$; therefore, we accept the null hypothesis that the sample groups have equal variances. With this knowledge in hand, we can then proceed to perform an analysis of variance or another similar test as we know the assumption of homogeneity of variance holds.

Related Posts