OLS_DIAGNOSTICS
Overview
The OLS_DIAGNOSTICS function performs a suite of diagnostic tests on Ordinary Least Squares (OLS) regression residuals to assess whether the standard regression assumptions are satisfied. Validating these assumptions is critical because the reliability of OLS estimates depends on conditions including homoskedasticity, absence of autocorrelation, and normally distributed errors.
This implementation uses the statsmodels library, which provides a comprehensive set of regression diagnostic tests. The function performs three key diagnostic tests:
Heteroskedasticity: Breusch-Pagan Test
The Breusch-Pagan test, developed by Trevor Breusch and Adrian Pagan (1979), tests whether the variance of regression errors is constant across observations. The test regresses squared residuals on the original predictors and uses a Lagrange Multiplier (LM) statistic that follows a chi-squared distribution under the null hypothesis of homoskedasticity. A significant p-value (< 0.05) indicates heteroskedasticity is present, which may require weighted least squares or heteroskedasticity-consistent standard errors.
Autocorrelation: Ljung-Box Test
The Ljung-Box test, developed by Greta M. Ljung and George E. P. Box (1978), tests whether residuals exhibit serial correlation. The test statistic is:
Q = n(n+2) \sum_{k=1}^{h} \frac{\hat{\rho}_k^2}{n-k}
where n is the sample size, \hat{\rho}_k is the sample autocorrelation at lag k, and h is the number of lags tested. Under the null hypothesis of no autocorrelation, Q follows a chi-squared distribution with h degrees of freedom.
Normality: Jarque-Bera Test
The Jarque-Bera test assesses whether residuals follow a normal distribution by examining skewness (S) and kurtosis (K):
JB = \frac{n}{6}\left(S^2 + \frac{(K-3)^2}{4}\right)
The test statistic asymptotically follows a chi-squared distribution with two degrees of freedom. A significant result indicates non-normality, which may affect the validity of hypothesis tests and confidence intervals derived from the regression.
The function returns a table containing each test name, test statistic, p-value, and an interpretation based on a 0.05 significance threshold.
This example function is provided as-is without any representation of accuracy.
Excel Usage
=OLS_DIAGNOSTICS(residuals, exog, diag_test)
residuals(list[list], required): Column vector of regression residuals from an OLS model.exog(list[list], required): Matrix of independent variables (predictors) from the OLS model.diag_test(str, optional, default: “all”): Which diagnostic test(s) to perform.
Returns (list[list]): 2D list with diagnostic tests, or error message string.
Examples
Example 1: Demo case 1
Inputs:
| residuals | exog | diag_test | |
|---|---|---|---|
| 0.1 | 1 | 1 | all |
| 0.2 | 1 | 2 | |
| -0.1 | 1 | 3 | |
| 0.05 | 1 | 4 | |
| -0.15 | 1 | 5 | |
| 0.08 | 1 | 6 | |
| -0.05 | 1 | 7 | |
| 0.12 | 1 | 8 | |
| -0.08 | 1 | 9 | |
| 0.03 | 1 | 10 |
Excel formula:
=OLS_DIAGNOSTICS({0.1;0.2;-0.1;0.05;-0.15;0.08;-0.05;0.12;-0.08;0.03}, {1,1;1,2;1,3;1,4;1,5;1,6;1,7;1,8;1,9;1,10}, "all")
Expected output:
| test_name | statistic | p_value | interpretation |
|---|---|---|---|
| Breusch-Pagan | 2.306 | 0.1289 | No problem detected |
| Ljung-Box | 4.819 | 0.08988 | No problem detected |
| Jarque-Bera | 0.5026 | 0.7778 | Normal |
Example 2: Demo case 2
Inputs:
| residuals | exog | diag_test | |
|---|---|---|---|
| 0.5 | 1 | 1 | heteroskedasticity |
| -0.3 | 1 | 2 | |
| 0.2 | 1 | 3 | |
| -0.1 | 1 | 4 | |
| 0.4 | 1 | 5 | |
| -0.2 | 1 | 6 |
Excel formula:
=OLS_DIAGNOSTICS({0.5;-0.3;0.2;-0.1;0.4;-0.2}, {1,1;1,2;1,3;1,4;1,5;1,6}, "heteroskedasticity")
Expected output:
| test_name | statistic | p_value | interpretation |
|---|---|---|---|
| Breusch-Pagan | 1.564 | 0.2111 | No problem detected |
Example 3: Demo case 3
Inputs:
| residuals | exog | diag_test | |
|---|---|---|---|
| 0.01 | 1 | 2 | normality |
| 0.02 | 1 | 3 | |
| -0.01 | 1 | 4 | |
| 0.03 | 1 | 5 | |
| -0.02 | 1 | 6 | |
| 0.015 | 1 | 7 | |
| -0.015 | 1 | 8 | |
| 0.025 | 1 | 9 |
Excel formula:
=OLS_DIAGNOSTICS({0.01;0.02;-0.01;0.03;-0.02;0.015;-0.015;0.025}, {1,2;1,3;1,4;1,5;1,6;1,7;1,8;1,9}, "normality")
Expected output:
| test_name | statistic | p_value | interpretation |
|---|---|---|---|
| Jarque-Bera | 0.8672 | 0.6482 | Normal |
Example 4: Demo case 4
Inputs:
| residuals | exog | diag_test | |
|---|---|---|---|
| 0.1 | 1 | 1.5 | autocorrelation |
| 0.15 | 1 | 2.5 | |
| 0.12 | 1 | 3.5 | |
| 0.18 | 1 | 4.5 | |
| 0.14 | 1 | 5.5 | |
| 0.16 | 1 | 6.5 | |
| 0.13 | 1 | 7.5 | |
| 0.17 | 1 | 8.5 |
Excel formula:
=OLS_DIAGNOSTICS({0.1;0.15;0.12;0.18;0.14;0.16;0.13;0.17}, {1,1.5;1,2.5;1,3.5;1,4.5;1,5.5;1,6.5;1,7.5;1,8.5}, "autocorrelation")
Expected output:
| test_name | statistic | p_value | interpretation |
|---|---|---|---|
| Ljung-Box | 1.957 | 0.1618 | No problem detected |
Python Code
import math
import numpy as np
from statsmodels.stats.diagnostic import het_breuschpagan, acorr_ljungbox
from statsmodels.stats.stattools import jarque_bera
def ols_diagnostics(residuals, exog, diag_test='all'):
"""
Performs diagnostic tests on OLS regression residuals.
See: https://www.statsmodels.org/stable/diagnostic.html
This example function is provided as-is without any representation of accuracy.
Args:
residuals (list[list]): Column vector of regression residuals from an OLS model.
exog (list[list]): Matrix of independent variables (predictors) from the OLS model.
diag_test (str, optional): Which diagnostic test(s) to perform. Valid options: All Tests, Heteroskedasticity, Autocorrelation, Normality. Default is 'all'.
Returns:
list[list]: 2D list with diagnostic tests, or error message string.
"""
def to2d(x):
return [[x]] if not isinstance(x, list) else x
def validate_2d_numeric(data, name):
# Validate 2D list structure
if not isinstance(data, list):
return f"Invalid input: {name} must be a 2D list."
if not data:
return f"Invalid input: {name} cannot be empty."
for i, row in enumerate(data):
if not isinstance(row, list):
return f"Invalid input: {name} row {i} must be a list."
if not row:
return f"Invalid input: {name} row {i} cannot be empty."
for j, val in enumerate(row):
if not isinstance(val, (int, float)):
return f"Invalid input: {name}[{i}][{j}] must be numeric."
if math.isnan(val) or math.isinf(val):
return f"Invalid input: {name}[{i}][{j}] must be finite."
return None
def interpret_pvalue(p_value, test_name):
# Interpretation based on p-value threshold of 0.05
if test_name in ['heteroskedasticity', 'autocorrelation']:
if p_value < 0.05:
return 'Problem detected'
else:
return 'No problem detected'
elif test_name == 'normality':
if p_value < 0.05:
return 'Non-normal'
else:
return 'Normal'
return 'N/A'
# Normalize inputs
residuals_2d = to2d(residuals)
exog_2d = to2d(exog)
# Validate inputs
error = validate_2d_numeric(residuals_2d, 'residuals')
if error:
return error
error = validate_2d_numeric(exog_2d, 'exog')
if error:
return error
# Validate test_type parameter
valid_tests = ['all', 'heteroskedasticity', 'autocorrelation', 'normality']
if diag_test not in valid_tests:
return f"Invalid input: diag_test must be one of {valid_tests}."
# Convert to numpy arrays
try:
resid_array = np.array(residuals_2d, dtype=float)
exog_array = np.array(exog_2d, dtype=float)
except Exception as exc:
return f"Invalid input: unable to convert inputs to arrays: {exc}"
# Flatten residuals to 1D array
if resid_array.shape[1] != 1:
return "Invalid input: residuals must be a column vector (2D list with 1 column)."
resid_1d = resid_array.flatten()
# Validate dimensions
n_obs = len(resid_1d)
if exog_array.shape[0] != n_obs:
return f"Invalid input: number of residuals ({n_obs}) must match number of observations in exog ({exog_array.shape[0]})."
if n_obs < 3:
return "Invalid input: insufficient data for diagnostic tests (need at least 3 observations)."
# Initialize results
results = [['test_name', 'statistic', 'p_value', 'interpretation']]
# Determine which tests to run
tests_to_run = []
if diag_test == 'all':
tests_to_run = ['heteroskedasticity', 'autocorrelation', 'normality']
else:
tests_to_run = [diag_test]
# Run requested tests
try:
for test in tests_to_run:
if test == 'heteroskedasticity':
# Breusch-Pagan test
lm_stat, lm_pvalue, f_stat, f_pvalue = het_breuschpagan(resid_1d, exog_array)
interp = interpret_pvalue(lm_pvalue, 'heteroskedasticity')
results.append(['Breusch-Pagan', float(lm_stat), float(lm_pvalue), interp])
elif test == 'autocorrelation':
# Ljung-Box test (using lag=min(10, n_obs//5))
max_lag = min(10, max(1, n_obs // 5))
lb_result = acorr_ljungbox(resid_1d, lags=max_lag, return_df=True)
# Use the last lag result
last_row = lb_result.iloc[-1]
lb_stat = float(last_row['lb_stat'])
lb_pvalue = float(last_row['lb_pvalue'])
interp = interpret_pvalue(lb_pvalue, 'autocorrelation')
results.append(['Ljung-Box', lb_stat, lb_pvalue, interp])
elif test == 'normality':
# Jarque-Bera test
jb_stat, jb_pvalue, skew, kurtosis = jarque_bera(resid_1d)
interp = interpret_pvalue(jb_pvalue, 'normality')
results.append(['Jarque-Bera', float(jb_stat), float(jb_pvalue), interp])
except Exception as exc:
return f"Error running diagnostic tests: {exc}"
# Validate results
for row in results[1:]:
if not isinstance(row[1], (int, float)) or math.isnan(row[1]) or math.isinf(row[1]):
return "Error: test produced invalid statistic."
if not isinstance(row[2], (int, float)) or math.isnan(row[2]) or math.isinf(row[2]):
return "Error: test produced invalid p-value."
return results