lingamr estimates causal structure —
which variable causes which, and how strongly — from purely
observational data, using the LiNGAM family of algorithms (an R port of
the Python lingam package
by the Shimizu lab).
This vignette explains the core idea behind LiNGAM and walks through a minimal end-to-end workflow. Detailed guides for every method live on the package website; see Where to Go Next.
The Idea: Why Non-Gaussianity Reveals Causal Direction
Correlation alone cannot distinguish “x causes y” from “y causes x”: both models can produce exactly the same covariance matrix. Classical methods based on second-order statistics therefore return, at best, an equivalence class of structures.
LiNGAM (Linear Non-Gaussian Acyclic Model; Shimizu et al. 2006) resolves the direction by adding one assumption: the error terms are non-Gaussian. The model for each variable is
with mutually independent, non-Gaussian errors , arranged in a DAG. Under these assumptions the full causal structure is uniquely identifiable from observational data — not just up to an equivalence class.
The intuition fits in one experiment. Take the true model with a uniform (non-Gaussian) error, and regress in both directions:
n <- 1000
x <- runif(n, -1, 1)
y <- 1.5 * x + runif(n, -1, 1)
res_causal <- residuals(lm(y ~ x)) # correct direction: x -> y
res_anti <- residuals(lm(x ~ y)) # reverse direction: y -> x
oldpar <- par(mfrow = c(1, 2))
plot(x, res_causal, main = "Correct: residual of y ~ x", cex = 0.4)
plot(y, res_anti, main = "Reverse: residual of x ~ y", cex = 0.4)
par(oldpar)In the correct direction the residual is independent of the regressor (left panel: a featureless band). In the reverse direction it is not (right panel: the residual’s spread depends on ). Direct LiNGAM turns this asymmetry into an algorithm: the variable whose residuals are most independent of it is the most upstream (“exogenous”) one; peel it off, regress it out, and repeat.
Had the error been Gaussian, both panels would look identical — this is why non-Gaussianity is essential, and why the basic LiNGAM model rests on four assumptions:
- Linear relationships,
- an acyclic graph (a DAG),
- non-Gaussian, mutually independent errors,
- no latent confounder (all common causes observed),
plus i.i.d. observations. lingamr also ships estimators
that relax each of these — see
vignette("method-selection").
A Minimal Workflow
Estimate
generate_lingam_sample_6() generates data from a known
6-variable LiNGAM model, so we can compare estimates against the truth.
lingam_direct() runs Direct LiNGAM — by default,
independence is assessed via mutual information and coefficients are
estimated with adaptive LASSO.
x1k <- generate_lingam_sample_6(n = 1000)
model <- lingam_direct(x1k$data)
model
#> Direct LiNGAM Result
#> Variables : 6
#> Causal order: x3 -> x2 -> x0 -> x4 -> x5 -> x1
#>
#> Adjacency matrix (row = to, col = from):
#> x0 x1 x2 x3 x4 x5
#> x0 0.000 0 0.000 3.033 0 0
#> x1 2.988 0 2.002 0.000 0 0
#> x2 0.000 0 0.000 5.993 0 0
#> x3 0.000 0 0.000 0.000 0 0
#> x4 8.000 0 -1.000 0.000 0 0
#> x5 4.015 0 0.000 0.000 0 0The two key components of the result:
# The estimated causal order, upstream first
colnames(x1k$data)[model$causal_order]
#> [1] "x3" "x2" "x0" "x4" "x5" "x1"
# The adjacency matrix: B[i, j] is the direct effect of x_j on x_i
round(model$adjacency_matrix, 3)
#> x0 x1 x2 x3 x4 x5
#> x0 0.000 0 0.000 3.033 0 0
#> x1 2.988 0 2.002 0.000 0 0
#> x2 0.000 0 0.000 5.993 0 0
#> x3 0.000 0 0.000 0.000 0 0
#> x4 8.000 0 -1.000 0.000 0 0
#> x5 4.015 0 0.000 0.000 0 0Visualize
plot_adjacency() draws the causal graph; passing the
true structure via true_B color-codes the comparison (green
= correct, red = false positive, orange dashed = missed).
model$adjacency_matrix |>
plot_adjacency(
labels = colnames(x1k$data),
true_B = x1k$true_adjacency,
title = "Estimated vs. true structure"
)Intervene: Total Causal Effects
The total causal effect — how much a variable ultimately moves when another is changed by one unit, through all paths — is what you need to reason about interventions (a multiple-regression coefficient answers a different question; see the Direct LiNGAM article).
total_effects <- estimate_all_total_effects(x1k$data, model)
round(total_effects, 2)
#> x0 x1 x2 x3 x4 x5
#> x0 0.00 0 0.00 3.03 0 0
#> x1 2.87 0 1.94 21.06 0 0
#> x2 0.00 0 0.00 5.99 0 0
#> x3 0.00 0 0.00 0.00 0 0
#> x4 7.91 0 -1.13 18.28 0 0
#> x5 4.02 0 0.00 12.18 0 0Check the Assumptions
summary_lingam() bundles the two key diagnostics:
residuals should be mutually independent (assumption 4)
and non-Gaussian (assumption 3 — so normality being
rejected is good news here).
summary_lingam(x1k$data, model)
#> === Direct LiNGAM Model Summary ===
#> Variables: 6
#> Observations: 1000
#> Edges: 7
#> Causal order: x3 -> x2 -> x0 -> x4 -> x5 -> x1
#>
#> --- Assumption 1: Independence of residuals ---
#> Method: spearman
#> Dependent pairs: 0 / 15 (p < 0.050)
#> Min p-value: 0.0510
#> => Residuals appear mutually independent (assumption supported).
#>
#> --- Assumption 2: Non-Gaussianity of residuals ---
#> Method: shapiro
#> Non-Gaussian: 6 / 6 (p <= 0.050)
#> => All residuals are non-Gaussian (assumption supported).Quantify Stability
The bootstrap re-runs the estimation on resampled data and reports how often each edge recurs — low-probability edges should not be over-interpreted:
bs <- lingam_direct_bootstrap(x1k$data, n_sampling = 50L, seed = 42)
#> Bootstrap: 50 iterations, method=adaptive_lasso (sequential)
#> iteration 1 / 50
#> iteration 10 / 50
#> iteration 20 / 50
#> iteration 30 / 50
#> iteration 40 / 50
#> iteration 50 / 50
#> Completed in 1.7 seconds.
round(get_probabilities(bs), 2)
#> [,1] [,2] [,3] [,4] [,5] [,6]
#> [1,] 0.00 0.02 0.00 0.98 0.02 0
#> [2,] 0.98 0.00 0.98 0.00 0.00 0
#> [3,] 0.00 0.02 0.00 0.98 0.02 0
#> [4,] 0.00 0.00 0.02 0.00 0.00 0
#> [5,] 0.98 0.02 0.98 0.00 0.00 0
#> [6,] 1.00 0.00 0.00 0.00 0.00 0Where to Go Next
Which estimator fits your data — time series, latent confounders,
nonlinear relationships, mixed or missing data — is covered in
vignette("method-selection").
Detailed worked examples live on the package website:
| Article | Covers |
|---|---|
| Direct LiNGAM in depth | Prior knowledge, regression methods, non-Gaussianity experiments, high-dimensional data, failure modes |
| Bootstrap and diagnostics | Stability analysis, assumption checks, SEM fit, broom integration |
| Time series | VAR-LiNGAM, VARMA-LiNGAM |
| Latent confounders | BottomUpParceLiNGAM, RCD |
| Nonlinear methods | RESIT, CAM-UV |
| Special data | Mixed data (LiM), multiple groups, missing values |
A Japanese translation of this vignette is available as
vignette("lingamr-ja"); the website articles are also
available in Japanese.
