在解释预测模型以寻找因果关系时,请务必小心。
一篇关于因果关系和可解释机器学习的联合文章,作者包括来自微软的Eleanor Dillon、Jacob LaRiviere、Scott Lundberg、Jonathan Roth和Vasilis Syrgkanis。
像XGBoost这样的预测机器学习模型在与SHAP等解释性工具结合时变得更加强大。这些工具识别输入特征与预测结果之间最具信息量的关系,这对于解释模型在做什么、获得利益相关者的认同以及诊断潜在问题非常有用。人们很容易进一步认为,解释工具还可以识别决策者应该操纵哪些特征以在未来改变结果。然而,在本文中,我们将讨论使用预测模型来指导这种政策选择往往会产生误导。
原因与 相关性 和 因果关系 之间的根本区别有关。SHAP 使预测性机器学习模型捕捉到的相关性变得透明。但使相关性透明并不意味着它们就是因果关系!所有预测模型都隐含地假设每个人在未来都会保持相同的行为方式,因此相关模式将保持不变。要理解如果某人开始以不同方式行事会发生什么,我们需要构建因果模型,这需要做出假设并使用因果分析的工具。
订阅者保留示例
想象一下,我们的任务是建立一个模型来预测客户是否会续订他们的产品订阅。假设经过一番挖掘,我们设法获得了八个对预测客户流失至关重要的特征:客户折扣、广告支出、客户的月使用量、上次升级、客户报告的错误、与客户的互动、与客户的销售电话以及宏观经济活动。然后,我们使用这些特征来训练一个基本的 XGBoost 模型,以预测客户在订阅到期时是否会续订:
[1]:
# This cell defines the functions we use to generate the data in our scenario
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
[2]:
X, y = user_retention_dataset()
model = fit_xgboost(X, y)
一旦我们掌握了我们的XGBoost客户保留模型,我们就可以开始使用像SHAP这样的可解释性工具来探索它学到了什么。我们首先绘制模型中每个特征的全局重要性:
[3]:
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散点图展示了特征值的变化如何影响模型对续订概率的预测。如果蓝色点呈现上升趋势,这意味着特征值越大,模型预测的续订概率越高。
[4]:
shap.plots.scatter(
shap_values, ylabel="SHAP value\n(higher means more likely to renew)"
)
预测任务与因果任务
散点图显示了一些令人惊讶的发现:- 报告更多错误的用户更有可能续订!- 获得更大折扣的用户不太可能续订!
我们三重检查了代码和数据管道以排除错误,然后与一些业务合作伙伴进行了交谈,他们提供了一个直观的解释: - 高使用率且重视产品的用户更有可能报告错误并续订订阅。 - 销售团队倾向于向他们认为不太可能对产品感兴趣的客户提供高额折扣,而这些客户的流失率更高。
这些在模型中一开始看似违反直觉的关系是一个问题吗?这取决于我们的目标是什么!
我们最初的目标是预测客户留存率,这对于估计未来收入用于财务规划等项目非常有用。由于报告更多错误问题的用户实际上更有可能续订,因此在模型中捕捉这种关系有助于预测。只要我们的模型在样本外有良好的拟合度,我们就应该能够为财务部门提供良好的预测,因此不必担心模型中这种关系的方向。
这是一个被称为 预测任务 的任务类别的例子。在预测任务中,目标是在给定一组特征 X
的情况下预测一个结果 Y``(例如续订)。预测练习的一个关键组成部分是,我们只关心预测 ``model(X)
在类似于我们训练集的数据分布中接近 Y
。X
和 Y
之间的简单相关性可以帮助这些类型的预测。
然而,假设第二个团队接手了我们的预测模型,新的目标是确定我们的公司可以采取哪些行动来留住更多客户。这个团队非常关心每个 X
特征与 Y
之间的关系,不仅在我们训练数据的分布中,还包括当世界发生变化时产生的反事实情景。在这种情况下,仅仅识别变量之间的稳定相关性已经不够了;这个团队想知道是否通过操纵特征 X
会导致 Y
的变化。想象一下当你告诉工程主管你想让他引入新错误以增加客户续订率时他的表情!
这是一个被称为 因果任务 的任务类别的示例。在因果任务中,我们想要了解改变世界的一个方面 X(例如报告的错误)如何影响一个结果 Y``(续订)。在这种情况下,了解改变 ``X
是否会导致 Y
的增加,或者数据中的关系是否仅仅是相关性的,这一点至关重要。
估计因果效应的挑战
理解因果关系的一个有用工具是写下我们感兴趣的数据生成过程的因果图。我们示例的因果图说明了为什么我们的XGBoost客户保留模型挑选出的稳健预测关系与团队感兴趣的因果关系不同,该团队希望计划干预措施以增加保留率。这个图只是真实数据生成机制的总结(如上所述)。实心椭圆代表我们观察到的特征,而虚线椭圆代表我们未测量的隐藏特征。每个特征是其所有带箭头指向它的特征的函数,加上一些随机效应。
在我们的例子中,我们知道因果图,因为我们模拟了数据。在实践中,真正的因果图是未知的,但我们可能能够利用关于世界如何运作的特定领域知识来推断哪些关系可以或不可以存在。
[5]:
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
[5]:
这个图中有许多关系,但首先重要的是,我们能够测量的一些特征受到 未测量的混杂特征 的影响,如产品需求和遇到的错误。例如,报告更多错误的用户遇到更多错误是因为他们更多地使用产品,而且他们也更可能报告这些错误,因为他们更需要产品。产品需求对续订有其直接的因果效应。因为我们无法直接测量产品需求,所以在预测模型中,我们最终捕捉到的报告错误与续订之间的相关性结合了面对错误的小的负面直接效应和产品需求的大量正面混杂效应。下图绘制了我们示例中的SHAP值与每个特征的真实因果效应(在此示例中已知,因为我们生成了数据)。
[6]:
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)},
)
预测模型捕捉到了报告的错误对留存率的总体正面影响(如SHAP所示),尽管报告错误的因果效应为零,且遇到错误的效应为负面。
我们在折扣方面也看到了类似的问题,折扣同样受到客户对产品未观察到的需求驱动。我们的预测模型发现折扣与留存之间存在负相关关系,这是由与未观察到的特征——产品需求——的相关性驱动的,尽管折扣实际上对续订有轻微的正向因果效应!换句话说,如果两个客户具有相同的产品需求且其他方面相似,那么获得更大折扣的客户更有可能续订。
这个图表还揭示了当我们开始将预测模型解释为因果关系时,第二个更为隐蔽的问题。注意,广告支出也有类似的问题——它对留存没有因果效应(黑线是平的),但预测模型却捕捉到了一个正效应!
在这种情况下,广告支出仅由上次升级和每月使用情况驱动,因此我们没有*未观察到*的混杂问题,而是有一个*观察到*的混杂问题。广告支出与影响广告支出的特征之间存在统计冗余。当多个特征捕获相同的信息时,预测模型可以使用这些特征中的任何一个进行预测,即使它们并非全部具有因果关系。虽然广告支出本身对续费没有因果效应,但它与几个驱动续费的特征高度相关。我们的正则化模型将广告支出识别为有用的预测因子,因为它总结了多个因果驱动因素(从而导致模型更稀疏),但如果我们开始将其解释为因果效应,这就会变得非常误导。
现在我们将逐一解决我们示例中的每一部分,以说明预测模型何时可以准确测量因果效应,以及何时不能。我们还将介绍一些因果工具,这些工具有时可以在预测模型失败的情况下估计因果效应。
当预测模型能够回答因果问题时
让我们从我们的例子中的成功开始。注意,我们的预测模型很好地捕捉了经济特征(更好的经济对留存有积极影响)的真实因果效应。那么我们什么时候可以期望预测模型捕捉到真正的因果效应呢?
允许XGBoost为经济获得良好因果效应估计的重要因素是特征的强独立成分(在此模拟中);其对留存的预测能力并不与其他任何已测量的特征或未测量的混杂因素强冗余。因此,它不受未测量混杂因素或特征冗余引起的偏差影响。
[7]:
# Economy is independent of other measured features.
shap.plots.bar(shap_values, clustering=clust, clustering_cutoff=1)
由于我们在 SHAP 条形图的右侧添加了聚类,我们可以看到数据的冗余结构作为树状图。当特征在树状图的底部(左侧)合并时,这意味着这些特征包含的关于结果(续订)的信息非常冗余,模型可以使用任一特征。当特征在树状图的顶部(右侧)合并时,这意味着它们包含的关于结果的信息是相互独立的。
我们可以通过注意到经济在聚类树状图的顶部之前不与其他任何特征合并,来看到经济与其他所有测量特征是独立的。这告诉我们经济没有受到观察到的混杂因素的影响。但要相信经济效应是因果的,我们还需要检查未观察到的混杂因素。检查未测量的混杂因素更难,需要使用领域知识(在我们的例子中由业务合作伙伴提供)。
对于经典的预测性机器学习模型来说,要得出因果结果,特征不仅需要与其他模型中的特征独立,还需要与未观察到的混杂因素独立。通常很难找到自然表现出这种独立水平的感兴趣的驱动因素的例子,但当我们的数据包含一些实验时,我们通常可以找到独立特征的例子。
当预测模型无法回答因果问题时,因果推断方法可以提供帮助。
在大多数现实世界的数据集中,特征并不是独立且无混杂的,因此标准的预测模型将无法学习到真正的因果效应。结果是,使用SHAP解释它们将不会揭示因果效应。但并非一切都失去了,有时我们可以使用观察性因果推断的工具来修复或至少最小化这个问题。
观察到的混杂因素
因果推断在第一个场景中可以提供帮助的是观察到的混杂。当存在另一个特征,该特征因果地影响原始特征和我们正在预测的结果时,该特征被称为“混杂”。如果我们能够测量那个其他特征,它被称为*观察到的混杂因子*。
[8]:
# Ad spend is very redundant with Monthly usage and Last upgrade.
shap.plots.bar(shap_values, clustering=clust, clustering_cutoff=1)
在我们的场景中,一个例子是广告支出功能。尽管广告支出对留存没有直接的因果影响,但它与上次升级和每月使用功能相关,这些功能确实推动了留存。我们的预测模型将广告支出识别为留存的最佳单一预测因子之一,因为它通过相关性捕捉到了许多真正的因果驱动因素。XGBoost 施加了正则化,这是一种花哨的说法,即它试图选择仍然预测良好的最简单可能的模型。如果它可以用一个特征而不是三个特征同样好地预测,它将倾向于这样做以避免过拟合。但这意味着如果广告支出与上次升级和每月使用高度相关,XGBoost 可能会使用广告支出而不是因果特征!XGBoost 的这一特性(或任何其他具有正则化的机器学习模型)对于生成未来留存的稳健预测非常有用,但如果我们想增加留存,它并不适合理解我们应该操纵哪些特征。
这突显了将正确的建模工具与每个问题相匹配的重要性。与错误报告示例不同,增加广告支出会增加留存率的结论并没有直观的错误。如果没有对我们的预测模型所测量和未测量的内容给予适当关注,我们很容易就会根据这一发现继续行动,只有在增加广告支出后没有得到预期的续订结果时,才会意识到我们的错误。
观察性因果推断
对于广告支出来说,好消息是我们可以测量所有可能混淆它的特征(即在我们上面的因果图中指向广告支出的那些特征)。因此,这是一个观察性混淆的例子,我们应该能够仅使用我们已经收集的数据来解开相关模式;我们只需要使用观察性因果推断中的正确工具。这些工具允许我们指定哪些特征可能混淆广告支出,然后调整这些特征,以获得广告支出对产品续订的因果效应的**无混淆**估计。
一个特别灵活的观察性因果推断工具是双重/去偏机器学习。它使用你想要的任何机器学习模型首先去混淆感兴趣的特征(例如广告支出),然后估计改变该特征的平均因果效应(即因果效应的平均斜率)。
Double ML 的工作原理如下:1. 训练一个模型来预测感兴趣的特征(例如,广告支出),使用一组可能的混杂因素(即,不由广告支出引起的任何特征)。2. 训练一个模型来预测结果(例如,是否续订),使用相同的一组可能的混杂因素。3. 训练一个模型来预测结果的残差变化(在减去我们的预测后的剩余变化),使用感兴趣的因果特征的残差变化。
直觉上,如果广告支出导致续订,那么广告支出中不能被其他混淆特征预测的部分应与续订中不能被其他混淆特征预测的部分相关。换句话说,双重机器学习假设存在一个影响广告支出(因为广告支出并非完全由其他特征决定)的独立(未观察到的)噪声特征,因此我们可以估算这个独立噪声特征的值,然后基于这个独立特征训练模型来预测输出。
虽然我们可以手动完成所有双重机器学习步骤,但使用像 econML 或 CausalML 这样的因果推断包会更简单。这里我们使用 econML 的 LinearDML 模型。这将返回一个 P 值,以判断该处理是否具有非零的因果效应,并且在我们的场景中表现出色,正确识别出广告支出对续订没有因果效应的证据(P 值 = 0.85):
[9]:
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=(5, 3))
pred_xs = [xs.min(), xs.max()]
mid = (xs.min() + xs.max()) / 2
[effect.pred[0] * (xs.min() - mid), effect.pred[0] * (xs.max() - mid)]
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="Double ML slope",
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))
记住,双重机器学习(或任何其他观察性因果推断方法)只有在能够测量并识别出你想要估计因果效应的特征的所有可能混杂因素时才有效。这里我们知道因果图,可以看到月度使用量和上次升级是我们需要控制的两个直接混杂因素。但如果我们不知道因果图,我们仍然可以通过查看SHAP条形图中的冗余性,发现月度使用量和上次升级是最冗余的特征,因此是很好的控制候选(折扣和报告的错误也是如此)。
非混淆冗余
因果推断可以帮助的第二种情况是非混淆冗余。这种情况发生在我们想要因果效应的特征因果地驱动或被模型中包含的另一个特征驱动,但该另一个特征不是我们感兴趣特征的混淆因素。
[10]:
# Interactions and sales calls are very redundant with one another.
shap.plots.bar(shap_values, clustering=clust, clustering_cutoff=1)
一个例子是销售电话功能。销售电话直接影响留存率,但通过互动也对留存率有间接影响。当我们在模型中同时包含互动和销售电话功能时,两者共享的因果效应被迫在它们之间分散。我们可以在上面的SHAP散点图中看到这一点,这些图显示了XGBoost如何低估销售电话的真实因果效应,因为大部分效应被分配到了互动功能上。
非混淆冗余原则上可以通过从模型中移除冗余变量来修正(见下文)。例如,如果我们从模型中移除交互作用,那么我们将捕捉到销售电话对续订概率的全部影响。这种移除对于双重机器学习(Double ML)也很重要,因为如果你控制了由感兴趣特征引起的下游特征,双重机器学习将无法捕捉间接因果效应。在这种情况下,双重机器学习只会测量不通过其他特征的“直接”效应。然而,双重机器学习对于控制上游非混淆冗余是稳健的(其中冗余特征引起感兴趣的特征),尽管这会降低你检测真实效应的统计能力。
不幸的是,我们通常不知道真实的因果图,因此很难知道另一个特征是由于观察到的混淆还是非混淆冗余而与我们的感兴趣特征冗余。如果是由于混淆,那么我们应该使用双重ML等方法来控制该特征,而如果是下游结果,那么如果我们想要完整的因果效应而不是仅直接效应,我们应该从模型中删除该特征。控制我们不应该控制的特征往往会隐藏或分割因果效应,而未能控制我们应该控制的特征往往会推断出不存在的因果效应。这通常使得在不确定的情况下控制特征成为更安全的选择。
[11]:
# 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"])
},
)
当预测模型和去混淆方法都无法回答因果问题时
双重ML(或任何其他假设无混淆的因果推断方法)只有在您能够测量并识别出您想要估计因果效应的特征的所有可能混淆因素时才有效。如果您无法测量所有的混淆因素,那么您将面临最困难的情景:未观测到的混淆。
[12]:
# Discount and Bugs reported seem are fairly independent of the other features we can
# measure, but they are not independent of Product need, which is an unobserved confounder.
shap.plots.bar(shap_values, clustering=clust, clustering_cutoff=1)
折扣和报告的错误功能都受到未观察到的混杂因素的影响,因为并非所有重要变量(例如,产品需求和遇到的错误)都在数据中测量。尽管这两个功能相对于模型中的所有其他功能都是相对独立的,但存在一些重要的驱动因素未被测量。在这种情况下,需要混杂因素被观察到的预测模型和因果模型,如双重机器学习(double ML),将会失败。这就是为什么双重机器学习在控制所有其他观察到的特征时,仍然对折扣特征估计出一个大的负因果效应:
[13]:
# estimate the causal effect of Ad spend controlling for all the other features
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))
在没有能力测量之前未测量的特征(或与之相关的特征)的情况下,在存在未观察到的混杂因素时,寻找因果效应是困难的。在这些情况下,唯一能识别出可以指导政策的因果效应的方法是创造或利用某种随机化,打破感兴趣特征与未测量混杂因素之间的相关性。随机实验在这种背景下仍然是寻找因果效应的金标准。
基于工具变量、差异中的差异或回归不连续性等原理的专门因果工具,有时可以在无法进行完整实验的情况下利用部分随机化。例如,在无法随机分配处理的情况下,可以使用工具变量技术来识别因果效应,但我们能够随机推动一些客户接受处理,比如发送电子邮件鼓励他们探索新产品功能。差异中的差异方法在不同组别中逐步引入新处理时非常有用。最后,当处理模式表现出明显的截止点时(例如基于特定可测量特征如每月收入超过5000美元的资格治疗),回归不连续性方法是一个很好的选择。
摘要
像 XGBoost 或 LightGBM 这样的灵活预测模型是解决预测问题的强大工具。然而,它们本质上不是因果模型,因此在许多常见情况下使用 SHAP 解释它们将无法准确回答因果问题。除非模型中的特征是实验变异的结果,否则在不考虑混杂因素的情况下将 SHAP 应用于预测模型通常不是用于衡量因果影响以指导政策的适当工具。SHAP 和其他可解释性工具可以用于因果推断,并且 SHAP 已集成到许多因果推断包中,但这些用例本质上是因果的。为此,使用我们为预测问题收集的相同数据,并使用像双重 ML 这样特别设计来返回因果效应的因果推断方法,通常是指导政策的一个好方法。在其他情况下,只有实验或其他随机化来源才能真正回答假设问题。因果推断总是要求我们做出重要假设。本文的主要观点是,我们将正常预测模型解释为因果时所做的假设往往是不现实的。