ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

Direct-LiNGAM:从观测数据中识别因果顺序的实用指南

2026/9/16 21:26:30 拓冰建站 浏览量
Direct-LiNGAM:从观测数据中识别因果顺序的实用指南 在因果推断这个领域有一类问题特别让人头疼变量之间明明存在依赖关系但光凭观测数据很难说清楚到底是谁影响了谁。之前我在处理一组消费行为数据时想从购买金额、浏览时长、点击次数这几个变量里找出真正的因果链条试过相关性分析、回归模型全都只能给出“有关联”的结论完全答不了“谁先发生”这个问题。后来我把目光放到Direct-LiNGAM上才算真正找到了一套能从观测数据中还原因果顺序的可行方案。Direct-LiNGAM的全称是Direct Linear Non-Gaussian Acyclic Model由Shimizu等人提出核心思路非常直接在假设变量之间是线性关系、误差项服从非高斯分布、并且整个因果结构是有向无环图的前提下通过反复回归和非高斯性检验逐一识别出因果顺序中的“外生变量”从而得到完整的因果结构。相比早期基于ICA的LiNGAM方法Direct-LiNGAM不需要迭代式的盲源分离计算更稳定算法逻辑也更清晰。这篇文章我会从模型假设、核心原理、实际调参、代码实现和真实项目中的坑这几个方面把我这一年多使用Direct-LiNGAM的经验完整梳理一遍。无论你是刚接触因果推断还是已经在用其他因果发现算法想换方案这篇文章都能给你提供可以直接落地的参考。1. 先把模型假设讲透线性、非高斯、无环这三个词缺一不可1.1 数据生成过程到底长什么样LiNGAM模型假设每个观测变量都被写成前面变量的线性组合再加上一个噪声项。如果你有变量$x_1, x_2, \dots, x_p$那么每个变量都可以表示为$$ x_i \sum_{j \in \text{Pa}(i)} b_{ij} x_j e_i $$其中$\text{Pa}(i)$是$x_i$的直接原因集合$b_{ij}$是因果强度系数$e_i$是误差项并且所有$e_i$是相互独立的、非高斯分布的随机变量。这一点值得反复强调误差项相互独立、非高斯分布是LiNGAM能识别因果方向的根本条件。我一开始学习这个模型时一直有个疑问为什么要求误差项相互独立后来在模拟数据上跑了几轮才明白独立误差项意味着模型把所有变量之间的依赖关系都归结为“因果机制”而不是共享的隐变量或干扰项。如果误差项之间有关联模型就会误判因果路径。而“非高斯”这一点更是关键。高斯分布有很多漂亮的数学性质比如线性变换后仍是高斯分布但在因果识别上恰恰是这些性质毁了可识别性。这一点下面单独展开。1.2 非高斯性为什么是一把“钥匙”先举个例子。假设有两个变量$x$和$y$关系是$y 2x e$其中$e$服从均匀分布$x$也服从均匀分布且$x$和$e$独立。如果我们只知道观测到的$x$和$y$能不能判断是$x \to y$还是$y \to x$在非高斯情况下可以做回归如果用$x$预测$y$残差应该是独立的但反过来用$y$预测$x$由于$e$与$y$不独立实际上$e$会影响$y$残差会与$y$存在依赖关系。通过检验哪种方向的残差更接近独立我们就能判定因果方向。这里的生活化类比是假设x是“按下开关”y是“灯泡亮起”e是“电路抖动”。现实中e可能是偶尔的电压波动分布不一定是钟形的。正因为e不是高斯的我们才能从“开关与灯泡的观测数据”中区分出“开关导致灯泡亮”而不是“灯泡导致开关动”。如果用数学语言精确说在非高斯条件下Darmois-Skitovitch定理保证了如果误差项独立且非高斯那么从混合信号中可以唯一分离出独立成分因果顺序就是可识别的。1.3 高斯模型为什么不行如果误差项是高斯的那就麻烦了。两个高斯的独立变量做线性混合后联合分布仍然是高斯而且旋转对称的性质会导致x→y和y→x这两种模型产生完全相同的观测分布信息上根本无法区分。这就是我见过很多人在因果分析起步阶段踩的最大坑拿一堆近似正态分布的变量直接跑相关性或回归然后声称“x解释了y的变化所以x是y的原因”。在高斯框架下这只是拟合不是因果。所以使用Direct-LiNGAM前务必检查你的变量分布是否明显偏离正态。如果没有非高斯性方法效果会受到根本性限制。2. Direct-LiNGAM的算法逻辑逐个揪出最外层的“因”2.1 从找“第一个外生变量”开始外生变量指的是在因果图中没有祖先节点的变量它不受任何其他变量影响只影响别人。Direct-LiNGAM的策略不是一次性解出整个图而是循环做两件事找出当前变量集中最外生的那个变量然后把它对其他变量的影响回归掉更新剩余变量集直到排完所有变量。为什么这个策略有效如果某个变量是外生的它在回归其他变量时作为自变量理论上残差与其他变量独立而如果用它去预测其他变量残差应当具有非高斯噪声的特征。算法的巧妙之处在于利用这个不对称性逐层剥离外生变量。2.2 回归残差背后的信息假设当前的变量集合是$V$候选外生变量是$x_j$。对每个$i \ne j$用$x_j$回归$x_i$得到残差$r_i$$$ r_i x_i - \frac{\text{Cov}(x_i, x_j)}{\text{Var}(x_j)} x_j $$如果$x_j$真的是外生变量那么$x_i$中包含$x_j$的线性影响被移除后剩下的$r_i$应当与$x_j$相互独立。此时判断“独立性”可以转化为判断“$r_i$与$x_j$之间是否存在非线性依赖关系”。那么怎么自动化判断独立性通常用的方法是计算$x_j$与所有$r_i$之间的互信息或者采用基于核的独立检验又或者计算方法简单但有效的“残余非高斯性度量”。我在实际操作中发现如果直接用互信息估计小样本下方差很大。更好的套路是使用Shimizu等人论文中基于kurtosis或likelihood ratio的做法特别是样本量低于500时kurtosis度量虽然粗糙但稳定性比复杂的核方法好很多。2.3 完整伪代码与流程在明确了核心思想后我把整个算法流程翻译成了可操作的伪代码方便理解也方便逐步实现输入: 数据矩阵 X (n_samples, p_features) 输出: 因果顺序 ordering 1. 初始化一个空列表 ordering [] 2. 初始化剩余变量索引集合 U {1, 2, ..., p} 3. 重复以下步骤直到 U 为空: a. 对每个变量j ∈ U: 对每个i ∈ U, i ≠ j: 计算回归残差 r_i x_i 对 x_j 回归的残差 度量 x_j 与所有 r_i 的总依赖程度 T(j) b. 选择 T(j) 最小的变量作为外生变量把它加入 ordering 的末尾 c. 对剩余所有变量 x_i (i ∈ U, i ≠ j)用新变量去回归 x_i b_ij * x_j residual 然后用残差替换原来的 x_i意思是剥离掉这个外生变量的影响 x_i ← residual d. 从 U 中移除 j 4. 返回 ordering注意在第3步c里剥离外生变量对其他变量的影响是必要的。如果不剥离后续寻找“第二个外生变量”时包含第一个外生变量影响的数据会干扰非高斯性度量导致排序错误。我用Python手写过一次这个流程大概两百行代码。性能上在p5、样本量1000的条件下一个循环约需运行数秒到数十秒取决于非高斯性度量的复杂程度。3. 关键参数和评价机制这几点直接决定结果准不准3.1 该用哪种非高斯性度量Direct-LiNGAM中最重要的自由度就是步骤a中的T(j)如何计算。常见的有这么几种方法计算方式优点缺点峰度绝对值计算残差分布的kurtosis简单快速对异常值极其敏感KL散度估计残差与高斯分布的KL散度更稳健计算量大核独立准则HSIC / Hilbert-Schmidt独立准则能捕获非线性依赖参数选择敏感互信息估计k近邻估计法理论严谨小样本表现不稳定我当时测试了峰度和HSIC发现如果数据没有明显离群点峰度就够用但若数据噪声大或者有少量极端值峰度容易把高偏态的变量误判为外生变量。这种情况下核方法或KL散度更为可靠代价是计算速度下降。3.2 数据标准化、异常值和样本量在Direct-LiNGAM的官方实现中通常会先对数据做标准化。但标准化不等于白化Whitening后者还会去相关。我在使用中发现不要做白化因为白化会改变因果结构中的方差信息可能导致外生变量识别错误。标准化只调整尺度不改变变量间的独立关系安全。样本量是很实际的问题。LiNGAM系列方法本质上是基于分布的独立性判断需要足够样本才能判断出独立性是否成立。我的经验是少于200个样本时结果很不稳定500以上才勉强可用想稳定复现至少需要1000以上样本。异常值问题是另一个大坑。非高斯性度量对厚尾和离群点非常敏感几个异常值就能把一个本不是外生的变量推上外生变量的位置从而带偏整个因果顺序。我常用的办法是在跑算法前做一步箱线图探测把极端值截尾或者用缩尾再跑算法。这一步看起来简单但对结果影响巨大。3.3 结果可靠性的验证方式仅仅输出一组因果顺序是不够的还要验证它的稳定性。我一般做三件事Bootstrap重采样对原始数据做有放回抽样用不同的样本子集重复跑50到100次。如果每次得到的因果顺序一致度很高说明算法对该数据稳定否则结果要慎重解读。随机打乱变量名把变量顺序随机改变后再次运行看输出顺序是否出现逻辑矛盾。这一步主要排查实现层面的“顺序偏差”。已知方向检验如果你对某几个变量间的因果方向有一定领域知识可以只验证这几个方向是否符合预期这比全图检验更容易落地。我多次实践后的结论是Bootstrap验证是最有价值的一步——它不仅能评价稳定性还能帮你发现算法对小批量噪声的敏感程度。4. 与ICA-LiNGAM、PC算法的横向对比4.1 它们各自的底层思路ICA-LiNGAM是LiNGAM的初代版本用独立成分分析将混合矩阵分解然后从混合矩阵中读出因果顺序。它的问题是ICA是个迭代优化过程容易受局部最优值和初始化随机性影响不同次运行结果可能有差异。我在同一份数据上跑ICA-LiNGAM有时候两次跑出来的顺序会有一两个变量颠倒。PC算法则是基于条件独立性检验的约束类方法它的强项是能处理非线性关系也能发现隐变量通过FCI变体但它假设数据满足忠实性条件且在高维稀疏因果图上有优势低维强因果场景下表现反而不如LiNGAM。快速对比一下特性Direct-LiNGAMICA-LiNGAMPC算法模型假设线性、非高斯、无环同左条件独立忠实性对非高斯性要求必须有必须有无要求稳定性较高受ICA初始化影响受独立性检验选择影响可扩展性中中高维较好隐变量处理不支持不支持部分变体支持实现复杂度简单直接中较高4.2 什么场景下我会选Direct-LiNGAM如果你拿到的是连续型观测数据日志、行为记录、传感器数据这类变量的噪声通常是非高斯的而且你关心的是变量之间的精确方向此时Direct-LiNGAM是首选。之前我们处理一个电商场景的购买路径数据变量包括“进入页面次数”、“页面停留时间”、“点击商品次数”、“最终是否下单”。这些变量看起来高度相关但用PC算法给出的结果五花八门不同的条件独立性检验方法会给出不同图而用Direct-LiNGAM跑出来的因果顺序非常稳定也符合业务直觉。这就是它的强项在数据量够、非高斯条件满足时给出的答案更“确定”。反过来如果你怀疑系统里有隐藏的共同原因比如“天气”同时影响“销量”和“客流”这种场景下LiNGAM类方法就无能为力了因为隐藏的第三个变量会破坏误差项独立的假设。此时应转向FCI或其它能处理隐变量的方法。5. 实操演示用Python跑一遍DirectLiNGAM5.1 环境准备与模拟数据生成Python生态里有一个现成的库叫lingam封装了DirectLiNGAM、ICA-LiNGAM、VAR-LiNGAM等算法。安装很简单pip install lingam同时需要numpy、pandas、scipy这些基础库。然后我生成一份模拟数据构造一个有向无环图随机生成连接强度系数并加上非高斯噪声import numpy as np import pandas as pd import lingam # 设置随机种子 np.random.seed(42) # 变量数 p 5 # 用列表定义真实因果结构每个元组为 (指向的变量索引, 来源变量索引, 系数) true_edges [ (1, 0, 0.8), # x1 - 0.8*x0 (2, 0, 0.6), # x2 - 0.6*x0 (2, 1, 0.5), # x2 - 0.5*x1 (3, 2, 1.2), # x3 - 1.2*x2 (4, 1, 0.9), # x4 - 0.9*x1 ] n_samples 2000 # 非高斯噪声使用均匀分布和指数分布混合 def non_gaussian_noise(size): return np.random.uniform(-1, 1, size) np.random.exponential(0.5, size) X np.zeros((n_samples, p)) for target, source, coef in true_edges: X[:, target] coef * X[:, source] X non_gaussian_noise((n_samples, p))注意这里噪声同时加到所有变量上模拟的是每个变量都有独立的外部扰动的真实场景。有人会问为什么生成顺序是先算因果部分再加噪声因为LiNGAM模型定义里误差项就是独立加在变量上的所以这符合模型假设。5.2 调用DirectLiNGAM并查看因果顺序直接使用lingam库中的DirectLiNGAM类model lingam.DirectLiNGAM() model.fit(X) print(model.causal_order_) print(model.adjacency_matrix_)causal_order_会按因果顺序输出变量索引比如输出可能是[0, 1, 2, 3, 4]表示$x_0$是最外生的然后依次影响后续变量。adjacency_matrix_是一个$p \times p$矩阵第[i, j]个元素表示变量$i$对变量$j$的因果强度非零值为存在因果边。如果数据生成过程正确并且噪声明显非高斯那么得到的因果顺序应当和true_edges一致。这里我生成的顺序就是0 - 1 - 2 - 30 - 1 - 4和算法输出预期一致。5.3 结果解读与可视化有了因果顺序和邻接矩阵可以进一步整理成图结构绘制DAG检查是否有违反无环假设的边。我也习惯把因果强度按绝对值排序找出影响力最大的路径。一个需要注意的点lingam库输出的邻接矩阵中系数是标准化后的代表方向变化的标准差效应。要解读为“x增加一个单位y增加多少”这类原始量级解释需要用原始回归重新估计系数。我在实际项目里通常会在得到因果结构后再用普通回归去估计每一条边的精确系数和置信区间这样既有了因果骨架也有了统计学上可汇报的效应值。6. 真实项目里绕不开的坑与应对方法6.1 隐变量是最大的对手前面提到的模型假设里有一条很强所有相关变量都被观测到了。一旦有隐变量同时影响多个观测变量误差项独立性就会被破坏算法可能找出完全不存在的伪因果边或者因果顺序错乱。我在这上面吃过实实在在的亏。当时分析一个用户留存项目变量里有“活跃天数”和“消费金额”算法判断“活跃天数 - 消费金额”。但业务方回忆说可能有“会员等级”这个未记录变量在同时影响两者。后来补上了等级数据再跑因果顺序完全变了之前的结论根本站不住。所以拿到Direct-LiNGAM结果时务必要请领域专家评估是否可能遗漏了共同原因。这个步骤不能省。6.2 非线性关系会让因果顺序变乱Direct-LiNGAM假设关系是线性的。如果你的数据中存在明显非线性比如U型关系、对数关系残差独立性检验的效果会大幅下降。我不是说不能用而是应提前做非线性筛查。实际操作时我会先用scipy.stats里的相关性检验如Spearman与线性回归残差的模式图结合判断如果发现曲线关系明显要么对变量做适当的非线性变换使其趋于线性要么换用非线性因果发现算法比如基于加性噪声模型的方法。有些人在变量变换上犹豫担心变换后因果方向不可解释。实际上单调变换不会改变因果方向的基本含义比如对数值取对数影响的仍是原变量的因果地位。6.3 因果效应的绝对值不等于影响重要性Direct-LiNGAM输出的因果强度系数虽然有用但直接比较不同边的大小会误导。因为变量的尺度不同系数的绝对值也不可比。我在一次业务分析中某个系数是1.8另一个是0.2业务方以为前者影响力是后者的9倍。实际上前者对应的变量取值范围小很多单位变化带来的效应自然大。正确做法是使用标准化系数比较或者用“干预一个标准差变化导致的效应”作为影响重要性的指标。具体操作把原始数据标准化后再跑一次Direct-LiNGAM这样得到的系数就是标准化系数边的大小才可跨变量比较。6.4 稳定性验证的具体操作与判断标准Bootstrap验证时我习惯这样操作from lingam import DirectLiNGAM import numpy as np n_bootstrap 100 orders [] X_df pd.DataFrame(X) for i in range(n_bootstrap): idx np.random.choice(len(X_df), len(X_df), replaceTrue) X_boot X_df.iloc[idx].values model DirectLiNGAM() model.fit(X_boot) orders.append(model.causal_order_) # 计算每个变量排序位置的频次 orders_array np.array(orders) for var_idx in range(p): positions orders_array[:, var_idx] print(f变量{var_idx}: 排序位置分布 {np.bincount(positions, minlengthp)})如果某个变量的排序位置分布集中说明它的因果地位稳定如果分布非常分散运行多次结果不一致宁可认为该数据不适合这个方法也不要强行解释结果。我见过很多案例数据量只有几百条跑一次看起来有模有样但bootstrap一跑就露馅了。因果发现的输出看着“漂亮”不代表它就是真的稳定性验证是报告结果前必不可少的一步。我个人的习惯是把Bootstrap的稳定性概率直接写在分析报告中比如“变量x在100次抽样中有95次被识别为第一个外生变量”这种描述比单独的因果图更有说服力也更经得起业务方和评审的反复追问。最后再分享一个经验Direct-LiNGAM解决的是“从数据中恢复因果方向”这个环节它不负责解释业务机制。拿到结果后一定要回到业务场景里去检验每个方向的合理性与可解释性。曾经有一次算法反复提示“页面停留时间 - 是否下单”业务方不认可认为应该是“下单意愿 - 停留时间”。后来发现数据取的是“下单成功后的停留时间”时间窗切错了。这类问题算法救不了只能靠人对业务的理解去修正数据和解释结果。因果发现工具的价值在于把“可能的方向”摆到桌面上真正做判断的始终是人。