Do-sampler 简介#
作者:Adam Kelleher
“do-sampler” 是 do-why 中的一个新功能。虽然大多数潜在结果导向的估计器侧重于估计特定的对比 \(E[Y_0 - Y_1]\),但 Pearlian 推断则侧重于更基本量的估计,例如一组结果 Y 的联合分布 \(P(Y)\),这可以用于推导其他感兴趣的统计量。
一般来说,很难非参数地表示概率分布。即使可以,您也不想忽略用于生成数据时有限样本的问题。考虑到这些问题,我们决定通过使用一个称为“do-sampler”的对象从中抽样来表示干预分布。利用这些样本,我们可以希望能计算出干预数据的有限样本统计量。如果我们对许多此类样本进行自助抽样,甚至可以希望能获得这些统计量的良好抽样分布。
用户应注意,这仍然是一个活跃的研究领域,因此在使用 do-sampler 的自助抽样误差条时应谨慎,不要过于自信。
请注意,do sampler 是从结果分布中抽样,因此样本之间会有显著差异。要使用它们计算结果,建议生成多个此类样本,以便了解您感兴趣统计量的后验方差。
Pearlian 干预#
遵循 Pearlian 因果模型中的干预概念,我们的 do-sampler 实现了以下一系列步骤
中断原因
使其生效
传播并抽样
在第一阶段,我们想象切断对所有我们正在干预的变量的入边。在第二阶段,我们将这些变量的值设置为其干预量。在第三阶段,我们通过我们的模型将该值向前传播,并通过抽样过程计算干预结果。
在实践中,我们可以通过多种方式实现这些步骤。当我们使用 PyMC3 构建线性贝叶斯网络作为模型时,这些步骤最为明确,这也是 MCMC do sampler 的基础。在这种情况下,我们将一个贝叶斯网络拟合到数据上,然后构建一个代表干预网络的新网络。结构方程使用在初始网络中拟合的参数设置,然后我们从那个新网络中抽样以获得 do 样本。
在加权 do sampler 中,我们抽象地将“中断原因”视为通过倾向评分估计来考虑选择进入因果状态。这些评分包含用于阻断后门路径的信息,因此具有与切断进入因果状态的边相同的统计效应。我们通过选择数据集中具有正确因果状态值的子集来使处理生效。最后,我们使用逆倾向加权生成加权随机样本,以获得我们的 do 样本。
您可以通过其他方式实现这三个步骤,但公式是相同的。我们将它们抽象为抽象类方法,如果您想创建自己的 do sampler,应该覆盖这些方法!
状态性#
通过高级 pandas API 访问时,do sampler 默认是无状态的。这使得它易于使用,您可以通过重复调用 pandas.DataFrame.causal.do
来生成不同的样本。它也可以被设置为有状态,这有时很有用。
我们之前提到的三阶段过程是通过将一个内部 pandas.DataFrame
传递给这三个阶段来实现的,但将其视为临时数据框。在返回结果之前,内部数据框默认会被重置。
在生成样本之间保持 do sampler 的状态可以效率更高。当第一步需要拟合昂贵的模型时,尤其如此,例如 MCMC do sampler、核密度 sampler 和加权 sampler 的情况。
您可能希望一次性拟合模型,然后从 do sampler 生成许多样本,而不是为每个样本重新拟合模型。您可以在调用 pandas.DataFrame.causal.do
方法时设置 kwarg stateful=True
来实现这一点。要重置数据框的状态(删除模型以及内部数据框),您可以调用 pandas.DataFrame.causal.reset
方法。
通过低级 API,sampler 默认是有状态的。假定使用低级 API 的“高级用户”会希望对抽样过程有更多控制。在这种情况下,状态由内部数据框 self._df
携带,它是实例化时传入的数据框的副本。原始数据框保存在 self._data
中,并在用户重置状态时使用。
集成#
do-sampler 构建在 do-why 中使用的识别抽象之上。它会自动执行识别,并使用此识别自动构建所需的任何模型。
指定干预#
在 dowhy.do_sampler.DoSampler
对象上有一个名为 keep_original_treatment
的关键字参数。虽然干预可能是将所有单元的处理值设置为某个特定值,但通常更自然的做法是保持它们原有的设置,并在效应估计期间去除混淆偏差。如果您不想指定干预,可以将该关键字参数设置为 keep_original_treatment=True
,这样三阶段过程的第二阶段将被跳过。在这种情况下,抽样时指定的任何干预都将被忽略。
如果 keep_original_treatment
标志设置为 false(默认为 false),则在从 do sampler 抽样时必须指定干预。详情请参阅下面的演示!
演示#
首先,让我们生成一些数据和一个因果模型。这里,Z 与我们的因果状态 D 以及结果 Y 混淆。
[1]:
import os, sys
sys.path.append(os.path.abspath("../../../"))
[2]:
import numpy as np
import pandas as pd
import dowhy.api
[3]:
N = 5000
z = np.random.uniform(size=N)
d = np.random.binomial(1., p=1./(1. + np.exp(-5. * z)))
y = 2. * z + d + 0.1 * np.random.normal(size=N)
df = pd.DataFrame({'Z': z, 'D': d, 'Y': y})
[4]:
(df[df.D == 1].mean() - df[df.D == 0].mean())['Y']
[4]:
因此,朴素效应大约高出 60%。现在,让我们为这些数据构建一个因果模型。
[5]:
from dowhy import CausalModel
causes = ['D']
outcomes = ['Y']
common_causes = ['Z']
model = CausalModel(df,
causes,
outcomes,
common_causes=common_causes)
nx_graph = model._graph._graph
现在我们有了模型,我们可以尝试识别因果效应。
[6]:
identification = model.identify_effect(proceed_when_unidentifiable=True)
识别奏效了!虽然实际上在 do sampler 内部会完成这一步,但先检查一下识别是否奏效也无妨。现在,让我们构建 sampler。
[7]:
from dowhy.do_samplers.weighting_sampler import WeightingSampler
sampler = WeightingSampler(graph=nx_graph,
action_nodes=causes,
outcome_nodes=outcomes,
observed_nodes=df.columns.tolist(),
data=df,
keep_original_treatment=True,
variable_types={'D': 'b', 'Z': 'c', 'Y': 'c'}
)
现在,我们可以直接从干预分布中抽样了!由于我们将 keep_original_treatment
标志设置为 False
,这里传入的任何处理都将被忽略。在这里,我们只需传入 None
来表明我们知道不希望传入任何值。
如果您希望指定干预,只需在此处将干预值作为列表或 numpy 数组传入即可。
[8]:
interventional_df = sampler.do_sample(None)
[9]:
(interventional_df[interventional_df.D == 1].mean() - interventional_df[interventional_df.D == 0].mean())['Y']
[9]:
现在我们更接近真实效应了,真实效应大约是 1.0!