5 Hidden Assumptions in Linear Regression That Most People Ignore

5 Hidden Assumptions in Linear Regression That Most People Ignore
Image Author
 

You run a regression. The R² is 0.87. The coefficients are significant. You write it up, ship the model, and move on. Then someone asks whether you tested for homoscedasticity. You check your notes. You did not. You checked R². You checked p-values. You called it done.

That is how most people use linear regression, and it is how most people end up with models that are technically fitted but statistically unreliable. The five assumptions of linear regression are not bureaucratic checkboxes — they are the conditions under which the underlying math actually works. Violate them and your standard errors are wrong, your p-values mislead you, your confidence intervals lie, and your coefficients may not mean what you think they mean.

This article is a diagnostic checklist. For each assumption: what it means in plain language, what breaks when you violate it, how to test it in Python, and what to do when you catch a problem. Run this checklist before you interpret a single coefficient.

The five assumptions are linearity, independence of errors, homoscedasticity, normality of residuals, and no multicollinearity. Linearity and independence are foundational — if either breaks, residual-based tests for the other three can mislead you. Check them in order. Some violations also compound each other: heteroscedasticity and non-normality often appear together, and both can be symptoms of an underlying linearity problem that was never addressed.

Assumption 1: Linearity

Linear regression assumes that the relationship between each predictor and the outcome variable is linear. This is not an arbitrary restriction — it is the entire premise of the model. If the true relationship is a curve and you fit a straight line, the model will be systematically wrong in predictable ways, and no amount of tuning will fix a fundamentally misspecified structure.

Why Most People Miss It

R² is the culprit. A model can produce a decent R² on non-linear data because R² measures the proportion of variance explained, not whether the relationship is correctly specified. A curved relationship has variance, and a linear model can explain a meaningful chunk of it while still being structurally wrong. People see 0.80, feel satisfied, and never look at the residuals.

What Breaks

The coefficients are biased. Predictions are systematically too high in some ranges and too low in others. The residual plot will show a curve or arch pattern instead of random scatter — the clearest signal that the model is misspecified.

How to Detect It

Plot residuals against fitted values. If the relationship is truly linear, the residuals should form a flat, randomly scattered horizontal band around zero. Any visible curve, arch, or trend is a red flag. For multiple predictors, partial regression plots isolate each predictor’s relationship with the outcome while controlling for the others.

For a formal test, the Ramsey RESET test checks whether adding powers of the fitted values improves the model; if it does, the original specification was missing non-linear structure.

Python Code

 
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import statsmodels.api as sm
from statsmodels.stats.diagnostic import linear_reset

np.random.seed(42)
n = 200

# Generate data with a true non-linear (quadratic) relationship
x = np.linspace(1, 10, n)
y_true = 3 * x**2 - 10 * x + 5          # True relationship is curved
y = y_true + np.random.normal(0, 15, n)  # Add noise

X = sm.add_constant(x)  # Add intercept column to predictor matrix

# Fit a simple linear model (deliberately misspecified -- true relationship is quadratic)
model = sm.OLS(y, X).fit()
residuals = model.resid
fitted = model.fittedvalues

# --- Plot 1: Residuals vs Fitted for the misspecified linear model ---
fig, axes = plt.subplots(1, 2, figsize=(12, 5))

axes[0].scatter(fitted, residuals, alpha=0.5, color="steelblue", edgecolors="none")
axes[0].axhline(0, color="red", linewidth=1.2, linestyle="--")
axes[0].set_title("Linear Model -- Non-linear Data\n(Notice the arch in residuals)")
axes[0].set_xlabel("Fitted Values")
axes[0].set_ylabel("Residuals")

# Fix: add a quadratic term to the model
X_quad = sm.add_constant(np.column_stack([x, x**2]))  # Include x and x-squared
model_quad = sm.OLS(y, X_quad).fit()
residuals_quad = model_quad.resid
fitted_quad = model_quad.fittedvalues

axes[1].scatter(fitted_quad, residuals_quad, alpha=0.5, color="seagreen", edgecolors="none")
axes[1].axhline(0, color="red", linewidth=1.2, linestyle="--")
axes[1].set_title("Quadratic Model -- Same Data\n(Residuals are now flat and random)")
axes[1].set_xlabel("Fitted Values")
axes[1].set_ylabel("Residuals")

plt.tight_layout()
plt.savefig("linearity_residuals.png", dpi=150)
plt.show()

# --- RESET test: formally check whether non-linear terms improve the model ---
reset_result = linear_reset(model, power=2, use_f=True)
print(f"\nRESET Test on linear model:")
print(f"  F-statistic: {reset_result.fvalue:.4f}")
print(f"  p-value:     {reset_result.pvalue:.4f}")
print(f"  Verdict: {'Non-linearity detected -- model misspecified' if reset_result.pvalue < 0.05 else 'No non-linearity detected'}")

reset_result_quad = linear_reset(model_quad, power=2, use_f=True)
print(f"\nRESET Test on quadratic model:")
print(f"  F-statistic: {reset_result_quad.fvalue:.4f}")
print(f"  p-value:     {reset_result_quad.pvalue:.4f}")
print(f"  Verdict: {'Non-linearity detected' if reset_result_quad.pvalue < 0.05 else 'No non-linearity detected -- model correctly specified'}")

Linearity
What this code does: The first model fits a straight line to deliberately wrong quadratic data. The residuals vs. fitted plot shows a clear arch pattern — the visual signature of a linearity violation. The RESET test formalizes this: a significant p-value (below 0.05) means the model is missing non-linear structure. The fix is adding x**2 to the predictor matrix. The second model captures the true quadratic relationship, and both the residual plot and the RESET test confirm the fix worked.

Assumption 2: Independence of Errors

The residuals must be independent of each other — the error in one observation should tell you nothing about the error in another. This sounds abstract until you consider how data is actually collected: students from the same classroom, patients from the same hospital, transactions from the same user, measurements taken on consecutive days. Observations that share context tend to share error structure, and that correlation between errors is exactly what this assumption prohibits.

Why Most People Miss It

Independence is taught almost exclusively in the context of time series, which leads practitioners working with cross-sectional data to assume they are safe. They are often not. Any dataset with natural clustering — surveys from the same household, employees within the same company, students within the same school — can produce correlated errors that are invisible until you specifically look for them.

As research on regression assumptions confirms, this is one of the more serious violations. When errors are correlated, standard errors are typically understated, p-values are too small, and predictors that appear significant may not be.

How to Detect It

For time-ordered data, the Durbin-Watson test checks for first-order autocorrelation in residuals. The statistic ranges from 0 to 4, with a value near 2 indicating no autocorrelation. Values closer to 0 indicate positive autocorrelation; values closer to 4 indicate negative autocorrelation. Values between 1.5 and 2.5 are generally considered acceptable.

For clustered cross-sectional data, plot residuals against the grouping variable and look for within-group patterns.

The Independence Checklist

Work through these four questions before you trust your standard errors:

  1. Were observations drawn independently from the population, or are there natural groupings in your sampling design?
  2. Is there a time ordering to your data? If yes, run the Durbin-Watson test.
  3. Do observations share context that could produce correlated errors — same location, same subject, same time period?
  4. Have you plotted residuals against any potential grouping variable to check for within-group patterns?

Python Code

 
import numpy as np
import statsmodels.api as sm
from statsmodels.stats.stattools import durbin_watson

np.random.seed(42)
n = 100

# --- Example 1: Residuals with positive autocorrelation (violation) ---
# Simulate time-series data where each error is correlated with the previous one
x = np.arange(n)
errors_autocorrelated = np.zeros(n)
errors_autocorrelated[0] = np.random.normal(0, 1)

for t in range(1, n):
    # Each error is 0.8 * previous error + new noise -- strong positive autocorrelation
    errors_autocorrelated[t] = 0.8 * errors_autocorrelated[t - 1] + np.random.normal(0, 1)

y_autocorrelated = 2 * x + errors_autocorrelated

X = sm.add_constant(x)
model_auto = sm.OLS(y_autocorrelated, X).fit()

dw_autocorrelated = durbin_watson(model_auto.resid)

print("=== Autocorrelated Errors (Violation) ===")
print(f"Durbin-Watson statistic: {dw_autocorrelated:.4f}")
print(f"Interpretation: {'Near 2 -- no autocorrelation' if 1.5 < dw_autocorrelated < 2.5 else 'Outside 1.5-2.5 range -- autocorrelation likely present'}")

# --- Example 2: Residuals with no autocorrelation (assumption met) ---
errors_independent = np.random.normal(0, 1, n)  # Each error is truly independent
y_independent = 2 * x + errors_independent

model_indep = sm.OLS(y_independent, X).fit()
dw_independent = durbin_watson(model_indep.resid)

print("\n=== Independent Errors (Assumption Met) ===")
print(f"Durbin-Watson statistic: {dw_independent:.4f}")
print(f"Interpretation: {'Near 2 -- no autocorrelation detected' if 1.5 < dw_independent < 2.5 else 'Outside 1.5-2.5 -- investigate further'}")

# --- Visual: plot residuals over time to make autocorrelation visible ---
import matplotlib.pyplot as plt

fig, axes = plt.subplots(1, 2, figsize=(12, 4))

axes[0].plot(model_auto.resid, color="firebrick", linewidth=0.8)
axes[0].axhline(0, color="black", linewidth=1, linestyle="--")
axes[0].set_title(f"Autocorrelated Residuals\n(DW = {dw_autocorrelated:.2f} -- violation)")
axes[0].set_xlabel("Observation order")
axes[0].set_ylabel("Residual")

axes[1].plot(model_indep.resid, color="steelblue", linewidth=0.8)
axes[1].axhline(0, color="black", linewidth=1, linestyle="--")
axes[1].set_title(f"Independent Residuals\n(DW = {dw_independent:.2f} -- assumption met)")
axes[1].set_xlabel("Observation order")
axes[1].set_ylabel("Residual")

plt.tight_layout()
plt.savefig("independence_residuals.png", dpi=150)
plt.show()

Independence of Errors
What this code does: The first model builds autocorrelation directly into the error structure — each error is 0.8 times the previous error plus new noise (AR(1) positive autocorrelation). The Durbin-Watson statistic will land well below 1.5, signaling a clear violation. The second model uses truly independent errors, and its DW statistic will sit close to 2. The residual plots make the difference visible: autocorrelated residuals show smooth runs in the same direction rather than random noise.

Assumption 3: Homoscedasticity

The variance of the residuals must be constant across all levels of the predictors. If you plot residuals against fitted values, they should scatter in a flat, even horizontal band. Heteroscedasticity appears as a cone or fan shape — residuals spread out as fitted values get larger, or contract, or follow some other non-constant pattern.

Why It Is Dangerous

Heteroscedasticity does not necessarily wreck your coefficient estimates — predictions can still be in the right ballpark. What it wrecks is your standard errors and, by extension, every piece of inference you draw from the model. Your p-values are wrong. Your confidence intervals are wrong. A predictor might look significant when it is not, or vice versa.

According to Statistics Solutions, a scatterplot of residuals versus predicted values is the primary diagnostic; a cone-shaped pattern confirms heteroscedasticity.

The Homoscedasticity Checklist

Before trusting any inference from your model, work through this:

  1. Does a plot of residuals vs. fitted values show a cone, fan, or any other non-uniform spread pattern?
  2. Does residual variance appear to increase or decrease with the magnitude of fitted values?
  3. Does the Breusch-Pagan test return a p-value below 0.05? (The null hypothesis is homoscedasticity; rejecting it means heteroscedasticity is present.)
  4. If any of the above are true: apply a log or square root transformation to the outcome, use weighted least squares, or use robust (HC3) standard errors.

Python Code

 
import numpy as np
import statsmodels.api as sm
from statsmodels.stats.diagnostic import het_breuschpagan
import matplotlib.pyplot as plt

np.random.seed(42)
n = 200
x = np.linspace(1, 10, n)

# Generate heteroscedastic data: variance of errors grows with x
# This is the classic "cone" pattern -- larger x = more spread in residuals
errors_hetero = np.random.normal(0, 0.8 * x, n)   # Standard deviation scales with x
y_hetero = 4 * x + 10 + errors_hetero

# Generate homoscedastic data: constant error variance regardless of x
errors_homo = np.random.normal(0, 3, n)            # Constant standard deviation
y_homo = 4 * x + 10 + errors_homo

X = sm.add_constant(x)

model_hetero = sm.OLS(y_hetero, X).fit()
model_homo   = sm.OLS(y_homo,   X).fit()

# --- Breusch-Pagan test ---
# Returns: (LM statistic, LM p-value, F statistic, F p-value)
# We use the LM p-value (index 1) as the primary result
bp_hetero = het_breuschpagan(model_hetero.resid, model_hetero.model.exog)
bp_homo   = het_breuschpagan(model_homo.resid,   model_homo.model.exog)

print("=== Heteroscedastic Model ===")
print(f"Breusch-Pagan LM statistic: {bp_hetero[0]:.4f}")
print(f"p-value:                    {bp_hetero[1]:.4f}")
print(f"Verdict: {'Heteroscedasticity detected -- standard errors are unreliable' if bp_hetero[1] < 0.05 else 'No heteroscedasticity detected'}")

print("\n=== Homoscedastic Model ===")
print(f"Breusch-Pagan LM statistic: {bp_homo[0]:.4f}")
print(f"p-value:                    {bp_homo[1]:.4f}")
print(f"Verdict: {'Heteroscedasticity detected' if bp_homo[1] < 0.05 else 'Assumption satisfied -- residual variance is constant'}")

# --- Residual plots ---
fig, axes = plt.subplots(1, 2, figsize=(12, 5))

axes[0].scatter(model_hetero.fittedvalues, model_hetero.resid,
                alpha=0.5, color="firebrick", edgecolors="none")
axes[0].axhline(0, color="black", linewidth=1.2, linestyle="--")
axes[0].set_title(f"Heteroscedastic Model\n(Breusch-Pagan p = {bp_hetero[1]:.4f})")
axes[0].set_xlabel("Fitted Values")
axes[0].set_ylabel("Residuals")

axes[1].scatter(model_homo.fittedvalues, model_homo.resid,
                alpha=0.5, color="seagreen", edgecolors="none")
axes[1].axhline(0, color="black", linewidth=1.2, linestyle="--")
axes[1].set_title(f"Homoscedastic Model\n(Breusch-Pagan p = {bp_homo[1]:.4f})")
axes[1].set_xlabel("Fitted Values")
axes[1].set_ylabel("Residuals")

plt.tight_layout()
plt.savefig("homoscedasticity_residuals.png", dpi=150)
plt.show()

# --- Fix: robust standard errors (HC3) ---
# HC3 (MacKinnon-White) is the most commonly recommended heteroscedasticity-
# consistent covariance estimator for small to medium samples
model_robust = model_hetero.get_robustcov_results(cov_type="HC3")

print("\n=== Comparison: Regular vs Robust Standard Errors ===")
print(f"{'Predictor':<12} {'OLS SE':>10} {'HC3 Robust SE':>15}")
print("-" * 40)
for i, name in enumerate(model_hetero.model.exog_names):
    print(f"{name:<12} {model_hetero.bse[i]:>10.4f} {model_robust.bse[i]:>15.4f}")

Homoscedasticity
What this code does: The heteroscedastic model builds in a variance that scales directly with x — the textbook cone pattern. The Breusch-Pagan test will return a p-value well below 0.05, confirming the violation. The homoscedastic model uses constant error variance, and the test returns a non-significant p-value. The fix uses get_robustcov_results(cov_type=”HC3″) to obtain heteroscedasticity-consistent standard errors. HC3 is recommended for small to medium samples as it provides a more conservative correction than HC1 or HC2. The comparison table shows how much the standard errors change between the naive and robust estimates.

Assumption 4: Normality of Residuals

The residuals — not the predictors, not the outcome variable — must be normally distributed around zero. Testing whether your input variables are normally distributed is irrelevant to this assumption. What the model requires is that the errors in its predictions follow a normal distribution.

Why It Is Misunderstood

Two layers of confusion exist here. First, people test the wrong thing — they run normality tests on their predictors or outcome, not on the residuals from the fitted model. Second, people over-weight this assumption relative to the others. As the research confirms, violating normality is less catastrophic than violating linearity or independence. In large samples, the Central Limit Theorem provides a natural cushion. This assumption matters most in small samples where you cannot rely on that cushion.

What Breaks

In small samples: confidence intervals become unreliable, prediction intervals are off, and hypothesis tests lose their validity. In large samples, the consequences are typically mild. A mild skew in residuals with n = 500 is a non-issue; the same skew with n = 30 is a real problem.

How to Detect It

The Q-Q (quantile-quantile) plot is the primary visual diagnostic. Plot the quantiles of your residuals against those of a theoretical normal distribution — if the assumption holds, the points should follow the diagonal reference line closely. Systematic S-curves indicate skewness; heavy tails show up as points pulling away from the line at both ends.

For formal testing, the Shapiro-Wilk test is preferred for small samples (n < 50). The Kolmogorov-Smirnov test scales better with larger samples. In very large datasets, both tests will flag even trivial departures as significant, so use visual inspection alongside any formal test.

Python Code

 
import numpy as np
import statsmodels.api as sm
import matplotlib.pyplot as plt
from scipy import stats

np.random.seed(42)
n = 100
x = np.linspace(0, 10, n)

# --- Case 1: Normally distributed errors (assumption met) ---
y_normal = 3 * x + 5 + np.random.normal(0, 2, n)

X = sm.add_constant(x)
model_normal = sm.OLS(y_normal, X).fit()
resid_normal = model_normal.resid

# Shapiro-Wilk test: null hypothesis is that residuals are normally distributed
# p > 0.05 = fail to reject normality (assumption plausibly met)
# p < 0.05 = reject normality (assumption violated) sw_stat, sw_p = stats.shapiro(resid_normal) print("=== Normal Residuals ===") print(f"Shapiro-Wilk statistic: {sw_stat:.4f}") print(f"p-value: {sw_p:.4f}") print(f"Verdict: {'Normality not rejected -- assumption plausibly met' if sw_p > 0.05 else 'Normality rejected -- investigate further'}")

# --- Case 2: Skewed errors (assumption violated) ---
# Exponential distribution produces right-skewed, non-normal errors
y_skewed = 3 * x + 5 + np.random.exponential(scale=3, size=n) - 3

model_skewed = sm.OLS(y_skewed, X).fit()
resid_skewed = model_skewed.resid

sw_stat_sk, sw_p_sk = stats.shapiro(resid_skewed)
print("\n=== Skewed Residuals (Violation) ===")
print(f"Shapiro-Wilk statistic: {sw_stat_sk:.4f}")
print(f"p-value:                {sw_p_sk:.4f}")
print(f"Verdict: {'Normality not rejected' if sw_p_sk > 0.05 else 'Normality rejected -- residuals are non-normal'}")

# --- Q-Q plots: visual inspection ---
fig, axes = plt.subplots(1, 2, figsize=(12, 5))

# probplot draws the Q-Q plot and returns the theoretical vs sample quantiles
sm.qqplot(resid_normal, line="s", ax=axes[0], alpha=0.6,
          markerfacecolor="steelblue", markeredgewidth=0)
axes[0].set_title(f"Normal Residuals -- Q-Q Plot\n(Shapiro-Wilk p = {sw_p:.4f})")
axes[0].set_xlabel("Theoretical Quantiles")
axes[0].set_ylabel("Sample Quantiles")

sm.qqplot(resid_skewed, line="s", ax=axes[1], alpha=0.6,
          markerfacecolor="firebrick", markeredgewidth=0)
axes[1].set_title(f"Skewed Residuals -- Q-Q Plot\n(Shapiro-Wilk p = {sw_p_sk:.4f})")
axes[1].set_xlabel("Theoretical Quantiles")
axes[1].set_ylabel("Sample Quantiles")

plt.tight_layout()
plt.savefig("normality_qq.png", dpi=150)
plt.show()

Normality of Residuals
What this code does: Two models are fitted — one with truly normal errors, one with right-skewed errors from an exponential distribution. The Shapiro-Wilk test formalizes the difference: the normal model returns a non-significant p-value, while the skewed model returns a significant one. The Q-Q plots make the violation visible: normal residuals follow the diagonal reference line closely, while skewed residuals deviate at the upper end. In sm.qqplot, the line=”s” argument draws a standardized reference line fit to the sample data — the most informative option for regression diagnostics.

Assumption 5: No Multicollinearity

The predictor variables must not be highly correlated with each other. Some correlation is normal and expected. High multicollinearity — where one predictor can be closely predicted from a linear combination of the others — is the problem. The model cannot reliably separate the individual effects of predictors that move together.

Why It Is Dangerous

This is the assumption whose violation is most deceptive. The model’s overall predictions can still be accurate, and R² is unaffected. The damage shows up in the coefficients: high multicollinearity inflates standard errors, drives p-values up, and depresses t-statistics. A predictor that genuinely affects the outcome can appear non-significant simply because it shares too much information with another predictor. You end up with a model that predicts well but whose individual coefficients are meaningless for interpretation or inference.

The VIF Threshold Guide

The Variance Inflation Factor measures how much the variance of a coefficient is inflated by its correlation with the other predictors. Every predictor in your model gets its own VIF.

VIF Range Interpretation
VIF = 1 No correlation with other predictors
1 to 5 Moderate correlation, generally acceptable
5 to 10 Concerning — investigate further
Above 10 Severe multicollinearity — urgent action required

VIF above 10 is the widely cited threshold for severe multicollinearity, though some practitioners use 5 as a more conservative cutoff.

What to Do

Remove one of the correlated predictors. If both carry theoretical importance, combine them into a composite index or average. Apply PCA to reduce correlated predictors to uncorrelated components. Alternatively, use Ridge regression, which handles multicollinearity by design through L2 regularization.

Python Code

 
import numpy as np
import pandas as pd
import statsmodels.api as sm
from statsmodels.stats.outliers_influence import variance_inflation_factor

np.random.seed(42)
n = 200

# Create a dataset with two highly correlated predictors
x1 = np.random.normal(0, 1, n)
x2 = 0.95 * x1 + np.random.normal(0, 0.1, n)  # x2 is almost identical to x1
x3 = np.random.normal(0, 1, n)                 # x3 is independent -- no issue

y = 3 * x1 + 2 * x2 + 1.5 * x3 + np.random.normal(0, 1, n)

# Assemble the predictor DataFrame (no intercept -- VIF is computed per predictor)
X = pd.DataFrame({"x1": x1, "x2": x2, "x3": x3})

def compute_vif(df):
    """
    Compute the Variance Inflation Factor for each column in df.

    VIF = 1 / (1 - R^2) for each predictor regressed on all others.
    Higher VIF = more multicollinearity with other predictors.

    Parameters:
        df : DataFrame of predictors (no intercept column)

    Returns:
        DataFrame with predictor names and their VIF values
    """
    vif_data = pd.DataFrame()
    vif_data["Predictor"] = df.columns
    vif_data["VIF"] = [
        variance_inflation_factor(df.values, i) for i in range(df.shape[1])
    ]
    vif_data["Status"] = vif_data["VIF"].apply(
        lambda v: "Severe" if v > 10 else ("Concerning" if v > 5 else "Acceptable")
    )
    return vif_data.sort_values("VIF", ascending=False).reset_index(drop=True)


print("=== VIF Results (with multicollinearity) ===")
vif_results = compute_vif(X)
print(vif_results.to_string(index=False))

# --- Fix: drop one of the correlated predictors ---
X_fixed = X.drop(columns=["x2"])   # Remove x2 since it is nearly identical to x1

print("\n=== VIF Results (after dropping x2) ===")
vif_fixed = compute_vif(X_fixed)
print(vif_fixed.to_string(index=False))

# --- Show how multicollinearity inflates standard errors ---
X_with_const = sm.add_constant(X)
X_fixed_with_const = sm.add_constant(X_fixed)

model_multi = sm.OLS(y, X_with_const).fit()
model_fixed = sm.OLS(y, X_fixed_with_const).fit()

print("\n=== Standard Error Comparison: Multicollinear vs Fixed ===")
print(f"{'Predictor':<8} {'SE (with x2)':>14} {'SE (without x2)':>17}")
print("-" * 42)
for pred in ["x1", "x3"]:
    se_multi = model_multi.bse[pred]
    se_fixed = model_fixed.bse[pred]
    print(f"{pred:<8} {se_multi:>14.4f} {se_fixed:>17.4f}")

No Multicollinearity
What this code does: The code creates two predictors (x1 and x2) that are nearly identical, with a correlation of 0.95. The compute_vif function loops through each column, passes the full design matrix to variance_inflation_factor, and flags the severity of each result. With both predictors in the model, x1 and x2 will show very high VIF values, while x3 stays near 1. After dropping x2, all remaining predictors return acceptable VIFs. The standard error comparison table shows the practical damage multicollinearity was doing: the standard error for x1 shrinks substantially once the correlated predictor is removed.

The Full Diagnostic Checklist

Run this before interpreting any coefficient. Every row is a check. Every failure needs a fix before you trust the output.

Assumption Visual Check Formal Test Python Function Pass Criterion Fix If Violated
Linearity Residuals vs. fitted — no curve RESET test linear_reset() p > 0.05 Add polynomial term, log transform, or switch model
Independence Residuals vs. time — no runs Durbin-Watson durbin_watson() DW between 1.5 and 2.5 Use clustered SE, mixed models, or account for time structure
Homoscedasticity Residuals vs. fitted — no cone Breusch-Pagan het_breuschpagan() p > 0.05 Log/sqrt transform, weighted LS, or HC3 robust SE
Normality Q-Q plot — points on the diagonal Shapiro-Wilk stats.shapiro() p > 0.05 (small n) Log transform outcome, weighted LS, or HC3 robust SE
No Multicollinearity Correlation matrix — no pairs above 0.80 VIF variance_inflation_factor() All VIF below 5 (10 max) Drop predictor, combine, PCA, or use Ridge regression

Conclusion

Your model’s predictions are only as trustworthy as the assumptions underneath them. R² is not a validity check. Significant p-values are not a validity check. The five assumptions above are the validity check — and they are the ones that get skipped precisely because nothing in the standard model output will tell you they are broken.

Run the diagnostic checklist before you interpret a single coefficient. All five tests are in statsmodels and scipy, and the whole suite takes fewer than 20 lines of code. The difference between a model you can trust and one that looks fine until it fails is almost always in whether those 20 lines were ever run.

Leave a Reply

Your email address will not be published. Required fields are marked *