Variance Inflation Factors (VIF)
@warn "This page was drafted with the help of an LLM. Please report any errors or inconsistencies."
The Variance Inflation Factor (VIF) is a diagnostic for collinearity in a linear model's designmatrix. For each coefficient, it measures how much the variance of its estimate is inflated compared to the case where that predictor would be orthogonal to all other predictors:
\[\text{VIF}_j = \frac{1}{1 - R^2_j}\]
where $R^2_j$ is the coefficient of determination from regressing predictor $j$ on all other predictors. VIF = 1 means no collinearity; the larger the VIF, the more inflated the standard error of the corresponding coefficient. A common rule of thumb is to flag predictors with VIF > 10.
Unfold.jl extends vif for UnfoldModels, and it works for both time-expanded and mass-univariate models. Since it only uses the designmatrix (modelmatrix(m)), it also works for models that were only built, never fitted (fit(...; fit = false)).
Set things up
using Unfold
using UnfoldSim
using DataFrames
using RandomWe simulate some example data using UnfoldSim.jl and extract epochs, i.e. one data point per event and time point:
data, evts = UnfoldSim.predef_eeg()
data_e, times = Unfold.epoch(data = data, tbl = evts, τ = (-1, 1.9), sfreq = 100)
f = @formula 0 ~ 1 + condition + continuousFormulaTerm
Response:
0
Predictors:
1
condition(unknown)
continuous(unknown)Mass-univariate models
vif(m) returns a DataFrame with one row per column of the designmatrix (i.e. per term). Note that we don't even need to fit the model: the VIF only requires the designmatrix.
m_e = fit(UnfoldModel, [Any => (f, times)], evts, data_e; fit = false)
vif(m_e)| Row | eventname | coefname | VIF |
|---|---|---|---|
| String | String | Float64 | |
| 1 | Any | (Intercept) | Inf |
| 2 | Any | condition: face | 1.0 |
| 3 | Any | continuous | 1.0 |
The (Intercept) column is constant, so it has no defined VIF and is reported as Inf. The other two predictors are uncorrelated, so their VIFs are (nearly) 1.
Detecting collinearity
Let's create a collinearity problem: we duplicate the continuous predictor and add a bit of independent noise to both copies:
rng = MersenneTwister(1)
evts2 = deepcopy(evts)
true_c = evts2.continuous
evts2.continuous .= true_c .+ 0.01 .* randn(rng, nrow(evts2))
evts2.continuous2 = true_c .+ 0.01 .* randn(rng, nrow(evts2))2000-element Vector{Float64}:
3.878961014340591
-3.8783167383152013
-5.0047586669463175
-1.6646883667440724
3.8953250497723695
-3.8908254113135157
2.8092746788060103
4.989091808149295
-0.5556190782400693
2.7894924614546492
⋮
0.5432661451840383
1.6622682132140796
-0.5627620893645652
-5.0031680641552585
-1.6679731629688808
-2.792544286818597
1.6679846048403897
5.001277422214393
3.899032669266573The VIFs of the two near-duplicate terms now explode (~50,000), far beyond the rule of thumb of 10, while the unrelated condition stays at 1 — the diagnostic both flags the problem and tells us which coefficients are unstable. In time-expanded models, the same problem would show up on every tap of the affected predictors.
f2 = @formula 0 ~ 1 + condition + continuous + continuous2
m2 = fit(UnfoldModel, [Any => (f2, times)], evts2, data_e; fit = false)
vif(m2)| Row | eventname | coefname | VIF |
|---|---|---|---|
| String | String | Float64 | |
| 1 | Any | (Intercept) | Inf |
| 2 | Any | condition: face | 1.00121 |
| 3 | Any | continuous | 50355.0 |
| 4 | Any | continuous2 | 50355.0 |
Multiple events
For models with multiple events, the output gains an :eventname column:
evts3 = deepcopy(evts)
evts3.type .= repeat(["A", "B"], nrow(evts3) ÷ 2)
m_ab = fit(
UnfoldModel,
["A" => (f, times), "B" => (f, times)],
evts3,
data_e;
eventcolumn = "type",
fit = false,
)
vif(m_ab)| Row | eventname | coefname | VIF |
|---|---|---|---|
| String | String | Float64 | |
| 1 | A | (Intercept) | Inf |
| 2 | A | condition: face | 1.0009 |
| 3 | A | continuous | 1.0009 |
| 4 | B | (Intercept) | Inf |
| 5 | B | condition: face | 1.0009 |
| 6 | B | continuous | 1.0009 |
Time-expanded models
The same function works for time-expanded (FIR) models:
basisfunction = firbasis(τ = (-1, 1), sfreq = 100)
m = fit(UnfoldModel, [Any => (f, basisfunction)], evts, data)
v = vif(m)
first(v, 6)| Row | coefname | VIF |
|---|---|---|
| String | Float64 | |
| 1 | Any : (Intercept) : -1.0 | 2.10328 |
| 2 | Any : (Intercept) : -0.99 | 2.11888 |
| 3 | Any : (Intercept) : -0.98 | 2.13587 |
| 4 | Any : (Intercept) : -0.97 | 2.15392 |
| 5 | Any : (Intercept) : -0.96 | 2.17322 |
| 6 | Any : (Intercept) : -0.95 | 2.19361 |
For FIR basisfunctions, neighboring taps are strongly correlated, so moderate VIFs (up to ~3.4 here) are to be expected and not a problem. For higher-order splines or strongly overlapping basisfunctions, VIFs can be much larger, but are usually not an issue as long as the solver converges.
Using the rule of thumb, we can filter for the "worst offenders":
v[v.VIF .> 10, :]| Row | coefname | VIF |
|---|---|---|
| String | Float64 |
Here the result is an empty 0×2 DataFrame, i.e. there is no collinearity problem.
This page was generated using Literate.jl.