Be careful when interpreting predictive models in search of causal insights -- SHAP系列论文

作者: pdnbplus | 发布时间: 2024/10/26 | 阅读量: 78

Be careful when interpreting predictive models in search of causal insights -- SHAP系列论文

在解释预测模型以寻找因果见解时要小心

本文是SHAP与 Microsoft 的 Eleanor Dillon、Jacob LaRiviere、Scott Lundberg、Jonathan Roth 和 Vasilis Syrgkanis 合著的一篇关于因果关系和可解释机器学习的文章。

XGBoost 等预测性机器学习模型与 SHAP 等可解释性工具配合使用时,会变得更加强大。这些工具确定了输入特征和预测结果之间信息量最大的关系,这对于解释模型正在做什么、获得利益相关者的支持和诊断潜在问题非常有用。

我们很容易将这种分析更进一步,并假设解释工具也可以确定决策者如果想在未来改变结果,应该操纵哪些特征。然而,在本文中,我们讨论了使用预测模型来指导这种政策选择通常会产生误导。

原因与相关性和因果关系之间的根本区别有关。SHAP 使预测 ML 模型拾取的相关性变得透明。但是,使相关性透明并不意味着它们是因果关系!所有预测模型都隐含地假设每个人将来都会保持相同的行为方式,因此相关模式将保持不变。要了解如果某人开始行为不同会发生什么,我们需要建立因果模型,这需要做出假设并使用因果分析工具。

订阅者保留的例子

想象一下,我们的任务是构建一个模型来预测客户是否会续订他们的产品订阅。假设经过一番挖掘,我们设法获得了对预测客户流失很重要的八个功能:

  • 客户折扣
  • 广告支出
  • 客户的每月使用情况
  • 上次升级
  • 客户报告的错误
  • 与客户的互动
  • 与客户的销售电话
  • 宏观经济活动

然后,我们使用这些功能来训练一个基本的 XGBoost 模型,以预测客户是否会在订阅到期时续订(数据是自己编的):

import numpy as np
import pandas as pd
import scipy.stats
import sklearn
import xgboost


class FixableDataFrame(pd.DataFrame):
    """Helper class for manipulating generative models."""

    def __init__(self, *args, fixed={}, **kwargs):
        self.__dict__["__fixed_var_dictionary"] = fixed
        super().__init__(*args, **kwargs)

    def __setitem__(self, key, value):
        out = super().__setitem__(key, value)
        if isinstance(key, str) and key in self.__dict__["__fixed_var_dictionary"]:
            out = super().__setitem__(key, self.__dict__["__fixed_var_dictionary"][key])
        return out


# generate the data
def generator(n, fixed={}, seed=0):
    """The generative model for our subscriber retention example."""
    if seed is not None:
        np.random.seed(seed)
    X = FixableDataFrame(fixed=fixed)

    # the number of sales calls made to this customer
    X["Sales calls"] = np.random.uniform(0, 4, size=(n,)).round()

    # the number of sales calls made to this customer
    X["Interactions"] = X["Sales calls"] + np.random.poisson(0.2, size=(n,))

    # the health of the regional economy this customer is a part of
    X["Economy"] = np.random.uniform(0, 1, size=(n,))

    # the time since the last product upgrade when this customer came up for renewal
    X["Last upgrade"] = np.random.uniform(0, 20, size=(n,))

    # how much the user perceives that they need the product
    X["Product need"] = X["Sales calls"] * 0.1 + np.random.normal(0, 1, size=(n,))

    # the fractional discount offered to this customer upon renewal
    X["Discount"] = ((1 - scipy.special.expit(X["Product need"])) * 0.5 + 0.5 * np.random.uniform(0, 1, size=(n,))) / 2

    # What percent of the days in the last period was the user actively using the product
    X["Monthly usage"] = scipy.special.expit(X["Product need"] * 0.3 + np.random.normal(0, 1, size=(n,)))

    # how much ad money we spent per user targeted at this user (or a group this user is in)
    X["Ad spend"] = (
        X["Monthly usage"] * np.random.uniform(0.99, 0.9, size=(n,)) + (X["Last upgrade"] < 1) + (X["Last upgrade"] < 2)
    )

    # how many bugs did this user encounter in the since their last renewal
    X["Bugs faced"] = np.array([np.random.poisson(v * 2) for v in X["Monthly usage"]])

    # how many bugs did the user report?
    X["Bugs reported"] = (X["Bugs faced"] * scipy.special.expit(X["Product need"])).round()

    # did the user renew?
    X["Did renew"] = scipy.special.expit(
        7
        * (
            0.18 * X["Product need"]
            + 0.08 * X["Monthly usage"]
            + 0.1 * X["Economy"]
            + 0.05 * X["Discount"]
            + 0.05 * np.random.normal(0, 1, size=(n,))
            + 0.05 * (1 - X["Bugs faced"] / 20)
            + 0.005 * X["Sales calls"]
            + 0.015 * X["Interactions"]
            + 0.1 / (X["Last upgrade"] / 4 + 0.25)
            + X["Ad spend"] * 0.0
            - 0.45
        )
    )

    # in real life we would make a random draw to get either 0 or 1 for if the
    # customer did or did not renew. but here we leave the label as the probability
    # so that we can get less noise in our plots. Uncomment this line to get
    # noiser causal effect lines but the same basic results
    X["Did renew"] = scipy.stats.bernoulli.rvs(X["Did renew"])

    return X


def user_retention_dataset():
    """The observed data for model training."""
    n = 10000
    X_full = generator(n)
    y = X_full["Did renew"]
    X = X_full.drop(["Did renew", "Product need", "Bugs faced"], axis=1)
    return X, y


def fit_xgboost(X, y):
    """Train an XGBoost model with early stopping."""
    X_train, X_test, y_train, y_test = sklearn.model_selection.train_test_split(X, y)
    dtrain = xgboost.DMatrix(X_train, label=y_train)
    dtest = xgboost.DMatrix(X_test, label=y_test)
    model = xgboost.train(
        {"eta": 0.001, "subsample": 0.5, "max_depth": 2, "objective": "reg:logistic"},
        dtrain,
        num_boost_round=200000,
        evals=((dtest, "test"),),
        early_stopping_rounds=20,
        verbose_eval=False,
    )
    return model

X, y = user_retention_dataset()
model = fit_xgboost(X, y)

一旦我们有了 XGBoost 客户保留模型,我们就可以开始探索它使用 SHAP 等可解释性工具学到的东西。我们首先绘制模型中每个特征的全局重要性:

import shap

explainer = shap.Explainer(model)
shap_values = explainer(X)

clust = shap.utils.hclust(X, y, linkage="single")
shap.plots.bar(shap_values, clustering=clust, clustering_cutoff=1)

在这里插入图片描述

此条形图显示提供的折扣、广告支出和报告的错误数量是驱动模型预测客户保留率的前三个因素。这很有趣,乍一看似乎很合理。条形图还包括一个特征冗余聚类,我们稍后会用到。

然而,当我们更深入地研究并研究改变每个特征的值如何影响模型的预测时,我们会发现一些不直观的模式。SHAP 散点图显示更改特征的值如何影响模型对续订概率的预测。如果蓝点遵循递增模式,则意味着特征越大,模型的预测续订概率就越高。

shap.plots.scatter(shap_values, ylabel="SHAP value\n(higher means more likely to renew)")

在这里插入图片描述

散点图显示了一些令人惊讶的发现:

  • 报告更多 bug 的用户更有可能续订!
  • 折扣较大的用户续订的可能性较小!

我们会对代码和数据管道进行三重检查以排除错误,然后与一些业务合作伙伴交谈,他们提供了直观的解释:

  • 重视产品的高使用率用户更有可能报告错误并续订订阅。
  • 销售人员倾向于向他们认为不太可能对产品感兴趣的客户提供高折扣,并且这些客户的流失率更高。

模型中这些乍一看违反直觉的关系是有问题的吗?这取决于我们的目标是什么!

我们这个模型的最初目标是预测客户保留率,这对于估算财务规划的未来收入等项目非常有用。由于报告更多 bug 的用户实际上更有可能续订,因此在模型中捕获这种关系有助于预测。只要我们的模型具有良好的样本外拟合,我们应该能够为 Finance 提供良好的预测,因此不必担心这种关系在模型中的方向。

这是一类称为 prediction tasks 的任务示例。在预测任务中,目标是在给定一组特征 X 的情况下预测结果 Y(例如续订)。预测练习的一个关键组成部分是,我们只关心预测模型 (X) 在类似于训练集的数据分布中接近 Y。X 和 Y 之间的简单关联对于这些类型的预测可能有所帮助。

SHAP能否识别出如何通过措施提高留存率?

但是,假设第二个团队采用我们的预测模型,其新目标是确定我们公司可以采取哪些措施来留住更多客户。这个团队非常关心每个 X 特征与 Y 的关系,不仅体现在我们的训练分布中,还体现出世界变化时产生的反事实场景。在该用例中,确定变量之间的稳定相关性已不再足够;该团队想知道操作特征 X 是否会导致 Y 发生变化。想象一下,当你告诉工程主管,你希望他引入新的错误来增加客户续订时,他的脸是什么样子!

这是一类称为因果任务的任务示例。在因果任务中,我们想知道改变世界 X 的某个方面(例如报告的 bug)如何影响结果 Y(续订)。在这种情况下,了解更改 X 是否会导致 Y 增加,或者数据中的关系是否仅仅是相关的,这一点至关重要。

估计因果效应的挑战

理解因果关系的一个有用工具是写下我们感兴趣的数据生成过程的因果图。

  • 我们示例的因果图说明了为什么我们的 XGBoost 客户保留模型所拾取的稳健预测关系与希望规划干预措施以提高保留率的团队感兴趣的因果关系不同。
  • 此图表只是真实数据生成机制(因为上面的数据是编的)的摘要。
  • 实心椭圆表示我们观察到的特征,而虚线椭圆表示我们未测量的隐藏特征。每个功能都是所有功能(带有箭头)的函数,外加一些随机效果。

在我们的示例中,我们知道因果图,因为我们模拟了数据。在实践中,真正的因果图是未知的,但我们可能能够使用关于世界如何运作的特定上下文领域知识来推断哪些关系可以存在,哪些关系不能存在。

pip install graphviz 要在用户变量和系统变量都进行修改,具体方法详见: https://www.cda.cn/discuss/post/details/5f362fbc922c1e31e6490c19

import graphviz

names = [
    "Bugs reported",
    "Monthly usage",
    "Sales calls",
    "Economy",
    "Discount",
    "Last upgrade",
    "Ad spend",
    "Interactions",
]
g = graphviz.Digraph()
for name in names:
    g.node(name, fontsize="10")
g.node("Product need", style="dashed", fontsize="10")
g.node("Bugs faced", style="dashed", fontsize="10")
g.node("Did renew", style="filled", fontsize="10")

g.edge("Product need", "Did renew")
g.edge("Product need", "Discount")
g.edge("Product need", "Bugs reported")
g.edge("Product need", "Monthly usage")
g.edge("Discount", "Did renew")
g.edge("Monthly usage", "Bugs faced")
g.edge("Monthly usage", "Did renew")
g.edge("Monthly usage", "Ad spend")
g.edge("Economy", "Did renew")
g.edge("Sales calls", "Did renew")
g.edge("Sales calls", "Product need")
g.edge("Sales calls", "Interactions")
g.edge("Interactions", "Did renew")
g.edge("Bugs faced", "Did renew")
g.edge("Bugs faced", "Bugs reported")
g.edge("Last upgrade", "Did renew")
g.edge("Last upgrade", "Ad spend")
g

在这里插入图片描述

这张图中有很多关系,但第一个重要的问题是,我们可以测量的一些特征会受到未测量的混杂特征的影响,比如产品需求和面临的错误。例如,报告更多 bug 的用户会遇到更多的 bug,因为他们使用产品更多,他们也更有可能报告这些 bug,因为他们更需要产品。产品需求对更新有其自身的直接因果效应。因为我们无法直接衡量产品需求,所以我们最终在预测模型中捕获的 bug 报告和续订之间的相关性结合了所面临 bug 的一个小的负面直接影响和产品需求的巨大积极混杂效应。

下图绘制了我们示例中的 SHAP 值与每个特征的真实因果效应(在本例中已知,因为我们生成了数据)。

def marginal_effects(generative_model, num_samples=100, columns=None, max_points=20, logit=True, seed=0):
    """Helper function to compute the true marginal causal effects."""
    X = generative_model(num_samples)
    if columns is None:
        columns = X.columns
    ys = [[] for _ in columns]
    xs = [X[c].values for c in columns]
    xs = np.sort(xs, axis=1)
    xs = [xs[i] for i in range(len(xs))]
    for i, c in enumerate(columns):
        xs[i] = np.unique([np.nanpercentile(xs[i], v, method="nearest") for v in np.linspace(0, 100, max_points)])
        for x in xs[i]:
            Xnew = generative_model(num_samples, fixed={c: x}, seed=seed)
            val = Xnew["Did renew"].mean()
            if logit:
                val = scipy.special.logit(val)
            ys[i].append(val)
        ys[i] = np.array(ys[i])
    ys = [ys[i] - ys[i].mean() for i in range(len(ys))]
    return list(zip(xs, ys))


shap.plots.scatter(
    shap_values,
    ylabel="SHAP value\n(higher means more likely to renew)",
    overlay={"True causal effects": marginal_effects(generator, 10000, X.columns)},
)

在这里插入图片描述

预测模型捕获了报告的 Bug 对留存率的总体积极影响(如 SHAP 所示),即使报告 Bug 的因果效应为零,而消除 Bug 的影响是负面的。

我们在折扣方面发现了类似的问题,这也是由未观察到的买家对商品的需求驱动的。我们的预测模型发现,折扣和留存率之间存在负相关关系,这是由与未观察到的特征 Product Need 的相关性驱动的,即使折扣对续订实际上存在很小的正因果效应!换句话说,如果两个买家具有相同的商品需求,并且在其他方面相似,则折扣较大的买家更有可能续订。

该图还揭示了第二个更隐蔽的问题,当我们开始将预测模型解释为因果关系时。请注意,广告支出也有类似的问题 - 它对留存率没有因果影响(黑线是平坦的),但预测模型正在产生积极影响!

在这种情况下,广告支出仅由上次升级和每月使用情况驱动,因此我们没有未观察到的混杂问题,而是存在观察到的混杂问题。Ad Spend 和影响 Ad Spend 的功能之间存在统计冗余。当我们拥有多个特征捕获的相同信息时,预测模型可以使用这些特征中的任何一个进行预测,即使它们并不都是因果关系。虽然广告支出对续订本身没有因果影响,但它与推动续订的几个功能密切相关。我们的正则化模型将 Ad Spend 确定为一个有用的预测因子,因为它总结了多个因果驱动因素(因此导致了一个更稀疏的模型),但如果我们开始将其解释为因果效应,这将变得严重误导。

预测模型何时可以回答因果问题

让我们从示例中的成功开始。请注意,我们的预测模型在捕获 Economy 特征的真实因果效应方面做得很好(更好的经济对留存率有积极影响)。那么,我们什么时候可以期望预测模型能够捕捉到真正的因果效应呢?

使 XGBoost 能够获得 Economy 的良好因果效应估计的重要因素是该功能的强大独立组件(在此模拟中);它对留存的预测能力与任何其他测量特征或任何未测量的混杂因素都没有很强的冗余。因此,它不会受到未测量的混杂因素或特征冗余的偏见的影响。

回到之前的SHAP条状图,由于我们在 SHAP 条形图的右侧添加了聚类,因此我们可以将数据的冗余结构视为树状图。当特征在树状图的底部(左侧)合并在一起时,这意味着这些特征包含的有关结果(更新)的信息非常冗余,模型可能使用了任一特征。当特征在树状图的顶部(右侧)合并在一起时,这意味着它们包含的有关结果的信息彼此独立。

在这里插入图片描述

我们可以看到 Economy 独立于所有其他测量的特征,方法是注意到 Economy 直到聚类树状图的最顶部才会与任何其他特征合并。这告诉我们,经济不会受到观察到的混淆的影响。但是,要相信经济效应是因果关系,我们还需要检查未观察到的混杂因素。检查未测量的混杂因素更难,并且需要使用领域知识(由上面示例中的业务合作伙伴提供)。

对于要提供因果结果的经典预测 ML 模型,这些特征不仅需要独立于模型中的其他特征,还需要独立于未观察到的混杂因素。找到自然表现出这种独立性水平的感兴趣驱动因素的示例并不常见,但是当我们的数据包含一些实验时,我们通常可以找到独立特征的示例。

当预测模型无法回答因果问题但因果推理方法可以提供帮助时

在大多数真实数据集中,特征不是独立且无混杂的,因此标准预测模型不会学习真正的因果效应。因此,用 SHAP 解释它们不会揭示因果效应。但并不是一切都会丢失,有时我们可以使用观察因果推理工具来解决或至少最小化这个问题。

Observed confounding 观察到的混杂

因果推理可以提供帮助的第一个场景是观察到的混淆。当有另一个特征因果地影响原始特征和我们预测的结果时,一个特征是“混淆的”。如果我们能测量到另一个特征,它就被称为观察到的混杂因素。

在我们的场景中,这方面的一个例子是 Ad Spend 功能。尽管广告支出对留存率没有直接的因果影响,但它与上次升级和每月使用功能相关,这些功能确实可以提高留存率。我们的预测模型将广告支出确定为留存的最佳单一预测指标之一,因为它通过相关性捕捉了许多真正的因果驱动因素。XGBoost 强加了正则化,这是一种花哨的说法,它试图选择最简单的模型,但仍然能很好地预测。如果它可以使用一个特征而不是三个特征进行同样好的预测,那么它往往会这样做以避免过度拟合。但这意味着,如果广告支出与上次升级和每月使用量高度相关,XGBoost 可能会使用广告支出而不是因果特征!XGBoost(或任何其他具有正则化功能的机器学习模型)的这一特性对于生成对未来留存率的稳健预测非常有用,但不利于理解如果我们想提高留存率,我们应该操纵哪些特征。

这凸显了为每个问题匹配正确的建模工具的重要性。与错误报告示例不同,增加广告支出会增加留存率的结论在直觉上并没有错。如果没有适当注意我们的预测模型是衡量什么,而不是衡量什么,我们很容易继续进行这一发现,只有在增加广告支出但没有获得我们预期的续订结果后,我们才意识到我们的错误。

Observational Causal Inference 观察因果推理

广告支出的好消息是,我们可以衡量所有可能混淆它的特征(在上面的因果图中,带有箭头的特征进入广告支出)。因此,这是一个观察到的混杂示例,我们应该能够仅使用我们已经收集的数据来解开相关模式;我们只需要使用来自观察因果推理的正确工具。这些工具允许我们指定哪些功能可能会混淆 Ad Spend,然后针对这些功能进行调整,从而对 Ad Spend 对产品续订的因果影响进行无混淆的估计。

用于观察因果推理的一种特别灵活的工具是双重/去偏机器学习。它使用您想要的任何机器学习模型,首先消除感兴趣的特征(即广告支出),然后估计更改该特征的平均因果效应(即因果效应的平均斜率)。

Double ML 的工作原理如下:

  1. 使用一组可能的混杂因素(即不是由广告支出引起的任何特征)训练模型来预测感兴趣的特征(即广告支出)。
  2. 使用同一组可能的混杂因素训练模型来预测结果(即是否续订 Did Renew)。
  3. 使用感兴趣的因果特征的残差变化(广告支出)训练模型来预测结果的残差变化(是否续订)。

直觉是,如果 Ad Spend 导致续订,那么 Ad Spend 中无法被其他混杂特征预测的部分应该与其他混杂特征无法预测的续订部分相关联。换句话说,双 ML 假设存在影响广告支出的独立(未观察到的)噪声特征(因为广告支出并非完全由其他特征决定),因此我们可以估算这个独立噪声特征的值,然后根据这个独立特征训练模型来预测输出。

虽然我们可以手动执行所有双 ML 步骤,但使用 econML 或 CausalML 等因果推理包会更容易。这里我们使用 causalml 的 BaseTLearner 模型。这将返回一个 P 值,该值表示该处理是否具有非零的因果效应,并且在我们的场景中效果很好,正确地识别出没有证据表明广告支出对续订有因果效应(P 值 = 0.172):

import matplotlib.pyplot as plt
from econml.dml import LinearDML
from sklearn.base import BaseEstimator, clone


class RegressionWrapper(BaseEstimator):
    """Turns a classifier into a 'regressor'.

    We use the regression formulation of double ML, so we need to approximate the classifer
    as a regression model. This treats the probabilities as just quantitative value targets
    for least squares regression, but it turns out to be a reasonable approximation.
    """

    def __init__(self, clf):
        self.clf = clf

    def fit(self, X, y, **kwargs):
        self.clf_ = clone(self.clf)
        self.clf_.fit(X, y, **kwargs)
        return self

    def predict(self, X):
        return self.clf_.predict_proba(X)[:, 1]


# Run Double ML, controlling for all the other features
def double_ml(y, causal_feature, control_features):
    """Use doubleML from econML to estimate the slope of the causal effect of a feature."""
    xgb_model = xgboost.XGBClassifier(objective="binary:logistic", random_state=42)
    est = LinearDML(model_y=RegressionWrapper(xgb_model))
    est.fit(y, causal_feature, W=control_features)
    return est.effect_inference()


def plot_effect(effect, xs, true_ys, ylim=None):
    """Plot a double ML effect estimate from econML as a line.

    Note that the effect estimate from double ML is an average effect *slope* not a full
    function. So we arbitrarily draw the slope of the line as passing through the origin.
    """
    plt.figure(figsize=(10, 5))

    pred_xs = [xs.min(), xs.max()]
    mid = (xs.min() + xs.max()) / 2
    [effect.pred[0] * (xs.min() - mid), effect.pred[0] * (xs.max() - mid)]

     # 获取 p 值
    p_value = effect.pvalue()[0]

    plt.plot(xs, true_ys - true_ys[0], label="True causal effect", color="black", linewidth=3)
    point_pred = effect.point_estimate * pred_xs
    pred_stderr = effect.stderr * np.abs(pred_xs)
    plt.plot(
        pred_xs,
        point_pred - point_pred[0],
        label=f"Double ML slope (p-value: {p_value:.4f})",
        color=shap.plots.colors.blue_rgb,
        linewidth=3,
    )
    # 99.9% CI
    plt.fill_between(
        pred_xs,
        point_pred - point_pred[0] - 3.291 * pred_stderr,
        point_pred - point_pred[0] + 3.291 * pred_stderr,
        alpha=0.2,
        color=shap.plots.colors.blue_rgb,
    )
    plt.legend()
    plt.xlabel("Ad spend", fontsize=13)
    plt.ylabel("Zero centered effect")
    if ylim is not None:
        plt.ylim(*ylim)
    plt.gca().xaxis.set_ticks_position("bottom")
    plt.gca().yaxis.set_ticks_position("left")
    plt.gca().spines["right"].set_visible(False)
    plt.gca().spines["top"].set_visible(False)
    plt.show()


# estimate the causal effect of Ad spend controlling for all the other features
causal_feature = "Ad spend"
control_features = [
    "Sales calls",
    "Interactions",
    "Economy",
    "Last upgrade",
    "Discount",
    "Monthly usage",
    "Bugs reported",
]
effect = double_ml(y, X[causal_feature], X.loc[:, control_features])

# plot the estimated slope against the true effect
xs, true_ys = marginal_effects(generator, 10000, X[["Ad spend"]], logit=False)[0]
plot_effect(effect, xs, true_ys, ylim=(-0.2, 0.2))

在这里插入图片描述

而将causal_feature换成Sales calls,p值就会比较小,约0.04;

Remember,双 ML(或任何其他观察性因果推理方法)仅在您可以测量和识别要估计因果效应的特征的所有可能的混杂因素时才有效。在这里,我们知道了因果图,可以看到 Monthly Usage 和 Last Upgrade 是我们需要控制的两个直接混杂因素。但是,如果我们不知道因果图,我们仍然可以查看 SHAP 条形图中的冗余,并看到 Monthly Usage 和 Last Upgrade 是最冗余的功能,因此是很好的控制候选者(折扣和报告的错误也是如此)。

非混淆冗余

因果推理可以提供帮助的第二种情况是非混淆冗余。当我们想要因果效应的特征驱动或由模型中包含的另一个特征驱动,但该另一个特征不是我们感兴趣的特征的混杂因素时,就会发生这种情况。

这方面的一个例子是 Sales Calls 功能。销售电话会直接影响留存率,但也会通过交互间接影响留存率。当我们在模型中同时包括 Interactions (与用户的交互) 和 Sales Calls 特征时,这两个特征共享的因果效应会被迫在它们之间分散。我们可以在上面的 SHAP 散点图中看到这一点,它显示了 XGBoost 如何低估了销售电话的真正因果效应,因为大部分影响都被放在了交互功能上。

在这里插入图片描述 原则上可以通过从模型中删除冗余变量来修复非混杂冗余(见下文)。例如,如果我们从模型中删除 交互,那么我们将捕获拨打销售电话对续订概率的全部影响。这种删除对于双 ML 也很重要,因为如果您控制由相关特征引起的下游特征,双 ML 将无法捕获间接因果效应。

在这种情况下,双重机器学习只会测量不通过其他特征传递的“直接”效应。然而,双重机器学习对于控制上游非混淆冗余(即冗余特征导致了感兴趣的特征)是稳健的,尽管这样做会降低你检测真实效应的统计功效。

不幸的是,我们通常不知道真正的因果图,因此由于观察到的混杂冗余与非混杂冗余,很难知道另一个特征何时与我们感兴趣的特征冗余。如果是因为混杂,那么我们应该使用双 ML 之类的方法控制该特征,而如果它是下游结果,那么如果我们想要完整的因果效应而不仅仅是直接效应,那么我们应该从模型中删除该特征。控制我们不应该控制的特征往往会隐藏或拆分因果效应,而未能控制我们应该控制的特征往往会推断出不存在的因果效应。当您不确定时,这通常使控制功能成为更安全的选择。

# Fit, explain, and plot a univariate model with just Sales calls
# Note how this model does not have to split of credit between Sales calls and
# Interactions, so we get a better agreement with the true causal effect.
sales_calls_model = fit_xgboost(X[["Sales calls"]], y)
sales_calls_shap_values = shap.Explainer(sales_calls_model)(X[["Sales calls"]])
shap.plots.scatter(
    sales_calls_shap_values,
    overlay={"True causal effects": marginal_effects(generator, 10000, ["Sales calls"])},
)

在这里插入图片描述

通过只使用“销售电话”这一特征来训练模型,你可以避免将因果效应分散到其他相关但不是混杂因素的特征上,比如“互动”。

为什么这个代码是良好的实践

  1. 聚焦单一特征:该代码专注于单个特征“销售电话”,这样可以更直接地估计其对目标变量(如续订概率)的因果效应。
  2. 避免效应分散:当模型中包含多个相关特征时,机器学习算法可能会将因果效应分散到这些特征上,导致每个特征的效应被低估。通过移除“互动”特征,我们可以确保“销售电话”的全部效应被正确估计。
  3. SHAP值解释:使用 SHAP 值来解释模型预测,并将其与真实的因果效应进行比较,可以帮助验证模型是否正确捕捉到了“销售电话”的真实影响。

如果SHAP 值散点图与真实的因果效应(True causal effects)重合,这说明模型很好地捕捉到了“销售电话”对续订概率的真实因果效应。

  1. 准确的因果效应估计:模型能够正确地估计“销售电话”对续订概率的影响。SHAP 值反映了特征对模型预测的贡献,而这些贡献与实际的因果效应一致。
  2. 无混淆因素干扰:在只使用“销售电话”这一特征的情况下,模型没有受到其他潜在混杂因素的影响,因此能够更准确地估计其因果效应。
  3. 无效应分散:由于没有包含“互动”等其他相关但非混杂的特征,模型不会将“销售电话”的效应分散到这些特征上。这样,“销售电话”的全部效应都被归因于该特征本身。

当预测模型和非混杂方法都无法回答因果问题时

Double ML(或任何其他假设非混杂性的因果推理方法)仅在您可以测量和识别要估计因果效应的特征的所有可能的混杂因素时才有效。如果您无法测量所有混杂因素,那么您就处于最困难的情景中:未观察到的混杂因素。

在这里插入图片描述 在这里插入图片描述

Discount 和 Bugs Reported 特征都存在未观察到的混杂问题,因为并非所有重要变量(例如,产品需求和面临的错误)都在数据中衡量。尽管这两个特征都相对独立于模型中的所有其他特征,但仍有一些重要的驱动因素未被测量。在这种情况下,需要观察混杂因素的预测模型和因果模型(如双 ML)都将失败。这就是为什么双 ML 估计 Discount 特征的负面因果效应很大的原因,即使在控制所有其他观察到的特征时也是如此:

causal_feature = "Discount"
control_features = [
    "Sales calls",
    "Interactions",
    "Economy",
    "Last upgrade",
    "Monthly usage",
    "Ad spend",
    "Bugs reported",
]
effect = double_ml(y, X[causal_feature], X.loc[:, control_features])

# plot the estimated slope against the true effect
xs, true_ys = marginal_effects(generator, 10000, X[[causal_feature]], logit=False)[0]
plot_effect(effect, xs, true_ys, ylim=(-0.5, 0.2))

在这里插入图片描述

除非能够测量以前未测量的特征(或与之相关的特征),否则在存在未观察到的混杂因素的情况下找到因果效应是很困难的。在这些情况下,确定可以为政策提供信息的因果效应的唯一方法是创建或利用一些随机化,以打破感兴趣特征与未测量的混杂因素之间的相关性。在这种情况下,随机实验仍然是寻找因果效应的黄金标准。

基于工具变量、双重差分或回归不连续性原理的专用因果工具有时可以利用部分随机化,即使在无法进行完整实验的情况下也是如此。例如,在我们无法随机分配实验组的情况下,可以使用工具变量技术来识别因果效应,但我们可以随机推动一些客户进行实验,例如发送电子邮件鼓励他们探索新的产品功能。当新实验的引入在各组之间交错时,双重差分方法可能会有所帮助。最后,当实验模式表现出明显的临界值时(例如,根据特定的、可衡量的特征(如每月收入超过 5,000 美元)获得实验资格),回归不连续性方法是一个不错的选择。

总结

XGBoost 或 LightGBM 等灵活的预测模型是解决预测问题的强大工具。然而,它们本身并不是因果模型,因此在许多常见情况下,用 SHAP 解释它们将无法准确回答因果问题。除非模型中的特征是实验变化的结果,否则在不考虑混杂因素的情况下将 SHAP 应用于预测模型通常不是衡量用于为政策提供信息的因果影响的合适工具。

SHAP 和其他可解释性工具可用于因果推理,并且 SHAP 已集成到许多因果推理包中,但这些用例本质上是明确的因果关系。为此,使用我们为预测问题收集的相同数据,并使用专为返回因果效应而设计的因果推理方法(如双 ML),通常是为政策提供信息的好方法。

在其他情况下,只有实验或其他随机化来源才能真正回答 what if 问题。因果推理总是要求我们做出重要的假设。本文的要点是,我们将正常的预测模型解释为因果关系而做出的假设通常是不切实际的。