目录
Synthetic Control with Covariants
Synthetic Control with Covariants
通常,合成控制只使用控制单元的前干预结果作为预测 的特征。这是因为这些特征往往是您所掌握的最具预测性的特征。但是,如果您认为一些额外的协变量具有很好的预测能力,您可能希望在模型中加入这些协变量。不过这种情况比较少见,所以如果时间不够,可以跳过这一部分。
假设您设法获得了主要竞争对手的每日下载量数据,并根据市场规模(comp_down load_pct)对这些数据进行了归一化处理。您认为这个协变量可以很好地预测 ,因此希望将其纳入合成控制模型:
df_norm_cov = (pd.read_csv("./data/online_mkt_cov.csv")
.astype({"date":"datetime64[ns]"}))
df_norm_cov.head()
用数学符号表示,您希望构建一个合成控制,使权重 不仅乘以
,还乘以这个额外的协变量
,以近似
。这里的问题是,
和
的比例可能完全不同,或者其中一个比另一个更具预测性,这就是为什么在求解 SC 优化问题之前,需要将包括
在内的每个协变量乘以比例因子 v。为了考虑到这一点,可以用协变量 X 来重写目标,将
视为另一个协变量:
然而,这个目标并没有告诉你如何找到最优 v。要做到这一点,你必须将整个合成控制打包到另一个优化目标中。如果听起来很复杂,不用担心。用代码来理解要容易得多。首先,让我们为两个协变量 comp_download_pct 和 、app_download_pct 创建 X 矩阵:
from toolz import partial
reshaper = partial(reshape_sc_data,
df=df_norm_cov,
geo_col="city",
time_col="date",
tr_geos=treated,
tr_start=str(tr_period))
y_pre_co, y_pre_tr, y_post_co, y_post_tr = reshaper(
y_col="app_download_pct"
)
x_pre_co, _, x_post_co, _ = reshaper(y_col="comp_download_pct")
接下来,让我们编写一个函数,当给定一个 vs 列表(每个协变量一个)时,返回合成控制权重和优化损失。请记住,您可以使用 .loss_ 访问拟合 SyntheticControl 模型的目标损失。
要检查是否有效,可以传递 [1, 0] 作为 vs 和 [y_pre_co, x_pre_co] 作为协变量列表。由于在这种情况下额外的协变量权重为零,因此您应该得到原始的合成控制:
def find_w_given_vs(vs, x_co_list, y_tr_pre):
X_times_v = sum([x*v for x, v in zip(x_co_list, vs)])
model = SyntheticControl()
model.fit(X_times_v, y_tr_pre)
return {"loss": model.loss_, "w": model.w_}
find_w_given_vs([1, 0],
[y_pre_co, x_pre_co],
y_pre_tr.mean(axis=1)).get("w").round(3)
array([-0. , -0. , 0. , -0. , -0. , -0. , 0.084, 0.039,
0.085, 0.003, -0. , -0. , -0. , -0. , -0. , 0. ,
0.062, 0.121, -0. , 0.072, -0. , 0. , -0. , 0. ,
-0. , -0. , 0. , -0. , -0. , 0. , -0. , 0.095,
0. , -0. , 0. , 0.022, 0.116, -0. , 0.068, 0.046,
-0. , -0. , -0. , 0.088, 0. , 0.098, -0. ])
最后,可以将 find_w_given_vs 封装在一个函数中,该函数只接收 vs 数组并返回优化损失。然后,你可以将这个函数传递给 scipy minimize 函数,它会反复寻找最佳 vs 并返回给你:
from scipy.optimize import minimize
def v_loss(vs):
return find_w_given_vs(vs,
[y_pre_co, x_pre_co],
y_pre_tr.mean(axis=1)).get("loss")
v_solution = minimize(v_loss, [0, 0], method='L-BFGS-B')
v_solution.x
array([1.88034589, 0.00269853])
有了最优 vs,就可以回到 find_w_given_vs 来获取考虑协变量的合成控制权重。不过需要注意的一点是,最终的解与不考虑协变量的解没有太大区别。这并不奇怪,因为 comp_download_pct 协变量的最优 v 是一个很小的数字,而且它并不比 app_download_pct 大很多:
w_cov = find_w_given_vs(v_solution.x,
[y_pre_co, x_pre_co],
y_pre_tr.mean(axis=1)).get("w").round(3)
w_cov
array([-0. , -0. , 0. , -0. , -0. , -0. , 0.078, 0.001,
0.033, 0. , -0. , 0.034, -0. , -0. , -0. , 0. ,
0.016, 0.047, 0.03 , 0.01 , -0. , -0. , 0. , 0.055,
-0. , 0. , -0. , 0. , 0. , 0. , -0. , 0.046,
0.078, 0.007, 0. , 0. , 0.138, 0. , 0.022, 0.008,
-0. , 0.201, 0. , 0.035, 0. , 0.161, -0. ])
有了这些权重,你可以对Y0进行预测,并获得考虑协变量的最终ATT数据:
y0_hat = sum([x*v for x, v
in zip([y_post_co, x_post_co], v_solution.x)]).dot(w_cov)
att = y_post_tr.mean(axis=1) - y0_hat
下面的图显示了所得到的ATT,以及来自典型SC的ATT估计值,并且没有协变量。正如你所看到的,两者都非常相似:
加入协变量虽然不难,但需要一定的额外复杂度。出于这个原因,而且由于 往往足以预测
,我通常不会费心添加协变量。但也许你能找到非常有预测性的特征,从而证明添加协变量是合理的。