Do-sampler 介绍#

作者:Adam Kelleher

“do-sampler”是do-why中的一个新功能。虽然大多数基于潜在结果的估计器专注于估计特定的对比\(E[Y_0 - Y_1]\),但Pearl推理则专注于更基本的量,如一组结果Y的联合分布\(P(Y)\),这可以用来推导出其他感兴趣的统计量。

通常,很难以非参数方式表示概率分布。即使可以,你也不会想要忽视用于生成它的数据中的有限样本问题。考虑到这些问题,我们决定通过使用一个名为“do-sampler”的对象从中采样来表示干预分布。通过这些样本,我们可以希望计算干预数据的有限样本统计量。如果我们自举许多这样的样本,我们甚至可以希望这些统计量有良好的采样分布。

用户应注意,这仍然是一个活跃的研究领域,因此在使用do-samplers的自举误差条时应谨慎,不要过于自信。

请注意,采样器是从结果分布中采样的,因此样本之间会有显著差异。为了使用它们来计算结果,建议生成多个这样的样本,以了解您感兴趣的统计量的后验方差。

珍珠干预#

根据Pearl因果模型中的干预概念,我们的do-samplers实现了一系列步骤:

  1. 中断原因

  2. 使有效

  3. 传播和采样

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

在实践中,我们可以通过多种方式实现这些步骤。当我们在PyMC3中将模型构建为线性贝叶斯网络时,这些步骤最为明确,这也是MCMC do采样器的基础。在这种情况下,我们首先将贝叶斯网络拟合到数据上,然后构建一个新的网络来表示干预网络。结构方程使用初始网络中拟合的参数进行设置,我们从该新网络中采样以获得我们的do样本。

在加权抽样器中,我们通过倾向得分估计来考虑进入因果状态的选择,从而抽象地认为“干扰原因”。这些得分包含了用于阻断后门路径的信息,因此具有与切断进入因果状态的边缘相同的统计效果。我们通过选择具有正确因果状态值的数据集子集来使处理有效。最后,我们使用逆倾向加权生成加权随机样本,以获得我们的do样本。

还有其他方法可以实现这三个步骤,但公式是相同的。我们已经将它们抽象为抽象类方法,如果您想创建自己的do sampler,应该重写这些方法。

状态性#

当通过高级pandas API访问时,do sampler默认是无状态的。这使得它使用起来很直观,你可以通过重复调用pandas.DataFrame.causal.do生成不同的样本。它可以被设置为有状态,这在某些情况下很有用。

我们之前提到的三阶段过程是通过将内部的pandas.DataFrame传递给每个阶段来实现的,但将其视为临时的。默认情况下,内部数据框在返回结果之前会被重置。

在生成样本之间在do采样器中维护状态可能会更加高效。当步骤1需要拟合一个昂贵的模型时,这一点尤其正确,例如MCMC do采样器、核密度采样器和加权采样器的情况。

与其为每个样本重新拟合模型,您可能希望只拟合一次,然后从do采样器中生成许多样本。您可以通过在调用pandas.DataFrame.causal.do方法时设置stateful=True来实现这一点。要重置数据框的状态(删除模型以及内部数据框),您可以调用pandas.DataFrame.causal.reset方法。

通过较低级别的API,采样器默认是有状态的。假设使用低级API的“高级用户”希望更多地控制采样过程。在这种情况下,状态由内部数据帧self._df携带,这是实例化时传递的数据帧的副本。原始数据帧保存在self._data中,并在用户重置状态时使用。

集成#

do-sampler 构建在 do-why 中使用的识别抽象之上。它会自动执行识别,并使用此识别自动构建所需的任何模型。

指定干预措施#

dowhy.do_sampler.DoSampler对象上有一个名为keep_original_treatment的kwarg。虽然干预可能是将所有单位的治疗值设置为某个特定值,但通常更自然的是保持它们原来的设置,并在效果估计期间消除混杂偏差。如果您不希望指定干预,可以将kwarg设置为keep_original_treatment=True,这样将跳过3阶段过程的第二阶段。在这种情况下,任何在采样时指定的干预都将被忽略。

如果keep_original_treatment标志设置为false(默认情况下),那么在使用do采样器进行采样时,您必须指定一个干预。详情请参见下面的演示!

演示#

首先,让我们生成一些数据和一个因果模型。在这里,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采样器完成,但在继续之前检查识别工作是否正常也无妨。现在,让我们构建采样器。

[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左右!