SHAP 能帮我们精准看穿预测模型的因果关系吗?
SHAP 能帮我们精准看穿预测模型的因果关系吗?
当像XGBoost这样的预测性机器学习模型与SHAP等可解释性工具相结合时,其功能会变得更加强大。这些工具能够识别输入特征与预测结果之间最具信息价值的关系,这对于解释模型的行为、获得利益相关者的认可以及诊断潜在问题都非常有用。人们很容易进一步假设,这些解释性工具也能够识别出决策者若想在未来改变结果时应该操控哪些特征。然而,在本文中,我们将探讨使用预测模型来指导这类政策选择往往会产生误导性的原因。
这一问题的原因与“相关性”和“因果关系”之间的根本区别有关。SHAP能够使预测性机器学习模型捕捉到的相关性变得透明。但让相关性透明并不意味着就能使其具有因果性! 所有的预测模型都隐含地假设未来每个人的行为方式都将保持不变,因此相关性模式也会保持恒定。为了理解当有人开始改变行为时会发生什么,我们需要构建因果模型,这就需要做出假设并运用因果分析工具。
客户留存预测模型构建示例
假设我们的任务是构建一个模型,用于预测客户是否会续订其产品订阅。假设经过一番挖掘,我们成功获得了八个对预测客户流失很重要的特征:客户折扣、广告支出、客户每月使用量、上次产品升级时间、客户报告的漏洞数量、与客户的互动次数、与客户的销售电话次数以及宏观经济活动情况。然后,我们使用这些特征来训练一个基本的XGBoost模型,以预测客户在订阅到期时是否会续订:
# 此代码块定义了我们在场景中用于生成数据的函数
import numpy as np
import pandas as pd
import scipy.stats
import sklearn
import xgboost
class FixableDataFrame(pd.DataFrame):
"""用于操作生成模型的辅助类。"""
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
# 生成数据
def generator(n, fixed={}, seed=0):
"""我们的客户留存示例的生成模型。"""
if seed is not None:
np.random.seed(seed)
X = FixableDataFrame(fixed=fixed)
# 对该客户进行的销售电话次数
X["Sales calls"] = np.random.uniform(0, 4, size=(n,)).round()
# 对该客户进行的销售电话次数
X["Interactions"] = X["Sales calls"] + np.random.poisson(0.2, size=(n,))
# 该客户所在地区的经济状况
X["Economy"] = np.random.uniform(0, 1, size=(n,))
# 该客户订阅到期时距离上次产品升级的时间
X["Last upgrade"] = np.random.uniform(0, 20, size=(n,))
# 用户对产品的需求程度
X["Product need"] = X["Sales calls"] * 0.1 + np.random.normal(0, 1, size=(n,))
# 客户续订时所享受的折扣比例
X["Discount"] = ((1 - scipy.special.expit(X["Product need"])) * 0.5 + 0.5 * np.random.uniform(0, 1, size=(n,))) / 2
# 在上一时期用户实际使用产品的天数占比
X["Monthly usage"] = scipy.special.expit(X["Product need"] * 0.3 + np.random.normal(0, 1, size=(n,)))
# 我们针对该用户(或该用户所在的用户组)在每个用户身上花费的广告费用
X["Ad spend"] = (
X["Monthly usage"] * np.random.uniform(0.99, 0.9, size=(n,)) + (X["Last upgrade"] < 1) + (X["Last upgrade"] < 2)
)
# 自上次续订以来该用户遇到的漏洞数量
X["Bugs faced"] = np.array([np.random.poisson(v * 2) for v in X["Monthly usage"]])
# 用户报告了多少个漏洞?
X["Bugs reported"] = (X["Bugs faced"] * scipy.special.expit(X["Product need"])).round()
# 用户续订了吗?
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
)
)
# 在现实生活中,我们会进行随机抽样,以确定客户是否续订(结果为0或1)。但在这里,我们将标签保留为概率形式,这样我们在绘图时可以减少噪声。取消注释以下行可以得到更具噪声的因果效应线,但基本结果是相同的
X["Did renew"] = scipy.stats.bernoulli.rvs(X["Did renew"])
return X
def user_retention_dataset():
"""用于模型训练的观测数据。"""
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):
"""使用早停法训练XGBoost模型。"""
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)")
预测任务 VS 因果任务:目标与关注点的本质差异
散点图显示了一些令人惊讶的发现:
- 报告更多漏洞的用户更有可能续订!
- 享受更大折扣的用户续订的可能性更低!
我们反复检查代码和数据管道以排除错误,然后与一些业务合作伙伴交流,他们给出了一个直观的解释:
- 高使用量且重视产品的用户更有可能报告漏洞,也更有可能续订他们的订阅。
- 销售团队倾向于给他们认为对产品不太感兴趣的客户提供高折扣,而这些客户的流失率更高。
模型中这些乍一看违反直觉的关系是个问题吗?这取决于我们的目标是什么!
我们构建这个模型的最初目标是预测客户留存情况,这对于像为财务规划估计未来收入这样的项目很有用。由于实际上报告更多漏洞的用户确实更有可能续订,在模型中捕捉到这种关系有助于预测。只要我们的模型在样本外有良好的拟合度,我们就应该能够为财务部门提供一个好的预测,因此无需担心模型中这种关系的方向。
这是一类被称为预测任务的示例。在预测任务中,目标是在给定一组特征 X
的情况下预测结果 Y
(例如,客户是否续订)。预测工作的一个关键部分是,我们只关心在与训练集相似的数据分布中,预测值 model(X)
与 Y
接近。X
和 Y
之间的简单相关性对于这类预测可能会有帮助。
然而,假设另一个团队采用了我们的预测模型,其新目标是确定公司可以采取哪些行动来留住更多客户。这个团队非常关心每个 X
特征与 Y
之间的关系,不仅仅是在我们的训练数据分布中,还包括在世界发生变化时产生的反事实场景中。在这种情况下,仅仅识别变量之间的稳定相关性已经不够了;这个团队想知道操控特征 X
是否会导致 Y
发生变化。想象一下,当你告诉工程主管你希望他引入新的漏洞以增加客户续订率时,他会是什么表情!
这是一类被称为因果任务的示例。在因果任务中,我们想知道改变世界的某个方面 X
(例如,报告的漏洞数量)如何影响结果 Y
(续订情况)。在这种情况下,关键是要知道改变 X
是否会导致 Y
增加,或者数据中的这种关系仅仅是相关性。
估计因果效应面临的挑战剖析
理解因果关系的一个有用工具是写下我们感兴趣的数据生成过程的因果图。我们示例中的因果图说明了为什么我们的XGBoost客户留存模型捕捉到的稳健预测关系与希望规划干预措施以提高留存率的团队所关注的因果关系不同。这个图只是真实数据生成机制(在上面已定义)的一个总结。实心椭圆表示我们观察到的特征,而虚线椭圆表示我们未测量到的隐藏特征。每个特征都是所有指向它的特征的函数,再加上一些随机效应。
在我们的示例中,因为我们模拟了数据,所以知道因果图。在实际情况中,真实的因果图是未知的,但我们可以利用关于世界运行方式的特定领域知识来推断哪些关系可能存在或不存在。
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
这个图中有很多关系,但首先要关注的重要问题是,我们能够测量的一些特征受到了未测量的混杂特征的影响,比如产品需求和遇到的漏洞数量。例如,报告更多漏洞的用户遇到的漏洞更多,是因为他们使用产品的频率更高,而且他们也更有可能报告这些漏洞,因为他们对产品的需求更大。产品需求对续订有其自身的直接因果影响。由于我们无法直接测量产品需求,我们最终在预测模型中捕捉到的报告的漏洞与续订之间的相关性,结合了遇到漏洞的小的负向直接影响和来自产品需求的大的正向混杂影响。下面的图绘制了我们示例中的SHAP值与每个特征的真实因果效应(在这个示例中我们生成了数据,所以是已知的)的对比。
def marginal_effects(generative_model, num_samples=100, columns=None, max_points=20, logit=True, seed=0):
"""用于计算真实边际因果效应的辅助函数。"""
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能够对经济状况这一特征的因果效应进行良好估计的重要因素是该特征具有很强的独立成分(在这个模拟中);它对客户留存的预测能力与任何其他已测量的特征,或任何未测量的混杂因素都没有很强的冗余性。因此,它不会受到未测量混杂因素或特征冗余带来的偏差影响。
# 经济状况与其他已测量的特征相互独立。
shap.plots.bar(shap_values, clustering=clust, clustering_cutoff=1)
由于我们在SHAP条形图的右侧添加了聚类,我们可以将数据的冗余结构看作一个树状图。当特征在树状图的底部(左侧)合并在一起时,这意味着这些特征包含的关于结果(客户续订)的信息非常冗余,模型可以使用其中任何一个特征。当特征在树状图的顶部(右侧)合并在一起时,这意味着它们包含的关于结果的信息彼此独立。
我们可以看到,经济状况与所有其他已测量的特征相互独立,方法是注意到经济状况在聚类树状图的最顶部才与其他特征合并。这告诉我们,经济状况不存在已观察到的混杂问题。但要相信经济状况的效应是因果性的,我们还需要检查是否存在未观察到的混杂因素。检查未测量的混杂因素更加困难,需要使用领域知识(在上面的示例中由业务合作伙伴提供)。
对于经典的预测性机器学习模型来说,要得出因果性的结果,特征不仅需要与模型中的其他特征相互独立,还需要与未观察到的混杂因素相互独立。在现实中,自然地找到具有这种程度独立性的感兴趣驱动因素的例子并不常见,但当我们的数据包含一些实验时,我们通常可以找到独立特征的例子。
预测模型在因果问题上的局限性及因果推断方法的助力场景
在大多数现实世界的数据集中,特征既不是相互独立的,也不是没有混杂因素的,所以标准的预测模型无法学习到真实的因果效应。因此,使用SHAP来解释这些模型并不能揭示因果效应。但并非毫无希望,有时我们可以使用观察性因果推断工具来解决或至少最小化这个问题。
已观察到的混杂问题
因果推断能够提供帮助的第一种情况是已观察到的混杂问题。当存在另一个特征,它对原始特征和我们正在预测的结果都有因果影响时,这个特征就被称为“混杂的”。如果我们能够测量那个其他特征,它就被称为“已观察到的混杂因素”。
# 广告支出与每月使用量和上次产品升级时间高度冗余。
shap.plots.bar(shap_values, clustering=clust, clustering_cutoff=1)
在我们的场景中,广告支出这一特征就是一个例子。尽管广告支出对客户留存没有直接的因果影响,但它与上次产品升级时间和每月使用量这两个特征相关,而这两个特征确实会影响客户留存。我们的预测模型将广告支出识别为客户留存的最佳单一预测因子之一,因为它通过相关性捕捉到了许多真实的因果驱动因素。XGBoost应用了正则化,这是一种复杂的说法,意思是它试图选择在仍然能够良好预测的前提下最简单的模型。如果使用一个特征就能像使用三个特征一样准确地进行预测,它会倾向于这样做以避免过拟合。但这意味着如果广告支出与上次产品升级时间和每月使用量高度相关,XGBoost可能会使用广告支出而不是那些具有因果关系的特征!XGBoost(或任何其他带有正则化的机器学习模型)的这种特性对于生成对未来客户留存的稳健预测非常有用,但对于理解如果我们想提高客户留存率应该操控哪些特征却没有帮助。
这凸显了为每个问题选择合适的建模工具的重要性。与报告漏洞的例子不同,得出增加广告支出会提高客户留存率的结论在直觉上并没有什么错误。如果不恰当地关注我们的预测模型测量了什么以及没有测量什么,我们很容易就会基于这个发现继续行动,而只有在增加了广告支出却没有得到预期的客户续订结果时才会意识到自己的错误。
观测性因果推断:原理、方法与应用实例
广告支出这一案例的好消息是,我们能够测量所有可能使其产生混杂的特征(即上述因果图中指向广告支出的那些特征)。因此,这是一个已观察到混杂因素的例子,我们应该能够仅使用已收集的数据来理清相关模式;我们只需要运用观测性因果推断中的合适工具即可。这些工具使我们能够确定哪些特征可能会混淆广告支出,然后针对这些特征进行调整,以获得广告支出对产品续订的无混杂因果效应估计。
观测性因果推断中一种特别灵活的工具是双重/去偏机器学习。它可以使用任何你想用的机器学习模型,首先对感兴趣的特征(即广告支出)进行去混杂,然后估计改变该特征所产生的平均因果效应(即因果效应的平均斜率)。
双重机器学习的工作原理如下:
- 使用一组可能的混杂因素(即任何不由广告支出导致的特征)训练一个模型,以预测感兴趣的特征(即广告支出)。
- 使用相同的一组可能的混杂因素训练一个模型,以预测结果(即是否续订)。
- 使用感兴趣的因果特征的残差变化来训练一个模型,以预测结果的残差变化(减去我们的预测后剩下的变化)。
其背后的逻辑是,如果广告支出导致了续订,那么广告支出中无法由其他混杂特征预测的部分,应该与续订中无法由其他混杂特征预测的部分相关。换一种说法,双重机器学习假设存在一个独立的(未被观察到的)噪声特征会影响广告支出(因为广告支出并非完全由其他特征决定),所以我们可以估算这个独立噪声特征的值,然后基于这个独立特征训练一个模型来预测输出。
虽然我们可以手动完成双重机器学习的所有步骤,但使用像econML或CausalML这样的因果推断工具包会更简便。这里我们使用econML的LinearDML模型。该模型会返回一个P值,用于判断该处理是否具有非零的因果效应,并且在我们的场景中效果很好,能够正确识别出没有证据表明广告支出对续订有因果效应(P值 = 0.85):
import matplotlib.pyplot as plt
from econml.dml import LinearDML
from sklearn.base import BaseEstimator, clone
class RegressionWrapper(BaseEstimator):
"""将分类器转换为“回归器”。
我们使用双重机器学习的回归形式,所以需要将分类器近似为回归模型。这将概率视为最小二乘回归的定量值目标,事实证明这是一个合理的近似。
"""
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]
# 运行双重机器学习,控制所有其他特征
def double_ml(y, causal_feature, control_features):
"""使用econML中的双重机器学习来估计一个特征的因果效应斜率。"""
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):
"""将econML中的双重机器学习效应估计绘制成一条线。
请注意,双重机器学习的效应估计是平均效应*斜率*,而不是完整的函数。所以我们随意将线的斜率绘制为经过原点。
"""
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%置信区间
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()
# 估计控制所有其他特征后广告支出的因果效应
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])
# 将估计的斜率与真实效应进行绘图
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条形图中的冗余情况,发现每月使用量和上次升级时间是冗余度最高的特征,因此是进行控制的良好候选特征(折扣和报告的漏洞数量也是如此)。
非混杂冗余:定义、表现形式及应对策略
因果推断能够提供帮助的第二种情况是非混杂冗余。当我们想要其因果效应的特征,对模型中包含的另一个特征具有因果驱动作用,或者受该特征驱动,但该其他特征并非我们感兴趣特征的混杂因素时,就会出现这种情况。
# 互动次数和销售电话次数彼此之间高度冗余。
shap.plots.bar(shap_values, clustering=clust, clustering_cutoff=1)
销售电话次数这一特征就是一个例子。销售电话次数直接影响客户留存,但也通过互动次数对客户留存产生间接影响。当我们在模型中同时包含互动次数和销售电话次数这两个特征时,这两个特征共有的因果效应会被迫在它们之间分摊。我们可以在上面的SHAP散点图中看到这一点,该图显示XGBoost如何低估了销售电话次数的真实因果效应,因为大部分效应被分配到了互动次数这一特征上。
原则上,通过从模型中删除冗余变量可以解决非混杂冗余问题(见下文)。例如,如果我们从模型中删除互动次数,那么我们将捕捉到进行一次销售电话对续订概率的全部影响。这种删除对于双重机器学习也很重要,因为如果你控制了由感兴趣特征导致的下游特征,双重机器学习将无法捕捉到间接因果效应。在这种情况下,双重机器学习将只测量不通过其他特征传递的“直接”效应。不过,双重机器学习对于控制上游非混杂冗余(其中冗余特征导致感兴趣的特征)是稳健的,尽管这会降低检测真实效应的统计功效。
不幸的是,我们通常不知道真实的因果图,因此很难判断另一个特征与我们感兴趣的特征之间的冗余是由于已观察到的混杂因素,还是非混杂冗余。如果是由于混杂因素,那么我们应该使用像双重机器学习这样的方法来控制该特征;而如果是下游结果,那么如果我们想要完整的因果效应而不仅仅是直接效应,就应该从模型中删除该特征。控制一个我们不应该控制的特征往往会隐藏或分割因果效应,而未能控制一个我们应该控制的特征则往往会推断出不存在的因果效应。因此,在不确定的情况下,控制一个特征通常是更安全的选择。
# 仅使用销售电话次数拟合、解释并绘制单变量模型
# 请注意,这个模型不必在销售电话次数和互动次数之间分摊影响,所以我们能更好地与真实因果效应达成一致。
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"])},
)
未观测混杂因素下因果问题的困境与突破
双重机器学习(或任何其他假设无混杂性的因果推断方法)仅在你能够测量并识别出你想要估计因果效应的特征的所有可能混杂因素时才有效。如果你无法测量所有的混杂因素,那么你就处于最棘手的情况:未观察到的混杂因素。
# 折扣和报告的漏洞数量似乎与我们能够测量的其他特征相当独立,但它们与产品需求并不独立,而产品需求是一个未观察到的混杂因素。
shap.plots.bar(shap_values, clustering=clust, clustering_cutoff=1)
折扣和报告的漏洞数量这两个特征都受到未观察到的混杂因素的影响,因为并非所有重要变量(例如产品需求和遇到的漏洞数量)都在数据中被测量。即使这两个特征与模型中的所有其他特征相对独立,但存在一些未测量的重要驱动因素。在这种情况下,预测模型和像双重机器学习这样需要观察到混杂因素的因果模型都会失效。这就是为什么即使在控制了所有其他观察到的特征后,双重机器学习对折扣特征仍估计出一个较大的负因果效应:
# 估计控制所有其他特征后折扣的因果效应
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])
# 将估计的斜率与真实效应进行绘图
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已被集成到许多因果推断工具包中,但这些用例本质上是明确的因果性的。为此,使用我们为预测问题收集的相同数据,并运用像双重机器学习这样专门设计用于返回因果效应的因果推断方法,通常是为政策提供依据的一种好方法。在其他情况下,只有实验或其他随机化来源才能真正回答“如果……会怎样”的问题。因果推断总是需要我们做出重要假设。本文的主要观点是,将普通预测模型解读为因果模型时所做的假设往往是不现实的。