|
| 1 | +# Copyright (c) Microsoft Corporation. All rights reserved. |
| 2 | +# Licensed under the MIT License. |
| 3 | +import numpy as np |
| 4 | +from econml.utilities import cross_product |
| 5 | +from statsmodels.tools.tools import add_constant |
| 6 | + |
| 7 | + |
| 8 | +class _BaseDynamicPanelDGP: |
| 9 | + |
| 10 | + def __init__(self, n_periods, n_treatments, n_x): |
| 11 | + self.n_periods = n_periods |
| 12 | + self.n_treatments = n_treatments |
| 13 | + self.n_x = n_x |
| 14 | + return |
| 15 | + |
| 16 | + def create_instance(self, *args, **kwargs): |
| 17 | + pass |
| 18 | + |
| 19 | + def _gen_data_with_policy(self, n_units, policy_gen, random_seed=123): |
| 20 | + pass |
| 21 | + |
| 22 | + def static_policy_data(self, n_units, tau, random_seed=123): |
| 23 | + def policy_gen(Tpre, X, period): |
| 24 | + return tau[period] |
| 25 | + return self._gen_data_with_policy(n_units, policy_gen, random_seed=random_seed) |
| 26 | + |
| 27 | + def adaptive_policy_data(self, n_units, policy_gen, random_seed=123): |
| 28 | + return self._gen_data_with_policy(n_units, policy_gen, random_seed=random_seed) |
| 29 | + |
| 30 | + def static_policy_effect(self, tau, mc_samples=1000): |
| 31 | + Y_tau, _, _, _ = self.static_policy_data(mc_samples, tau) |
| 32 | + Y_zero, _, _, _ = self.static_policy_data( |
| 33 | + mc_samples, np.zeros((self.n_periods, self.n_treatments))) |
| 34 | + return np.mean(Y_tau[np.arange(Y_tau.shape[0]) % self.n_periods == self.n_periods - 1]) - \ |
| 35 | + np.mean(Y_zero[np.arange(Y_zero.shape[0]) % |
| 36 | + self.n_periods == self.n_periods - 1]) |
| 37 | + |
| 38 | + def adaptive_policy_effect(self, policy_gen, mc_samples=1000): |
| 39 | + Y_tau, _, _, _ = self.adaptive_policy_data(mc_samples, policy_gen) |
| 40 | + Y_zero, _, _, _ = self.static_policy_data( |
| 41 | + mc_samples, np.zeros((self.n_periods, self.n_treatments))) |
| 42 | + return np.mean(Y_tau[np.arange(Y_tau.shape[0]) % self.n_periods == self.n_periods - 1]) - \ |
| 43 | + np.mean(Y_zero[np.arange(Y_zero.shape[0]) % |
| 44 | + self.n_periods == self.n_periods - 1]) |
| 45 | + |
| 46 | + |
| 47 | +class DynamicPanelDGP(_BaseDynamicPanelDGP): |
| 48 | + |
| 49 | + def __init__(self, n_periods, n_treatments, n_x): |
| 50 | + super().__init__(n_periods, n_treatments, n_x) |
| 51 | + |
| 52 | + def create_instance(self, s_x, sigma_x=.5, sigma_y=.5, conf_str=5, hetero_strength=0, hetero_inds=None, |
| 53 | + autoreg=.5, state_effect=.5, random_seed=123): |
| 54 | + np.random.seed(random_seed) |
| 55 | + self.s_x = s_x |
| 56 | + self.conf_str = conf_str |
| 57 | + self.sigma_x = sigma_x |
| 58 | + self.sigma_y = sigma_y |
| 59 | + self.hetero_inds = hetero_inds.astype( |
| 60 | + int) if hetero_inds is not None else hetero_inds |
| 61 | + self.endo_inds = np.setdiff1d( |
| 62 | + np.arange(self.n_x), hetero_inds).astype(int) |
| 63 | + # The first s_x state variables are confounders. The final s_x variables are exogenous and can create |
| 64 | + # heterogeneity |
| 65 | + self.Alpha = np.random.uniform(-1, 1, |
| 66 | + size=(self.n_x, self.n_treatments)) |
| 67 | + self.Alpha /= np.linalg.norm(self.Alpha, axis=1, ord=1, keepdims=True) |
| 68 | + self.Alpha *= state_effect |
| 69 | + if self.hetero_inds is not None: |
| 70 | + self.Alpha[self.hetero_inds] = 0 |
| 71 | + |
| 72 | + self.Beta = np.zeros((self.n_x, self.n_x)) |
| 73 | + for t in range(self.n_x): |
| 74 | + self.Beta[t, :] = autoreg * np.roll(np.random.uniform(low=4.0**(-np.arange( |
| 75 | + 0, self.n_x)), high=4.0**(-np.arange(1, self.n_x + 1))), t) |
| 76 | + if self.hetero_inds is not None: |
| 77 | + self.Beta[np.ix_(self.endo_inds, self.hetero_inds)] = 0 |
| 78 | + self.Beta[np.ix_(self.hetero_inds, self.endo_inds)] = 0 |
| 79 | + |
| 80 | + self.epsilon = np.random.uniform(-1, 1, size=self.n_treatments) |
| 81 | + self.zeta = np.zeros(self.n_x) |
| 82 | + self.zeta[:self.s_x] = self.conf_str / self.s_x |
| 83 | + |
| 84 | + self.y_hetero_effect = np.zeros(self.n_x) |
| 85 | + self.x_hetero_effect = np.zeros(self.n_x) |
| 86 | + if self.hetero_inds is not None: |
| 87 | + self.y_hetero_effect[self.hetero_inds] = np.random.uniform(.5 * hetero_strength, 1.5 * hetero_strength) / \ |
| 88 | + len(self.hetero_inds) |
| 89 | + self.x_hetero_effect[self.hetero_inds] = np.random.uniform(.5 * hetero_strength, 1.5 * hetero_strength) / \ |
| 90 | + len(self.hetero_inds) |
| 91 | + |
| 92 | + self.true_effect = np.zeros((self.n_periods, self.n_treatments)) |
| 93 | + self.true_effect[0] = self.epsilon |
| 94 | + for t in np.arange(1, self.n_periods): |
| 95 | + self.true_effect[t, :] = (self.zeta.reshape( |
| 96 | + 1, -1) @ np.linalg.matrix_power(self.Beta, t - 1) @ self.Alpha) |
| 97 | + |
| 98 | + self.true_hetero_effect = np.zeros( |
| 99 | + (self.n_periods, (self.n_x + 1) * self.n_treatments)) |
| 100 | + self.true_hetero_effect[0, :] = cross_product( |
| 101 | + add_constant(self.y_hetero_effect.reshape(1, -1), has_constant='add'), |
| 102 | + self.epsilon.reshape(1, -1)) |
| 103 | + for t in np.arange(1, self.n_periods): |
| 104 | + self.true_hetero_effect[t, :] = cross_product( |
| 105 | + add_constant(self.x_hetero_effect.reshape(1, -1), has_constant='add'), |
| 106 | + self.zeta.reshape(1, -1) @ np.linalg.matrix_power(self.Beta, t - 1) @ self.Alpha) |
| 107 | + return self |
| 108 | + |
| 109 | + def hetero_effect_fn(self, t, x): |
| 110 | + if t == 0: |
| 111 | + return (np.dot(self.y_hetero_effect, x.flatten()) + 1) * self.epsilon |
| 112 | + else: |
| 113 | + return (np.dot(self.x_hetero_effect, x.flatten()) + 1) *\ |
| 114 | + (self.zeta.reshape(1, -1) @ np.linalg.matrix_power(self.Beta, t - 1) |
| 115 | + @ self.Alpha).flatten() |
| 116 | + |
| 117 | + def _gen_data_with_policy(self, n_units, policy_gen, random_seed=123): |
| 118 | + np.random.seed(random_seed) |
| 119 | + Y = np.zeros(n_units * self.n_periods) |
| 120 | + T = np.zeros((n_units * self.n_periods, self.n_treatments)) |
| 121 | + X = np.zeros((n_units * self.n_periods, self.n_x)) |
| 122 | + groups = np.zeros(n_units * self.n_periods) |
| 123 | + for t in range(n_units * self.n_periods): |
| 124 | + period = t % self.n_periods |
| 125 | + if period == 0: |
| 126 | + X[t] = np.random.normal(0, self.sigma_x, size=self.n_x) |
| 127 | + T[t] = policy_gen(np.zeros(self.n_treatments), X[t], period) |
| 128 | + else: |
| 129 | + X[t] = (np.dot(self.x_hetero_effect, X[t - 1]) + 1) * np.dot(self.Alpha, T[t - 1]) + \ |
| 130 | + np.dot(self.Beta, X[t - 1]) + \ |
| 131 | + np.random.normal(0, self.sigma_x, size=self.n_x) |
| 132 | + T[t] = policy_gen(T[t - 1], X[t], period) |
| 133 | + Y[t] = (np.dot(self.y_hetero_effect, X[t]) + 1) * np.dot(self.epsilon, T[t]) + \ |
| 134 | + np.dot(X[t], self.zeta) + \ |
| 135 | + np.random.normal(0, self.sigma_y) |
| 136 | + groups[t] = t // self.n_periods |
| 137 | + |
| 138 | + return Y, T, X[:, self.hetero_inds] if self.hetero_inds else None, X[:, self.endo_inds], groups |
| 139 | + |
| 140 | + def observational_data(self, n_units, gamma=0, s_t=1, sigma_t=0.5, random_seed=123): |
| 141 | + """ Generated observational data with some observational treatment policy parameters |
| 142 | +
|
| 143 | + Parameters |
| 144 | + ---------- |
| 145 | + n_units : how many units to observe |
| 146 | + gamma : what is the degree of auto-correlation of the treatments across periods |
| 147 | + s_t : sparsity of treatment policy; how many states does it depend on |
| 148 | + sigma_t : what is the std of the exploration/randomness in the treatment |
| 149 | + """ |
| 150 | + Delta = np.zeros((self.n_treatments, self.n_x)) |
| 151 | + Delta[:, :s_t] = self.conf_str / s_t |
| 152 | + |
| 153 | + def policy_gen(Tpre, X, period): |
| 154 | + return gamma * Tpre + (1 - gamma) * np.dot(Delta, X) + \ |
| 155 | + np.random.normal(0, sigma_t, size=self.n_treatments) |
| 156 | + return self._gen_data_with_policy(n_units, policy_gen, random_seed=random_seed) |
0 commit comments