- 机器学习
- 数据分析
【免费下载链接】dowhy
DoWhy is a Python library for causal inference that supports explicit modeling and testing of causal assumptions. DoWhy is based on a unified language for causal inference, combining causal graphical models and potential outcomes frameworks.
本篇技术文章聚焦 DoWhy 库的dowhy.causal_identifier包——这是 DoWhy「识别(Identification)」环节的核心引擎,负责在给定因果图、处理变量、结果变量与已观测节点的前提下,判断因果效应是否可识别,并输出对应的估计量(estimand)表达式与调整集。读完本文,你将理解该包 7 个子模块的职责划分、identify_effect_auto的主流程(后门/工具变量/前门/广义调整四条识别路径)、7 种BackdoorAdjustment方法名背后的搜索策略与复杂度控制,以及IdentifiedEstimand结果对象如何被下游估计器消费。
1. 文档定位:dowhy.causal_identifier.rst 讲了什么
Sphinx 模块文档页 本身是标准的自动 API 文档,它通过 7 个.. automodule::指令声明了该包对外暴露的全部子模块:
- adjustment_set 模块:
AdjustmentSet调整集容器类; - auto_identifier 模块:
AutoIdentifier类与identify_effect_auto主识别函数; - backdoor 模块:优化的后门集搜索(DFS + 命中集算法);
- efficient_backdoor 模块:基于统计效率最优准则的门控调整集算法;
- id_identifier 模块:Shpitser–Tse 的 ID 算法实现;
- identified_estimand 模块:
IdentifiedEstimand结果对象; - identify_effect 模块:
CausalIdentifier协议与顶层identify_effect入口函数。
包级init.py 的__all__清单进一步确认了公开 API 面:AutoIdentifier、CausalIdentifier、identify_effect_auto、identify_effect_id、BackdoorAdjustment、GeneralizedAdjustment、EstimandType、IdentifiedEstimand、IDIdentifier、identify_effect,以及三个估计量构造函数construct_adjustment_estimand、construct_frontdoor_estimand、construct_iv_estimand。这意味着文档页所索引的 7 个模块共同构成了「识别」这一因果推断四步法(建模 → 识别 → 估计 → 反驳)中第二步的全部能力。
2. 入口层:identify_effect 函数与 CausalIdentifier 协议
identify_effect.py 只有约 50 行,却是理解整个包设计的关键。它包含两个组件:
2.1 CausalIdentifier 协议类
CausalIdentifier是一个typing.Protocol,规定了任何识别器至少需要实现一个identify_effect(graph, action_nodes, outcome_nodes, **kwargs)方法。源码 docstring 明确写道:该类「为与 CausalModel 的向后兼容而存在,未来将以函数式调用auto_identify_effect()取代而弃用」。从源码结构看,DoWhy 正在从「面向对象式识别器」迁移到「函数式 API」,协议类是这一迁移期的兼容层。
2.2 identify_effect 顶层函数
def identify_effect( graph: nx.DiGraph, action_nodes: Union[str, List[str]], outcome_nodes: Union[str, List[str]], observed_nodes: Union[str, List[str]], ) -> IdentifiedEstimand: return identify_effect_auto( graph, action_nodes, outcome_nodes, observed_nodes, EstimandType.NONPARAMETRIC_ATE, backdoor_adjustment=BackdoorAdjustment.BACKDOOR_DEFAULT, optimize_backdoor=False, )这是一个薄封装:固定使用非参数平均处理效应(ATE)估计量类型、默认后门策略、不启用优化搜索,直接委托给identify_effect_auto。四个位置参数中,observed_nodes尤其重要——它界定了「哪些变量在数据中可见」,直接影响哪些调整集是可执行的(详见第 5 节的可观测性过滤)。
3. 主识别引擎:identify_effect_auto 与四种估计量类型
auto_identifier.py(1164 行)是本包体量最大、逻辑最核心的模块。它首先定义了三个枚举,它们是调用方必须掌握的「配置词汇表」:
3.1 EstimandType:四种估计量类型
class EstimandType(Enum): # Average total effect NONPARAMETRIC_ATE = "nonparametric-ate" # Natural direct effect NONPARAMETRIC_NDE = "nonparametric-nde" # Natural indirect effect NONPARAMETRIC_NIE = "nonparametric-nie" # Controlled direct effect NONPARAMETRIC_CDE = "nonparametric-cde"identify_effect_auto对这四类估计量的分派逻辑各不相同:
| 估计量类型 | 分派函数 | 使用的方法 |
|---|---|---|
nonparametric-ate | identify_ate_effect | 后门 + 工具变量 + 前门 + 广义调整,四路并行尝试 |
nonparametric-nie/nonparametric-nde | identify_nie_effect/identify_nde_effect | 先做后门识别,再识别中介变量,构造中介估计量 |
nonparametric-cde | identify_cde_effect | 在「删除处理→结果直接边」的图上做后门识别(direct_effect=True),引用 Vanderwheele (2011) |
值得注意的两个边界处理:
- 无有向路径短路:
identify_effect_auto一开始就调用has_directed_path检查,若处理变量到结果变量之间不存在有向路径,直接返回带no_directed_path=True标志的IdentifiedEstimand,并打印「Causal Effect is zero」——此时无需任何调整集; - 不支持的类型抛
ValueError,错误信息中列出全部四个合法枚举值。
3.2 ATE 识别的四路流水线(identify_ate_effect)
identify_ate_effect的执行顺序是理解本包的骨架:
- 后门识别。根据
backdoor_adjustment是否属于EFFICIENT_METHODS集合分流:非效率方法调用identify_backdoor(当optimize_backdoor=True时走 backdoor.py 的Backdoor优化搜索),效率方法调用identify_efficient_backdoor; - 工具变量识别。调用
get_instruments找出满足「与处理相关、与结果独立(给定处理)、不被处理造成」三条件的工具变量,若存在则用construct_iv_estimand生成 IV 估计量,estimands_dict["iv"]中存放的 sympy 表达式为E[∂Y/∂Z / (∂T/∂Z)],并附带两条显式假设:As-if-random(Z 不被潜在变量 U 影响)与Exclusion(移除 Z→T 后 Z 不再影响 Y); - 前门识别。
identify_frontdoor在候选变量(处理的非自身、非结果的下游节点 ∩ 观测节点)中穷举,逐一验证三条前门条件:条件 1 用check_valid_frontdoor_set检查候选集是否拦截所有有向路径;条件 2 用空调整集检查「处理与候选变量之间无混杂」;条件 3 在删除候选集出边的手术图上检查「处理阻断候选变量与结果间的全部混杂」。命中后由construct_frontdoor_estimand生成估计量E[(∂Z/∂T)(∂Y/∂Z)],并额外计算两阶段的混杂变量(一阶段为处理→中介,二阶段为中介→结果); - 广义调整识别。这是后门判据的推广,只在Python ≥ 3.10下启用(因为依赖
nx.algorithms.find_minimal_d_separator),低版本 Python 会记录 warning 并跳过;结果写入estimands_dict["general_adjustment"]。
最后,所有结果被打包进一个IdentifiedEstimand对象返回,字段包括estimands(各策略对应的估计量字典)、backdoor_variables、general_adjustment_variables、instrumental_variables、frontdoor_variables及中介相关字段。
3.3 后门集搜索的七种策略
BackdoorAdjustment枚举定义了 7 种方法名,配合identify_backdoor与find_valid_adjustment_sets的实现可以精确定位每种策略的行为:
class BackdoorAdjustment(Enum): BACKDOOR_DEFAULT = "default" BACKDOOR_EXHAUSTIVE = "exhaustive-search" BACKDOOR_MIN = "minimal-adjustment" BACKDOOR_MAX = "maximal-adjustment" BACKDOOR_EFFICIENT = "efficient-adjustment" BACKDOOR_MIN_EFFICIENT = "efficient-minimal-adjustment" BACKDOOR_MINCOST_EFFICIENT = "efficient-mincost-adjustment"identify_backdoor的搜索流程(auto_identifier.py L531-L633)值得逐步拆解:
- d-分离算法选择:
dseparation_algo="default"时对图做 do-手术(删除处理的出边,direct_effect=True时只删除指向结果的直接边),在手术图上做 d-分离判定;"naive"模式则退化为枚举后门路径; - 空集优先:先检查空调整集是否已满足后门判据(即处理与结果本身已 d-分离)。若满足且方法为
minimal-adjustment,立即返回空集——这是最小调整的自然下界; - 候选变量过滤:候选集 = 全图节点 − 处理节点 − 结果节点(非中介情形再减去处理的所有下游),随后剔除与处理或结果本身 d-分离的变量(这类变量不可能出现在任何有效调整集中);
- 按集合规模穷举:
find_valid_adjustment_sets用itertools.combinations枚举候选集组合。minimal-adjustment从小集合向大集合枚举(找到第一个即停),maximal-adjustment/default从大集合向小枚举;default策略找到第一个有效集合后会再跑一遍BACKDOOR_MIN,从而在结果中同时保留默认集与最小集;exhaustive-search会枚举至多MAX_BACKDOOR_ITERATIONS = 100000次迭代以防组合爆炸; - 全观测剪枝:当图中所有节点均可观测且最大候选集合都不满足后门判据时,其任意子集也不可能满足,搜索提前终止。
三种efficient-*方法不在上面这条穷举路径上,而是进入identify_efficient_backdoor,其 docstring 引用 Rotnitzky & Smucler (2020)、Smucler, Sapienza & Rotnitzky (2021)、Smucler & Rotnitzky (2022),目标是在非参数图形模型下寻找渐近方差最小的调整集;docstring 同时指出,按 Henckel, Perkovic & Maathuis (2020) 的结论,这些最优集在线性图形模型 + OLS 估计下同样保持最优。
3.4 默认调整集的选择逻辑
get_default_adjustment_set_id揭示了「backdoor」这一默认键背后的两条规则:
- 最少工具变量优先:在所有有效调整集中,选取包含最少工具变量(instrumental variables)的集合——避免把工具变量纳入调整集(对工具变量做条件化会引入坏控制问题);
- 最少变量数优先:在满足第一条的集合中,选取调整变量数目最少的一个,作为
default_backdoor_id。
4. 优化后门搜索:backdoor.py 的命中集算法
optimize_backdoor=True时绕开组合枚举,改用 backdoor.py 中的Backdoor类。其设计由三个数据结构协作完成:
Path:表示两个节点之间的一条具体路径,记录「是否被阻断」与「需要条件化的中间节点集合」;NodePair:聚合某节点对之间的所有路径,_condition_vars是一个集合的集合(每条路径对应一个需条件化的变量集),update方法在发现新路径时合并信息,并支持把子问题的解回填到前缀路径(_path_search_util中的递归记忆化逻辑);HittingSetAlgorithm:经典的命中集(hitting set)近似最小化——把「找到能击中每条后门路径至少一个非碰撞节点的最小变量集」建模为集合覆盖问题:统计每个变量(排除碰撞节点)出现在多少条路径中,贪心挑选出现次数最多的变量加入解集,迭代直到全部路径被覆盖。碰撞节点被显式排除,因为对碰撞节点做条件化会打开而非关闭路径。
get_backdoor_vars的入口把有向图转无向邻接表后对每个「处理节点 × 结果节点」配对执行 DFS 路径搜索(_path_search_util用prev_arrow标志识别碰撞结构:连续两条入向箭头构成碰撞节点,该分支被标记为 blocked 并剪枝),最终把HittingSetAlgorithm.find_set()的结果包装为AdjustmentSet返回。相比穷举法,这套 DFS + 贪心命中集的方式避免了O(2^n)的组合枚举,适合变量较多的图。
5. 结果对象:IdentifiedEstimand 的数据结构与消费方式
identified_estimand.py 定义了识别步骤的唯一输出契约。构造函数接收 14 个字段(其中identifier已被标记为待弃用),核心字段包括:treatment_variable/outcome_variable(经parse_state规范化为列表)、estimand_type、estimands(策略名 → 估计量字典)、backdoor_variables/general_adjustment_variables(策略名 → 调整变量列表)、instrumental_variables、frontdoor_variables、mediator_variables、两阶段中介混杂、default_backdoor_id、default_adjustment_set_id与identifier_method。
对下游最有用的访问器:
get_adjustment_set(key=None)/set_adjustment_set:当identifier_method == "general_adjustment"时自动路由到广义调整变量,否则走后门变量;get_backdoor_variables(key):若未显式传 key 且当前估计器是 backdoor 类方法,返回该策略对应的调整集;否则返回默认集——这解释了为什么估计器可以无感地拿到「正确的」调整集;__str__(only_target_estimand, show_all_backdoor_sets):人类可读的报告,no_directed_path=True时输出「因果效应为零」;否则用sp.pretty打印每个估计量的 sympy 表达式,逐条列出其假设(如Unconfoundedness:若 U→T 且 U→Y,则 P(Y|T,X,U)=P(Y|T,X))。
每个estimands字典项的值是一个{"estimand": sympy表达式, "assumptions": {假设名: 假设文本}}结构。例如调整估计量construct_adjustment_estimand生成D[E[Y|Z]/dT]形式的导数表达式(并附带字符串表达式d(Y|z1,z2)/dt);工具变量与前门估计量分别生成二阶导数比与乘积形式的 sympyExpectation对象。这些符号表达式是 DoWhy「先识别、后估计」范式的数据基础:估计器据此知道要对哪些变量回归、做何种加权。
6. 调整集容器与广义调整
adjustment_set.py 是一个 30 行的轻量数据类:AdjustmentSet(adjustment_type, adjustment_variables, num_paths_blocked_by_observed_nodes=None),两个类型常量区分BACKDOOR = "backdoor"与GENERAL = "general"。注释特意说明:广义调整集在数学上涵盖了后门集,但鉴于后门判据的普遍性,两者被显式区分。
广义调整的核心实现在identify_generalized_adjustment_set:先构造「proper backdoor graph」与「proper causal path nodes」(因果路径上的节点及其下游),然后调用nx.algorithms.find_minimal_d_separator,把搜索限制在「非因果路径节点的观测节点」内——即调整集绝不包含中介变量。docstring 引用 van der Zander, Liśkiewicz & Textor 的 UAI 2014 论文「Constructing Separators and Adjustment Sets in Ancestral Graphs」,强调该准则是完备的:只要存在某个调整集,此算法就能找到,而不像后门判据那样可能漏掉不满足后门条件但依然有效的调整集。当前exhaustive-search分支会显式抛出「尚未支持」的ValueError,默认行为只返回单个最小调整集。
7. ID 算法:超越后门/前门/工具的完整判据
id_identifier.py 实现了 Pearl 学派 Shpitser–Tse 的 ID 算法(源码 docstring 给出 Shpitser 博士论文链接及伪代码位置 Pg 40)。它的定位是:后门、前门、工具变量只是 ID 算法的特例;ID 算法能识别某些三者均失败的情形。
实现要点:
- 入口
identify_effect_id(graph, action_nodes, outcome_nodes)先做拓扑排序以验证输入是 DAG(否则抛ValueError),随后将图转为邻接矩阵并递归求解__adjacency_matrix_identify_effect; - 递归结构与论文伪代码逐行对应:第 1 行(无处理时返回观测分布的边缘化)、第 2 行(只对结果的祖先子图求解)、第 3 行(对不影响 Y 的节点强制 do 操作)、第 4 行(利用 C-component 分解把问题拆成乘积 + 边缘化)、第 5 行(hedge 结构导致不可识别,返回
None)、第 6 行(无双向边时把 do 替换为条件化,沿拓扑序输出因子分解P(y|do(x)) = Π P(v_i | pa_i) ...)、第 7 行(最复杂的子问题缩减); - 输出类型
IDExpression维护_product(待相乘的估计器列表)与_sum(待边缘化的变量列表),__str__把嵌套结构渲染成「Sum over {...}: Predictor: P(y|...)」的缩进文本;若图不可识别,打印「The graph is not identifiable」。
对应的测试覆盖位于 tests/causal_identifiers/test_id_identifier.py,可作为各分支行为的验证参考。
8. 实践路径:从函数调用到测试用例
综合以上源码,dowhy.causal_identifier包的最小可用调用形如:
import networkx as nx from dowhy.causal_identifier import identify_effect graph = nx.DiGraph() graph.add_edges_from([("U", "T"), ("U", "Y"), ("T", "Y")]) estimand = identify_effect( graph=graph, action_nodes="T", outcome_nodes="Y", observed_nodes=["T", "Y", "U"], ) print(estimand) # 打印 ATE 估计量表达式与 Unconfoundedness 假设若需要精细控制,直接调用identify_effect_auto并传入EstimandType与BackdoorAdjustment枚举;需要costs(形如[(node, {"cost": x}) for node in nodes],仅efficient-mincost-adjustment使用,缺省视为全 1 且仅对正数有效)或conditional_node_names(用于界定「决定处理分配的变量」,未提供时假定干预把处理设为常数)时,注意 efficient_backdoor.py 的EfficientBackdoor.__init__会对多维处理/多维结果直接抛错(效率后门方法只支持一维),并校验 costs 必须为正数、条件变量必须在observed_nodes内。
识别质量由三层测试保障:tests/causal_identifiers/test_auto_identifier.py、tests/causal_identifiers/test_backdoor_identifier.py、tests/causal_identifiers/test_efficient_backdoor_identifier.py 与 tests/causal_identifiers/test_id_identifier.py,其中 efficient 与 complete adjustment 测试还依赖独立的示例图库 example_graphs_efficient.py。这些测试对应的教程示例(如 efficient backdoor 教程、ID 算法教程)展示了从识别到估计的完整流水线。
9. 小结:包内模块职责速查
| 模块 | 核心对象/函数 | 一句话职责 |
|---|---|---|
| identify_effect.py | identify_effect、CausalIdentifier | 顶层薄封装入口 + 兼容协议 |
| auto_identifier.py | identify_effect_auto、identify_backdoor、identify_frontdoor、construct_*_estimand | 四路识别主引擎与 sympy 估计量构造 |
| backdoor.py | Backdoor、HittingSetAlgorithm | DFS + 贪心命中集的优化后门搜索 |
| efficient_backdoor.py | EfficientBackdoor | 方差/成本最优调整集(Rotnitzky 系算法) |
| id_identifier.py | identify_effect_id、IDExpression | 完整 ID 算法(C-component 递归分解) |
| identified_estimand.py | IdentifiedEstimand | 识别结果的数据契约与人类可读报告 |
| adjustment_set.py | AdjustmentSet | backdoor/general 两类调整集的容器 |
需要留意的适用前提:AutoIdentifier类与IDIdentifier类均已标注向后兼容、未来弃用,新代码应优先使用identify_effect/identify_effect_auto/identify_effect_id函数;广义调整识别要求 Python ≥ 3.10;效率后门方法仅支持一维处理与结果;exhaustive-search后门策略受 10 万次迭代上限约束,变量数较多时可能提前中止并给出 warning。这些边界条件均可在对应源码的 docstring 与枚举定义中逐一复核。
- 机器学习
- 数据分析
【免费下载链接】dowhy
DoWhy is a Python library for causal inference that supports explicit modeling and testing of causal assumptions. DoWhy is based on a unified language for causal inference, combining causal graphical models and potential outcomes frameworks.
相关推荐
DoWhy因果模型构建与识别
DoWhy因果模型构建与识别 本文系统介绍了DoWhy因果推断库的核心功能与应用方法。文章首先详细阐述了因果图模型的多种构建方式,包括基础构建方法(Networ
机器学习数据分析如何用 DoWhy 构建因果图并做 machine-learning-for-trading 的因果识别
如何用 DoWhy 构建因果图并做 machine learning for trading 的因果识别 这篇文章解决一个具体任务:在 machine lear
示例工程金融科技机器学习人工智能深度学习【亲测免费】 探索因果世界:Dowhy —— 强大的因果推断库
探索因果世界:Dowhy —— 强大的因果推断库 在这个数据驱动的时代,了解变量间的影响关系对于决策至关重要。我们不仅需要预测结果,还需要理解当某个变量发生变化
机器学习数据分析
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考