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 实现了以下一系列步骤

  1. 中断原因

  2. 使其生效

  3. 传播并抽样

在第一阶段,我们想象切断对所有我们正在干预的变量的入边。在第二阶段,我们将这些变量的值设置为其干预量。在第三阶段,我们通过我们的模型将该值向前传播,并通过抽样过程计算干预结果。

在实践中,我们可以通过多种方式实现这些步骤。当我们使用 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]:
$\displaystyle 1.63466412300499$

因此,朴素效应大约高出 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]:
$\displaystyle 1.07623132942734$

现在我们更接近真实效应了,真实效应大约是 1.0!