Python因果分析工具选哪个?六大库实战对比与应用场景解析

Python因果推断库指南:对比六大工具,助你高效选择适合项目需求的利器。

原文标题:Python因果分析选哪个?六个贝叶斯推断库实测对比(含代码示例)

原文作者:数据派THU

冷月清谈:

本文对Python中六个主流因果推断库进行了详细对比:Bnlearn、Pgmpy、CausalNex、DoWhy、PyAgrum和CausalImpact。这些库可大致分为两大类:Bnlearn、Pgmpy、CausalNex、PyAgrum专注于发现变量间的因果结构;而DoWhy和CausalImpact则侧重于量化干预或处理的实际效应。文章通过统一数据集的实测,分析了各库的特点、使用场景及限制。Bnlearn易用性高,适合快速验证;Pgmpy提供底层灵活性,适合研究;CausalNex擅长DAG学习,但要求数据离散化;DoWhy则强调因果假设的严谨验证;PyAgrum学习过程透明,但数据预处理繁琐;CausalImpact专为时间序列干预评估设计。用户应根据具体项目需求、数据类型及模型可解释性要求来选择最合适的工具。

怜星夜思:

1、文章里提到了这么多因果推断库,看起来都挺强大的。但在咱们日常工作里,真正要用因果推断解决问题,光是选对库够吗?感觉最难的可能不是代码实现,而是怎么把业务问题转化成一个合适的因果模型,大家觉得呢?
2、文中提到CausalNex和PyAgrum对数据类型(只支持离散)要求比较严格,DoWhy要求处理变量二值化。这在实际中是不是个大坑?如果我的数据很多连续变量或高基数特征,该怎么处理才能用这些库呢?离散化会不会损失很多信息?
3、文章讲了因果推断重在回答“为什么”,而不是“多少”,而且很多库也提供可视化。但是这在实际业务中,我们是优先追求模型结果的准确性(比如预测),还是更看重能解释清楚“为什么”?这两种目标有时候好像挺矛盾的,大家怎么权衡?

原文内容

图片
来源:DeepHub IMBA
本文约5000字,建议阅读5分钟
本文将对比了六个目前社区中最常用的因果推断库:Bnlearn、Pgmpy、CausalNex、DoWhy、PyAgrum 和 CausalImpact


Python 生态里能用的因果库有很多选哪个往往要看你对模型的理解程度,以及项目对“可解释性”的要求。这篇文章将对比了六个目前社区中最常用的因果推断库:Bnlearn、Pgmpy、CausalNex、DoWhy、PyAgrum 和 CausalImpact

贝叶斯因果模型


在因果推断里所有变量可以粗略分成两种:驱动变量(driver variables)乘客变量(passenger variables)驱动变量会直接影响结果,而乘客变量虽然跟结果有关但并不直接影响结果。区分这两者是整个因果分析的关键。比如在预测性维护或设备故障分析里,如果能识别出“导致故障”的那几个变量,后续的监控与优化策略就能有针对性地落地。

有时候,看似无关的变量其实藏着重要的效应。比如说假设某个工厂的发动机故障率在不同地区差异很大,你可能认为这是地理差异,其实真正的原因可能是工厂里湿度、保养周期或人员经验这样的隐含驱动因子。因果推断的价值就在这里——它帮助区分“看上去相关”和“真正原因”的区别。

相比纯预测模型,因果推断更像是在回答“为什么”,而不是“多少”。通过找出系统中真正起作用的变量,才能解释模型的行为,也才能对系统做出有效干预。

数据集与整体实验思路


为了让对比更直观,所有实验都使用相同的数据:Census Income 数据集。这个经典的数据集包含 48,842 条记录和 14 个变量,多数为离散特征。目标也很简单:拥有研究生(postgraduate)学历是否能显著提高年收入超过 50K 美元的概率?

下面的代码用于载入和清理数据。连续变量与敏感特征(如性别、种族等)被移除,以便专注在离散特征的因果结构学习上。

# 安装
pip install datazets

# 导入库
import datazets as dz  
import pandas as pd  
import numpy as np  
import matplotlib.pyplot as plt  

# 导入数据集并删除连续型与敏感特征
df = dz.import_example(data='census_income')

# 数据清洗
drop_cols = ['age', 'fnlwgt', 'education-num', 'capital-gain', 'capital-loss', 'hours-per-week', 'race', 'sex']
df.drop(labels=drop_cols, axis=1, inplace=True)

# 打印样本
df.head()


1、Bnlearn


bnlearn 是一个封装度很高的贝叶斯网络库,几乎把因果分析的标准流程都集成在一套 API 里。它支持离散、连续和混合类型数据,可以进行 结构学习参数学习(CPD 估计)推理(inference)合成数据生成(synthetic data generation)。更重要的是,它把常用的独立性检验、评分函数、拓扑排序、模型比较和可视化都做成了开箱即用的函数。

下面是使用 HillClimbSearch 和 BIC 评分方法学习结构的完整流程代码

# 安装
pip install bnlearn

# 加载库
import bnlearn as bn  

# 结构学习
model = bn.structure_learning.fit(df, methodtype='hillclimbsearch', scoretype='bic')  

# 检验边显著性并剪枝
model = bn.independence_test(model, df, test="chi_square", alpha=0.05, prune=True)  

# 参数学习(可选)
model = bn.parameter_learning.fit(model, df)  

# 绘制图形
G = bn.plot(model, interactive=False)  
dotgraph = bn.plot_graphviz(model)
dotgraph.view(filename=r'c:/temp/bnlearn_plot')

模型学得的 DAG(有向无环图)结构如下:

静态图展示了 bnlearn 学出的 Census Income 因果结构图。交互式版本可以直接查看每条边的条件概率分布。

图片

DAG 学成之后,可以直接执行推理。例如计算当教育水平为博士时,收入大于 50K 的后验概率:

# 开始推理
query = bn.inference.fit(model, variables=['salary'], evidence={'education':'Doctorate'})
print(query)

结果如下:

salary <=50K : 29.1%
salary >50K  : 70.9%


这个概率几乎符合我们的理解:博士学历的确带来更高的收入区间。

换成高中毕业(HS-grad)再试一次:

query = bn.inference.fit(model, variables=['salary'], evidence={'education':'HS-grad'})

得到:

salary <=50K : 83.8%
salary >50K  : 16.2%

还可以组合多个条件,例如:

# 当 education=Doctorate 且 marital-status=Never-married 时,预测 workclass
query = bn.inference.fit(model, variables=['workclass'], evidence={'education':'Doctorate', 'marital-status':'Never-married'})

从整体来看,bnlearn 适合快速构建因果结构并做概率推理,功能完整。输入数据可为离散、连续或混合类型。对于想在业务场景中快速验证因果假设的人,这个库的学习曲线非常平缓。

2、 Pgmpy 


Pgmpy 是一个更偏底层的概率图模型库,如果说 bnlearn 是“开箱即用”那 pgmpy 更像是一套“拼装工具箱”。他的灵活性非常高,但也意味着需要较强的贝叶斯建模功底。

这两个库的功能其实有重叠,因为 bnlearn 的底层实现部分依赖 pgmpy。但在 pgmpy 中,所有步骤都要自己搭建:数据处理、建模、参数估计、推理、可视化都要自己写。

下面是用 HillClimbSearch 和 BIC 评分方法做结构学习的例子:

# 安装 pgmpy
pip install pgmpy

# 导入函数
from pgmpy.estimators import HillClimbSearch, BicScore, BayesianEstimator
from pgmpy.models import BayesianNetwork, NaiveBayes
from pgmpy.inference import VariableElimination

# 导入数据并删除连续与敏感特征
df = bn.import_example(data='census_income')
drop_cols = ['age', 'fnlwgt', 'education-num', 'capital-gain', 'capital-loss', 'hours-per-week', 'race', 'sex']
df.drop(labels=drop_cols, axis=1, inplace=True)

# 创建估计器与评分方法
est = HillClimbSearch(df)
scoring_method = BicScore(df)

# 建立模型并打印边
model = est.estimate(scoring_method=scoring_method)
print(model.edges())


建好结构后需要手动拟合条件概率分布(CPD):

vec = {  
    'source': ['education', 'marital-status', 'occupation', 'relationship', 'relationship', 'salary'],  
    'target': ['occupation', 'relationship', 'workclass', 'education', 'salary', 'education'],  
    'weight': [True, True, True, True, True, True]  
}  
vec = pd.DataFrame(vec)

# 创建贝叶斯模型
bayesianmodel = BayesianNetwork(vec)

# 拟合模型
bayesianmodel.fit(df, estimator=BayesianEstimator, prior_type='bdeu', equivalent_sample_size=1000)

# 推理
model_infer = VariableElimination(bayesianmodel)
query = model_infer.query(variables=['salary'], evidence={'education':'Doctorate'})
print(query)


输出结果与 bnlearn 基本一致:

 salary <=50K : 29.1%
 salary >50K  : 70.9%


不同的是,这个过程需要你自己定义 DAG、参数估计和推理方式。这个库灵活性很高,但显然更适合研究者或开发自定义因果框架的场景,而不是想“直接上手跑”的业务人员。

3、CausalNex


CausalNex 是一个专注于从数据中学习因果图并量化因果效应的 Python 库。它只支持离散分布,这点很重要。所以在建模前必须把所有连续变量或高基数特征离散化,否则模型无法拟合。

文档里也明确提到,如果特征太多、状态太复杂,模型效果会明显下降。不过好处是,它提供了一些便捷函数来帮助降低类别数量和处理标签编码。

下面是一个完整的示例,仍然使用同样的 Census Income 数据。第一步需要把所有类别变量转为数值型(因为 NOTEARS 算法要求矩阵计算):

# 安装
pip install causalnex

# 导入库
from causalnex.structure.notears import from_pandas
from causalnex.network import BayesianNetwork
import networkx as nx
import datazets as dz
from sklearn.preprocessing import LabelEncoder
import matplotlib.pyplot as plt
le = LabelEncoder()

# 导入数据并删除连续与敏感特征
df = dz.get(data='census_income')

drop_cols = ['age', 'fnlwgt', 'education-num', 'capital-gain', 'capital-loss', 'hours-per-week', 'race', 'sex']
df.drop(labels=drop_cols, axis=1, inplace=True)

# 转换为数值型
df_num = df.copy()
for col in df_num.columns:
    df_num[col] = le.fit_transform(df_num[col])


结构学习部分用的是 NOTEARS 算法,可以自动从相关矩阵中推导 DAG。不过输出可能太密集,需要设定阈值过滤掉弱边:

# 结构学习
sm = from_pandas(df_num)

# 阈值剪枝
sm.remove_edges_below_threshold(0.8)

# 绘图
plt.figure(figsize=(15,10))
edge_width = [d['weight']*0.3 for (u,v,d) in sm.edges(data=True)]
nx.draw_networkx(sm, node_size=400, arrowsize=20, alpha=0.6, edge_color='b', width=edge_width)

上图是 CausalNex 学出的结构图。没有标签的节点表示因为阈值太高被剪掉的弱边。也可以通过调整参数 w_threshold 控制网络稀疏度,或者用 tabu_edges 禁止某些边生成。

建好结构后,需要学习节点的条件概率分布:

# 第一步:创建 BayesianNetwork 实例
bn = BayesianNetwork(sm)

# 第二步:降低分类特征基数(可选)
# 第三步:定义每个节点的状态字典
# 第四步:拟合节点状态
bn = bn.fit_node_states(df)

# 第五步:拟合条件概率分布
bn = bn.fit_cpds(df, method="BayesianEstimator", bayes_prior="K2")

# 输出某个节点的 CPD
result = bn.cpds["education"]
print(result)


CausalNex 的可解释性和结构学习能力都不错,但预处理要求多、类型限制严格,对 Python 版本也挑剔(仅兼容 3.6–3.10)。如果只想跑标准数据,它表现稳定,但要集成到更复杂的生产环境,需要额外工作。

4、DoWhy 


DoWhy 是一个专注于因果推断验证的库,设计理念与前面提到的贝叶斯网络工具完全不同。它并不尝试从数据中直接学习因果图,而是要求用户显式定义因果假设,包括:

  • 结果变量(outcome variable)

  • 处理变量(treatment variable)

  • 潜在混杂变量(common causes)


也就是说DoWhy 更像是一个验证框架用来系统地质疑和检验你提出的因果假设,而不是生成因果结构。

如果没有提供 DAG(因果图),它会自动把所有变量连接到结果与处理变量上。但实际使用中最好结合领域知识自行定义 DAG,否则模型会过度简化。

使用同样的 Census Income 数据集,只是处理变量定义为“是否拥有博士学位”:

# 安装  
pip install dowhy

# 导入库  
from dowhy import CausalModel  
import dowhy.datasets  
import datazets as dz  
from sklearn.preprocessing import LabelEncoder  
import numpy as np  
le = LabelEncoder()  

# 导入数据并删除连续和敏感特征  
df = dz.get(data='census_income')  

drop_cols = ['age', 'fnlwgt', 'education-num', 'capital-gain', 'capital-loss', 'hours-per-week', 'race', 'sex']  
df.drop(labels=drop_cols, axis=1, inplace=True)  

# 处理变量必须为二元  
df['education'] = df['education']=='Doctorate'  

# 将数据转换为数值型  
df_num = df.copy()  
for col in df_num.columns:  
    df_num[col] = le.fit_transform(df_num[col])  

# 指定处理变量、结果变量和潜在混杂变量  
treatment = "education"  
outcome = "salary"  

# Step 1: 创建因果图
model = CausalModel(  
        data=df_num,  
        treatment=treatment,  
        outcome=outcome,  
        common_causes=list(df.columns[~np.isin(df.columns, [treatment, outcome])]),  
        graph_builder='ges',  
        alpha=0.05,  
        )  

# 查看模型  
model.view_model()


上图展示了 DoWhy 自动生成的 DAG,结果变量为“salary”,处理变量为“education”。可以看到,模型假设教育水平会影响薪资,并且两者都可能受婚姻状态、工作类型等混杂因素的干扰。

接下来是识别和估计因果效应的步骤:

# Step 2: 识别因果效应  
identified_estimand = model.identify_effect(proceed_when_unidentifiable=True)
print(identified_estimand)

一旦模型识别出可估计的因果效应,就可以计算平均处理效应(ATE):

 # Step 3: 估计效应  
 estimate = model.estimate_effect(identified_estimand, method_name="backdoor.propensity_score_stratification")
 print(estimate)

输出的平均效应值约为 0.47,说明博士学位与高收入存在明显的正向因果关系。最后再做稳健性检验:

# Step 4: 稳健性验证  
refute_results = model.refute_estimate(identified_estimand, estimate, method_name="random_common_cause")

DoWhy在学术界非常常见,优点是框架清晰、假设透明、验证逻辑完备; 缺点是对输入要求高(变量需二值化、分类需数值编码),且不能从数据自动学习结构。

换句话说,它假设你已经知道潜在的因果关系,现在只想验证这个假设是否合理。

5、PyAgrum 


PyAgrum 是另一个功能相对完整的概率图模型库。它支持贝叶斯网络、马尔可夫网络以及决策图的构建,能做结构学习、参数学习、推理和可视化。并且语法偏工程化,比 pgmpy 直观一些,但要求所有变量都必须是离散型。

如果数据里有缺失值或连续变量需要进行处理,否则算法会直接报错。下面的例子展示了从数据清洗到结构学习的完整流程:

# 安装  
pip install pyagrum  

# 可选:安装可视化依赖  
pip install setgraphviz

import datazets as dz  
import pandas as pd  
import pyagrum as gum  
import pyagrum.lib.notebook as gnb  
import pyagrum.lib.explain as explain  
import pyagrum.lib.bn_vs_bn as bnvsbn  

# 导入可视化设置  
from setgraphviz import setgraphviz  
setgraphviz()  

# 导入数据并删除连续与敏感特征  
df = dz.get(data='census_income')  

drop_cols = ['age', 'fnlwgt', 'education-num', 'capital-gain', 'capital-loss', 'hours-per-week', 'race', 'sex']  
df.drop(labels=drop_cols, axis=1, inplace=True)  

# 清理缺失值  
df2 = df.dropna().copy()  
df2 = df2.fillna("missing").replace("?", "missing")  

# 所有列转换为分类类型  
for col in df2.columns:  
    df2[col] = df2[col].astype("category")  

# 创建学习器  
learner = gum.BNLearner(df2)  
learner.useScoreBIC()  
learner.useGreedyHillClimbing()  

# 学习结构  
bn = learner.learnBN()  

# 学习参数  
bn2 = learner.learnParameters(bn.dag())  

# 可视化结果  
gnb.showBN(bn2)

上图展示了 PyAgrum 学到的贝叶斯网络结构。节点之间的箭头表示潜在的因果方向。

它提供了一系列辅助模块,可以比较不同结构(bn_vs_bn)、解释单个节点(explain)或生成报告。

PyAgrum 的特点是:学习过程透明、支持约束学习(可指定禁止/强制边),但代价是输入准备相对繁琐。 没有自动缺失值处理、连续特征必须离散化,而且某些函数依赖 Graphviz 进行可视化。

6、CausalImpact 


CausalImpact 原本是 Google 的开源项目,用于通过贝叶斯结构时间序列模型来衡量干预效果。 它与前面几个库不同,不学习因果图而是针对时间序列数据计算干预带来的量化影响。

常见场景是评估营销活动、价格调整或新功能上线的效果。比如,一个电商网站想知道广告活动是否带来了实际销售提升。CausalImpact 会先用干预前的数据建立基线模型,再将干预后的实际观测值与预测值比较,计算“反事实差异”。

下面是一个简洁的示例,构造 100 个样本点,并在第 71 个时间点之后人为引入干预效应:

# 安装  
pip install causalimpact

# 导入库  
import numpy as np  
import pandas as pd  
from statsmodels.tsa.arima_process import arma_generate_sample  
import matplotlib.pyplot as plt  
from causalimpact import CausalImpact  

# 生成样本  
x1 = arma_generate_sample(ar=[0.999], ma=[0.9], nsample=100) + 100  
y = 1.2 * x1 + np.random.randn(100)  
y[71:] = y[71:] + 10  
data = pd.DataFrame(np.array([y, x1]).T, columns=["y","x1"])  

# 初始化模型  
impact = CausalImpact(data, pre_period=[0,69], post_period=[71,99])  

# 执行推断  
impact.run()  

# 绘图与结果  
impact.plot()  
impact.summary()

图中上半部分显示实际值与模型预测值的对比;中间部分是两者的差值(即时效应);下半部分是累计效应。

结果显示,干预带来了显著提升,概率为 100%,P 值为 0。这种方法假设干预前后协变量与响应变量的关系稳定,且协变量本身不受干预影响。若满足这些假设,CausalImpact 在定量评估干预效应方面非常可靠。

总结


六个库的思路差异很大。从整体来看,可以分成两类:

结构学习型BnlearnPgmpyCausalNexPyAgrum 用于发现变量间的潜在因果关系,适合探索性分析与结构建模。

因果效应型DoWhyCausalImpact 侧重在给定结构或时间序列下量化干预效果,适合政策分析或实验验证。

从工程角度看:Bnlearn 的易用性最高,接口友好、兼容性强,是入门和快速验证的好工具;Pgmpy 灵活性最强,适合深度研究与自定义算法;CausalNex 拥有良好的可解释图形界面,但依赖特定 Python 版本;DoWhy 是学术研究常用工具,强调理论严谨性;PyAgrum 稳定、透明,但数据准备较重;CausalImpact 专注时间序列因果评估。

六个因果库覆盖了从结构学习到因果效应估计的完整路径。

BnlearnPgmpyCausalNex 和 PyAgrum 负责构建网络、识别驱动因素;

DoWhy 和 CausalImpact 则更像“量化工具”,用于评估处理或干预带来的实际影响。

作者:Erdogan T

编辑:文婧



关于我们

数据派THU作为数据科学类公众号,背靠清华大学大数据研究中心,分享前沿数据科学与大数据技术创新研究动态、持续传播数据科学知识,努力建设数据人才聚集平台、打造中国大数据最强集团军。




新浪微博:@数据派THU

微信视频号:数据派THU

今日头条:数据派THU


关于“在业务中权衡模型准确性与可解释性”的问题,这关乎模型选择的战略定位。预测模型追求的是高精度地预估未来事件,其价值在于决策自动化和效率提升。因果模型则侧重于机制理解和归因,其价值在于探究“为什么”并指导更有效的干预策略。两者并非互斥,而是互补的。在探索阶段,因果可解释性至关重要,它能帮助业务方建立对问题的深层理解,指导假设的形成。在成熟阶段,可将因果洞察融入预测模型构建,提升其稳健性和在干预下的预测能力。关键在于明确业务目标:是需要一个能‘预测未来’的黑箱,还是一个能‘解释过去’的白盒,或者两者兼顾。

我觉得这要看你的业务优先级了。如果是那种’我就是要一个准到爆的模型来推荐商品/预测销量’,那性能第一,解释性可以放放。毕竟能赚钱就好,谁管它为什么赚钱。但如果业务场景是需要‘找出增长的真正原因’、‘为什么用户会流失’,那可解释性就太重要了。不能光告诉我用户流失了,至少得告诉我可能是因为哪些具体操作引起的,我才能去改进啊。所以,如果是偏决策、偏战略引导的,可解释性更高;如果是偏执行、偏自动化生产的,预测性能更关键。

这不就是我们一直在纠结的世纪难题嘛:是做个‘知其然更知其所以然’的聪明人,还是做个‘能干事儿就行’的实用主义者?老板可能说‘我只要结果’,但当结果不好的时候,第一个问的就是‘为什么’。这时候如果答不上来,或者模型是个黑箱,那可就尴尬了。所以嘛,‘可解释性’就像是给‘准确性’买的保险,平时可能用不着,但关键时刻能救命。哈哈,最好是能‘鱼和熊掌兼得’,不然就看谁嗓门大、谁给的钱多了!

关于“连续变量或高基数特征对CausalNex、PyAgrum等库的限制及离散化影响”的问题,确实,这些库侧重于离散贝叶斯网络,对连续数据处理能力有限。常见策略包括:1. 分箱(Binning):根据业务经验或统计方法将连续变量离散化,但需注意选择合适的分箱数量和边界,避免信息损失过大。2. 特征选择/降维:对高基数特征进行筛选或通过嵌入学习降低其维度。3. 结合其他算法:例如,先用连续型数据训练一个预测模型,再将其输出的类别或分数作为离散特征输入因果库。离散化确实可能带来信息损失,尤其是在因果效应的细微层面,因此需权衡。

这真的是痛点!我以前用CausalNex的时候,手头的业务数据都是连续的交易额、访问量,一跑就报错,心都凉了半截。后来只能把数值型数据都做分箱处理,比如把年龄分成’青年’、‘中年’、‘老年’,收入分成’低收入’、‘中等收入’、‘高收入’。效果嘛……总觉得有点‘为了用而用’,分箱分得不好就感觉很粗糙,丢失了很多细节,模型的解释力也变弱了。要是能有更智能的连续变量处理方法就好了,或者直接找支持连续变量的库。

啊哈,这不是在逼我们走’曲线救国’的路子嘛!连续变量不能直接用?那就把它变成离散的!高基数特征太复杂?那就给它‘砍几刀’!听起来是不是有点像为了穿上不合脚的鞋,硬生生把脚削小一点?离散化肯定会损失信息啊,就像把高清电影硬压成马赛克片。关键是怎么削才能削得艺术,削完还能看出原片的神韵,这才是学问!不然因果关系没搞明白,倒是先把自己搞成了‘数据整形师’。

针对“日常工作中,将业务问题转化为因果模型是否比选库更难”这一讨论,我的看法是,这确实是因果分析的核心挑战。技术库只是实现工具,其效用高度依赖于因果图的正确构建和识别。这需要深厚的领域知识,对潜在混杂因素、中介变量和因果方向有清晰的理解。错误的因果假设会导致偏差估计甚至得出相反结论。所以,建模前的因果假设制定和验证(例如通过DoWhy的框架)远比单纯的代码实现重要。

同感同感!我以前也觉得因果推断酷毙了,学了点皮毛就想上手。结果发现,最头疼的不是敲代码,而是怎么定义“处理变量”和“结果变量”,还有那些“混杂因素”。比如,老板说想提高用户留存,这算是一个结果。那“新功能上线”是处理变量吗?还是“客服电话回访”?又有哪些因素(比如用户年龄、使用时长)同时影响这两个?光想这些就大了一个头,感觉比写几十行代码难多了。

嗐,这不就是那句老话吗,'工欲善其事,必先利其器’是没错,但’器’再好,你得知道’事’在哪儿啊!选库?那是下午茶时间顺手就能搞定的事。把业务掰开了揉碎了,搞清楚谁是爸爸谁是儿子(这比喻可能不恰当,但你懂的),那才是真正的掉头发工程。有时候好不容易搞明白了因果关系,结果发现数据压根儿不支持,那才是人间真实!