├── .gitignore
├── LICENSE
├── PythonCodeBoxes.ipynb
├── RCodeBoxes.R
├── README.md
├── Results.RData
├── StataCodeBoxes.do
├── rhc.Rdata
├── rhc.csv
└── rhc.dta
/.gitignore:
--------------------------------------------------------------------------------
1 | ### Python ###
2 | .ipynb_checkpoints/*
3 | ### R ###
4 | # History files
5 | .Rhistory
6 | .Rapp.history
7 | # User-specific files
8 | .Ruserdata
9 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020 Miguel Angel Luque Fernandez
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
6 |
7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
8 |
9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NON INFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
10 |
--------------------------------------------------------------------------------
/PythonCodeBoxes.ipynb:
--------------------------------------------------------------------------------
1 | {
2 | "cells": [
3 | {
4 | "cell_type": "markdown",
5 | "metadata": {},
6 | "source": [
7 | "# Tutorial: causal inference methods made easy for applied resarchers/epidemiologists/statisticians \n",
8 | "\n",
9 | "### ICON-LSHTM, LONDON, 16th October 2020\n",
10 | "\n",
11 | "Miguel Angel Luque Fernandez PhD, Assistant Professor of Epidemiology and Biostatistics\n",
12 | "\n",
13 | "Matthew Smith PhD, Research Fellow Inequalities in Cancer Outcomes Network, LSHTM, London, UK\n",
14 | "\n",
15 | "Paul Zivich, University of North Carolina at Chapel Hill\n",
16 | "\n",
17 | "Copyright (c) 2020 Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the \"Software\"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.\n",
18 | "\n",
19 | "THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n",
20 | "\n",
21 | "Bug reports: miguel-angel.luque at lshtm.ac.uk\n",
22 | "\n",
23 | "The rhc dataset can be dowloaded at http://biostat.mc.vanderbilt.edu/wiki/Main/DataSets"
24 | ]
25 | },
26 | {
27 | "cell_type": "code",
28 | "execution_count": 1,
29 | "metadata": {},
30 | "outputs": [
31 | {
32 | "name": "stdout",
33 | "output_type": "stream",
34 | "text": [
35 | "patsy 0.5.1\n",
36 | "scipy 1.5.2\n",
37 | "numpy 1.19.1\n",
38 | "pandas 1.1.0\n",
39 | "statsmodels 0.11.1\n",
40 | "matplotlib 3.3.1\n",
41 | "zepid 0.9.0\n"
42 | ]
43 | }
44 | ],
45 | "source": [
46 | "%matplotlib inline\n",
47 | "\n",
48 | "# Importing libraries for the tutorial\n",
49 | "import patsy\n",
50 | "import scipy\n",
51 | "import numpy as np\n",
52 | "import pandas as pd\n",
53 | "import statsmodels.api as sm\n",
54 | "import statsmodels.formula.api as smf\n",
55 | "import matplotlib\n",
56 | "import matplotlib.pyplot as plt\n",
57 | "import zepid\n",
58 | "\n",
59 | "from scipy.stats.kde import gaussian_kde\n",
60 | "from scipy.stats import logistic\n",
61 | "from zepid.calc import probability_to_odds, odds_to_probability\n",
62 | "\n",
63 | "print(\"patsy \", patsy.__version__)\n",
64 | "print(\"scipy \", scipy.__version__)\n",
65 | "print(\"numpy \", np.__version__)\n",
66 | "print(\"pandas \", pd.__version__)\n",
67 | "print(\"statsmodels\", sm.__version__)\n",
68 | "print(\"matplotlib \", matplotlib.__version__)\n",
69 | "print(\"zepid \", zepid.__version__)"
70 | ]
71 | },
72 | {
73 | "cell_type": "markdown",
74 | "metadata": {},
75 | "source": [
76 | "## Setting up the Data"
77 | ]
78 | },
79 | {
80 | "cell_type": "code",
81 | "execution_count": 2,
82 | "metadata": {},
83 | "outputs": [],
84 | "source": [
85 | "# Box 1: Setting up the data\n",
86 | "data = pd.read_csv(\"rhc.csv\")\n",
87 | "data.rename(columns={\"rhc\": \"A\", \n",
88 | " \"death_d30\": \"Y\",\n",
89 | " \"gender\": \"C\",\n",
90 | " \"age\": \"W1\",\n",
91 | " \"edu\": \"W2\",\n",
92 | " \"race\": \"W3\",\n",
93 | " \"carcinoma\": \"W4\",\n",
94 | " }, inplace=True)\n",
95 | "data['A'] = np.where(data['A'] == \"Yes\", 1, 0)\n",
96 | "data['C'] = np.where(data['C'] == \"Female\", 0, 1)\n",
97 | "\n",
98 | "data = data[[\"Y\", \"A\", \"C\", \"W1\", \"W2\", \"W3\", \"W4\"]].copy()"
99 | ]
100 | },
101 | {
102 | "cell_type": "markdown",
103 | "metadata": {},
104 | "source": [
105 | "## Naive estimate of the ATE"
106 | ]
107 | },
108 | {
109 | "cell_type": "code",
110 | "execution_count": 3,
111 | "metadata": {},
112 | "outputs": [
113 | {
114 | "name": "stdout",
115 | "output_type": "stream",
116 | "text": [
117 | "0.07352\n"
118 | ]
119 | },
120 | {
121 | "data": {
122 | "text/html": [
123 | "
\n",
124 | "
OLS Regression Results
\n",
125 | "
\n",
126 | "
Dep. Variable:
Y
R-squared:
0.006
\n",
127 | "
\n",
128 | "
\n",
129 | "
Model:
OLS
Adj. R-squared:
0.005
\n",
130 | "
\n",
131 | "
\n",
132 | "
Method:
Least Squares
F-statistic:
16.59
\n",
133 | "
\n",
134 | "
\n",
135 | "
Date:
Mon, 21 Dec 2020
Prob (F-statistic):
6.58e-08
\n",
136 | "
\n",
137 | "
\n",
138 | "
Time:
06:30:58
Log-Likelihood:
-3812.9
\n",
139 | "
\n",
140 | "
\n",
141 | "
No. Observations:
5735
AIC:
7632.
\n",
142 | "
\n",
143 | "
\n",
144 | "
Df Residuals:
5732
BIC:
7652.
\n",
145 | "
\n",
146 | "
\n",
147 | "
Df Model:
2
\n",
148 | "
\n",
149 | "
\n",
150 | "
Covariance Type:
nonrobust
\n",
151 | "
\n",
152 | "
\n",
153 | "
\n",
154 | "
\n",
155 | "
coef
std err
t
P>|t|
[0.025
0.975]
\n",
156 | "
\n",
157 | "
\n",
158 | "
Intercept
0.3049
0.010
29.354
0.000
0.285
0.325
\n",
159 | "
\n",
160 | "
\n",
161 | "
A
0.0735
0.013
5.739
0.000
0.048
0.099
\n",
162 | "
\n",
163 | "
\n",
164 | "
C
0.0027
0.013
0.219
0.826
-0.022
0.027
\n",
165 | "
\n",
166 | "
\n",
167 | "
\n",
168 | "
\n",
169 | "
Omnibus:
36985.427
Durbin-Watson:
1.981
\n",
170 | "
\n",
171 | "
\n",
172 | "
Prob(Omnibus):
0.000
Jarque-Bera (JB):
993.118
\n",
173 | "
\n",
174 | "
\n",
175 | "
Skew:
0.696
Prob(JB):
2.22e-216
\n",
176 | "
\n",
177 | "
\n",
178 | "
Kurtosis:
1.511
Cond. No.
3.07
\n",
179 | "
\n",
180 | "
Warnings: [1] Standard Errors assume that the covariance matrix of the errors is correctly specified."
181 | ],
182 | "text/plain": [
183 | "\n",
184 | "\"\"\"\n",
185 | " OLS Regression Results \n",
186 | "==============================================================================\n",
187 | "Dep. Variable: Y R-squared: 0.006\n",
188 | "Model: OLS Adj. R-squared: 0.005\n",
189 | "Method: Least Squares F-statistic: 16.59\n",
190 | "Date: Mon, 21 Dec 2020 Prob (F-statistic): 6.58e-08\n",
191 | "Time: 06:30:58 Log-Likelihood: -3812.9\n",
192 | "No. Observations: 5735 AIC: 7632.\n",
193 | "Df Residuals: 5732 BIC: 7652.\n",
194 | "Df Model: 2 \n",
195 | "Covariance Type: nonrobust \n",
196 | "==============================================================================\n",
197 | " coef std err t P>|t| [0.025 0.975]\n",
198 | "------------------------------------------------------------------------------\n",
199 | "Intercept 0.3049 0.010 29.354 0.000 0.285 0.325\n",
200 | "A 0.0735 0.013 5.739 0.000 0.048 0.099\n",
201 | "C 0.0027 0.013 0.219 0.826 -0.022 0.027\n",
202 | "==============================================================================\n",
203 | "Omnibus: 36985.427 Durbin-Watson: 1.981\n",
204 | "Prob(Omnibus): 0.000 Jarque-Bera (JB): 993.118\n",
205 | "Skew: 0.696 Prob(JB): 2.22e-216\n",
206 | "Kurtosis: 1.511 Cond. No. 3.07\n",
207 | "==============================================================================\n",
208 | "\n",
209 | "Warnings:\n",
210 | "[1] Standard Errors assume that the covariance matrix of the errors is correctly specified.\n",
211 | "\"\"\""
212 | ]
213 | },
214 | "execution_count": 3,
215 | "metadata": {},
216 | "output_type": "execute_result"
217 | }
218 | ],
219 | "source": [
220 | "# Box 2: Regression naive approach\n",
221 | "fm = smf.ols(\"Y ~ A + C\", data).fit()\n",
222 | "print(np.round(fm.params['A'], 5)) # ATE = 0.07352\n",
223 | "fm.summary() # Full model results"
224 | ]
225 | },
226 | {
227 | "cell_type": "code",
228 | "execution_count": 4,
229 | "metadata": {},
230 | "outputs": [
231 | {
232 | "name": "stdout",
233 | "output_type": "stream",
234 | "text": [
235 | "Prop. Male 0.56\n",
236 | "Prop. Female 0.44\n"
237 | ]
238 | }
239 | ],
240 | "source": [
241 | "# Box 3: Marginal probabilities\n",
242 | "pr_c1 = np.mean(data['C'])\n",
243 | "pr_c0 = 1 - pr_c1\n",
244 | "print(\"Prop. Male \", np.round(pr_c1, 2))\n",
245 | "print(\"Prop. Female\", np.round(pr_c0, 2))"
246 | ]
247 | },
248 | {
249 | "cell_type": "markdown",
250 | "metadata": {},
251 | "source": [
252 | "## 3. G-Formula\n",
253 | "\n",
254 | "### 3.1 Non-parametric g-formula"
255 | ]
256 | },
257 | {
258 | "cell_type": "code",
259 | "execution_count": 5,
260 | "metadata": {},
261 | "outputs": [
262 | {
263 | "name": "stdout",
264 | "output_type": "stream",
265 | "text": [
266 | "ATE 0.073692\n"
267 | ]
268 | }
269 | ],
270 | "source": [
271 | "# Box 4: Non-parametric g-formula for the ATE\n",
272 | "pr_y_a1c1 = np.mean(data.loc[(data['C'] == 1) & (data['A'] == 1), 'Y'])\n",
273 | "pr_y_a0c1 = np.mean(data.loc[(data['C'] == 1) & (data['A'] == 0), 'Y'])\n",
274 | "pr_y_a1c0 = np.mean(data.loc[(data['C'] == 0) & (data['A'] == 1), 'Y'])\n",
275 | "pr_y_a0c0 = np.mean(data.loc[(data['C'] == 0) & (data['A'] == 0), 'Y'])\n",
276 | "\n",
277 | "ate = (pr_y_a1c1 - pr_y_a0c1)*pr_c1 + (pr_y_a1c0 - pr_y_a0c0)*pr_c0\n",
278 | "print(\"ATE\", np.round(ate, 6))"
279 | ]
280 | },
281 | {
282 | "cell_type": "code",
283 | "execution_count": 6,
284 | "metadata": {},
285 | "outputs": [
286 | {
287 | "name": "stdout",
288 | "output_type": "stream",
289 | "text": [
290 | "ATT 0.073248\n"
291 | ]
292 | }
293 | ],
294 | "source": [
295 | "# Box 5: Non-parametric g-formula for the ATT\n",
296 | "pr_c1_a1 = np.mean(data.loc[data['A'] == 1, 'C'])\n",
297 | "pr_c0_a1 = 1 - pr_c1_a1\n",
298 | "\n",
299 | "att = (pr_y_a1c1 - pr_y_a0c1)*pr_c1_a1 + (pr_y_a1c0 - pr_y_a0c0)*pr_c0_a1\n",
300 | "print(\"ATT\", np.round(att, 6))"
301 | ]
302 | },
303 | {
304 | "cell_type": "code",
305 | "execution_count": 7,
306 | "metadata": {},
307 | "outputs": [
308 | {
309 | "name": "stdout",
310 | "output_type": "stream",
311 | "text": [
312 | "95% Confidence limits for the ATE\n",
313 | "Percentile method: [0.047741 0.099149]\n",
314 | "Normal Approx method: [0.04798 0.099404]\n",
315 | "\n",
316 | "95% Confidence limits for the ATT\n",
317 | "Percentile method: [0.048054 0.098375]\n",
318 | "Normal Approx method: [0.047811 0.098686]\n"
319 | ]
320 | }
321 | ],
322 | "source": [
323 | "# Box 6: Bootstrap the 95% confidence intervals (CI) for the\n",
324 | "# ATE/ATT estimated using the non-parametric G-Formula\n",
325 | "\n",
326 | "def ate_nonparm_gformula(d):\n",
327 | " \"\"\"Function to estimate the ATE using the nonparametric\n",
328 | " g-formula\"\"\"\n",
329 | " pr_c1 = np.mean(d['C'])\n",
330 | " pr_c0 = 1 - pr_c1\n",
331 | "\n",
332 | " pr_y_11 = np.mean(d.loc[(d['C'] == 1) & (d['A'] == 1), 'Y'])\n",
333 | " pr_y_01 = np.mean(d.loc[(d['C'] == 1) & (d['A'] == 0), 'Y'])\n",
334 | " pr_y_10 = np.mean(d.loc[(d['C'] == 0) & (d['A'] == 1), 'Y'])\n",
335 | " pr_y_00 = np.mean(d.loc[(d['C'] == 0) & (d['A'] == 0), 'Y'])\n",
336 | " \n",
337 | " return (pr_y_11 - pr_y_01)*pr_c1 + (pr_y_10 - pr_y_00)*pr_c0\n",
338 | "\n",
339 | "## ATE ##\n",
340 | "ate_rs = []\n",
341 | "for i in range(1000): # Drawing 1000 bootstrapped samples\n",
342 | " d_star = data.sample(n=data.shape[0], # Same size as input data\n",
343 | " replace=True) # Draw with replacement\n",
344 | " ate_rs.append(ate_nonparm_gformula(d=d_star))\n",
345 | "\n",
346 | "print(\"95% Confidence limits for the ATE\")\n",
347 | "ci_perc = np.percentile(ate_rs, q=[2.5, 97.5])\n",
348 | "print(\"Percentile method: \", np.round(ci_perc, 6))\n",
349 | "ate_se = np.std(ate_rs, ddof=1)\n",
350 | "print(\"Normal Approx method:\", np.round([ate - 1.96*ate_se,\n",
351 | " ate + 1.96*ate_se], 6))\n",
352 | "\n",
353 | "\n",
354 | "def att_nonparm_gformula(d):\n",
355 | " \"\"\"Function to estimate the ATT using the nonparametric\n",
356 | " g-formula\"\"\"\n",
357 | " pr_c1_a1 = np.mean(d.loc[data['A'] == 1, 'C'])\n",
358 | " pr_c0_a1 = 1 - pr_c1_a1\n",
359 | "\n",
360 | " pr_y_11 = np.mean(d.loc[(d['C'] == 1) & (d['A'] == 1), 'Y'])\n",
361 | " pr_y_01 = np.mean(d.loc[(d['C'] == 1) & (d['A'] == 0), 'Y'])\n",
362 | " pr_y_10 = np.mean(d.loc[(d['C'] == 0) & (d['A'] == 1), 'Y'])\n",
363 | " pr_y_00 = np.mean(d.loc[(d['C'] == 0) & (d['A'] == 0), 'Y'])\n",
364 | " \n",
365 | " return (pr_y_11 - pr_y_01)*pr_c1_a1 + (pr_y_10 - pr_y_00)*pr_c0_a1\n",
366 | "\n",
367 | "\n",
368 | "## ATT ##\n",
369 | "att_rs = []\n",
370 | "for i in range(1000): # Drawing 1000 bootstrapped samples\n",
371 | " d_star = data.sample(n=data.shape[0], # Same size as input data\n",
372 | " replace=True) # Draw with replacement\n",
373 | " att_rs.append(att_nonparm_gformula(d=d_star))\n",
374 | "\n",
375 | "print(\"\\n95% Confidence limits for the ATT\")\n",
376 | "ci_perc = np.percentile(att_rs, q=[2.5, 97.5])\n",
377 | "print(\"Percentile method: \", np.round(ci_perc, 6))\n",
378 | "att_se = np.std(att_rs, ddof=1)\n",
379 | "print(\"Normal Approx method:\", np.round([att - 1.96*att_se,\n",
380 | " att + 1.96*att_se], 6)) "
381 | ]
382 | },
383 | {
384 | "cell_type": "code",
385 | "execution_count": 8,
386 | "metadata": {},
387 | "outputs": [
388 | {
389 | "name": "stdout",
390 | "output_type": "stream",
391 | "text": [
392 | "ATE 0.073692\n"
393 | ]
394 | }
395 | ],
396 | "source": [
397 | "# Box 7: Non-parametric g-formula using saturated regression model (A)\n",
398 | "data[\"A1\"] = np.where(data['A'] == 1, 1, 0)\n",
399 | "data[\"A0\"] = np.where(data['A'] == 0, 1, 0)\n",
400 | "fm = smf.ols(\"Y ~ A1 + A0 + A1:C + A0:C - 1\", data).fit()\n",
401 | "betas = fm.params\n",
402 | "ate = np.mean((betas[\"A1\"] + betas[\"A1:C\"]*data[\"C\"]) -\n",
403 | " (betas[\"A0\"] + betas[\"A0:C\"]*data[\"C\"]))\n",
404 | "\n",
405 | "print(\"ATE\", np.round(ate, 6))"
406 | ]
407 | },
408 | {
409 | "cell_type": "code",
410 | "execution_count": 9,
411 | "metadata": {},
412 | "outputs": [
413 | {
414 | "name": "stdout",
415 | "output_type": "stream",
416 | "text": [
417 | "ATE 0.073692\n"
418 | ]
419 | }
420 | ],
421 | "source": [
422 | "# Box 8: G-formula with saturated regression model using zEpid\n",
423 | "g_formula = zepid.causal.gformula.TimeFixedGFormula(data, \n",
424 | " exposure=\"A\", \n",
425 | " outcome=\"Y\")\n",
426 | "g_formula.outcome_model(\"A + C + A:C\", # Estimating model\n",
427 | " print_results=False)\n",
428 | "\n",
429 | "g_formula.fit(\"all\") # all sets A=1\n",
430 | "y_a1 = g_formula.marginal_outcome\n",
431 | "\n",
432 | "g_formula.fit(\"none\") # none sets A=0\n",
433 | "y_a0 = g_formula.marginal_outcome\n",
434 | "\n",
435 | "print(\"ATE\", np.round(y_a1 - y_a0, 6))"
436 | ]
437 | },
438 | {
439 | "cell_type": "markdown",
440 | "metadata": {},
441 | "source": [
442 | "### 3.2 Parametric g-formula"
443 | ]
444 | },
445 | {
446 | "cell_type": "code",
447 | "execution_count": 10,
448 | "metadata": {},
449 | "outputs": [
450 | {
451 | "name": "stdout",
452 | "output_type": "stream",
453 | "text": [
454 | "ATE 0.073692\n"
455 | ]
456 | }
457 | ],
458 | "source": [
459 | "# Box 9: Parametric g-formula by hand\n",
460 | "f = sm.families.family.Binomial() # Using logit model unlike prev\n",
461 | "fm_a1 = smf.glm(\"Y ~ C\", data.loc[data[\"A\"] == 1], family=f).fit()\n",
462 | "fm_a0 = smf.glm(\"Y ~ C\", data.loc[data[\"A\"] == 0], family=f).fit()\n",
463 | "\n",
464 | "y_a1 = fm_a1.predict(data['C'])\n",
465 | "y_a0 = fm_a0.predict(data['C'])\n",
466 | "ate = np.mean(y_a1 - y_a0)\n",
467 | "\n",
468 | "print(\"ATE\", np.round(ate, 6))"
469 | ]
470 | },
471 | {
472 | "cell_type": "code",
473 | "execution_count": 11,
474 | "metadata": {},
475 | "outputs": [
476 | {
477 | "name": "stdout",
478 | "output_type": "stream",
479 | "text": [
480 | "ATE 0.073514\n"
481 | ]
482 | }
483 | ],
484 | "source": [
485 | "# Box 10: Parametric Regression Adjustment\n",
486 | "f = sm.families.family.Binomial()\n",
487 | "fm = smf.glm(\"Y ~ A + C\", data, family=f).fit()\n",
488 | "\n",
489 | "da1 = data.copy()\n",
490 | "da1['A'] = 1\n",
491 | "y_a1 = fm.predict(da1)\n",
492 | "\n",
493 | "da0 = data.copy()\n",
494 | "da0['A'] = 0\n",
495 | "y_a0 = fm.predict(da0)\n",
496 | "ate = np.mean(y_a1 - y_a0)\n",
497 | "\n",
498 | "print(\"ATE\", np.round(ate, 6))"
499 | ]
500 | },
501 | {
502 | "cell_type": "code",
503 | "execution_count": 12,
504 | "metadata": {},
505 | "outputs": [
506 | {
507 | "name": "stdout",
508 | "output_type": "stream",
509 | "text": [
510 | "95% Confidence limits for the ATE\n",
511 | "Percentile method: [0.048036 0.100379]\n",
512 | "Normal approx method: [0.047652 0.099376]\n"
513 | ]
514 | }
515 | ],
516 | "source": [
517 | "# Box 11: Bootstrap for the parametric regression adjustment\n",
518 | "ate_rs = []\n",
519 | "for i in range(1000): # Drawing 1000 bootstrapped samples\n",
520 | " d_star = data.sample(n=data.shape[0], # Same size as input data\n",
521 | " replace=True) # Draw with replacement\n",
522 | " fm = smf.glm(\"Y ~ A + C\", d_star, family=f).fit()\n",
523 | " da = d_star.copy()\n",
524 | " da['A'] = 1\n",
525 | " y_a1 = fm.predict(da)\n",
526 | " da['A'] = 0\n",
527 | " y_a0 = fm.predict(da)\n",
528 | " ate_rs.append(np.mean(y_a1 - y_a0))\n",
529 | "\n",
530 | "print(\"95% Confidence limits for the ATE\")\n",
531 | "ci_perc = np.percentile(ate_rs, q=[2.5, 97.5])\n",
532 | "print(\"Percentile method: \", np.round(ci_perc, 6))\n",
533 | "ate_se = np.std(ate_rs, ddof=1)\n",
534 | "print(\"Normal approx method:\", np.round([ate - 1.96*ate_se,\n",
535 | " ate + 1.96*ate_se], 6))"
536 | ]
537 | },
538 | {
539 | "cell_type": "code",
540 | "execution_count": 13,
541 | "metadata": {},
542 | "outputs": [
543 | {
544 | "name": "stdout",
545 | "output_type": "stream",
546 | "text": [
547 | "ATE 0.083929\n"
548 | ]
549 | }
550 | ],
551 | "source": [
552 | "# Box 12: Parametric multivariate regression adjustment implementation\n",
553 | "f = sm.families.family.Binomial()\n",
554 | "fm_a1 = smf.glm(\"Y ~ C + W1 + W2 + W3 + W4\", \n",
555 | " data.loc[data[\"A\"] == 1], family=f).fit()\n",
556 | "fm_a0 = smf.glm(\"Y ~ C + W1 + W2 + W3 + W4\", \n",
557 | " data.loc[data[\"A\"] == 0], family=f).fit()\n",
558 | "\n",
559 | "y_a1 = fm_a1.predict(data)\n",
560 | "y_a0 = fm_a0.predict(data)\n",
561 | "ate = np.mean(y_a1 - y_a0)\n",
562 | "\n",
563 | "print(\"ATE\", np.round(ate, 6))"
564 | ]
565 | },
566 | {
567 | "cell_type": "code",
568 | "execution_count": 14,
569 | "metadata": {},
570 | "outputs": [
571 | {
572 | "name": "stdout",
573 | "output_type": "stream",
574 | "text": [
575 | "ATE 0.083929\n"
576 | ]
577 | }
578 | ],
579 | "source": [
580 | "# Box 13: Multivariate regression with zEpid\n",
581 | "g_formula = zepid.causal.gformula.TimeFixedGFormula(data, \n",
582 | " exposure=\"A\", \n",
583 | " outcome=\"Y\")\n",
584 | "g_formula.outcome_model(\"A + C + W1 + W2 + W3 + W4 + \"\n",
585 | " \"A:C + A:W1 + A:W2 + A:W3 + A:W4\",\n",
586 | " print_results=False)\n",
587 | "\n",
588 | "g_formula.fit(\"all\") # all sets A=1\n",
589 | "y_a1 = g_formula.marginal_outcome\n",
590 | "\n",
591 | "g_formula.fit(\"none\") # none sets A=0\n",
592 | "y_a0 = g_formula.marginal_outcome\n",
593 | "\n",
594 | "print(\"ATE\", np.round(y_a1 - y_a0, 6))"
595 | ]
596 | },
597 | {
598 | "cell_type": "code",
599 | "execution_count": 15,
600 | "metadata": {},
601 | "outputs": [],
602 | "source": [
603 | "# Box 14: Not Available for Python\n",
604 | "# zEpid does not support two version of the parametric g-formula"
605 | ]
606 | },
607 | {
608 | "cell_type": "code",
609 | "execution_count": 16,
610 | "metadata": {},
611 | "outputs": [
612 | {
613 | "name": "stdout",
614 | "output_type": "stream",
615 | "text": [
616 | "95% Confidence limits for the ATE\n",
617 | "Percentile method: [0.059649 0.106662]\n",
618 | "Normal approx method: [0.058851 0.109006]\n"
619 | ]
620 | }
621 | ],
622 | "source": [
623 | "# Box 15: Bootstrap for multivariate adjustment\n",
624 | "ate_rs = []\n",
625 | "for i in range(1000): # Drawing 1000 bootstrapped samples\n",
626 | " d_star = data.sample(n=data.shape[0], # Same size as input data\n",
627 | " replace=True) # Draw with replacement\n",
628 | " fm_a1 = smf.glm(\"Y ~ C + W1 + W2 + W3 + W4\", \n",
629 | " d_star.loc[d_star[\"A\"] == 1], \n",
630 | " family=f).fit()\n",
631 | " fm_a0 = smf.glm(\"Y ~ C + W1 + W2 + W3 + W4\", \n",
632 | " d_star.loc[d_star[\"A\"] == 0], \n",
633 | " family=f).fit()\n",
634 | " ate_rs.append(np.mean(fm_a1.predict(data) - fm_a0.predict(data)))\n",
635 | "\n",
636 | "print(\"95% Confidence limits for the ATE\")\n",
637 | "ci_perc = np.percentile(ate_rs, q=[2.5, 97.5])\n",
638 | "print(\"Percentile method: \", np.round(ci_perc, 6))\n",
639 | "ate_se = np.std(ate_rs, ddof=1)\n",
640 | "print(\"Normal approx method:\", np.round([ate - 1.96*ate_se,\n",
641 | " ate + 1.96*ate_se], 6))"
642 | ]
643 | },
644 | {
645 | "cell_type": "code",
646 | "execution_count": 17,
647 | "metadata": {},
648 | "outputs": [
649 | {
650 | "name": "stdout",
651 | "output_type": "stream",
652 | "text": [
653 | "RR 1.2766\n"
654 | ]
655 | }
656 | ],
657 | "source": [
658 | "# Box 16: Computing the parametric marginal risk ratio\n",
659 | "f = sm.families.family.Binomial()\n",
660 | "fm_a1 = smf.glm(\"Y ~ C + W1 + W2 + W3 + W4\", \n",
661 | " data.loc[data[\"A\"] == 1], family=f).fit()\n",
662 | "fm_a0 = smf.glm(\"Y ~ C + W1 + W2 + W3 + W4\", \n",
663 | " data.loc[data[\"A\"] == 0], family=f).fit()\n",
664 | "\n",
665 | "y_a1 = fm_a1.predict(data)\n",
666 | "y_a0 = fm_a0.predict(data)\n",
667 | "risk_ratio = np.mean(y_a1) / np.mean(y_a0)\n",
668 | "\n",
669 | "print(\"RR\", np.round(risk_ratio, 4))"
670 | ]
671 | },
672 | {
673 | "cell_type": "markdown",
674 | "metadata": {},
675 | "source": [
676 | "## 4. Inverse Probability of Treatment Weighting\n",
677 | "\n",
678 | "### 4.1 Inverse probability of treatment weighting based on the propensity score plus regression adjustment"
679 | ]
680 | },
681 | {
682 | "cell_type": "code",
683 | "execution_count": 18,
684 | "metadata": {},
685 | "outputs": [
686 | {
687 | "name": "stdout",
688 | "output_type": "stream",
689 | "text": [
690 | "ATE 0.083294\n"
691 | ]
692 | }
693 | ],
694 | "source": [
695 | "# Box 17: Computation of the IPTW estimator for the ATE\n",
696 | "f = sm.families.family.Binomial()\n",
697 | "fm_pa = smf.glm(\"A ~ C + W1 + W2 + W3 + W4\", \n",
698 | " data, family=f).fit()\n",
699 | "p_score = fm_pa.predict(data) # Calculating propensity scores\n",
700 | "data['p_score'] = p_score\n",
701 | "\n",
702 | "iptw = 1 / np.where(data['A'] == 1, p_score, 1 - p_score) # IPTW\n",
703 | "data['iptw'] = iptw\n",
704 | "\n",
705 | "d_a1 = data.loc[data[\"A\"] == 1].copy()\n",
706 | "d_a0 = data.loc[data[\"A\"] == 0].copy()\n",
707 | "ate = (np.average(d_a1['Y'], weights=d_a1['iptw']) - \n",
708 | " np.average(d_a0['Y'], weights=d_a0['iptw']))\n",
709 | "print(\"ATE\", np.round(ate, 6))"
710 | ]
711 | },
712 | {
713 | "cell_type": "code",
714 | "execution_count": 19,
715 | "metadata": {},
716 | "outputs": [
717 | {
718 | "name": "stdout",
719 | "output_type": "stream",
720 | "text": [
721 | "95% Confidence limits for the ATE\n",
722 | "Percentile method: [0.057431 0.106584]\n",
723 | "Normal approx method: [0.058198 0.10839 ]\n"
724 | ]
725 | }
726 | ],
727 | "source": [
728 | "# Box 18: Bootstrap computation for the IPTW estimator\n",
729 | "ate_rs = []\n",
730 | "for i in range(1000): # Drawing 1000 bootstrapped samples\n",
731 | " d_star = data.sample(n=data.shape[0], # Same size as input data\n",
732 | " replace=True) # Draw with replacement\n",
733 | " fm_pa = smf.glm(\"A ~ C + W1 + W2 + W3 + W4\", \n",
734 | " d_star, family=f).fit()\n",
735 | " ps_score = fm_pa.predict(d_star) # Calculating propensity scores\n",
736 | " d_star['iptw'] = 1 / np.where(d_star['A'] == 1, \n",
737 | " ps_score, 1 - ps_score) \n",
738 | " ds_a1 = d_star.loc[d_star[\"A\"] == 1].copy()\n",
739 | " ds_a0 = d_star.loc[d_star[\"A\"] == 0].copy()\n",
740 | " ate_rs.append(np.average(ds_a1['Y'], weights=ds_a1['iptw']) - \n",
741 | " np.average(ds_a0['Y'], weights=ds_a0['iptw']))\n",
742 | "\n",
743 | "print(\"95% Confidence limits for the ATE\")\n",
744 | "ci_perc = np.percentile(ate_rs, q=[2.5, 97.5])\n",
745 | "print(\"Percentile method: \", np.round(ci_perc, 6))\n",
746 | "ate_se = np.std(ate_rs, ddof=1)\n",
747 | "print(\"Normal approx method:\", np.round([ate - 1.96*ate_se,\n",
748 | " ate + 1.96*ate_se], 6))"
749 | ]
750 | },
751 | {
752 | "cell_type": "code",
753 | "execution_count": 20,
754 | "metadata": {},
755 | "outputs": [
756 | {
757 | "name": "stdout",
758 | "output_type": "stream",
759 | "text": [
760 | " RD SE(RD) 95%LCL 95%UCL\n",
761 | "labels \n",
762 | "Intercept 0.303444 0.007712 0.288328 0.318559\n",
763 | "A 0.083294 0.013046 0.057723 0.108864\n"
764 | ]
765 | }
766 | ],
767 | "source": [
768 | "# Box 19: IPTW estimator using zEpid\n",
769 | "ipw = zepid.causal.ipw.IPTW(data, treatment=\"A\", outcome=\"Y\")\n",
770 | "ipw.treatment_model(\"C + W1 + W2 + W3 + W4\", \n",
771 | " stabilized=False, # Set to True for stabilized\n",
772 | " print_results=False)\n",
773 | "ipw.marginal_structural_model(\"A\")\n",
774 | "ipw.fit()\n",
775 | "print(ipw.risk_difference)"
776 | ]
777 | },
778 | {
779 | "cell_type": "code",
780 | "execution_count": 21,
781 | "metadata": {},
782 | "outputs": [
783 | {
784 | "name": "stdout",
785 | "output_type": "stream",
786 | "text": [
787 | " Confounder Raw Weighted\n",
788 | "2 C 0.093144 0.000325\n",
789 | "3 W1 -0.061352 -0.003754\n",
790 | "4 W2 0.091364 -0.002439\n",
791 | "0 W3 0.035606 0.002426\n",
792 | "1 W4 0.071853 0.000404\n"
793 | ]
794 | },
795 | {
796 | "data": {
797 | "image/png": "iVBORw0KGgoAAAANSUhEUgAAAXQAAAEGCAYAAAB1iW6ZAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjMuMSwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy/d3fzzAAAACXBIWXMAAAsTAAALEwEAmpwYAAAeD0lEQVR4nO3de3xV5Z3v8c8vICByUUAqiiHgeA1Y5OIZLqHYqdgWS8eqdTDOlL7Gg7dTa+z0aF+ZStWTc5wjVg+2Hic4jqONo1PBVntRtEpBAsVgqVw9VgUG9KVchEIRMfA7fzwrYSdmJzvJ3tnJw/f9euWVtdde61m/Z2/4Zu1n7f1sc3dERKTrK8h3ASIikh0KdBGRSCjQRUQioUAXEYmEAl1EJBLd83XgQYMGeVFRUb4On3M7d+4EYODAgXmuRERismrVqh3ufmJT9+Ut0IuKiqipqcnX4XPukUceAWDWrFl5rUNE4mJmm9PdpyEXEZFIKNBFRCKhQBcRiUTextBFpPP75JNP2Lp1KwcOHMh3KUedXr16MXToUI455piM91Ggi0haW7dupW/fvhQVFWFm+S7nqOHu7Ny5k61btzJ8+PCM98vakIuZnWRmT5jZW2a2ysx+ZWZnpNt+zRooKICiIqiqylYVeVJVFToSTYdEggMHDjBw4ECFeQczMwYOHNjqV0ZZCXQLz/bTwGJ3P83dxwLfAz6Tbp+DB8EdNm+G2bO7cAZWVYUObN7csEPvv5/vykSyQmGeH2153LN1hn4B8Im7P1i3wt3/4O5LM9l5/34oL89SJR2tvDx0INX+/fDOO/mpR0SOWtkK9JHAqpY2MrPZZlZjZp/6RNGWLVmqpKOlK/zjjzu2DpFIbdq0iZEjRzZY94Mf/IC5c+fm5HgTJ05scZuioiJ27NjxqfWLFy+murq61cdM115rdejbFt290t3Hufu4xvcVFnZkJVmUrvCePTu2DpHOIILrSW0J5DptDfRsyVagrwPGtnXn3r2hoiJLlXS0iorQgVS9e0MrrkyLRCHd9aQchvrUqVO55ZZbOP/88znjjDNYujSM8k6fPp3XX38dgPPOO4877rgDgNtuu4358+cDcPfddzN+/HjOPfdc5syZU99mnz59ADh8+DDXX389Z511FhdeeCFf/vKXeeqpp+q3u//++xkzZgyjRo1i48aNbNq0iQcffJB7772X0aNHs3TpUrZv386ll17K+PHjGT9+PMuWLQPCXE/Tpk2juLiYq6++mmx9c1y2Av0loKeZza5bYWbnmllJuh169AAzGDYMKiuhtDRLlXS00tLQgWHDGnboM2mvB4vEKd31pBxfIKutrWXlypXcd9993H777QCUlJSwdOlS9uzZQ/fu3euDdOnSpUyZMoVFixbx5ptvsnLlSlavXs2qVatYsmRJg3YXLlzIpk2bWL9+PY899hjLly9vcP+gQYN47bXXuO6665g7dy5FRUVce+21lJWVsXr1akpKSvj2t79NWVkZr776KgsWLODqq68G4Pbbb2fy5MmsW7eOSy65hC1ZGnPOyvvQ3d3N7BLgPjO7BTgAbAJuSrfPqFEQzdxcpaWf/ouUTM4lctRIF0rtDKt07/aoW/+1r30NgLFjx7Jp0yYgBPq8efMYPnw406dP54UXXmD//v288847nHnmmcyfP59FixZx3nnnAbBv3z7efPNNpkyZUt/+K6+8wuWXX05BQQEnnXQSF1xwQYPjpx534cKFTdb44osvsn79+vrbf/rTn9i3bx9Lliyp32f69OmccMIJrX1YmpS1Dxa5+7vA17PVnoh0MYWFYZilqfXtMHDgQD788MMG63bt2lX/gZueyfWqbt26UVtbC8D48eOpqalhxIgRXHjhhezYsYP58+czdmwYGXZ3vve973HNNde0ua6mjtvY4cOHWbFiBb169WrzcVpDc7mISHaku57Uzgtkffr0YciQIbz00ktACPPnnnuOyZMnp92nR48enHrqqfz0pz9lwoQJlJSUMHfu3Poz8IsuuoiHH36Yffv2AbBt2zY++OCDBm1MmjSJBQsWcPjwYd5//30WL17cYq19+/Zl79699benTZvG/fffX3979erVAEyZMoXHH38cgF//+tef+oPVVgp0EcmOdNeTsnCB7NFHH+XOO+9k9OjRfP7zn2fOnDmcdtppze5TUlLC4MGDOfbYYykpKWHr1q2UlITLetOmTePKK69kwoQJjBo1issuu6xBEANceumlDB06lHPOOYerrrqKMWPG0L9//2aP+ZWvfIWnn366/qLovHnzqKmp4dxzz+Wcc87hwQfDR3XmzJnDkiVLKC4uZuHChRRm6W1+lq2rq601btw41xdciHRuGzZs4Oyzz853GXmzb98++vTpw86dOzn//PNZtmwZJ510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\n",
798 | "text/plain": [
799 | ""
800 | ]
801 | },
802 | "metadata": {
803 | "needs_background": "light"
804 | },
805 | "output_type": "display_data"
806 | }
807 | ],
808 | "source": [
809 | "# Box 20: Assessing IPTW balance\n",
810 | "rename_cols = {\"smd_w\": \"Weighted\", \"smd_u\": \"Raw\", \n",
811 | " \"labels\": \"Confounder\"}\n",
812 | "\n",
813 | "smd = ipw.standardized_mean_differences().rename(columns=rename_cols)\n",
814 | "smd = smd.sort_values(by='Confounder')\n",
815 | "print(smd[['Confounder', 'Raw', 'Weighted']])\n",
816 | "\n",
817 | "# zEpid plotting functionality\n",
818 | "ipw.plot_love()\n",
819 | "plt.show()"
820 | ]
821 | },
822 | {
823 | "cell_type": "code",
824 | "execution_count": 22,
825 | "metadata": {},
826 | "outputs": [
827 | {
828 | "data": {
829 | "image/png": "iVBORw0KGgoAAAANSUhEUgAAAYkAAAEKCAYAAADn+anLAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjMuMSwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy/d3fzzAAAACXBIWXMAAAsTAAALEwEAmpwYAABCr0lEQVR4nO3dd3iT5frA8e+T7l0oZYNsCpRdZIoCDtxbxA0eURwIKs6fRzzHDSoOHOhRnDhRWbIFQTYyRFH2KrtQumee3x9PoG2a0kCbvEl7f64rV5J3JHcD7Z33GfejtNYIIYQQrtisDkAIIYTvkiQhhBCiTJIkhBBClEmShBBCiDJJkhBCCFEmSRJCCCHK5LEkoZT6SCl1SCm1sdi2mkqpuUqpLY77Gp56fyGEEBXnySuJScBAp22PA/O11i2B+Y7nQgghfJTy5GQ6pVQTYLrWOtHx/B/gPK31fqVUPWCh1rq1xwIQQghRIYFefr86Wuv9jscHgDplHaiUGgYMA4iIiOiakJDghfCEEKLqWLNmzRGtdXxFXsPbSeIkrbVWSpV5GaO1nghMBEhKStKrV6/2WmxCCFEVKKV2VfQ1vD266aCjmQnH/SEvv78QQojT4O0kMRW43fH4duAnL7+/EEKI0+DJIbCTgWVAa6XUXqXUncBLwAVKqS3A+Y7nQgghfJTH+iS01oPL2DXAU+8phPBf+fn57N27l5ycHKtD8TuhoaE0bNiQoKCgSn9tyzquhRCiuL179xIVFUWTJk1QSlkdjt/QWpOSksLevXtp2rRppb++lOUQQviEnJwc4uLiJEGcJqUUcXFxHrsCkyQhhPAZkiDOjCc/N0kSQgghyiRJQgghHAICAujUqROJiYlcfvnlpKamArBz504SExNLHDtmzBjGjRt38vm4ceNISEigU6dOdOvWjU8//fSM48jJySEhIYE//vjj5LaxY8dy9913n/FrnilJEkKcgdxc8GDZM2GRsLAw1q1bx8aNG6lZsyYTJkxw67z33nuPuXPnsnLlStatW8f8+fOpSF280NBQxo8fz7333ovWmuTkZN577z1eesn7swYkSQhxGr78Elq3htBQqFkTnnwS8vOtjkp4Qs+ePUlOTnbr2BdeeIF3332X6OhoAKKjo7n99tvLOevUBg4cSL169fj0008ZNWoUY8aMoUYN76+uIENghXCD3Q733AMffADNmsHQobB5M7z4IqxaBT//DIHy21RpRo6Edesq9zU7dYLx4907trCwkPnz53PnnXee3LZt2zY6dep08vmBAwd45JFHSEtLIz09nWbNmpX7umPHjuWLL74otb1v3768+eabpbaPHz+es88+m5YtW3Lrrbe6F3wlk//WQrjhgQdMghg0CEaNKkoIX30F48aZbW+9ZW2MouKys7Pp1KkTycnJtGnThgsuuODkvubNm7OuWOYaM2bMab/+6NGjGT16tNvH169fn/79+3PZZZed9ntVFkkSQpTju+/gnXfg2mvhkUeg+GjDG2+ELVtgwgS46Sbo2dO6OKsSd7/xV7YTfRJZWVlcdNFFTJgwgREjRpzynOjoaCIjI9m+fXu5VxOneyUBYLPZsNms6xmQPgkhTmHfPrjrLmjeHB5+uGSCOGHUKNM/8cAD0pldVYSHh/Pmm2/y6quvUlBQUO7xTzzxBPfddx9paWkAZGRkuBzdNHr0aNatW1fqVlaC8AWSJIQog91urhSysuCllyA42PVxkZGmj2LNGpg927sxCs/p3LkzHTp0YPLkyeUeO3z4cPr160e3bt1ITEzknHPOsfTbf2Xy6PKllUUWHRJWeOUVeOwxGD3a9EWciv1wClOu+4ILgxfRL/Gw6d2+9VYYIPUs3bVp0ybatGljdRh+y9Xnp5Rao7VOqsjrVo1UJ0QlW7cOnn4aevSAG244xYF2O7U/HUeXa87ixcwHaXxsHdnJR+Gbb+D88+HKKyEjw1thC1HpJEkI4SQnxzQzhYfDf/7juh8CwJaVQfNRl9HozdHkNG3L2se/IiFgC490mQ/z5pnOjOnToX9/yM727g8hRCWRJCGEk4cfhn/+gX//23RIu2LLyqDV3f2IWTqbg4MfZPfj7xDargXndUxl8uw48gNC4O674dlnzUSKf/3Luz+EEJVEkoQQxcyYYYa7Xn019O3r+hiVn0eLEZcQ/vfvJA9/jqOX3HrycuOKc45yLD2Q2UvNzFsuvhhuu81M1f7xR+/8EEJUIkkSQjjs22f6mps0MVcTZWn08v1ErVvM/qFPkt7zwhL7+nZMIzKsgC9+LnYJMny4edERIyAvzyOxC+EpkiSEAAoLTQd1Vha8/LKpzeRK3E8fEf/jB6RceCPHz7uq1P7gIE3/LqlMWxJLdo6jMyMoCB58EPbsgXff9dwPIYQHSJIQAjMR7rffzBVE8+aujwneu51Gr9xPVssOHLppZJmvdWnPo2RmBzBnRXTRxj59oF07GDsW3JicJazhK6XCT7ynUoq3itV7uf/++5k0aVKFXvd0SZIQ1d64ceYL/vXXwzXXlHGQ3U6Tf5sCa8n3vgABZVe06d42g7CQQn5cEFO0USnTN5GcDD/8UInRi8rkK6XCT6hduzZvvPEGeRY2U0qSENXaa6+ZyXJ9+pi6TGWJ//otojYs5dDgkRTUqnvK1wwO0vRom8bPS2NKluk47zyoXRtef71SYheeZXWpcID4+HgGDBjAJ598UuHXOlNS4E9USwUFJjmMH28SxNixEBDg+tjAIwdo8M5TZCZ0IbV/WZcaJfXvmsova2vw+99hdG3jmCMREABXXQUTJ8KuXXDWWZXys1RJFtcK95VS4QCPPfYYF198MUOHDnUr9somSUJUO5s2wZAhsGKFmRD9xBOnXgui4esPYcvLZf/Qp8qeWefkvE5pKKWZsiC2KEkAXHaZSRIffwxnUGpaeJavlQoHaNasGd27d+fLL7887ferDJIkRLWxa5dZJOh//4OQEFN248orT31O+PqlxM2eTMrAm8mv5/43/7iYAto1yWTmkhiev29/0Y769SExEb74Ap55xu2kU+1YVCvcF0uFAzz55JNcd911nHvuue79IJVI+iRElaU1bN8OH35oKmM0b24eX3ih6TsuL0Fgt3PWS/dSEF2TI9cMO+33752Yxvqt4Rw97tSOdfHFsHUrbNx42q8pvMPXSoUnJCTQtm1bpk2bdmY/UAVIkhBVQkEB/PknfPKJGc7apw/ExJjEcNdd8NdfppLrTz+ZekxlldsortaPHxC+ZT0HBz2APSzitGPq0zENrRVzV0SV3HGiMux33532awrv8bVS4U899RR79+6t1Nd0h5QKF35ryxb47DOYO9f0cebkmO0hIWaCc8uW0KYNdOwIrVufXsuOLSON9lc2I79mbXY+++kZNQvlF0DPezpx44VH+d8zu0vuvOkmU0Fw7drTft2qSkqFV4ynSoVLn4TwO5s3m0lv06eb5y1bwiWXmKb+du3MoKFTdUS7o/77zxB4PIU9o149436DoEDo0iqd+auiSu/s29e0fR04AHVPPaRWCCtJc5PwK5MmmZGM8+fDHXeYRDF5Mjz5JFxxhWleqmiCCN67nfhvJ3C8+wXkNE8s/4RT6JWYxq4DoexIdlrW7kSTkxT9Ez5OkoTwG6+9ZoautmgB338P99/vmS/hjV4bBTYbh24aVeHXOqdjOgA/L40uuaNFC9MxIuudluAPzd++yJOfmyQJ4Re++so0MfXuDe+/byYue0LkmkXE/jqVlIE3U1Cz4m/SrH4OtWLymLPMqclJKejSBX791SymLQgNDSUlJUUSxWnSWpOSkkJoWVUpK0j6JITP27QJhg41ndBjx0JwcPnnnBG7nUZjR5AfW4uUKypndqtS0K11OkvWRaO1U/dGz55mBbsNG0wbWjXXsGFD9u7dy+HDh60Oxe+EhobSsGFDj7y2JAnh0+x2uP1208/w6qseTBBA3NSPCd+6geS7nkGHVN63su7t0vl5ZRybdoTStllO0Y4ePcz9rFmSJICgoCCaNm1qdRjCiTQ3CZ/2zjtm9c+RIz3XxAQQcPwoDd98lOwmbUjrc2mlvnaPdqZfYt6KyJI76tSBhg3NGF4hfJQlSUIpNUop9adSaqNSarJSyjONacKvZWSYyhUdOpiRS57UcPzDBGSksv/Op6CSJ0E1qp1HfEweC1e7GArbtSssWwb5+ZX6nkJUFq8nCaVUA2AEkKS1TgQCgBu9HYfwfa++CkePmqsIT5Y4ili3hFrTJnFswHXkNkmo9NdXysyXWLI+ilJ9sklJkJ1d+RVPhagkVjU3BQJhSqlAIBzYZ1EcwkelpZkhrz16mCsJT7FlZ9L0mdvIj43n0A33e+x9urfN4HBqEJt3hZTc0aWLuf/lF4+9txAV4fUkobVOBsYBu4H9wHGt9Rzn45RSw5RSq5VSq2W0Q/UzcaJJFPfc49n3aTR2BMH7drLv7jHo0HCPvU/3E/0SK52anOrUMbdff/XYewtREVY0N9UArgSaAvWBCKXULc7Haa0naq2TtNZJ8fHx3g5TWKiwEN56C9q2NaU2PCV2zlfUmvoRxy4YRFZid8+9EdCkbi41o/JZuDqy9M727WH5ckq3RQlhPSuam84HdmitD2ut84EpQC8L4hA+aupU2L3b1MArS0BqCiE7/8aWmX5G7xH+5yqajhlCdtM2HLrx1OsFVIaT/RLrXPRLdOkCKSmwbZvH4xDidFmRJHYDPZRS4UopBQwANlkQh/BRb70FcXFw/vml94X9tZrWQ3rS6fxaJF7Xhs7nRpNwaxJxP3wABe6NEArbvJ6WDwykMCKKPQ+PRwd5cPJFMd3bpnPgaDDbnes4de1q7qVfQvggK/okVgDfAb8DfzhimOjtOIRv2rcPFi0yVV2dC/XVmPsNCUN7Ebrzbw5f+S+Sh43hyOVDCDx2mCbPD6P9Fc2I/+otVG6O6xcHopfMoNWwc9FKsfvxdyiMifPwT1Ske7sMAOY5ry/RtClERpofXAgfY8mMa631M8AzVry38G0ffWRmWV91Vcnt4euX0vT/biancSt2P/oW9siYk/sOX38vkWt/pdYPH9B43AjqffgfDl93L8cuHETOWa1Ba8I3rabOF69Rc9635NZvwp6HXie/TiOv/mzN6+cQG5nPL6uiuPvalKIdNpupcb5smVfjEcIdsuiQ8BlaQ6tWpvRG8ZUfbVkZtLumNcpeyPYXJmOPiC7zBcL/XEmtn/5HxN+/m022ANAape3Yg0I4NuA6Dl8/HB1szfzN+15txua94eyd5bR06cSJ5paS4t6yeUK4QRYdElXKqlVm6efHHiu5vd57/yb4yD52PvVB2QkCQCmyEruzO7E7gSkHiFy3hOCDe9E2RW6D5mR06Xvq872ge9t0fllXg537gmlSP69ox4naTcuWwaWVWxZEiIqQJCF8xqefmn6IgQOLtgXv30Wdr9/ieI+LyE7o7PZrFcTVJXXAdR6IsmJ6JJrRWPNXRnLnVUeLdrRrZ4ZA/fabJAnhU6TAn/AJWptV5jp3hqhi/bp1//c8AIcGe36Yqje0aJBDdHgBC5yXNI2IMOuuLl9uTWBClEGShPAJmzbBrl1w7rlF2wKPHiJuxqcc734BBTXrWBdcJbLZoHNLM1+ilLZtYc0amVQnfIokCeETvv3W3J93XtG22l+9iSrII+WqOy2JyVPObpPB7oMh7D4QVHJHx46mFsmWLdYEJoQLkiSET5g2DZo3L7Zmtd1O3LSPyWrdmbx6TawMrdKdqONUqsmpY0dzv2SJlyMSomySJITlDh+GtWtLXkVELZ9D8OF9HOt3tWVxeUpC42yiXPVLNG0KYWGm81oIHyFJQlhuyhQzga54koif8j6F4ZFkJPW3LC5PsdmgY/MMFv/uVOwvIABat4aVK60JTAgXJEkIy/34o6nVlOBY78eWcZyYJTNI634hOjjklOf6q7PbpLPzQCjJh5z6JRITTS9+VpY1gQnhRJKEsFRurilZ1KdP0epzsQt/xFaQz/FKXmval/R0zJf4xbl0eMeOplb6mjUWRCVEaZIkhKVmzTKrd/brV7St5uzJ5MfEkd2ivXWBeVjCWdlEhhUw33kRohPL8C1e7P2ghHBBkoSw1A8/QEiIWeoZwJaZTtTqX8joep5pvK+iAhz9Er+udUoScXFmpTqZVCd8RNX9LRQ+T2tzJdGtG4Q66u3FLvwRW34ex3sOPPXJVUC3hAy2J4ey/4hTdZy2bU0hKyF8gCQJYZk1a+DgwZJNTTXmfk1BdE2yW3W0LjAvOVHH6RfnobDt28OBA5CcbEFUQpQkSUJY5rvvTGd1nz7mucrLJWrVAjI69a7STU0ntG2SRXhIYen5EsUrwgphsar/myh81owZZthrnGNxuMg1CwnIzSa963mWxuUtgQGmX2KR83yJhARTDldmXgsfIElCWGLvXti4seQEutiFP2IPDCKz7dmWxeVt3RLS2bo3jIMpxfolgoOhRQvpvBY+QZKEsMT335v74lVfY5bNJrtVR3RomDVBWaBne9MvsXCN09VEYiKsWwd5eaVPEsKLJEkIS0ydakZ6Nm9unofs2UrIvh1kdDrH2sC8rG2TLMKCC5nnPF+iSxcz03DtWmsCE8JBkoTwuqws09x+zjlFs6xjFv4IQHqXc8s+sQoKCoQOzTNY/LuLJAGwcKHXYxKiOEkSwutmzDCtKMWHvsYsmUFe7Ybk12loXWAW6ZaQwT+7wzh8rFi/RK1apm66zLwWFpMkIbzuhx8gPLzoy7ItO5PIDUvJaN/T2sAsUjRfwqlfol07WLFCVqoTlpIkIbzKboc5c6B7dwhyFECNXDkfW34eGV36WhucRdo3yyQspJA5y13MlzhyBHbssCQuIUCShPCy5cshJcWpqWnZLOxBwWQldLEuMAsFBULnFuksWB1dckfXrub+11+9H5QQDpIkhFd9/72ZTN27d9G2qJXzyW6eWGXXjnBH7/bp7NgXyq79wUUbmzc37XKLFlkXmKj2JEkIr5o+3UwBiIkxzwOPHCBs92Yy21WfCXSu9OmYBsDsZcWanAICoE0bmVQnLCVJQnjNjh2weXPJWdbRy2YBkNmxt+uTqokWDXKIi85j1lKnJqeOHeGffyA11ZK4hJAkIbzmxCzrEkli+WwKw6PIOauVJTH5CqXg7IR0Fv0ehd1ebEdSkhndJE1OwiKSJITXTJ0KjRpB48aODVoTtXohWa07gy3A0th8Qe8OaRxNC2L95mJlSTp2NLWcZs+2LjBRrUmSEF6RlmYqX58oCw4QsnsLwSkHyGjfw7rAfEifDma+xM/Fm5xCQswiRL/8YlFUorqTJCG8YsYMKCiA/v2LtkX/NhOAzA7VcxKds9o18mlSN5t5K5zmS3TrBn//DYcOWROYqNYkSQivmDIFoqOhQ4eibdHL55Bfszb5tatfKY6y9GibxtINUeTkqqKNvXqZ+7lzrQlKVGuSJITHFRSYv289ephRnQAUFhK1bjGZbZKKqvwJ+nRIJzffxpJ1xUp0tGlj5ktIv4SwgCVJQikVq5T6Tin1t1Jqk1JK2huqsCVL4PjxkqOawv9aRUBWhjQ1OeneNp0Am+bn34o1OQUGmkswqQgrLGDVlcQbwCytdQLQEdhkURzCC77/3vydKz7LOmapY35Eu+4WReWbIsLstG+WwZzlMSV39OoFe/aYORNCeJHXk4RSKgboC/wPQGudp7VO9XYcwntmzDAjOSMiirZFrZxHToNmFMbUtC4wH9W343E2bg9n3+Ggoo0nil1NmWJNUKLasuJKoilwGPhYKbVWKfWhUirC+SCl1DCl1Gql1OrDhw97P0pRKf75x8y0Lr5MqcrJIuLPlWS1TbIuMB/Wr8txAGYsLjYUtl49aNYMfvrJoqhEdWVFkggEugDvaq07A5nA484Haa0naq2TtNZJ8fHx3o5RVJITs6yLV32N/P1XbAX5ZLTvZU1QPq5Voxxqx+YxbbFTk1Pv3rBqlSmjK4SXWJEk9gJ7tdYrHM+/wyQNUQVNnWq+ANerV7QtZtksdEAgWW3kn90VpaBX4nF+WR1NfkGxHQMGmAU55GpCeJHXk4TW+gCwRynV2rFpAPCXt+MQnpeSYr74Fp9lDY7S4E3boEPDrQnMD/TrcpyM7ICSQ2HbtoW4OPjmG+sCE9WOVaObHgC+UEptADoBL1gUh/CgadPMF9/is6wDUlMI2/5ntS8NXp5e7dMJDLAzdWGxJiebDc4/H+bNkyYn4TWWJAmt9TpHf0MHrfVVWutjVsQhPOunn6BGDfMF+ISolfNQWsv8iHJEhNrp3CKDn5c69UtcfjkUFsLkydYEJqodt5KEUupypZTMzhZuy8+H+fPNWta2Yv9zYpbNojAkjOxmidYF5yf6djzOP7vDSq5W17q1KaP72WfWBSaqFXf/8A8CtiilXlFKJXgyIFE1LFkC6eklRzUBRK1aQHbLjmZ2nTil/klmKOzURcWGwioFl1wCK1eaon9CeJhbSUJrfQvQGdgGTFJKLXPMY4gq51RRTf3wg8kDPYpVAQ/ev4uQA7vJTJT+CHc0qZtLw1o5/PBLbMkd114LQUHw2muWxCWqF7ebkLTWaZjhql8B9YCrgd+VUg94KDbhx2bONOWGSsyyXmGqmGZ2kPkR7lAK+nVJZfG6KI5nFPtVrVHDXKJ98YVZqEMID3K3T+JKpdQPwEIgCDhba30xpu7Sw54LT/ijbdvMrW/fktujl8+lIDKG3AbNrAnMD13UPZWCQhtTFzl1YN90E2RlwYQJ1gQmqg13rySuAV7XWrfXWo/VWh8C0FpnAXd6LDrhl374wdwXr/qK1kT9vojsVp1K9mSLU+rUIpO46Hy+n1+j5I7EROjcGcaNg4wMa4IT1YK7v60HtNa/Ft+glHoZQGs9v9KjEn5t2jSzlnXDYmsJhezeQtDRg2QmStXX02GzQd+OqcxdEV1yISKA++6Do0dh/HhLYhPVg7tJ4gIX2y6uzEBE1ZCebtay7uXU7RC9zCyYI+tZn76Lzk4lKzeAOcujS+7o1AnOPhteeUWWNhUec8okoZQarpT6A0hQSm0odtsBbPBOiMKfzJ5t5kg4D32NXjmP/Nha5NdpZE1gfqxHYjqRYQV8Oy+29M7RoyE7G0aO9HZYopoo70riS+By4CfH/YlbV8ewWCFK+PFHM6KpU6diG+12ItcuJqt1Z1mq9AwEB2p6Jx5n5pIYCgqcdjZtCoMGmRnY86XlV1S+8pKE1lrvBO4D0ovdUErJajGiBLvdXEl061ZyrlzYto0Eph+T/ogKuKh7KkfTg1iw2sXUpHvuMWV2b7sNUlO9Hpuo2ty5kgBYA6x23K8p9lyIk9asgSNHSi4wBBC1zLFUaaL0R5yp8zofJzykkM9muPhuFhYGzz0HBw7AXXd5PzhRpZ0ySWitL3PcN9VaN3Pcn7jJYHdRwg8/mNakc84puT165Xzy4utTUKuuNYFVAaHBmn6dj/HTolhy81w02XXsCHfcAd99B5984vX4RNXl7mS63ieWGFVK3aKUek0p1dizoQl/M3MmJCRAbGyxjQUFRK5fSlaCLDBUUZf1PkZ6ViAzlkS7PmDYMGjXzgyN3bbNu8GJKsvdIbDvAllKqRMzrLcBUoZSnHT0KGzYAD2dKoCHb1pNQHaGDH2tBL3bpxEbmc/nrpqcwHQEvfSSeXz99WaYmRAV5G6SKNBaa+BK4G2t9QRAivuJk2bPBq3NMszFxTjmR2S1laJ+FRUYABd0PcbMpbGkZ5bxq1uvHjz+OKxdC08+6d0ARZXkbpJIV0o9AdwCzHCsLRHkubCEv5k50wx9bdeu5PaoVfPJrd+EwhgZDFcZLu9zlNx8G98tiC37oIsvNrdXX5VhsaLCTmc9iVzgTsca1Q2BsR6LSvgVrWHBAlNKqPjQV5WXS8TGlWQmdLUuuCqmS6tM6sXl8vFPcac+8IknoH59uOUW0xYoxBlydz2JA1rr17TWix3Pd2utP/VsaMJfbN4M+/aVLsURsWEptvxcMqU/otLYbHD1OUdYvC6abXuDyz4wPBxefNGMSR461HsBiirH3dFN1yiltiiljiul0pRS6UopKWQvAJg+3dy7qteklY2sNkneD6oKu75fCjalef/7Wqc+sG1bMyz2p59M1UUhzoC7zU2vAFdorWO01tFa6yitdRnj8ER1M3u26S8tXvUVIHr1L+Q2aoE9QsY4VKY6NfPp2e44n0yPK12mw9nQoaYk7/DhZv0JIU6Tu0nioNZ6k0cjEX4pPx9++82U4ijOlp1J+N9ryGwr/RGecEP/Ixw6Fsz0JTGnPjA4GJ56CpKT4emnvROcqFLcTRKrlVJfK6UGO5qerlFKXePRyIRf+O038wW1T5+S2yN//xVVWEhmYk/XJ4oKOa/zceKi83nnm3KanACSkuDCC+Gtt2D7ds8HJ6oUd5NENJAFXEhRJdjLPBWU8B8zZpjOVOcriejls9EBgabyq6h0QYFwTd/DzF0Zy5bdIeWfMHKk+YcaNcrjsYmqxd3RTUNc3GTIhGDuXFOKI8qp2yFq9S9kN0lAh4ZZE1g1cMtFhwkMsPPqZ7XLP7h2bbMu9tSpsGSJ54MTVYa7o5taKaXmK6U2Op53UEr9n2dDE77u2DH44w/o4TTCNSDtGGFbN8qoJg+Ljy3gwqRjfDYzjtT0gPJPGDIEatQwVxVaezw+UTW429z0AfAEkA+gtd4A3OipoIR/mDXLrCHhPPQ1cvVClLaT0UH6IzztjksOkpUbwPvflzO5DszciXvuMTXdv/nG88GJKsHdJBGutV7ptK28wXeiivv5Z/N3JzGx5PboFbOxB4WQ06K9NYFVI4nNsunUIp23vqpNvju/kVdeaYbEPvUU5Y+fFcL9JHFEKdUc0ABKqeuA/R6LSvi8skpxAEStXkh283booFPMCBaV5q7LD5B8JIRJU924mggMLCol/tFHng9O+D13k8R9wPtAglIqGRgJ3OOpoITv27rVDL13Lg0emHKQsF3/kNm2m+sTRaU7r3MaCY0yef6jeu5dHAwYAK1bw5gxkJPj6fCEnztlklBKPaSUegi4CpgJPA+8B0wBrvV4dMJnnSjF4VwaPGqlqTqa2V76I7xFKRh+9X52HQjhs5/dqLarFIwYAfv3w5tvej5A4dfKu5KIctySgOFADSAWcxUhS41VY3PmQN26LkpxrJhDYWg4OU0TrAmsmjo/6Tgt6mfx3If1KCx044Tu3aFLF1MEME3KsImylbfG9bNa62cxpcG7aK0f0Vo/DHQFZPnSaio/HxYvNhPolNNyy1FrFpLdogMEBLo+WXiEUnDP1fvZnhzKZzPdXLtjxAhITYWXX/ZobMK/udsnUQfIK/Y8z7FNVEPLlkFmZummpqADuwnZv4vMdrIKnRUGnp1Kq4ZZ/Pvd+uTmqfJPSEyEc86B8ePh0CGPxyf8k7tJ4lNgpVJqjFJqDLACmOSpoIRvmznTVHg42ykXRK2YC0CmzI+whM0GowYls+dQCBPcqekE5moiJweefdazwQm/5W5ZjueBIcAxx22I1vrFiryxUipAKbVWKTW9Iq8jvG/uXGjVCqKdisXHLJ9LQUQ0uQ2bWxOYoG/HNLq0TOOFj+qVvQ52cU2bwkUXwQcfwK5dng9Q+B13ryTQWv+utX7DcVtbCe/9ICDlx/1MaiqsW2f6PUvQmqjfF5HdqpP5SissoRSMHpxMSloQL01ys0X43nvN/ZNPei4w4bcs+W1WSjUELgU+tOL9xZmbO9eU4nDujwjZs5WglANkJkp/hNU6tsyif+djvDG5DoeOujGAoF49uPpq+Oor+Osvzwco/IpVX/nGA48C9rIOUEoNU0qtVkqtPnz4sNcCE6c2cyaEhUGHDiW3Ry+fDcj8CF/x8I3J5OTZePrdeu6dMGwYhITAo496NjDhd7yeJJRSlwGHtNZrTnWc1nqi1jpJa50UHx/vpehEeebPh06dSpfiiF4+l/yYOPLqyshoX9C0fi5X9jnCR1Pj2brHjfIosbEweLBZIGT5co/HJ/yHFVcSvYErlFI7ga+A/kqpzy2IQ5ymbdtgz57SpcGx24n8fRFZbbqWnjghLPPg9fsIDLDz6BsN3DvhjjsgJgYeeURKiYuTvJ4ktNZPaK0baq2bYMqNL9Ba3+LtOMTpmznT3DsvVRr+z1oCM46TIU1NPiU+toBbLjzEDwtrsnJjePknhIebRPHbbzB7tsfjE/5BhqEIt82aZRY4a+zUohT9m8keMj/C9wy74gAxEfk8/HoD9y4OBg0y/8iPPipXEwKwOElorRdqrWWtbD9QUGBKcSQllW5Ril4+l9x6Z1EY6+YELuE1kWF27rlyP0vWRzNjSXT5JwQHw913myUHv/7a8wEKnydXEsItK1ZAenrpoa8qJ5uIP1dIaXAfNviCI9SvlcujbzTAXuZ4wmIuuwzOOksWJhKAJAnhpunTzRw55/UjItctxpafR0bHXq5PFJYLDtQ8eF0ym3aGM2maG8X/AgLMwkTbt8OHMpWpupMkIdwyZ04ZpTh++xkdEEhWG7mS8GWX9jxGQqNMnn63Pjm5boxA69cP2rQxNZ2ysz0foPBZkiREuY4dK6MUBxC9ch7ZTdugQ8O8Hpdwn80GjwxOZt+REN740o15RycWJjpwAF5/3fMBCp8lSUKUa84cU4rjnHNKbg9IPULYto1SGtxP9GqfTo+2x3npk3qkpgeUf0K3bmakwtixcPy45wMUPkmShCjX9OkQEWGWHyguetkcADI69XFxlvBFjwxO5nhmAP/9wM3ifw8+aKo6Pv+8R+MSvkuShDglrWHePOjcuXQpjphlsygMiyCnaRtrghOnrW2TbAaefZQJ39Zh78Gg8k9o08b0T7z9tixMVE1JkhCn9Pffplm6l/PgJa2JWrWArFadZKlSP/PQoH3Y7fDE2/XdO+GBByA3F55+2rOBCZ8kSUKc0nTHklDO/REhu7cQfDiZzETnQk7C1zWIz+P68w7z5aw4/tgSWv4JjRvDpZfCRx/Bjh2eD1D4FEkS4pR+/hkaNjRLDhQX8+tUADK6nOPiLOHr7r92P+EhhTwy3s3if8OHmyFSsjBRtSNJQpQpJweWLTODXJzFLJlJXu2G5Ndu6P3ARIXFRhVy56UHmLMilvkrI8s/oXZtuPZaU6pj40bPByh8hiQJUaZ580yiOO+8kttVThaRG34jo700NfmzOy45RJ0auTz0WkP3ynX8619mxanHHvN4bMJ3SJIQZZoyBUJDzVD54qJWzjelOLr0tSYwUSlCgjUPXLuPDVsj+HymG+U6YmLgpptMzfiVKz0foPAJkiSES1qb/oiuXc2qlsXF/joNe3AIWW2SXJ8s/MZV5xylVcMsnpzgZrmOW281tVlkmdNqQ5KEcGn9ejP0ta/zxYLWRC+bRVarTuggN5bFFD7NZoNHbtpL8uEQxn9Zu/wTIiLg9tth0SJYsMDzAQrLSZIQLk2ZYu6d+yNCdm0m5OAeMjrLqKaqok/7dHq2Pc6Lk+qSkupGuY4bb4RateDxx2VhompAkoRwado0aN0a4uJKbo9d9BMA6V3OtSAq4SmP3bKXzOwA94bEhoTAkCGwalXRRBpRZUmSEKUcOGCqvjqvZQ0Qs2Q6uXUbU1CrXumdwm+1apTDjf0P8cmMWizb4MZ62NdeaybPPP447g2NEv5KkoQo5SdzsUC/fiW3B6SnErlhKRkde5c+Sfi9kTfsIy46n3teaExhYTkHBwbCsGHw11+yzGkVJ0lClPLtt1CnjmluKi7612mowkLSul9gTWDCoyLC7Dx20142bI3gra/cWHPi0kvNMqf/93+yzGkVJklClHDsmBm40q+fWXemuJrzv6MguiY5zRNdnyz83iU9j3F2Qhr/924Ddu4rZ/SazQb33muWOf34Y+8EKLxOkoQoYcoU86XwootKblc52UStmGuammzy36aqUgqeu2sXdg23Pn1W+d0N/fubS85nnjGVYkWVI7/tooRvvoH4eBcLDK2YS0BuNmndz7cmMOE1DWvnMfrGPSxZH82b5TU7KQX33w/795s1J0SVI0lCnHT8OPzyC5x7bummphrzvqEwNFyWKq0mBg1IoWfb4zzxdkP+3hFy6oN79oSOHeGFF2SZ0ypIkoQ46ccfIT8fBg502lFYSMySmWQmdodAN1YzE35PKXjh7l2EBNm59tFmZOeUU7Jj1Cg4ehTGjPFKfMJ7JEmIk774wkye69Ch5PaoVQsITD9G2tnS1FSd1KmZzwt37+CvHeHc+1KjUx+cmAgXXggTJpiObFFlSJIQgFm+eMEC83vu3C8dN/Mz7CFhZHSVWdbVTb/OadwxcD+TpsfzyfRyKsWOGmX+84wc6ZXYhHdIkhAAfP45FBbCFVeU3K7ycold9BMZHXujg91Y6lJUOQ/duI+OzdO598XGbDpV/0R8PNxyi6np8ssv3gtQeJQkCQHAJ59A8+bQsmXJ7dFLfyYgM43U3pdYE5iwXGAAjB+xg+AgO1c91JzM7FP82RgyxBT/GzGC8qdtC38gSUKwaRNs2ACXuMgDcTM+pTAimswOvbwfmPAZdWrm88rwHWzZG8qQMY3LLv4aGmqGxG7cCB995NUYhWdIkhD873+mKfnSS0tut2VnErP0Z9K79DW1ekS11qdDOvdeuY9v58fx1te1yj7w0kuhTRt48kkZElsFSJKo5goKzKimrl1NK0FxsfO+wZabQ2qfy6wJTvic4VcfoFe74zwyvhHL/yijWqxSZh3slBRT10n4NUkS1dz06aY0+LXXlt4X//1E8mrVJ7tNV+8HJnySzQbj7ttBzah8rhvdrOxFihITTfvlu+/Cn396N0hRqSRJVHMTJpi5Ea5WoIvcuJzUvpeVnn4tqrXYqELGj9jOwWNBDHq8adn1nUaONH0Uw4fLCnZ+zOtJQinVSCn1i1LqL6XUn0qpB70dgzB27oT5800TsnOXQ/z376FtNo73u8aS2IRv69gii0dv2sv81TH8Z2Jd1wfVqGHWnFi82NSfF37JiiuJAuBhrXVboAdwn1KqrQVxVHvvvGMuEm64wWlHQT41Z35OZruzKYg9RQelqNZuvuAwF519lP/+rz5zlkW5PmjQIGja1Ey0y8ryboCiUng9SWit92utf3c8Tgc2AW4srCsqU34+TJoE3bpBXacvgrELfiAo9TDH+stVhCjbibLijevkMPippuw/4mIEXGCg6cTetw/+8x/vBykqzNI+CaVUE6AzsMLFvmFKqdVKqdWHDx/2emxV3eTJcPiw+aLnrM6Xr5EfV5eMLlKGQ5xaRKidNx/cTkZ2ALf/+yzXXQ9JSWbdiddfl7pOfsiyJKGUigS+B0ZqrdOc92utJ2qtk7TWSfHxbiylKNymNYwbB40aQZ8+JfeF/f07kRtXcHTAdWArY+SKEMW0aJjDg9cmM3dlLO9/H+f6oEceMUOj7rvPu8GJCrMkSSilgjAJ4gut9RQrYqjOFi2CP/6AwYNLF/Or+9k47MGhpPZ3MSZWiDLcfvEhurRM5+HxjdiR7GLZ09q1TcmOWbPMuGvhN6wY3aSA/wGbtNavefv9BbzyCkRHly7mF5hykNj533G850DsEWV0RArhgs0GLw/fCRpufbqJ62Gxt90GDRrAAw/IUqd+xIorid7ArUB/pdQ6x02qx3nJli3my9xVV5kh7MXV/eRlVGEhKZfeaklswr81iM9j9OA9/LYhire/dtFEHBQEo0ebsddjx3o9PnFmrBjdtERrrbTWHbTWnRy3md6Oo7p6+WUICICbbiq5PSA1hVpT3ie967nk1zvLmuCE37uhfwrdEtJ4ckID9h50sYphnz7Qq5dZ6jQ52fsBitMmM66rkf374bPPzMJCznWa6nzxKgE5WRy5Zpg1wYkqQSn47792UVCouPv5Mlaze/RRUzTsQZlH6w8kSVQjzz9vfjfvuqvkdlvGcWp//TbpHfuQ26il65OFcFPjOnncc+U+Zi6twTdzY0sf0LAh3HwzfP+9LE7kByRJVBOHDpmS4AMGmKGvxdWd9BIBWekcvvZua4ITVc6dlx2kZcMsHnilEanpLoZS/+tfZsTTvfeaby7CZ0mSqCZeecUMKBnm1JoUeOQAtSe/QXqXc8lt2saa4ESVExhgmp2OpAbx8GsuCiqEhsJDD8Hff8Mbb3g/QOE2SRLVwNGjpmLzueeaMjrF1X/nKWz5eRwcLO3DonJ1aJ7F4AGH+GhaPIvWRJQ+YMAAMxv7mWekE9uHSZKoBl5+2dRWu9upNSlkx9/Umv4JqX2vIL9uY2uCE1XayEH7qFsjl3/99yxycp1KzisFTz1lCondc481AYpySZKo4g4dgrfegr59oaVTn3Sj10ehg4I5fN1wa4ITVV5EqJ1nhu5m694wxkysV/qARo1g6FAzC/v7770foCiXJIkq7plnTF/EiBElt0cvnk7M0lkcufQ2CmNqWhOcqBbO7ZTGxd1TePXzuqzfHFb6gDvuMO2g990HaaXKuAmLSZKownbuhA8/hIsvhiZNirar3Bwav3w/eXUakXL5HRZFJ6qTp2/fQ2RYAUPGnEVhodPOwEB4+mlz2Tt6tCXxibJJkqjCnnjC1NS5996S2+t+9DwhB3ax//ZHIdDFrFghKllsVCFP3LKHtZsjePXz2qUP6NABrr4aPvjArGQnfIYkiSrqjz/g66/hmmugTp2i7cF7tlL307GkdT2PrPY9rQtQVDuX9TpGn/apPPN+A7btdVEp9sEHzdyJW26BjAzvByhckiRRRY0aBWFhZs7SSXY7TZ+5HR0QwMHbHrUsNlE9KQXP3rkbm9IMGeNigaKICHj2Wdizp3QnmrCMJIkqaNo0mD/fDBqJjS3aHv/120RuWMqhGx+koKaLS34hPKxeXD6jbtjL4nXRvPedi/XTk5LMousff2z+IwvLSZKoYvLyYORIU7b/5puLtgcn76DB24+T2SaJVFm7Wlho8PlH6NoqjUfGN2TzrpDSB4wYYUZaDB0KR454PT5RkiSJKubEMsIPP2zK9wNgt9PkmdsA2DfsGXPdL4RFbDZ45d6dBARoBj/ZlHzn0k0hIfDcc5Caar7puFzBSHiLJIkq5OBB87vVrZuZPHdCnU9eJmrdEg4NfpCCWi4mNAnhZfXi8nnmjl38/k8ET79bv/QBCQlw//0wZw689JL3AxQnSZKoQoYPh5wcU67/hPCNK2jw3tOkd+oj61YLn3JJz1Qu63mEsZ/WZcGqyNIH3Hyz+bbz73/LsFgLSZKoIqZOhR9+MMsInyjiZ8tIo9njgyiIqsm+u5+VZibhc54ZsocG8bkMeqIZyYec5uwoBf/5jxkWe/310j9hEUkSVUBGhrmKOOusYgsKac1Z/72T4IN7SL7vOeyRMZbGKIQrEWF23h65jcwsG1c91IzcPKcvMpGRpkJlSoqZbJefb02g1ZgkiSrgoYfM0qRPP13UWV37s3HUnP8dR64cSnZCV2sDFOIUWjbK4b//2snqvyO554VGpedPtG0Ljz0GS5aYfgrhVYFWByAqZto0U8ng+uuhUyezLWr5HBq+/QQZHXtz5GpZs1r4vkt6pvLnjv18PL0ezRrk8fRdB0oecPXVsGULTJwI7dtLsvAiSRJ+7OBBGDLE9EGMHGm2Be/ZSrPHbyCvdkOS73vBjDcUwg88fOM+9qcE8+/3G1A/Po87rzrqdMDDsGOH+c+ekADnn29JnNWN/AXxU4WFcNNNkJ4OL7xghpYHphyk1fABKLudPQ+/jj3MxWpgQvgomw1eGr6Lbq3TuPuFJkyeHVvygIAAGDsW6tc3RcnWrbMizGpHkoSfevxxWLDA1ERr2dKMZGp57/kEHT3I7odfl5XmhF8KDtS88/A2EptmcMv/NWPSNKe1TiIj4Z13zLeiCy80M0eFR0mS8ENffgnjxsGll5oyN7bMdFrefxFhO/5i730vktOqk9UhCnHGIsLs/O/xrXRumc7Q/zRh3Ke1S3Zm16sHEyaY1bT69YMDB8p8LVFxkiT8zPz5ph+ibVuzPHBgRiqt7u5HxF8rSb77WTK79C3/RYTwceGhdj54bCt9O6Qy+s1GDBnTmLz8YsNjmzeH8ePNQkV9+kBysmWxVnWSJPzIypVw5ZVQty68+SZEHN1D6zv7EL5lPXvve4H0XhdbHaIQlSY0WDPhoe0MvXg/n8yIp/ttrflnZ7GCgB06mESxbx/06gW7dlkWa1UmScJPLFwIAwaYJtl334X6u5fR5pauBCdvZ8/IcWScLSM9RNVjs8EjN+3j1fu2sS05hM43t2H8l/FFS6AmJZmmp5QU6NFDOrM9QJKEH/jmGxg4EGrUgA8n2ukw6xVaDzsXAmzs/PdHZHbqY3WIQnjUxT1SmfriX3RolsGo1xrTaXAbfv3dMXqvQwd4/30zG7tXL5gyxdpgqxhJEj4sPx8eeAAGDTJzISY/v51zn+1Pw7ceI7NtEjv++wV5jVtaHaYQXlGnZj4fPbGVcfdu49DRQM4dlsD597Rg2YYIM2/iiy+gUSO47jp45BGzuIqoMKVLzYH3PUlJSXr16tVWh+FVy5aZpUf/+gtuvDyTsbHPUf+r10DZOHjjg6Sef50U7BPVVlaOjU9+jufT2XU5nhnIuZ3TGHnzIS7vdpCAV16EGTPMFcbnn5sZ2tWUUmqN1jqpQq8hScK3bNliKiN//TU0jk3j065v0Gvl6wSmHSOtW38O3vKILD0qhENmjo3PZ8Xz5bzaHD4eTKPaudxz3WHurTGZ2Leeg8xMuPNOM+M0Ls7qcL1OkkQVUVho1lZ5913zBahbwBr+23Ai/Q58SWB2Bhltu3Hk2rvJlvkPQriUXwBzV8fyxezarN0aRYBNc3Wn7TzP/9Fy3Teo0FAzdnz0aFMuuZqQJOHHsrJg3jz46SeYMV1T99B6BgX/wM0h39E4/S/sQcFkdO5LyiW3kNM80epwhfAbW/aGMmVhHD+vqMmh1GCSQtbzWsx/6X34RxR2VM+eMHgwXH55lU8YkiT8RGoqbN4Mf/wBy5fDihWQ9ddOehcuYkDAQi4IWEC9vN1opchu2pa0nhdy/JwrsEdEWR26EH6r0A7L/4zix19rsnhDLDFZ+xiqJjE06DMa520FwN6wEbZePU0J5c6dITHR1IaqIoUx/TZJKKUGAm8AAcCHWutTLmLrq0kiI8NUYj140FQG2L/f3A4cMLeDBzRHt6cSdWwXrfmHjqyni20dXWzrqF2wH4CC8ChyWrQnvUtf0pP6UxhTs5x3FUKcroJCWLs5knmrYli8IZqIg9vpzwIGqAV0t62ibuG+k8cWBgSTX7s+9kZnYW/UBNW0CbZmZxHQtDG2Rg0JaNwAFeViuVUf5JdJQikVAGwGLgD2AquAwVrrv8o6x90koXXRzW4Hbdcnb/ZCXeq5vVCTlQVZmZqsTE1mRtEt9Zgm5Ygm/XAOGUdyyDySTc6xbHJTs8lPy6YwI4vAghwiyaAGx6jBMWpylBoco3ZACvXVfhradxNlTzsZn90WQF6dRuQ1akFWi/ZktTub3IbNq8y3FiH8xbH0AFb/E8nvf0eyeU8Yx/dnEX/sH1rzD03ZQRN2nryvw6FS56cSwz4asE81YL/N3A4ENiDNVoPMwGgyA2PJCowmOziGgPAQQiKDCI0yt7DoICKjbURHQ1QUREebW2Rk0S0qquTzsLAzG8zor0miJzBGa32R4/kTAFrrF8s6p4sK0IsJRaHLvQHY8P7VkV3ZyA+Nwh4WgT0sgvzwGPJi4smLrU1OXH1y4hqQE98IHRjs9diEEOXLybeRkhHC0YxgUtKDSc8OIq/Ahs7NJSzzMFGZh4jNOUBM9gFi8g4Rm3uIGnkHzS3/MDbsbr9XITYKCUBT9Je/sh4XF01GhZOEFYsONQD2FHu+F+jufJBSahhwYlm13EiyNnohtjOn7ZB93NxKqwX46yru/hw7SPxWk/hdsjtuHte6oi/gsyvTaa0nAhMBlFKrK5oNreTP8ftz7CDxW03it5ZSqsKduVY0hicDjYo9b+jYJoQQwsdYkSRWAS2VUk2VUsHAjcBUC+IQQghRDq83N2mtC5RS9wOzMUNgP9Ja/1nOaRM9H5lH+XP8/hw7SPxWk/itVeH4/WIynRBCCGvIAH0hhBBlkiQhhBCiTJYmCaXUQKXUP0qprUqpx09x3LVKKa2USnI8b6KUylZKrXPc3vNe1CXiOmX8Sqk7lFKHi8X5r2L7bldKbXHcbvdu5CdjqEj8hcW2WzLwwJ3/P0qpG5RSfyml/lRKfVlsu89//o5jyorf5z9/pdTrxWLcrJRKLbbP0s+/grH7w2ffWCn1i1JqrVJqg1LqkmL7nnCc949S6qJy30xrbckN02m9DWgGBAPrgbYujosCfgWWA0mObU2AjVbF7m78wB3A2y7OrQlsd9zXcDyu4S/xO/Zl+MHn3xJYe+KzBWr72efvMn5/+fydjn8AM0jF8s+/IrH7y2eP6bAe7njcFthZ7PF6IARo6nidgFO9n5VXEmcDW7XW27XWecBXwJUujvsv8DKQ483g3OBu/K5cBMzVWh/VWh8D5gIDPRRnWSoSvy9wJ/67gAmOzxit9YkiPP7y+ZcVvy843f8/g4HJjsdWf/4Vid0XuBO/BqIdj2OAExUMrwS+0lrnaq13AFsdr1cmK5OEq/IcDYofoJTqAjTSWs9wcX5Tx6XUIqXUOR6Msyzlxu9wreNy7zul1IlJhO6e60kViR8gVCm1Wim1XCl1lScDLYM78bcCWimlfnPEOfA0zvW0isQP/vH5A6CUOgvzrXXB6Z7rIRWJHfzjsx8D3KKU2gvMxFwNuXtuCT5blkMpZQNewzR5ONsPNNZapyilugI/KqXaaa3TXBxrpWnAZK11rlLqbuAToL/FMZ2OU8V/ltY6WSnVDFiglPpDa73NskhdC8Q02ZyHmdn/q1LKnxY8dhm/1joV//j8T7gR+E5rXWh1IGfAVez+8NkPBiZprV9VpqjqZ0qpM1q9zMorifLKc0QBicBCpdROoAcwVSmV5LhUSgHQWq/BtKu18krURcotL6K1TtFa5zqefgh0dfdcL6hI/Gitkx3324GFQGdPBuuCO5/hXmCq1jrfcWm9GfNH1y8+f8qO318+/xNupGRzjdWff0Vi95fP/k7gGwCt9TIgFFOs8PQ/ews7XwIxHVZNKep8aXeK4xdS1HEdj6OzBdN5kwzU9LX4gXrFHl8NLHc8rgnswHTa1XA89qf4awAhjse1gC2couPPwvgHAp8Ui3MPEOdHn39Z8fvF5+84LgHYiWPirmObpZ9/BWP3i88e+Bm4w/G4DaZPQgHtKNlxvZ1yOq699oOV8cNegvl2tA14yrHtP8AVLo5dSFGSuBb4E1gH/A5c7ovxAy864lwP/AIkFDt3KKbTaCswxJ/iB3oBfzi2/wHc6aPxK0yT5V+OOG/0s8/fZfz+8vk7no8BXnJxrqWf/5nG7i+fPWYU02+OONcBFxY79ynHef8AF5f3XlKWQwghRJlkxrUQQogySZIQQghRJkkSQgghyiRJQgghRJkkSQghhCiTJAlhGaXUR0qpQ0qpjWd4/kJHJcv1jtIVrSs7xopQSi113DdRSt10mueGK6W+UEr9oZTaqJRaopSK9EykQpRNkoSw0iQqXtjtZq11R0zJkLHOO5VSARV8/TOmte7leNgEOK0kATwIHNRat9daJ2Jm0OZXJB6llM+W4RG+S5KEsIzW+lfgaCW93K9ACwClVIZS6lWl1Hqgp1LqIce38Y1KqZGOY5oopf52fFvf5ChgGO7Y19VROHKNUmq2UqqeY/tCpdTLSqmVjjUGznFsb+fYts5RDLHliTgcsb0EnOPYP0op9atSqtOJwB1XCR2dfp56FCuXoLX+RztKpCilbnO8z3ql1GfFfp4Fju3zlVKNHdsnKaXeU0qtAF5RSjVXSs1y/GyLlVIJlfT5i6rKitmCcpPbiRsVWBuEkrPwRwNfOx5r4AbH466YmbERQCRmBnlnx/tqoLfjuI+AR4AgYCkQ79g+iKJ1EBYCrzoeXwLMczx+C3NFA6ZMQpjjcYbj/jxgerG4bwfGOx63Ala7+Nk6AYeAZcBzQEvH9naYmba1HM9rOu6nAbc7Hg8FfnQ8ngRMp6iMzfxir9UdWGD1/wG5+fZNLj+Fv/tCKZWNqbFzohxyIfC943Ef4AetdSaAUmoKcA4wFdijtf7NcdznwAhgFqaw5FylFJgFXvYXe78pjvs1mEQD5g/5U0qphsAUrfWWcmL+FnhaKTUa8wd9kvMBWut1jiqjFwLnA6sc1Tz7A99qrY84jjtxJdYTuMbx+DPgleLvp7UudPRp9AK+dfxsYGr4CFEmSRLCZzn6E9Y4nk7VWv/bxWE3a61XO23L0e6VpXauSaMx9ZL+1Fr3LOOcE1VxC3H8/mitv3Q051wKzFRK3a21XlDG+Wits5RSczELwNxAseq6TsdlYJLSFKWUHXP1kufGz+Us03FvA1K11p3O4DVENSV9EsJnaa0LtdadHDdXCcIdi4GrHKOFIjDVbBc79jV2fDsH07G8BFP0LP7EdqVUkFKq3anewPGNf7vW+k3gJ6CD0yHpmNL3xX0IvAms0o6V55xes7dSqobjcTCmYNsuzOI31yul4hz7ajpOWYopaw1wc7Gf8SRt1lvZoZS63nGuctEXIkQJkiSEZZRSkzFNNa2VUnuVUndW9ntorX/HNOesBFYAH2qt1zp2/wPcp5TahCkB/a42y0FeB7zs6Pheh2miOZUbgI1KqXWYpqpPnfZvAAodHc2jHHGtAdKAj8t4zebAIqXUH5h1rlcD32ut/wSed+xbj6kSC6apbYhSagNwK2Z0lCs3A3c6zv0T/1qyVlhAqsCKakkp1QTTmXxGq3VVwvvXx3SEJ2it7VbEIIQ75EpCCC9TSt2Guap5ShKE8HVyJSGEEKJMciUhhBCiTJIkhBBClEmShBBCiDJJkhBCCFEmSRJCCCHK9P9ma8LtkQSvrAAAAABJRU5ErkJggg==\n",
830 | "text/plain": [
831 | ""
832 | ]
833 | },
834 | "metadata": {
835 | "needs_background": "light"
836 | },
837 | "output_type": "display_data"
838 | }
839 | ],
840 | "source": [
841 | "# Box 21: Assessing IPTW overlap by hand\n",
842 | "density_t = gaussian_kde(1 - data.loc[data[\"A\"] == 1, 'p_score'])\n",
843 | "density_u = gaussian_kde(1 - data.loc[data[\"A\"] == 0, 'p_score'])\n",
844 | "\n",
845 | "x = np.linspace(0, 1, 10000)\n",
846 | "\n",
847 | "ax = plt.gca()\n",
848 | "ax.fill_between(x, density_t(x), color=\"b\", alpha=0.2, label=None)\n",
849 | "ax.plot(x, density_t(x), color=\"b\", label='RHC = Y')\n",
850 | "ax.fill_between(x, density_u(x), color=\"r\", alpha=0.2, label=None)\n",
851 | "ax.plot(x, density_u(x), color=\"r\", label='RHC = N')\n",
852 | "ax.set_ylim([0, 10])\n",
853 | "ax.set_ylabel(\"density\")\n",
854 | "ax.set_xlim([0.45, 0.8])\n",
855 | "ax.set_xlabel(\"1 - Propensity Score\")\n",
856 | "ax.legend()\n",
857 | "plt.show()"
858 | ]
859 | },
860 | {
861 | "cell_type": "code",
862 | "execution_count": 23,
863 | "metadata": {},
864 | "outputs": [
865 | {
866 | "data": {
867 | "image/png": "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\n",
868 | "text/plain": [
869 | ""
870 | ]
871 | },
872 | "metadata": {
873 | "needs_background": "light"
874 | },
875 | "output_type": "display_data"
876 | }
877 | ],
878 | "source": [
879 | "# Box 22: Assessing IPTW overlap using zEpid\n",
880 | "ipw.plot_kde()\n",
881 | "plt.ylim([0, 10])\n",
882 | "plt.xlim([0.2, 0.55])\n",
883 | "plt.show()"
884 | ]
885 | },
886 | {
887 | "cell_type": "markdown",
888 | "metadata": {},
889 | "source": [
890 | "### 4.2 Marginal structural model with stabilised weights"
891 | ]
892 | },
893 | {
894 | "cell_type": "code",
895 | "execution_count": 24,
896 | "metadata": {},
897 | "outputs": [
898 | {
899 | "name": "stderr",
900 | "output_type": "stream",
901 | "text": [
902 | "/home/pzivich/.pyenv/versions/3.6.5/lib/python3.6/site-packages/statsmodels/genmod/generalized_estimating_equations.py:501: DomainWarning: The identity link function does not respect the domain of the Binomial family.\n",
903 | " DomainWarning)\n",
904 | "/home/pzivich/.pyenv/versions/3.6.5/lib/python3.6/site-packages/statsmodels/genmod/generalized_linear_model.py:278: DomainWarning: The identity link function does not respect the domain of the Binomial family.\n",
905 | " DomainWarning)\n"
906 | ]
907 | },
908 | {
909 | "name": "stdout",
910 | "output_type": "stream",
911 | "text": [
912 | "Unstabilized Weights\n",
913 | "ATE 0.083294\n",
914 | "95% CL [0.05772325 0.10886425]\n"
915 | ]
916 | },
917 | {
918 | "name": "stderr",
919 | "output_type": "stream",
920 | "text": [
921 | "/home/pzivich/.pyenv/versions/3.6.5/lib/python3.6/site-packages/statsmodels/genmod/generalized_estimating_equations.py:501: DomainWarning: The identity link function does not respect the domain of the Binomial family.\n",
922 | " DomainWarning)\n",
923 | "/home/pzivich/.pyenv/versions/3.6.5/lib/python3.6/site-packages/statsmodels/genmod/generalized_linear_model.py:278: DomainWarning: The identity link function does not respect the domain of the Binomial family.\n",
924 | " DomainWarning)\n"
925 | ]
926 | },
927 | {
928 | "name": "stdout",
929 | "output_type": "stream",
930 | "text": [
931 | "\n",
932 | "Stabilized Weights\n",
933 | "ATE 0.083294\n",
934 | "95% CL [0.05772325 0.10886425]\n"
935 | ]
936 | }
937 | ],
938 | "source": [
939 | "# Box 23: Computation of the IPTW estimator using a MSM\n",
940 | "\n",
941 | "### Unstabilized IPTW ###\n",
942 | "fm_pa = smf.glm(\"A ~ C + W1 + W2 + W3 + W4\", \n",
943 | " data, family=f).fit()\n",
944 | "p_score = fm_pa.predict(data) # Calculating propensity scores\n",
945 | "iptw = 1 / np.where(data['A'] == 1, p_score, 1 - p_score) # IPTW\n",
946 | "# Estimating Marginal Structural Model\n",
947 | "f = sm.families.family.Binomial(sm.families.links.identity())\n",
948 | "fm = smf.gee(\"Y ~ A\", data.index, data,\n",
949 | " cov_struct=sm.cov_struct.Independence(), \n",
950 | " family=f, weights=iptw).fit()\n",
951 | "print(\"Unstabilized Weights\")\n",
952 | "print(\"ATE \", np.round(fm.params['A'], 6))\n",
953 | "print(\"95% CL\", np.asarray(fm.conf_int().loc[\"A\"]))\n",
954 | "\n",
955 | "### Stabilized IPTW ###\n",
956 | "f = sm.families.family.Binomial()\n",
957 | "# Numerator\n",
958 | "fm_ma = smf.glm(\"A ~ 1\", data, family=f).fit()\n",
959 | "num = np.where(data['A'] == 1, fm_ma.predict(data), \n",
960 | " 1 - fm_ma.predict(data))\n",
961 | "# Denominator\n",
962 | "fm_pa = smf.glm(\"A ~ C + W1 + W2 + W3 + W4\", \n",
963 | " data, family=f).fit()\n",
964 | "den = np.where(data['A'] == 1, fm_pa.predict(data), \n",
965 | " 1 - fm_pa.predict(data))\n",
966 | "# IPTW\n",
967 | "iptw = num / den\n",
968 | "# Estimating Marginal Structural Model\n",
969 | "f = sm.families.family.Binomial(sm.families.links.identity())\n",
970 | "fm = smf.gee(\"Y ~ A\", data.index, data,\n",
971 | " cov_struct=sm.cov_struct.Independence(), \n",
972 | " family=f, weights=iptw).fit()\n",
973 | "print(\"\\nStabilized Weights\")\n",
974 | "print(\"ATE \", np.round(fm.params['A'], 6))\n",
975 | "print(\"95% CL\", np.asarray(fm.conf_int().loc[\"A\"]))"
976 | ]
977 | },
978 | {
979 | "cell_type": "markdown",
980 | "metadata": {},
981 | "source": [
982 | "### 4.3 IPTW with regression adjustment"
983 | ]
984 | },
985 | {
986 | "cell_type": "code",
987 | "execution_count": 25,
988 | "metadata": {},
989 | "outputs": [
990 | {
991 | "name": "stdout",
992 | "output_type": "stream",
993 | "text": [
994 | "ATE 0.083929\n",
995 | "ATE 0.083426\n"
996 | ]
997 | }
998 | ],
999 | "source": [
1000 | "# Box 24: Computation of the IPTW-RA estimator\n",
1001 | "f = sm.families.family.Binomial()\n",
1002 | "\n",
1003 | "fm_a1 = smf.glm(\"Y ~ C + W1 + W2 + W3 + W4\", \n",
1004 | " data.loc[data[\"A\"] == 1], # Only A=1\n",
1005 | " weights=data.loc[data[\"A\"] == 1, 'iptw'], # Box 17\n",
1006 | " family=f).fit()\n",
1007 | "y_a1 = fm_a1.predict(data)\n",
1008 | "\n",
1009 | "fm_a0 = smf.glm(\"Y ~ C + W1 + W2 + W3 + W4\", \n",
1010 | " data.loc[data[\"A\"] == 0], # Only A=0\n",
1011 | " weights=data.loc[data[\"A\"] == 0, 'iptw'], # Box 17\n",
1012 | " family=f).fit()\n",
1013 | "y_a0 = fm_a0.predict(data)\n",
1014 | "\n",
1015 | "ate = np.mean(y_a1 - y_a0)\n",
1016 | "print(\"ATE\", np.round(ate, 6))\n",
1017 | "ate = (np.mean(data['iptw']*data['A']*y_a1) / np.mean(data['iptw']*data['A']) - \n",
1018 | " np.mean(data['iptw']*(1-data['A'])*y_a0) / np.mean(data['iptw']*(1-data['A'])))\n",
1019 | "print(\"ATE\", np.round(ate, 6))"
1020 | ]
1021 | },
1022 | {
1023 | "cell_type": "code",
1024 | "execution_count": 26,
1025 | "metadata": {},
1026 | "outputs": [],
1027 | "source": [
1028 | "# Box 25: IPTW-RA \n",
1029 | "# Not supported by zEpid"
1030 | ]
1031 | },
1032 | {
1033 | "cell_type": "markdown",
1034 | "metadata": {},
1035 | "source": [
1036 | "## 5. Augmented Inverse Probability Weighting"
1037 | ]
1038 | },
1039 | {
1040 | "cell_type": "code",
1041 | "execution_count": 27,
1042 | "metadata": {},
1043 | "outputs": [
1044 | {
1045 | "name": "stdout",
1046 | "output_type": "stream",
1047 | "text": [
1048 | "ATE 0.083796\n",
1049 | "95% Confidence limits for the ATE\n",
1050 | "Percentile method: [0.058572 0.109738]\n",
1051 | "Normal approx method: [0.058901 0.108691]\n"
1052 | ]
1053 | }
1054 | ],
1055 | "source": [
1056 | "# Box 26: Computation of the AIPTW estimator\n",
1057 | "f = sm.families.family.Binomial()\n",
1058 | "\n",
1059 | "# Step 1: g-computation\n",
1060 | "fm_a1 = smf.glm(\"Y ~ C + W1 + W2 + W3 + W4\", \n",
1061 | " data.loc[data[\"A\"] == 1], family=f).fit()\n",
1062 | "fm_a0 = smf.glm(\"Y ~ C + W1 + W2 + W3 + W4\", \n",
1063 | " data.loc[data[\"A\"] == 0], family=f).fit()\n",
1064 | "y_a1 = fm_a1.predict(data)\n",
1065 | "y_a0 = fm_a0.predict(data)\n",
1066 | "\n",
1067 | "# Step 2: propensity scores\n",
1068 | "fm_pa = smf.glm(\"A ~ C + W1 + W2 + W3 + W4\", \n",
1069 | " data, family=f).fit()\n",
1070 | "p_score = fm_pa.predict(data)\n",
1071 | "\n",
1072 | "# Step 3: analytic formula\n",
1073 | "ys_a1 = ((data['A'] * data['Y'])/ (p_score) + \n",
1074 | " (y_a1*(p_score-data['A'])) / p_score)\n",
1075 | "ys_a0 = (((1-data['A']) * data['Y'])/ (1-p_score) + \n",
1076 | " (y_a0*(data['A']-p_score)) / (1-p_score))\n",
1077 | "ate = np.mean(ys_a1 - ys_a0)\n",
1078 | "print(\"ATE\", np.round(ate, 6))\n",
1079 | "\n",
1080 | "# Step 4: bootstrap for inference\n",
1081 | "ate_rs = []\n",
1082 | "for i in range(1000):\n",
1083 | " d_star = data.sample(n=data.shape[0], # Same size as input data\n",
1084 | " replace=True) # Draw with replacement\n",
1085 | " fm_a1 = smf.glm(\"Y ~ C + W1 + W2 + W3 + W4\", \n",
1086 | " d_star.loc[d_star[\"A\"] == 1], family=f).fit()\n",
1087 | " fm_a0 = smf.glm(\"Y ~ C + W1 + W2 + W3 + W4\", \n",
1088 | " d_star.loc[d_star[\"A\"] == 0], family=f).fit()\n",
1089 | " y_a1 = fm_a1.predict(d_star)\n",
1090 | " y_a0 = fm_a0.predict(d_star)\n",
1091 | " fm_pa = smf.glm(\"A ~ C + W1 + W2 + W3 + W4\", \n",
1092 | " d_star, family=f).fit()\n",
1093 | " p_score = fm_pa.predict(d_star)\n",
1094 | " ys_a1 = ((d_star['A'] * d_star['Y'])/ (p_score) + \n",
1095 | " (y_a1*(p_score-d_star['A'])) / p_score)\n",
1096 | " ys_a0 = (((1-d_star['A']) * d_star['Y'])/ (1-p_score) + \n",
1097 | " (y_a0*(d_star['A']-p_score)) / (1-p_score))\n",
1098 | " ate_rs.append(np.mean(ys_a1 - ys_a0))\n",
1099 | "\n",
1100 | "\n",
1101 | "print(\"95% Confidence limits for the ATE\")\n",
1102 | "ci_perc = np.percentile(ate_rs, q=[2.5, 97.5])\n",
1103 | "print(\"Percentile method: \", np.round(ci_perc, 6))\n",
1104 | "ate_se = np.std(ate_rs, ddof=1)\n",
1105 | "print(\"Normal approx method:\", np.round([ate - 1.96*ate_se,\n",
1106 | " ate + 1.96*ate_se], 6))"
1107 | ]
1108 | },
1109 | {
1110 | "cell_type": "code",
1111 | "execution_count": 28,
1112 | "metadata": {},
1113 | "outputs": [
1114 | {
1115 | "name": "stdout",
1116 | "output_type": "stream",
1117 | "text": [
1118 | "ATE 0.083796\n",
1119 | "95% CL [0.058546 0.109046]\n"
1120 | ]
1121 | }
1122 | ],
1123 | "source": [
1124 | "# Box 27: AIPTW estimator with zEpid\n",
1125 | "aipw = zepid.causal.doublyrobust.AIPTW(data, \n",
1126 | " exposure=\"A\", \n",
1127 | " outcome=\"Y\")\n",
1128 | "aipw.exposure_model(\"C + W1 + W2 + W3 + W4\", \n",
1129 | " print_results=False)\n",
1130 | "aipw.outcome_model(\"A + C + W1 + W2 + W3 + W4 + \"\n",
1131 | " \"A:C + A:W1 + A:W2 + A:W3 + A:W4\", \n",
1132 | " print_results=False)\n",
1133 | "aipw.fit()\n",
1134 | "\n",
1135 | "print(\"ATE \", np.round(aipw.risk_difference, 6))\n",
1136 | "print(\"95% CL\", np.round(aipw.risk_difference_ci, 6))\n",
1137 | "# zEpid calculates the variance using influence curves"
1138 | ]
1139 | },
1140 | {
1141 | "cell_type": "markdown",
1142 | "metadata": {},
1143 | "source": [
1144 | "## 6. Data-Adaptive Estimation: Ensemble Learning Targeted Maximum Likelihood Estimation"
1145 | ]
1146 | },
1147 | {
1148 | "cell_type": "code",
1149 | "execution_count": 29,
1150 | "metadata": {},
1151 | "outputs": [
1152 | {
1153 | "name": "stdout",
1154 | "output_type": "stream",
1155 | "text": [
1156 | "ATE 0.083796\n",
1157 | "95% CL [0.058546 0.109047]\n"
1158 | ]
1159 | }
1160 | ],
1161 | "source": [
1162 | "# Box 28: Computation of TMLE by hand\n",
1163 | "f = sm.families.family.Binomial()\n",
1164 | "n = data.shape[0]\n",
1165 | "\n",
1166 | "# Step 1: g-computation\n",
1167 | "fm_a1 = smf.glm(\"Y ~ C + W1 + W2 + W3 + W4\", \n",
1168 | " data.loc[data[\"A\"] == 1], family=f).fit()\n",
1169 | "fm_a0 = smf.glm(\"Y ~ C + W1 + W2 + W3 + W4\", \n",
1170 | " data.loc[data[\"A\"] == 0], family=f).fit()\n",
1171 | "y_a1 = fm_a1.predict(data)\n",
1172 | "y_a0 = fm_a0.predict(data)\n",
1173 | "y_a_ = np.where(data['A'] == 1, y_a1, y_a0)\n",
1174 | "\n",
1175 | "# Step 2: propensity scores\n",
1176 | "fm_pa = smf.glm(\"A ~ C + W1 + W2 + W3 + W4\", \n",
1177 | " data, family=f).fit()\n",
1178 | "p_score = fm_pa.predict(data)\n",
1179 | "\n",
1180 | "# Step 3: targeting step\n",
1181 | "logodds_y1 = np.log(probability_to_odds(y_a1))\n",
1182 | "logodds_y0 = np.log(probability_to_odds(y_a0))\n",
1183 | "logodds_ya = np.log(probability_to_odds(y_a_))\n",
1184 | "\n",
1185 | "clever_cov_a1 = data['A']/p_score\n",
1186 | "clever_cov_a0 = - (1-data['A'])/(1-p_score)\n",
1187 | "\n",
1188 | "submodel = sm.GLM(data['Y'], \n",
1189 | " np.column_stack((clever_cov_a1, clever_cov_a0)), \n",
1190 | " offset=logodds_ya,\n",
1191 | " family=f).fit()\n",
1192 | "epsilon = submodel.params\n",
1193 | "\n",
1194 | "# Step 4: calculating ATE\n",
1195 | "ys_a1 = logistic.cdf(logodds_y1 + epsilon[0] / p_score)\n",
1196 | "ys_a0 = logistic.cdf(logodds_y0 - epsilon[1] / (1-p_score))\n",
1197 | "ate = np.mean(ys_a1 - ys_a0)\n",
1198 | "print(\"ATE\", np.round(ate, 6))\n",
1199 | "\n",
1200 | "# Step 5: inference via influence curve\n",
1201 | "ic = (clever_cov_a1 + clever_cov_a0) * (data['Y'] - y_a_) + (y_a1 - y_a0) - ate\n",
1202 | "sd = np.sqrt(np.nanvar(ic, ddof=1) / n)\n",
1203 | "cl = [ate - 1.96*sd, ate + 1.96*sd]\n",
1204 | "print(\"95% CL\", np.round(cl, 6))"
1205 | ]
1206 | },
1207 | {
1208 | "cell_type": "code",
1209 | "execution_count": 30,
1210 | "metadata": {},
1211 | "outputs": [
1212 | {
1213 | "name": "stdout",
1214 | "output_type": "stream",
1215 | "text": [
1216 | "ATE 0.083796\n",
1217 | "95% CL [0.058546 0.109047]\n"
1218 | ]
1219 | }
1220 | ],
1221 | "source": [
1222 | "# Box 29: TMLE with zEpid\n",
1223 | "tmle = zepid.causal.doublyrobust.TMLE(data, \n",
1224 | " exposure=\"A\", \n",
1225 | " outcome=\"Y\")\n",
1226 | "tmle.exposure_model(\"C + W1 + W2 + W3 + W4\", \n",
1227 | " print_results=False)\n",
1228 | "tmle.outcome_model(\"A + C + W1 + W2 + W3 + W4 + \"\n",
1229 | " \"A:C + A:W1 + A:W2 + A:W3 + A:W4\", \n",
1230 | " print_results=False)\n",
1231 | "tmle.fit()\n",
1232 | "\n",
1233 | "print(\"ATE \", np.round(tmle.risk_difference, 6))\n",
1234 | "print(\"95% CL\", np.round(tmle.risk_difference_ci, 6))"
1235 | ]
1236 | },
1237 | {
1238 | "cell_type": "markdown",
1239 | "metadata": {},
1240 | "source": [
1241 | "## 7. Simulation"
1242 | ]
1243 | },
1244 | {
1245 | "cell_type": "code",
1246 | "execution_count": 31,
1247 | "metadata": {},
1248 | "outputs": [],
1249 | "source": [
1250 | "def data_generator(n, true_ate=False):\n",
1251 | " \"\"\"Function to generate data consisting of `n` observations\"\"\"\n",
1252 | " d = pd.DataFrame()\n",
1253 | " # Confounders\n",
1254 | " d['W1'] = np.round(np.random.uniform(low=1, high=5, size=n))\n",
1255 | " d['W2'] = np.random.binomial(n=1, p=0.45, size=n)\n",
1256 | " d['W3'] = np.round(np.random.uniform(low=0, high=1, size=n)\n",
1257 | " + 0.8*d['W1'] + 0.75*d['W2'])\n",
1258 | " d['W3'] = np.where(d['W3'] > 4, 1, d['W3'])\n",
1259 | " d['W4'] = np.round(np.random.uniform(low=0, high=1, size=n)\n",
1260 | " + 0.2*d['W1'] + 0.75*d['W2'])\n",
1261 | " # Treatment\n",
1262 | " pr_a = logistic.cdf(-1 - 0.15*d['W4'] + 1.5*d['W2'] + \n",
1263 | " 0.75*d['W3'] + 0.25*d['W1'] + \n",
1264 | " 0.8*d['W2']*d['W4'])\n",
1265 | " d['A'] = np.random.binomial(n=1, \n",
1266 | " p=pr_a, \n",
1267 | " size=n)\n",
1268 | " # Potential outcomes\n",
1269 | " pr_y1 = logistic.cdf(-3 + 1 + 0.25*d['W4'] + 0.75*d['W3'] + \n",
1270 | " 0.8*d['W2']*d['W4'] + 0.05*d['W1'])\n",
1271 | " y1 = np.random.binomial(n=1, p=pr_y1, size=n)\n",
1272 | " pr_y0 = logistic.cdf(-3 + 0 + 0.25*d['W4'] + 0.75*d['W3'] + \n",
1273 | " 0.8*d['W2']*d['W4'] + 0.05*d['W1'])\n",
1274 | " y0 = np.random.binomial(n=1, p=pr_y0, size=n)\n",
1275 | " # Causal consistency\n",
1276 | " d['Y'] = np.where(d['A'] == 1, y1, y0)\n",
1277 | " # Return generated data\n",
1278 | " if true_ate:\n",
1279 | " return np.mean(y1 - y0), np.mean(y1) / np.mean(y0)\n",
1280 | " else:\n",
1281 | " return d\n",
1282 | "\n",
1283 | "\n",
1284 | "# Generating true value from super-population\n",
1285 | "true_ate, true_rr = data_generator(n=1000000, true_ate=True)\n",
1286 | "\n",
1287 | "# Setting up SuperLearner libraries\n",
1288 | "from zepid.superlearner import SuperLearner, StepwiseSL\n",
1289 | "from sklearn.linear_model import LogisticRegression\n",
1290 | "from pygam import LogisticGAM, f, s\n",
1291 | "\n",
1292 | "import warnings\n",
1293 | "warnings.simplefilter('ignore', RuntimeWarning) # Hides some NumPy errors for sparse models\n",
1294 | "\n",
1295 | "family = sm.families.family.Binomial()\n",
1296 | "\n",
1297 | "sl_library_main = [LogisticRegression(penalty='none', solver='lbfgs'),\n",
1298 | " StepwiseSL(family, selection=\"backward\"), \n",
1299 | " StepwiseSL(family, selection=\"forward\", order_interaction=1)]\n",
1300 | "sl_main_labs = [\"LogR\", \"Step.zero\", \"Step.one\"]\n",
1301 | "sl_main = SuperLearner(sl_library_main, sl_main_labs, folds=5,\n",
1302 | " loss_function='nloglik')\n",
1303 | "\n",
1304 | "sl_library_alt = [LogisticRegression(penalty='none', solver='lbfgs'),\n",
1305 | " StepwiseSL(family, selection=\"backward\"), \n",
1306 | " StepwiseSL(family, selection=\"forward\", order_interaction=1),\n",
1307 | " LogisticGAM(f(0) + s(1) + f(2) + s(3) + s(4), \n",
1308 | " lam=0.6)]\n",
1309 | "sl_alt_labs = [\"LogR\", \"Step.zero\", \"Step.one\", \"GAM\"]\n",
1310 | "sl_alt = SuperLearner(sl_library_alt, sl_alt_labs, folds=5,\n",
1311 | " loss_function='nloglik')\n",
1312 | "\n",
1313 | "# Simulation\n",
1314 | "naive_rd, gform_rd, iptw_rd, aipw_rd, tmle_rd = [], [], [], [], []\n",
1315 | "aipw_slm_rd, aipw_sla_rd, tmle_slm_rd, tmle_sla_rd = [], [], [], []\n",
1316 | "\n",
1317 | "for i in range(1000): \n",
1318 | " data = data_generator(n=1000)\n",
1319 | "\n",
1320 | " # Naive\n",
1321 | " fm = smf.ols(\"Y ~ A + W1 + W2 + W3 + W4\", data).fit()\n",
1322 | " naive_rd.append(fm.params['A'])\n",
1323 | " \n",
1324 | " # G-formula\n",
1325 | " g_formula = zepid.causal.gformula.TimeFixedGFormula(data, \n",
1326 | " exposure=\"A\", \n",
1327 | " outcome=\"Y\")\n",
1328 | " g_formula.outcome_model(\"A + W1 + W2 + W3 + W4\",\n",
1329 | " print_results=False)\n",
1330 | " g_formula.fit(\"all\") # all sets A=1\n",
1331 | " y_a1 = g_formula.marginal_outcome\n",
1332 | " g_formula.fit(\"none\") # none sets A=0\n",
1333 | " y_a0 = g_formula.marginal_outcome\n",
1334 | " gform_rd.append(y_a1 - y_a0)\n",
1335 | " \n",
1336 | " # IPTW\n",
1337 | " ipw = zepid.causal.ipw.IPTW(data, treatment=\"A\", outcome=\"Y\")\n",
1338 | " ipw.treatment_model(\"W1 + W2 + W3 + W4\", bound=0.01,\n",
1339 | " stabilized=True, print_results=False)\n",
1340 | " ipw.marginal_structural_model(\"A\")\n",
1341 | " ipw.fit()\n",
1342 | " iptw_rd.append(ipw.risk_difference)\n",
1343 | "\n",
1344 | " # AIPW\n",
1345 | " aipw = zepid.causal.doublyrobust.AIPTW(data, \n",
1346 | " exposure=\"A\", \n",
1347 | " outcome=\"Y\")\n",
1348 | " aipw.exposure_model(\"W1 + W2 + W3 + W4\", bound=0.01,\n",
1349 | " print_results=False)\n",
1350 | " aipw.outcome_model(\"A + W1 + W2 + W3 + W4\", \n",
1351 | " print_results=False)\n",
1352 | " aipw.fit()\n",
1353 | " aipw_rd.append(aipw.risk_difference)\n",
1354 | " \n",
1355 | " # AIPW -- Super Learner main\n",
1356 | " aipw = zepid.causal.doublyrobust.AIPTW(data, \n",
1357 | " exposure=\"A\", \n",
1358 | " outcome=\"Y\")\n",
1359 | " aipw.exposure_model(\"W1 + W2 + W3 + W4\", bound=0.01,\n",
1360 | " custom_model=sl_main,\n",
1361 | " print_results=False)\n",
1362 | " aipw.outcome_model(\"A + W1 + W2 + W3 + W4\", \n",
1363 | " custom_model=sl_main,\n",
1364 | " print_results=False)\n",
1365 | " aipw.fit()\n",
1366 | " aipw_slm_rd.append(aipw.risk_difference)\n",
1367 | " \n",
1368 | " # AIPW -- Super Learner alternative\n",
1369 | " aipw = zepid.causal.doublyrobust.AIPTW(data, \n",
1370 | " exposure=\"A\", \n",
1371 | " outcome=\"Y\")\n",
1372 | " aipw.exposure_model(\"W1 + W2 + W3 + W4\", bound=0.01,\n",
1373 | " custom_model=sl_alt,\n",
1374 | " print_results=False)\n",
1375 | " aipw.outcome_model(\"A + W1 + W2 + W3 + W4\", \n",
1376 | " custom_model=sl_alt,\n",
1377 | " print_results=False)\n",
1378 | " aipw.fit()\n",
1379 | " aipw_sla_rd.append(aipw.risk_difference)\n",
1380 | " \n",
1381 | " # TMLE\n",
1382 | " tmle = zepid.causal.doublyrobust.TMLE(data, \n",
1383 | " exposure=\"A\", \n",
1384 | " outcome=\"Y\")\n",
1385 | " tmle.exposure_model(\"W1 + W2 + W3 + W4\", bound=0.01,\n",
1386 | " print_results=False)\n",
1387 | " tmle.outcome_model(\"A + W1 + W2 + W3 + W4\", \n",
1388 | " print_results=False)\n",
1389 | " tmle.fit()\n",
1390 | " tmle_rd.append(tmle.risk_difference)\n",
1391 | "\n",
1392 | " # TMLE -- Super Learner main\n",
1393 | " tmle = zepid.causal.doublyrobust.TMLE(data, \n",
1394 | " exposure=\"A\", \n",
1395 | " outcome=\"Y\")\n",
1396 | " tmle.exposure_model(\"W1 + W2 + W3 + W4\", bound=0.01,\n",
1397 | " custom_model=sl_main,\n",
1398 | " print_results=False)\n",
1399 | " tmle.outcome_model(\"A + W1 + W2 + W3 + W4\", \n",
1400 | " custom_model=sl_main,\n",
1401 | " print_results=False)\n",
1402 | " tmle.fit()\n",
1403 | " tmle_slm_rd.append(tmle.risk_difference)\n",
1404 | " \n",
1405 | " # TMLE -- Super Learner alternative\n",
1406 | " tmle = zepid.causal.doublyrobust.TMLE(data, \n",
1407 | " exposure=\"A\", \n",
1408 | " outcome=\"Y\")\n",
1409 | " tmle.exposure_model(\"W1 + W2 + W3 + W4\", bound=0.01,\n",
1410 | " custom_model=sl_alt,\n",
1411 | " print_results=False)\n",
1412 | " tmle.outcome_model(\"A + W1 + W2 + W3 + W4\", \n",
1413 | " custom_model=sl_alt,\n",
1414 | " print_results=False)\n",
1415 | " tmle.fit()\n",
1416 | " tmle_sla_rd.append(tmle.risk_difference)\n",
1417 | " \n"
1418 | ]
1419 | },
1420 | {
1421 | "cell_type": "code",
1422 | "execution_count": 32,
1423 | "metadata": {},
1424 | "outputs": [
1425 | {
1426 | "name": "stdout",
1427 | "output_type": "stream",
1428 | "text": [
1429 | "=============================\n",
1430 | "Naive\n",
1431 | "-----------------------------\n",
1432 | "Abs. Bias: 0.048\n",
1433 | "Rel. Bias: 26.7\n",
1434 | "=============================\n",
1435 | "=============================\n",
1436 | "G-formula\n",
1437 | "-----------------------------\n",
1438 | "Abs. Bias: 0.002\n",
1439 | "Rel. Bias: 0.8\n",
1440 | "=============================\n",
1441 | "=============================\n",
1442 | "IPTW\n",
1443 | "-----------------------------\n",
1444 | "Abs. Bias: 0.106\n",
1445 | "Rel. Bias: 58.5\n",
1446 | "=============================\n",
1447 | "=============================\n",
1448 | "AIPW\n",
1449 | "-----------------------------\n",
1450 | "Abs. Bias: 0.003\n",
1451 | "Rel. Bias: 1.9\n",
1452 | "=============================\n",
1453 | "=============================\n",
1454 | "AIPW-SL1\n",
1455 | "-----------------------------\n",
1456 | "Abs. Bias: 0.003\n",
1457 | "Rel. Bias: 1.5\n",
1458 | "=============================\n",
1459 | "=============================\n",
1460 | "AIPW-SL2\n",
1461 | "-----------------------------\n",
1462 | "Abs. Bias: 0.003\n",
1463 | "Rel. Bias: 1.7\n",
1464 | "=============================\n",
1465 | "=============================\n",
1466 | "TMLE\n",
1467 | "-----------------------------\n",
1468 | "Abs. Bias: 0.009\n",
1469 | "Rel. Bias: 4.8\n",
1470 | "=============================\n",
1471 | "=============================\n",
1472 | "TMLE-SL1\n",
1473 | "-----------------------------\n",
1474 | "Abs. Bias: 0.006\n",
1475 | "Rel. Bias: 3.5\n",
1476 | "=============================\n",
1477 | "=============================\n",
1478 | "TMLE-SL2\n",
1479 | "-----------------------------\n",
1480 | "Abs. Bias: 0.008\n",
1481 | "Rel. Bias: 4.5\n",
1482 | "=============================\n"
1483 | ]
1484 | }
1485 | ],
1486 | "source": [
1487 | "# Results\n",
1488 | "result = [naive_rd, gform_rd, iptw_rd, aipw_rd, aipw_slm_rd, \n",
1489 | " aipw_sla_rd, tmle_rd, tmle_slm_rd, tmle_sla_rd]\n",
1490 | "labels = [\"Naive\", \"G-formula\", \"IPTW\", \"AIPW\", \"AIPW-SL1\", \n",
1491 | " \"AIPW-SL2\", \"TMLE\", \"TMLE-SL1\", \"TMLE-SL2\"]\n",
1492 | "for x, y in zip(result, labels):\n",
1493 | " print(\"=============================\")\n",
1494 | " print(y)\n",
1495 | " print(\"-----------------------------\")\n",
1496 | " print(\"Abs. Bias:\", np.round(np.mean(x - true_ate), 3))\n",
1497 | " rel_bias = np.abs(np.mean((x - true_ate) / true_ate)*100)\n",
1498 | " print(\"Rel. Bias:\", np.round(rel_bias, 1))\n",
1499 | " print(\"=============================\")\n"
1500 | ]
1501 | },
1502 | {
1503 | "cell_type": "markdown",
1504 | "metadata": {},
1505 | "source": [
1506 | "END"
1507 | ]
1508 | }
1509 | ],
1510 | "metadata": {
1511 | "kernelspec": {
1512 | "display_name": "Python 3",
1513 | "language": "python",
1514 | "name": "python3"
1515 | },
1516 | "language_info": {
1517 | "codemirror_mode": {
1518 | "name": "ipython",
1519 | "version": 3
1520 | },
1521 | "file_extension": ".py",
1522 | "mimetype": "text/x-python",
1523 | "name": "python",
1524 | "nbconvert_exporter": "python",
1525 | "pygments_lexer": "ipython3",
1526 | "version": "3.6.5"
1527 | }
1528 | },
1529 | "nbformat": 4,
1530 | "nbformat_minor": 4
1531 | }
1532 |
--------------------------------------------------------------------------------
/RCodeBoxes.R:
--------------------------------------------------------------------------------
1 | ###################################################################################################
2 |
3 | # Tutorial: causal inference methods made easy for applied resarchers/epidemiologists/statisticians
4 |
5 | # ICON-LSHTM, LONDON, 16th October 2020
6 |
7 | # Miguel Angel Luque Fernandez, PhD
8 | # Assistant Professor of Epidemiology and Biostatistics
9 | # Matthew Smith, PhD
10 | # Research Fellow
11 |
12 | # Inequalities in Cancer Outcomes Network, LSHTM, London, UK
13 |
14 | # Copyright (c) 2020 Permission is hereby granted, free of charge, to any person obtaining a copy
15 | # of this software and associated documentation files (the "Software"), to deal in the Software
16 | # without restriction, including without limitation the rights to use, copy, modify, merge,
17 | # publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to
18 | # whom the Software is furnished to do so, subject to the following conditions: The above
19 | # copyright notice and this permission notice shall be included in all copies or substantial
20 | # portions of the Software.
21 |
22 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING
23 | # BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NON
24 | # INFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES
25 | # OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
26 | # IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
27 |
28 | # Bug reports: miguel-angel.luque at lshtm.ac.uk
29 |
30 | # The rhc dataset can be dowloaded at http://biostat.mc.vanderbilt.edu/wiki/Main/DataSets
31 |
32 | ###################################################################################################
33 |
34 |
35 | # Preliminaries
36 | rm(list=ls())
37 |
38 | ### Box 1: Setting the data
39 | setwd("your path")
40 | #setwd("~/Dropbox/ESTIMATORSCIproject/R_Stata_master_files/Data")
41 | library(haven)
42 | #data <- read_dta("~/Dropbox/ESTIMATORSCIproject/R_Stata_master_files/Data/rhc.dta")
43 | data <- read_dta("rhc.dta")
44 | # Define the outcome (Y), exposure (A), confounder (C), and confounders (W)
45 | data$Y <- data$death_d30; data$Y <- as.numeric(data$Y); Y <- data$Y
46 | data$A <- data$rhc; data$A <- as.numeric(data$A); A <- data$A
47 | data$C <- data$gender; data$C <- as.numeric(data$C); C <- data$C
48 | data$w1 <- data$age; data$w1 <- as.numeric(data$w1); w1 <- data$w1
49 | data$w2 <- data$edu; data$w2 <- as.numeric(data$w2); w2 <- data$w2
50 | data$w3 <- data$race; data$w3 <- as.numeric(data$w3); w3 <- data$w3
51 | data$w4 <- data$carcinoma; data$w4 <- as.numeric(data$w4); w4 <- data$w4
52 | data2 <- as.data.frame(Y); data2$A <- A; data2$C <- C; data2$w1 <- w1; data2$w2 <- w2; data2$w3 <- w3; data2$w4 <- w4
53 |
54 |
55 | ### Box 2: Naive estimate of the ATE
56 | naive <- lm(Y ~ A + C, data=data); naive # Naive estimate of the ATE is 0.07352
57 |
58 |
59 | # 3. G-Formula
60 |
61 | ## 3.1 Non-parametric G-formula
62 |
63 | ### Box 3: Non-parametric G-formula for the ATE
64 | mean(data$A[data$C==1], na.rm=TRUE) #
65 | mean(data$A[data$C==0], na.rm=TRUE) #
66 | mean(data$Y[data$A==1], na.rm=TRUE) - mean(data$Y[data$A==0],na.rm=TRUE) # Unadjusted Estimate
67 | reg <- lm(Y ~ A, data=data); reg # Unadjusted Estimate Regression
68 | pr.l <- prop.table(table(data$C)); pr.l # Marginal probability of C
69 | tab.out <- aggregate(Y ~ A + C, data, mean); tab.out # Table of Means in
70 | ATE <- ((mean(data$Y[data$A==1 & data$C==1]) - mean(data$Y[data$A==0 & data$C==1]))*pr.l[2]) +
71 | (mean(data$Y[data$A==1 & data$C==0]) - mean(data$Y[data$A==0 & data$C==0]))*pr.l[1] # G-formula Non-parametric ATE
72 | ATE; rm(ATE) # The ATE from the non-parametric estimator is 0.073692
73 |
74 | ### Box 4: Non-parametric G-formula for the ATT
75 | ATTm <- mean(data$C[data$A==1], na.rm=TRUE) # Proportion of those who are male amongst treated
76 | ATTf <- 1-mean(data$C[data$A==1], na.rm=TRUE) # Proportion of those who are female amongst treated
77 | ATT <-((mean(data$Y[data$A==1 & data$C==1]) - mean(data$Y[data$A==0 & data$C==1]))*ATTm) +
78 | (mean(data$Y[data$A==1 & data$C==0]) - mean(data$Y[data$A==0 & data$C==0]))*ATTf # G-formula Non-parametric ATT
79 | ATT # The ATT from the non-parametric estimator is 0.073248
80 | rm(ATT)
81 |
82 | ### Box 5: Bootstrap the 95% confidence intervals (CI) for the ATE/ATT estimated using the non-parametric G-Formula
83 | # ATE
84 | library(boot)
85 | g.comp = function(data,indices) # Define the function to estimate the ATE
86 | {
87 | dat=data[indices,]
88 | pr.l <- prop.table(table(dat$C))
89 |
90 | ATE = ((mean(dat$Y[dat$A==1 & dat$C==1]) - mean(dat$Y[dat$A==0 & dat$C==1]))*pr.l[2]) +
91 | (mean(dat$Y[dat$A==1 & dat$C==0]) - mean(dat$Y[dat$A==0 & dat$C==0]))*pr.l[1] ; ATE
92 | }
93 | g.comp(data,indices=1:nrow(data)) # Can get original estimate, by plugging in indices 1:n
94 | boot.out=boot(data,g.comp,200) # Draw 200 bootstrap sample estimates
95 | boot.ci(boot.out,type="perc",conf=0.95) # compute confidence intervals using percentile method
96 | boot.ci(boot.out,type="norm",conf=0.95)
97 |
98 | # ATT
99 | g.comp = function(data,indices) # Define the function to estimate the ATT
100 | {
101 | dat=data[indices,]
102 |
103 | ATTm <- mean(dat$C[dat$A==1], na.rm=TRUE) # Proportion of those who are male among treated
104 | ATTf <- 1-mean(dat$C[dat$A==1], na.rm=TRUE)
105 |
106 | ((mean(dat$Y[dat$A==1 & dat$C==1]) - mean(dat$Y[dat$A==0 & dat$C==1]))*ATTm) +
107 | (mean(dat$Y[dat$A==1 & dat$C==0]) - mean(dat$Y[dat$A==0 & dat$C==0]))*ATTf
108 | }
109 | g.comp(data,indices=1:nrow(data)) # Can get original estimate, by plugging in indices 1:n
110 | boot.out=boot(data,g.comp,200) # Draw 200 bootstrap sample estimates
111 | boot.ci(boot.out,type="perc",conf=0.95) # compute confidence intervals using percentile method
112 | boot.ci(boot.out,type="norm",conf=0.95)
113 |
114 |
115 | ### Box 6: Non-parametric G-Formula using a fully saturated regression model in Stata (A)
116 | # Method 1: conditional probabilities
117 | data$A1 <- ifelse(data$A == 1, 1, 0)
118 | data$A0 <- ifelse(data$A == 0, 1, 0)
119 | data$C1 <- ifelse(data$C == 1, 1, 0)
120 | data$C0 <- ifelse(data$C == 0, 1, 0)
121 | reg <- glm(Y ~ -1 + (A1 + A0) + A1:(C1) + A0:(C1), data=data); summary(reg)
122 | ATE <- mean((reg$coefficients[1] + reg$coefficients[3]*C) - (reg$coefficients[2] + reg$coefficients[4]*C)); ATE
123 | rm(ATE)
124 |
125 | ### Box 7: Non-parametric G-Formula using a fully saturated regression model in Stata (B)
126 | # Method 2: Marginal probabilities
127 | install.packages("margins")
128 | library(margins)
129 | reg <- glm(Y ~ -1 + (A1 + A0) + A1:(C1) + A0:(C1), data=data); summary(reg)
130 | Y1 <- margins(reg, variables="A1"); Y1
131 | Y0 <- margins(reg, variables="A0"); Y0
132 | ATE <- Y1$fitted[A==1]-Y0$fitted[A==0]; mean(ATE)
133 | rm(ATE)
134 |
135 | ## 3.2 Parametric G-formula
136 | ### Box 8: Parametric G-formula by hand
137 | mod1 <- glm(Y ~ C, family="binomial", data=data[data$A==1,]) # Expected probability amongst those with RHC
138 | mod0 <- glm(Y ~ C, family="binomial", data=data[data$A==0,]) # Expected probability amongst those without RHC
139 | GcompRA <- cbind(Y1 = predict(mod1, newdata=data.frame(A = 1, C), type="response"),
140 | Y0 = predict(mod0, newdata=data.frame(A = 0, C), type="response"))
141 | GcompRA <- as.data.frame(GcompRA)
142 | Y.1 <- GcompRA$Y1
143 | Y.0 <- GcompRA$Y0
144 | ATE <- mean((Y.1) - (Y.0), na.rm=TRUE); ATE # Difference between expected probabilities (ATE)
145 | rm(ATE)
146 |
147 |
148 | ### Box 9: Parametric regression adjustment (one confounder) using stdReg R-package
149 | install.packages("stdReg")
150 | library(stdReg)
151 | reg <- glm(Y ~ A + C, data = data, family = poisson(link="log")); summary(reg)
152 | reg.std <- stdGlm(fit=reg, data = data, X = "A", x=seq(0,1))
153 | print(summary(reg.std, contrast = "difference", reference=0))
154 | plot(reg.std)
155 |
156 | ### Box 10: Bootstrap for the parametric regression adjustment one confounder)
157 | library(boot) # Install the Bootstrap package
158 | attach(data)
159 | g.comp=function(data,indices) # Define the function to estimate the ATE
160 | {
161 | dat=data[indices,]
162 | glm1 <- glm(Y ~ C, family="binomial", dat=dat[dat$A==1,])
163 | glm2 <- glm(Y ~ C, family="binomial", dat=dat[dat$A==0,])
164 | Y.1 = predict(glm1, newdata=data.frame(A = 1, C), type="response")
165 | Y.0 = predict(glm2, newdata=data.frame(A = 0, C), type="response")
166 | ATE <- mean((Y.1) - mean(Y.0)); ATE
167 | }
168 | g.comp(data,indices=1:nrow(data)) # Can get original estimate, by plugging in indices 1:n
169 | boot.out=boot(data,g.comp,200) # Draw 1000 bootstrap sample estimates of RD
170 | boot.ci(boot.out,type="norm",conf=0.95) # Bootstrapped 95% CI based on normal approximation
171 | boot.ci(boot.out,type="perc",conf=0.95) # Bootstrapped 95% CI based on percentiles of the bootstrap replicates
172 |
173 | # Now with more than one confounder
174 |
175 | ### Box 11: Parametric multivariate regression adjustment implementation of the G-Formula
176 | mod1 <- glm(Y ~ C + w1 + w2 + w3 + w4, family="binomial", data=data[data$A==1,]) # Expected probability amongst those with RHC
177 | mod0 <- glm(Y ~ C + w1 + w2 + w3 + w4, family="binomial", data=data[data$A==0,]) # Expected probability amongst those without RHC
178 | GcompRA <- cbind(Y1 = predict(mod1, newdata=data.frame(A = 1, C, w1, w2, w3, w4), type="response"),
179 | Y0 = predict(mod0, newdata=data.frame(A = 0, C, w1, w2, w3, w4), type="response"))
180 | GcompRA <- as.data.frame(GcompRA)
181 | Y.1 <- GcompRA$Y1
182 | Y.0 <- GcompRA$Y0
183 | ATE <- mean((Y.1) - (Y.0), na.rm=TRUE); ATE # ATE
184 | rm(ATE)
185 |
186 |
187 | ### Box 12: Parametric multivariate regression adjustment using "stdReg" R-package
188 | install.packages("stdReg")
189 | library(stdReg)
190 | reg <- glm(Y ~ A + C + w1 + w2 + w3 + w4, data = data, family = poisson(link="log")); summary(reg)
191 | reg.std <- stdGlm(fit=reg, data=data, X="A", x=seq(0,1))
192 | print(summary(reg.std, contrast="difference", reference=0))
193 | plot(reg.std)
194 |
195 |
196 | ### Box 13: Parametric multivariate regression adjustment using "margins" R-package
197 | reg1 <- glm(Y ~ -1 + (A1 + A0) + A1:(C1 + w1 + w2 + w3 + w4) + A0:(C0 + w1 + w2 + w3 + w4) , data=data); summary(reg1)
198 | poY1m <- margins(reg1, variables="A1"); poY1m
199 | poY0m <- margins(reg1, variables="A0"); poY0m
200 | ATE2 <- poY1m$fitted[A==1] - poY0m$fitted[A==0]; mean(ATE2)
201 |
202 | ### Box 14 Bootstrap for the multivariate parametric regression adjustment
203 | library(boot) # Install the Bootstrap package
204 | attach(data)
205 | g.comp=function(data,indices) # Define the function to estimate the ATE
206 | {
207 | dat=data[indices,]
208 | glm1 <- glm(Y ~ C + w1 + w2 + w3 + w4, family="binomial", dat=dat[dat$A==1,])
209 | glm2 <- glm(Y ~ C + w1 + w2 + w3 + w4, family="binomial", dat=dat[dat$A==0,])
210 | Y.1 = predict(glm1, newdata=data.frame(A = 1, C, w1, w2, w3, w4), type="response")
211 | Y.0 = predict(glm2, newdata=data.frame(A = 0, C, w1, w2, w3, w4), type="response")
212 | mean((Y.1) - mean(Y.0))
213 | }
214 | g.comp(data,indices=1:nrow(data)) # Can get original estimate, by plugging in indices 1:n
215 | boot.out=boot(data,g.comp,200) # Draw 1000 bootstrap sample estimates of RD
216 | boot.ci(boot.out,type="norm",conf=0.95) # Bootstrapped 95% CI based on normal approximation
217 | boot.ci(boot.out,type="perc",conf=0.95) # Bootstrapped 95% CI based on percentiles of the bootstrap replicates
218 |
219 |
220 | ### Box 15 Computing the parametric marginal risk ratio after regression adjustment
221 | reg <- glm(Y ~ A + C + w1 + w2 + w3 + w4, data=data2, family = binomial(link="logit")); summary(reg)
222 | reg.std <- stdGlm(fit=reg, data=data2, X="A", x=seq(0,1))
223 | print(summary(reg.std, contrast="ratio", reference=0)) # 27% (95% CI 1.18-1.37) increase in relative risk
224 | plot(reg.std)
225 |
226 | # 4. Inverse Probability of Treatment Weighting
227 |
228 | ## 4.1 Inverse probability of treatment weighting based on the propensity score plus regression adjustment
229 |
230 | # Box 16 (IPTW by hand)
231 | p.s <- glm(A ~ as.factor(C) + w1 + w2 + w3 + w4, data=data, family=binomial) # Propensity score mmodel for the exposure
232 | p.score <- ifelse(data$A == 0, 1 - predict(p.s, type = "response"), predict(p.s, type = "response")) # Assign Propensity score weights
233 | #table(p.score) # Table of Propensity Scores
234 | data$w <- 1/p.score # Generate IP Weights
235 | data2$w <- 1/p.score
236 | #table(data$w); summary(data$w); sd(data$w)
237 |
238 | ATE <- mean(data$w*as.numeric(data$A==1)*data$Y) - mean(data$w*as.numeric(data$A==0)*data$Y);ATE # Estimate ATE
239 | rm(ATE)
240 |
241 |
242 | # Box 17 Bootstrap computation for the IPTW estimator
243 | library(boot)
244 | iptw.w = function(data,indices) # Define the function to estimate the ATE
245 | {
246 | dat=data[indices,]
247 | mean(dat$w*as.numeric(dat$A==1)*dat$Y) - mean(dat$w*as.numeric(dat$A==0)*dat$Y)
248 | }
249 | iptw.w(data,indices=1:nrow(data)) # Can get original estimate, by plugging in indices 1:n
250 | boot.out=boot(data,iptw.w,100) # Draw 200 bootstrap sample estimates
251 | boot.ci(boot.out,type="perc",conf=0.95) # compute confidence intervals using percentile method
252 | boot.ci(boot.out,type="norm",conf=0.95)
253 |
254 |
255 | ### Box 18: Computation of the IPTW estimator for the ATE using IPW R-package
256 | install.packages("ipw", "survey")
257 | library(ipw)
258 | library(survey)
259 |
260 | # Univariable
261 | ipw.ATE <- ipwpoint(exposure = A, family = "binomial", link = "logit",
262 | numerator = ~ 1,
263 | denominator = ~ C,
264 | data = data2)
265 | summary(ipw.ATE$ipw.weights)
266 | ipwplot(weights = ipw.ATE$ipw.weights, logscale = FALSE, main = "Unstabilized weights", xlim = c(0.5, 2))
267 | summary(ipw.ATE$num.mod)
268 | summary(ipw.ATE$den.mod)
269 | data2$usw <- ipw.ATE$ipw.weights
270 | msm <- (svyglm(Y ~ A, design = svydesign(~ 1, weights = ~ usw, data = data2)))
271 | coef(msm); confint(msm)
272 |
273 | # Multivariable
274 | ipw.ATE <- ipwpoint(exposure = A, family = "binomial", link = "logit",
275 | numerator = ~ 1,
276 | denominator = ~ C + w1 + w2 + w3 + w4,
277 | data = data2)
278 | summary(ipw.ATE$ipw.weights)
279 | ipwplot(weights = ipw.ATE$ipw.weights, logscale = FALSE, main = "Unstabilized weights", xlim = c(0.5, 2))
280 | summary(ipw.ATE$num.mod)
281 | summary(ipw.ATE$den.mod)
282 | data2$usw <- ipw.ATE$ipw.weights
283 | msm <- (svyglm(Y ~ A, design = svydesign(~ 1, weights = ~ usw, data = data2)))
284 | coef(msm); confint(msm)
285 |
286 |
287 | ### Box 19: Assessing IPTW balance
288 | install.packages("twang")
289 | library(twang)
290 | ps.balance <- ps(A ~ C + w1 + w2 + w3 + w4, data = data2,
291 | n.trees=1000, interaction.depth=2, shrinkage=0.01, perm.test.iters=0,
292 | stop.method=c("es.mean","ks.max"), estimand = "ATE", verbose=FALSE)
293 | plot(ps.balance)
294 | summary(ps.balance$gbm.obj, n.trees=ps.balance$desc$ks.max.ATE$n.trees, plot=FALSE)
295 | data2.balance <- bal.table(ps.balance); data2.balance
296 |
297 |
298 | ### Box 20: Assessing IPTW overlap by hand
299 | install.packages("xtable")
300 | library(xtable)
301 | pretty.tab <- data2.balance$ks.max.ATE[,c("tx.mn","ct.mn","ks")]
302 | pretty.tab <- cbind(pretty.tab, data2.balance$unw[,"ct.mn"])
303 | names(pretty.tab) <- c("E(Y1|t=1)","E(Y0|t=1)","KS","E(Y0|t=0)")
304 | xtable(pretty.tab, caption = "Balance of the treatment and comparison groups",
305 | label = "tab:balance", digits = c(0, 2, 2, 2, 2), align=c("l","r","r","r","r"))
306 | plot(ps.balance, plots = 6)
307 |
308 |
309 | ### Box 21: Assessing overlap using plots
310 | # Fit a propensity score model
311 | m_PS<-glm(A ~ C + w1 + w2 + w3 + w4, data = data2, family=binomial(link="logit"))
312 | summary(m_PS)
313 |
314 | # Estimate the propensity score
315 | data$PS<-fitted.values(m_PS)
316 |
317 | # Histogram of the PS
318 | hist(data$PS[data$rhc==0])
319 | hist(data$PS[data$rhc==1])
320 | plot(density(data$PS[data$rhc==0]),col="red",lwd=2, xlab="PS")
321 | lines(density(data$PS[data$rhc==1]),col="blue",lwd=2)
322 | legend("topright", legend=c("No RHC", "RHC"), pch="--", col=c("red","blue"), bty="n", lwd=2)
323 |
324 | # Look at minimum and maximum PS in each exposure group
325 | min(data$PS[data$rhc==0])
326 | min(data$PS[data$rhc==1])
327 | max(data$PS[data$rhc==0])
328 | max(data$PS[data$rhc==1])
329 |
330 | # Investigate overlap (i.e. positivity)
331 | data$overlap <- ifelse(data$PS>=min(data$PS[data$rhc==1]) & data$PS<=max(data$PS[data$rhc==0]),1,0); table(data$overlap,data$rhc)
332 |
333 | ## 4.2 Marginal structural model with stabilised weights
334 | ### Box 22: Computation of the IPTW estimator for the ATE using a MSM
335 |
336 | # Unstabilized weights
337 | msm <- lm(Y ~ A + C + w1 + w2 + w3 + w4, data = data, weights = data$w) # MSM
338 | library(sandwich)
339 | SE <-sqrt(diag(vcovHC(msm, type="HC0"))) # robust standard errors
340 | beta <- coef(msm)
341 | lcl <- beta-1.96*SE
342 | ucl <- beta+1.96*SE
343 | cbind(beta, lcl, ucl)[2,]
344 |
345 | # Stabilized weights
346 | denom.fit <- glm(A ~ as.factor(C) + w1 + w2 + w3 + w4,
347 | family = binomial(), data = data)
348 | denom.p <- predict(denom.fit, type = "response") # Stablized Weights
349 |
350 | numer.fit <- glm(A ~ 1, family = binomial(), data = data)
351 | summary(numer.fit)
352 | numer.p <- predict(numer.fit, type = "response") # estimation of numerator of ip weights
353 |
354 | data$sw <- ifelse(data$A == 0, ((1-numer.p)/(1-denom.p)), (numer.p/denom.p))
355 |
356 | msm <- lm(Y ~ A, data = data, weights = sw)
357 |
358 | SE <-sqrt(diag(vcovHC(msm, type="HC0"))) # robust standard errors
359 | beta <- coef(msm)
360 | lcl <- beta-1.96*SE
361 | ucl <- beta+1.96*SE
362 | cbind(beta, lcl, ucl)[2,]
363 |
364 | ## 4.3 IPTW with regression adjustment
365 | ### Box 23: Computation of the IPTW-RA estimator for the ATE and bootstrap for statistical inference
366 | glm1 <- glm(Y ~ C + w1 + w2 + w3 + w4, weights = data$w[data$A==1], data=data[data$A==1,])
367 | Y.1 = predict(glm1, newdata=data.frame(A = 1, C, w1, w2, w3, w4), type="response")
368 | glm2 <- glm(Y ~ C + w1 + w2 + w3 + w4, weights = data$w[data$A==0], data=data[data$A==0,])
369 | Y.0 = predict(glm2, newdata=data.frame(A = 0, C, w1, w2, w3, w4), type="response")
370 | ATE <- mean(Y.1 - Y.0); ATE
371 | ATE2 <- mean(data$w*as.numeric(data$A==1)*Y.1)/mean( data$w*as.numeric(data$A==1)) - mean(data$w*as.numeric(data$A==0)*Y.0)/mean(data$w*as.numeric(data$A==0));ATE2
372 | rm(ATE, ATE2)
373 |
374 | ### Box 24: Computation of the IPTW-RA estimator for the ATE using the ipw R-package
375 | library(ipw)
376 | ipw.ATE <- ipwpoint(exposure = A, family = "binomial", link = "logit",
377 | numerator = ~ C,
378 | denominator = ~ C + w1 + w2 + w3 + w4,
379 | data = data2)
380 | summary(ipw.ATE$ipw.weights)
381 | ipwplot(weights = ipw.ATE$ipw.weights, logscale = FALSE, main = "Stabilized weights", xlim = c(0.5, 2))
382 | summary(ipw.ATE$num.mod)
383 | summary(ipw.ATE$den.mod)
384 |
385 | data2$sw <- ipw.ATE$ipw.weights
386 | msm <- (svyglm(Y ~ A, design = svydesign(~ 1, weights = ~ sw, data = data2)))
387 | coef(msm); confint(msm)
388 |
389 |
390 | # 5. Augmented inverse probability weighting
391 |
392 | ### Box 25: Computation of the AIPTW estimator for the ATE and bootstrap for statistical inference
393 | mod <- glm(Y ~ A + C + w1 + w2 + w3 + w4, family="binomial", data=data)
394 | PO <- cbind(Yhat = predict(mod),
395 | Y1 = predict(mod, newdata=data.frame(A = 1, C, w1, w2, w3, w4), type="response"),
396 | Y0 = predict(mod, newdata=data.frame(A = 0, C, w1, w2, w3, w4), type="response"))
397 | RA <- as.data.frame(PO) # Potential Outcomes
398 | Yhat <- RA$Yhat
399 | Y.1a <- RA$Y1
400 | Y.0a <- RA$Y0
401 |
402 | g <- glm(A ~ C + w1 + w2 + w3 + w4, family = binomial(), data = data)
403 | gw <- predict(g, type = "response")
404 | gws <- ifelse(data$A == 0, (-(1 - data$A)/(1 - gw)),(data$A/gw)); sum(gws) # estimation of weights
405 | AIPTW <- mean(gws*(data$Y - plogis(RA$Yhat)) + ((Y.1a) - (Y.0a))); AIPTW # ATE
406 | RR <- mean(Y.1a/Y.0a); RR # RR
407 |
408 | IC <- (gws*(data$Y - plogis(RA$Yhat)) + ((Y.1a) - (Y.0a)))-AIPTW # Estimate the influence function (functional Delta method)
409 | n <- nrow(data)
410 | varHat.IC <- var(IC)/n; varHat.IC
411 | lci <- AIPTW-1.96*sqrt(varHat.IC)
412 | uci <- AIPTW+1.96*sqrt(varHat.IC)
413 | cat(AIPTW,lci,uci) # Inference Influence function
414 |
415 | AIPTW.b = function(data,indices) # Inference using Bootstrap
416 | {
417 | dat=data[indices,]
418 | mod <- glm(Y ~ A + C + w1 + w2 + w3 + w4, family="binomial", data=data)
419 | Yhat = predict(mod)
420 | Y1 = predict(mod, newdata=data.frame(A = 1, C, w1, w2, w3, w4))
421 | Y0 = predict(mod, newdata=data.frame(A = 0, C, w1, w2, w3, w4))
422 | g <- glm(A ~ C + w1 + w2 + w3 + w4, family="binomial", data = data)
423 | gw <- predict(g,type="response")
424 | gws <- ifelse(A == 0, (-(1 - A)/(1 - gw)),(A/gw))
425 | mean(gws*(Y - plogis(Yhat)) + (plogis(Y1) - plogis(Y0)))
426 | }
427 | AIPTW.b(data,indices=1:nrow(data)) # Can get original estimate, by plugging in indices 1:n
428 | boot.out=boot(data,AIPTW.b,200) # Draw 200 bootstrap sample estimates
429 | boot.ci(boot.out,type="perc",conf=0.95) # compute confidence intervals using percentile method
430 | boot.ci(boot.out,type="norm",conf=0.95)
431 |
432 | ### Box 26: Computation of the AIPTW estimator for the ATE and marginal risk ratio
433 | w <- subset(data, select=c(C, w1, w2, w3 , w4))
434 | fit1 <- drtmle(W = w, A = A, Y = Y, # input data
435 | a_0 = c(0, 1), # return estimates for A = 0 and A = 1
436 | SL_Q = "SL.npreg", # use kernel regression for E(Y | A = a, W)
437 | glm_g = "C + w1 + w2 + w3 + w4", # use misspecified main terms glm for E(A | W)
438 | SL_Qr = "SL.npreg", # use kernel regression to guard against
439 | # misspecification of outcome regression
440 | #SL_gr = "SL.npreg", # use kernel regression to guard against
441 | # misspecification of propensity score
442 | returnModels = TRUE # for visualizing fits later
443 | )
444 | ATE <- ci(fit1, contrast = c(-1,1)); ATE
445 | RR <- riskRatio <- list(f = function(eff){ log(eff) },
446 | f_inv = function(eff){ exp(eff) },
447 | h = function(est){ est[2]/est[1] },
448 | fh_grad = function(est){ c(1/est[1],-1/est[2]) })
449 | ci(fit1, contrast = riskRatio)
450 | rm(ATE, RR)
451 |
452 | # 6. DATA-ADAPTIVE ESTIMATION: ENSEMBLE LEARNING TARGETED MAXIMUMLIKELIHOOD ESTIMATION
453 |
454 | ### Box 27: Computational implementation of TMLE by hand
455 | # Step 1
456 | Gcomp <- glm(Y ~ A + C + w1 + w2 + w3 + w4, family="binomial", data=data2)
457 | # Prediction for A, A=1 and, A=0
458 | QAW <- predict(Gcomp)
459 | Q1W = predict(Gcomp, newdata=data.frame(A = 1, data2[,c("C", "w1","w2","w3","w4")]))
460 | Q0W = predict(Gcomp, newdata=data.frame(A = 0, data2[,c("C", "w1","w2","w3","w4")]))
461 | # Step 2 estimation of the propensity score (ps)
462 | psm <- glm(A ~ C + w1 + w2 + w3 + w4, family = binomial, data=data2)
463 | gW = predict(psm, type = "response")
464 | g1W = (1 / gW)
465 | g0W = (-1 / (1-gW))
466 | # Step 3 computation of H and estimation of epsilon
467 | HAW <- (data2$A / gW -(1-data2$A) / (1 - gW))
468 | H1W = (1/gW)
469 | H0W = (-1 / (1 - gW))
470 | epsilon <- coef(glm(data2$Y ~ -1 + HAW + offset(QAW), family = "binomial"))
471 | # Step 4 ATE
472 | ATE<- mean(plogis(Q1W + epsilon * H1W) - plogis(Q0W + epsilon * H0W)); ATE
473 | # Step 5 Maringinal RR
474 | T1.EY1 <- mean(plogis(Q1W + epsilon * H1W))
475 | T1.EY0 <- mean(plogis(Q0W + epsilon * H0W))
476 | RR <- (T1.EY1/T1.EY0); RR
477 | rm(ATE, RR)
478 |
479 | ### Box 28: TMLE with data-adaptive estimation using the R package
480 | set.seed(777)
481 | library(tmle)
482 | w <- subset(data, select=c(C, w1, w2, w3 , w4))
483 | fittmle <- tmle(data$Y, data$A, W=w, family="binomial",
484 | Q.SL.library = c("SL.glm","SL.glm.interaction","SL.step.interaction","SL.gam","SL.randomForest"),
485 | g.SL.library = c("SL.glm","SL.glm.interaction","SL.step.interaction","SL.gam","SL.randomForest"))
486 | fittmle
487 |
488 | # 7. Simulation
489 | ### Box 29: Data generation for the Monte Carlo experiment
490 |
491 | rm(list=ls())
492 |
493 | # Super Learner libraries
494 | SL.library <- c("SL.glm","SL.step","SL.step.interaction","SL.glm.interaction","SL.gam") #"SL.randomForest","SL.glmnet"
495 |
496 | # Data generation A: dual misspecification for the model of the outcome and treatment
497 | set.seed(7777)
498 | generateData <- function(n){
499 | w1 <- round(runif(n, min=1, max=5), digits=0)
500 | w2 <- rbinom(n, size=1, prob=0.45)
501 | w3 <- round(runif(n, min=0, max=1), digits=0 + 0.75*w2 + 0.8*w1)
502 | w4 <- round(runif(n, min=0, max=1), digits=0 + 0.75*w2 + 0.2*w1)
503 | A <- rbinom(n, size=1, prob= plogis(-1 - 0.15*w4 + 1.5*w2 + 0.75*w3 + 0.25*w1 + 0.8*w2*w4))
504 | # Counterfactuals
505 | Y.1 <- rbinom(n, size=1, prob = plogis(-3 + 1 + 0.25*w4 + 0.75*w3 + 0.8*w2*w4 + 0.05*w1))
506 | Y.0 <- rbinom(n, size=1, prob = plogis(-3 + 0 + 0.25*w4 + 0.75*w3 + 0.8*w2*w4 + 0.05*w1))
507 | # Observed outcome
508 | Y <- Y.1*A + Y.0*(1 - A)
509 | # return data.frame
510 | data.frame(w1, w2, w3, w4, A, Y, Y.1, Y.0)
511 | }
512 |
513 | # True ATE
514 | ObsDataTrueATE <- generateData(n=5000000)
515 | True_ATE <- mean(ObsDataTrueATE$Y.1 - ObsDataTrueATE$Y.0);True_ATE
516 | True_EY.1 <- mean(ObsDataTrueATE$Y.1)
517 | True_EY.0 <- mean(ObsDataTrueATE$Y.0)
518 | True_RR <- (True_EY.1 / True_EY.0);True_RR
519 |
520 | #Simulations
521 | library(tmle)
522 | library(SuperLearner)
523 | #install.packages("dbarts")
524 | R <- 1000
525 | #Empty vectors
526 | naive_RR <- rep(NA,R)
527 | ATEtmle1 <- rep(NA,R)
528 | RRtmle1 <- rep(NA,R)
529 | ATE_AIPTW <- rep(NA,R)
530 | RR_AIPTW <- rep(NA,R)
531 | ATEtmle2 <- rep(NA,R)
532 | RRtmle2 <- rep(NA,R)
533 | ATEtmle3 <- rep(NA,R)
534 | RRtmle3 <- rep(NA,R)
535 | for(r in 1:R){
536 | print(paste("This is simulation run number",r))
537 | CancerData <- generateData(n=1000)
538 | # ATE naive approach
539 | naive_RR[r] <- exp(glm(data = CancerData, Y ~ A + w1 + w2 + w3 + w4, family = poisson(link="log"))$coef[2])
540 | # TMLE implementation by hand
541 | # Step 1
542 | gm <- glm(Y ~ A + w1 + w2 + w3 + w4, family="binomial", data=CancerData)
543 | # Prediction for A, A=1 and, A=0
544 | QAW <- predict(gm)
545 | Q1W = predict(gm, newdata=data.frame(A = 1, CancerData[,c("w1","w2","w3","w4")]))
546 | Q0W = predict(gm, newdata=data.frame(A = 0, CancerData[,c("w1","w2","w3","w4")]))
547 | # Step 2 estimation of the propensity score (ps)
548 | psm <- glm(A ~ w1 + w2 + w3 + w4, family = binomial, data=CancerData)
549 | gW = predict(psm, type = "response")
550 | g1W = (1 / gW)
551 | g0W = (-1 / (1-gW))
552 | # Step 3 computation of H and estimation of epsilon
553 | HAW <- (CancerData$A / gW -(1-CancerData$A) / (1 - gW))
554 | H1W = (1/gW)
555 | H0W = (-1 / (1 - gW))
556 | epsilon <- coef(glm(CancerData$Y ~ -1 + HAW + offset(QAW), family = "binomial"))
557 | # Step 4 updated ATE
558 | ATEtmle1[r] <- mean(plogis(Q1W + epsilon * H1W) - plogis(Q0W + epsilon * H0W))
559 | # Step 5 updated MOR
560 | T1.EY1 <- mean(plogis(Q1W + epsilon * H1W))
561 | T1.EY0 <- mean(plogis(Q0W + epsilon * H0W))
562 | RRtmle1[r] <- (T1.EY1 / T1.EY0)
563 |
564 | # Augmented inverse probability treatment weight (AIPTW) estimator
565 | ATE_AIPTW[r] <- mean((HAW*(CancerData$Y - plogis(QAW)) + (plogis(Q1W)-plogis(Q0W))))
566 | AIPTW1 <- mean(CancerData$A * (CancerData$Y - plogis(Q1W)) / gW + plogis(Q1W) )
567 | AIPTW0 <- mean((1- CancerData$A) * (CancerData$Y - plogis(Q0W)) / (1-gW) + plogis(Q0W))
568 | RR_AIPTW[r] <- mean( AIPTW1 / AIPTW0)
569 |
570 | # R-package tmle (base implementation includes SL.step, SL.glm and SL.glm.interaction)
571 | ATE2 <- tmle(Y=CancerData$Y, A=CancerData$A, W=CancerData[,c("w1","w2","w3","w4")], family="binomial")
572 | ATEtmle2[r] <- ATE2$estimates$ATE$psi
573 | RRtmle2[r] <- ATE2$estimates$RR$psi
574 |
575 | # Improved Super learner
576 | ATE3 <- tmle(Y = CancerData$Y, A=CancerData$A, W=CancerData[,c("w1","w2","w3","w4")], family="binomial", Q.SL.library=SL.library, g.SL.library=SL.library)
577 | ATEtmle3[r] <- ATE3$estimates$ATE$psi
578 | RRtmle3[r] <- ATE3$estimates$RR$psi
579 | }
580 | # Mean naive
581 | mean(naive_RR)
582 | # Mean AIPTW
583 | mean(ATE_AIPTW)
584 | mean(RR_AIPTW)
585 | # Estimate of TMLE by hand
586 | mean(ATEtmle1)
587 | mean(RRtmle1)
588 | # Estimate of TMLE + SL default implementation
589 | mean(ATEtmle2)
590 | mean(RRtmle2)
591 | # Estimate of TMLE + SL2 default plus more algorithms
592 | mean(ATEtmle3)
593 | mean(RRtmle3)
594 | save.image("your path\results.RData")
595 |
596 | # Relative Bias ATE
597 | abs(mean((True_ATE - ATE_AIPTW) / True_ATE)*100)
598 | abs(mean((True_ATE - ATEtmle1) / True_ATE)*100)
599 | abs(mean((True_ATE - ATEtmle2) / True_ATE)*100)
600 | abs(mean((True_ATE - ATEtmle3) / True_ATE)*100)
601 |
602 | # Relative Bias RR
603 | abs(mean((True_RR - naive_RR) / True_RR)*100)
604 | abs(mean((True_RR - RR_AIPTW) / True_RR)*100)
605 | abs(mean((True_RR - RRtmle1) / True_RR)*100)
606 | abs(mean((True_RR - RRtmle2) / True_RR)*100)
607 | abs(mean((True_RR - RRtmle3) / True_RR)*100)
608 |
609 |
610 |
611 |
612 |
613 |
614 |
615 |
616 |
617 |
618 |
619 |
620 |
621 |
622 |
623 |
624 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Tutorial: Introduction to computational causal inference for applied researchers and epidemiologists
2 |
3 | ### Matthew James Smith, Camille Maringe, Bernard Rachet, Mohammad A. Mansournia, Paul Zivich, Stephen R. Cole, Miguel Angel Luque Fernandez
4 |
5 | ### This repository makes available to the scientific community the data and code used in the preprint manuscript available at
6 |
7 | [Link to the preprint article](https://arxiv.org/abs/2012.09920)
8 |
9 | ### CITE this repository:
10 |
11 | [](https://zenodo.org/badge/latestdoi/272439035)
12 |
13 | ### Matthew James Smith, Camille Maringe, Bernard Rachet, Mohammad A. Mansournia, Paul Zivich, Stephen R. Cole, Miguel Angel Luque Fernandez
14 |
15 | ### ABSTRACT
16 | The purpose of many health studies is to estimate the effect of an exposure on an outcome. It is not always ethical to assign an exposure to individuals in randomised controlled trials, instead observational data and appropriate study design must be used. There are major challenges with observational studies, one of which is confounding that can lead to biased estimates of the causal effects. Controlling for confounding is commonly performed by simple adjustment for measured confounders; although, often this is not enough. Recent advances in the field of causal inference have dealt with confounding by building on classical standardisation methods. However, these recent advances have progressed quickly with a relative paucity of computational-oriented applied tutorials contributing to some confusion in the use of these methods among applied researchers. In this tutorial, we show the computational implementation of different causal inference estimators from a historical perspective where different estimators were developed to overcome the limitations of the previous one. Furthermore, we also briefly introduce the potential outcomes framework, illustrate the use of different methods using an illustration from the health care setting, and most importantly, we provide reproducible and commented code in Stata, R and Python for researchers to apply in their own observational study. The code can be accessed at
17 |
18 | [https://github.com/migariane/TutorialCausalInferenceEstimators](https://github.com/migariane/TutorialCausalInferenceEstimators)
19 |
20 | KEYWORDS: Causal Inference; Regression adjustment; G-methods; G-formula; Propensity score; Inverse probability weighting; Double-robust methods; Machine learning; Targeted maximum likelihood estimation; Epidemiology; Statistics; Tutorial
21 |
--------------------------------------------------------------------------------
/Results.RData:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/migariane/TutorialCausalInferenceEstimators/d809b657b382e227837d3032b1517612c478818d/Results.RData
--------------------------------------------------------------------------------
/StataCodeBoxes.do:
--------------------------------------------------------------------------------
1 | /*
2 | Tutorial: causal inference methods made easy for applied resarchers/epidemiologists/statisticians
3 | =================================================================================================
4 |
5 | ICON-LSHTM, LONDON, 16th October 2020
6 |
7 | Miguel Angel Luque Fernandez, PhD
8 | Assistant Professor of Epidemiology and Biostatistics
9 | Camille Maringe, PhD
10 | Assistant Professor
11 |
12 | Inequalities in Cancer Outcomes Network, LSHTM, London, UK
13 |
14 | Copyright (c) 2020 Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
15 |
16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NON INFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
17 |
18 | Bug reports: miguel-angel.luque@lshtm.ac.uk
19 |
20 | The rhc dataset can be dowloaded at http://biostat.mc.vanderbilt.edu/wiki/Main/DataSets
21 | */
22 |
23 |
24 |
25 |
26 | *** Preliminaries
27 | clear
28 | set more off
29 | cd "C:\Data" // this path should point to where the RHC data are
30 | use "rhc.dta", clear
31 | describe
32 | count
33 | * 83 variables and 5,735 observations
34 |
35 | /* Box 1: Setting the data */
36 | * Define the outcome (Y), exposure (A), confounder (C), and confounders (W)
37 | global Y death_d30
38 | global A rhc
39 | global C gender
40 | global W gender age edu race carcinoma
41 |
42 | /* Box 2: Naive estimate of the ATE */
43 | * Naive approach to estimate the causal effect
44 | regr $Y $A $C
45 | * The naive estimate of the causal effect is 0.07352
46 |
47 | /* 3. G-formula */
48 | /* 3.1 Non-parametric G-formula */
49 |
50 | * 1) ATE
51 | /* Box 3: Non-parametric G-Formula for the ATE */
52 | proportion $C
53 | matrix m=e(b)
54 | gen genderf = m[1,1]
55 | sum genderf
56 | gen genderm = m[1,2]
57 | sum genderm
58 | * you may need to install the command sumup, type:
59 | * ssc install sumup
60 | sumup $Y, by($A $C)
61 | * from sumup command extract the conditinal means by the given A and C levels i.e. zero and one
62 | * see matrix list y00: position subscript [3,1] is th one of interest
63 | matrix y00 = r(Stat1)
64 | matrix y01 = r(Stat2)
65 | matrix y10 = r(Stat3)
66 | matrix y11 = r(Stat4)
67 | gen EY1 = ((y11[3,1]-y01[3,1]))*genderm
68 | gen EY0 = ((y10[3,1]-y00[3,1]))*genderf
69 | qui: mean EY1 EY0
70 | matrix ATE = r(table)
71 | display "The ATE is: " ATE[1,1] + ATE[1,2]
72 | drop EY*
73 | * The ATE from non-parametric estimator is: 0.073692
74 | // Also one can try
75 | gen ATE = ((y11[3,1]-y01[3,1]))*genderm + ((y10[3,1]-y00[3,1]))*genderf
76 | qui sum ATE
77 | drop ATE
78 |
79 | * Check that Stata "teffects" command obtains the same estimate
80 | teffects ra ($Y $C) ($A)
81 | * The ATE from "teffects" implementation is: 0.073692
82 |
83 | * 2) ATT
84 | /* Box 4: Non-parametric G-Formula for the ATT */
85 | * Estimate the marginal probabilities
86 | proportion $C if $A==1
87 | matrix m=e(b)
88 | gen genderfatet = m[1,1]
89 | gen gendermatet = m[1,2]
90 | gen EY1 = ((y11[3,1]-y01[3,1]))*gendermatet
91 | gen EY0 = ((y10[3,1]-y00[3,1]))*genderfatet
92 | qui: mean EY1 EY0
93 | matrix ATT = r(table)
94 | display "The ATT is: " ATT[1,1] + ATT[1,2] // Applying the G-formula
95 | drop EY*
96 | * The ATT from non-parametric estimator is: 0.073248
97 | // Also one can try
98 | gen ATT = ((y11[3,1]-y01[3,1]))*gendermatet + ((y10[3,1]-y00[3,1]))*genderfatet
99 | qui sum ATT
100 | drop ATT
101 |
102 | * Check using Stata "teffects" command
103 | teffects ra ($Y $C) ($A), atet
104 | * The ATT from "teffects" implementation is: 0.073248
105 |
106 | /* Box 5: Bootstrap 95% Confidence Intervals (CI) for the ATE/ATT estimated using the Non-parametric G-Formula */
107 |
108 | * 1) For the ATE
109 | capture program drop ATE
110 | program define ATE, rclass
111 | capture drop y1
112 | capture drop y0
113 | capture drop ATE
114 | sumup $Y, by($A $C)
115 | matrix y00 = r(Stat1)
116 | matrix y01 = r(Stat2)
117 | matrix y10 = r(Stat3)
118 | matrix y11 = r(Stat4)
119 | gen ATE = ((y11[3,1]-y01[3,1]))*genderm + ((y10[3,1]-y00[3,1]))*genderf
120 | qui sum ATE
121 | return scalar ate = `r(mean)'
122 | end
123 |
124 | qui bootstrap r(ate), reps(1000): ATE
125 | estat boot, all
126 |
127 | * 2) For the ATT
128 | capture program drop ATT
129 | program define ATT, rclass
130 | capture drop y1
131 | capture drop y0
132 | capture drop ATT
133 | sumup $Y, by($A $C)
134 | matrix y00 = r(Stat1)
135 | matrix y01 = r(Stat2)
136 | matrix y10 = r(Stat3)
137 | matrix y11 = r(Stat4)
138 | gen ATT = ((y11[3,1]-y01[3,1]))*gendermatet + ((y10[3,1]-y00[3,1]))*genderfatet
139 | qui sum ATT
140 | return scalar att = `r(mean)'
141 | end
142 |
143 | qui bootstrap r(att), reps(1000): ATT
144 | estat boot, all
145 |
146 | drop ATE ATT
147 |
148 | /* Box 6: Non-parametric G-Formula using a fully saturated regression model in Stata (A) */
149 | * method 1: conditional probabilities
150 | regress $Y ibn.$A ibn.$A#c.($C) , noconstant vce(robust) coeflegend
151 | predictnl ATE = (_b[1.rhc] + _b[1.rhc#c.gender]*gender) - (_b[0bn.rhc] + _b[0bn.rhc#c.gender]*gender)
152 | qui: sum ATE
153 | display "The ATE is: " "`r(mean)'"
154 | drop ATE
155 |
156 | /* Box 7: Non-parametric G-Formula using a fully saturated regression model in Stata (B) */
157 | * method 2: marginal probabilities
158 | regress $Y ibn.$A ibn.$A#c.($C) , noconstant vce(robust) coeflegend
159 |
160 | * Marginal probability in each treatment group
161 | margins $A , vce(unconditional)
162 |
163 | * Difference in marginal probability between treatment groups
164 | margins r.$A , contrast(nowald)
165 |
166 | /* 3.2 PARAMETRIC G-FORMULA */
167 |
168 | * One confounder
169 |
170 | /* Box 8: Parametric G-formula */
171 | * Calculations by hand
172 | * Expected probability amongst treated
173 | regress $Y $C if $A==1
174 | predict double y1hat
175 |
176 | * Expected probability amongst untreated
177 | regress $Y $C if $A==0
178 | predict double y0hat
179 | mean y1hat y0hat
180 |
181 | * Difference between expected probabilities (ATE) and biased confidence interval
182 | lincom _b[y1hat] - _b[y0hat]
183 |
184 | /* Box 9: Parametric regression adjustment using Stata's teffects (one confounder) */
185 | teffects ra ($Y $C) ($A)
186 |
187 | /* Box 10: Bootstrap for the parametric regression adjustment */
188 | capture program drop ATE
189 | program define ATE, rclass
190 | capture drop y1
191 | capture drop y0
192 | reg $Y $C if $A==1
193 | predict double y1, xb
194 | quiet sum y1
195 | reg $Y $C if $A==0
196 | predict double y0, xb
197 | quiet sum y0
198 | mean y1 y0
199 | lincom _b[y1]-_b[y0]
200 | return scalar ace =`r(estimate)'
201 | end
202 | qui bootstrap r(ace), reps(1000): ATE
203 | estat boot, all
204 |
205 | * More than one confounder
206 |
207 | /* Box 11: Parametric multivariate regression adjustment implementation of the G-Formula */
208 | regress $Y $W if $A==1
209 | predict double y1hat
210 | regress $Y $W if $A==0
211 | predict double y0hat
212 | mean y1hat y0hat
213 | lincom _b[y1hat] - _b[y0hat]
214 |
215 | /* Box 12: Parametric multivariate regression adjustment using Stata’s teffects command */
216 | teffects ra ($Y $W) ($A)
217 |
218 | /* Box 13: Parametric multivariate regression adjustment using Stata’s margins command */
219 | regress $Y ibn.$A ibn.$A#c.($W) , noconstant vce(robust)
220 | margins $A, vce(unconditional)
221 | margins r.$A, contrast(nowald)
222 |
223 | /* Box 14: Bootstrap for the multivariate parametric regression adjustment */
224 | capture program drop ATE
225 | program define ATE, rclass
226 | capture drop y1
227 | capture drop y0
228 | reg $Y $W if $A==1
229 | predict double y1, xb
230 | quiet sum y1
231 | reg $Y $W if $A==0
232 | predict double y0, xb
233 | quiet sum y0
234 | mean y1 y0
235 | lincom _b[y1]-_b[y0]
236 | return scalar ace =`r(estimate)'
237 | end
238 | qui bootstrap r(ace), reps(1000): ATE dots
239 | estat boot, all
240 |
241 | /* Box 15: Computing the parametric marginal risk ratio after regression adjustment */
242 | teffects ra ($Y $W) ($A), aequations
243 | teffects ra ($Y $W) ($A), coeflegend
244 | nlcom 100*_b[ATE:r1vs0.$A]/_b[POmean:0.$A]
245 | * 27.4% increase in relative risk
246 | teffects ra ($Y $W) ($A), pom coeflegend
247 | nlcom _b[POmeans:1.rhc]/ _b[POmeans:0bn.rhc]
248 | * 27.4% increase in relative risk
249 |
250 | /* 4 Inverse probability of treatment weighting */
251 | /* 4.1 Inverse probability of treatment weighting based on the propensity score plus regression adjustment */
252 |
253 | /* Box 16: Computation of the IPTW estimator for the ATE */
254 | * propensity score model for the exposure
255 | logit $A $W, vce(robust) nolog
256 |
257 | * propensity score predictions
258 | predict double ps
259 |
260 | * Sampling weights for the treated group
261 | generate double ipw1 = ($A==1)/ps
262 |
263 | * Weighted outcome probability among treated
264 | regress $Y [pw=ipw1]
265 | scalar Y1 = _b[_cons]
266 |
267 | * Sampling weights for the non-treated group
268 | generate double ipw0 = ($A==0)/(1-ps)
269 | regress $Y [pw=ipw0]
270 | scalar Y0 = _b[_cons]
271 | display "ATE =" Y1 - Y0
272 |
273 | /* Box 17: Bootstrap computation for the IPTW estimator */
274 | * Bootstrap the confidence intervals
275 | capture program drop ATE
276 | program define ATE, rclass
277 | capture drop y1
278 | capture drop y0
279 | regress $Y [pw=ipw1]
280 | matrix y1 = e(b)
281 | gen double y1 = y1[1,1]
282 | regress $Y [pw=ipw0]
283 | matrix y0 = e(b)
284 | gen double y0 = y0[1,1]
285 | mean y1 y0
286 | lincom _b[y1]-_b[y0]
287 | return scalar ace = `r(estimate)'
288 | end
289 | qui bootstrap r(ace), reps(1000): ATE
290 | estat boot, all
291 |
292 | /* Box 18: Computation of the IPTW estimator for the ATE using Stata’s teffects command */
293 | teffects ipw ($Y) ($A $W, logit), nolog vsquish
294 |
295 | /* Box 19: Assessing IPTW balance */
296 | * Stata teffects and tebalance commands
297 | qui teffects ipw ($Y) ($A $W)
298 | tebalance summarize
299 |
300 | * By hand - with the example of gender
301 | egen genderst = std(gender) // Standardization
302 | logistic $A $W // Propensity score
303 | capture drop ps
304 | predict double ps
305 | gen ipw = .
306 | replace ipw=($A==1)/ps if $A==1
307 | replace ipw=($A==0)/(1-ps) if $A==0
308 | regress genderst $A // Raw difference
309 | regress genderst $A [pw=ipw] // Standardized difference
310 |
311 | /* Box 20: Assessing IPTW overlap by hand */
312 | sort $A
313 | by $A: summarize ps
314 | kdensity ps if $A==1, generate(x1pointsa d1A) nograph n(10000)
315 | kdensity ps if $A==0, generate(x0pointsa d0A) nograph n(10000)
316 | label variable d1A "density for RHC=1"
317 | label variable d0A "density for RHC=0"
318 | twoway (line d0A x0pointsa , yaxis(1))(line d1A x1pointsa, yaxis(2))
319 |
320 | /* Box 21: Assessing overlap using Stata's teffects overlap */
321 | qui: teffects ipw ($Y) ($A $W, logit), nolog vsquish
322 | teffects overlap
323 |
324 |
325 | /* 4.2 Marginal structural model with stabilized weights */
326 | /* Box 22: Computation of the IPTW estimator for the ATE using a MSM */
327 | * Baseline treatment probabilities
328 | logit $A, vce(robust) nolog
329 | predict double nps, pr
330 |
331 | * propensity score model
332 | logit $A $W, vce(robust) nolog
333 | predict double dps, pr
334 |
335 | * Unstabilized weight
336 | cap drop ipw
337 | gen ipw = .
338 | replace ipw=($A==1)/dps if $A==1
339 | replace ipw=($A==0)/(1-dps) if $A==0
340 | sum ipw
341 |
342 | * Stabilized weight
343 | gen sws = .
344 | replace sws = nps/dps if $A==1
345 | replace sws = (1-nps)/(1-dps) if $A==0
346 | sum sws
347 |
348 | * MSM
349 | reg $Y $A [pw=ipw], vce(robust) // MSM unstabilized weight
350 | reg $Y $A [pw=sws], vce(robust) // MSM stabilized weight
351 |
352 |
353 | /* 4.3 IPTW with regression adjustment */
354 |
355 | /* Box 23: Computation of the IPTW-RA estimator for the ATE and bootstrap for statistical inference */
356 | capture program drop ATE
357 | program define ATE, rclass
358 | capture drop y1
359 | capture drop y0
360 | reg $Y $W if $A==1 [pw=sws]
361 | predict double y1, xb
362 | quiet sum y1
363 | return scalar y1=`r(mean)'
364 | reg $Y $W if $A==0 [pw=sws]
365 | predict double y0, xb
366 | quiet sum y0
367 | return scalar y0=`r(mean)'
368 | mean y1 y0
369 | lincom _b[y1]-_b[y0]
370 | return scalar ace =`r(estimate)'
371 | end
372 | qui bootstrap r(ace), reps(10): ATE
373 | estat boot, all
374 |
375 | /* Box 24: Computation of the IPTW-RA estimator for the ATE using Stata’s teffects */
376 | teffects ipwra ($Y $W) ($A $W)
377 | nlcom 100*_b[r1vs0.$A]/_b[POmean:0.$A]
378 | teffects ipwra ($Y $W) ($A $W), pom coeflegend
379 | nlcom _b[POmeans:1.rhc]/ _b[POmeans:0bn.rhc]
380 | *eltmle to check marginal RR
381 | eltmle $Y $A $W, tmle
382 |
383 | /* 5. Augmented inverse probability weighting */
384 | /* Box 25: Computation of the AIPTW estimator for the ATE and bootstrap for statistical inference */
385 | * Step (i) prediction model for the outcome
386 | qui glm $Y $A $W, fam(bin)
387 | predict double QAW, mu
388 | qui glm $Y $W if $A==1, fam(bin)
389 | predict double Q1W, mu
390 | qui glm $Y $W if $A==0, fam(bin)
391 | predict double Q0W, mu
392 |
393 | * Step (ii): prediction model for the treatment
394 | cap drop dps nps sws y1 y0
395 | qui logit $A $W
396 | predict double dps, pr
397 | qui logit $A
398 | predict double nps, pr
399 | gen sws = .
400 | replace sws = nps/dps if $A==1
401 | replace sws = (1-nps)/(1-dps) if $A==0
402 |
403 | * Step (iii): Estimation equation
404 | gen double y1 = (sws*($Y-QAW) + (Q1W))
405 | quiet sum y1
406 | scalar y1=`r(mean)'
407 | gen double y0 = (sws*($Y-QAW) + (Q0W))
408 | quiet sum y0
409 | scalar y0=`r(mean)'
410 | mean y1 y0
411 | lincom _b[y1] - _b[y0]
412 |
413 | * Step (iv): Bootstrap confidence intervals
414 | capture program drop ATE
415 | program define ATE, rclass
416 | capture drop y1
417 | capture drop y0
418 | capture drop Q*
419 | qui glm $Y $A $W, fam(bin)
420 | predict double QAW, mu
421 | qui glm $Y $W if $A==1, fam(bin)
422 | predict double Q1W, mu
423 | qui glm $Y $W if $A==0, fam(bin)
424 | predict double Q0W, mu
425 | gen double y1 = (sws*($Y-QAW) + (Q1W))
426 | quiet sum y1
427 | return scalar y1=`r(mean)'
428 | gen double y0 = (sws*($Y-QAW) + (Q0W))
429 | quiet sum y0
430 | return scalar y0=`r(mean)'
431 | mean y1 y0
432 | lincom _b[y1] - _b[y0]
433 | return scalar ace =`r(estimate)'
434 | end
435 | qui bootstrap r(ace), reps(1000): ATE
436 | estat boot, all
437 |
438 | /* Box 26: Computation of the AIPTW estimator for the ATE and marginal risk ratio using Stata’s teffects */
439 | teffects aipw ($Y $W) ($A $W, logit)
440 | * marginal Relative Risk
441 | nlcom 100*_b[r1vs0.$A]/_b[POmean:0.$A]
442 | * another way to compute it
443 | teffects aipw ($Y $W) ($A $W, logit), pom coeflegend
444 | nlcom _b[POmeans:1.rhc]/ _b[POmeans:0bn.rhc]
445 |
446 | /* 6. DATA-ADAPTIVE ESTIMATION: ENSEMBLE LEARNING TARGETED MAXIMUMLIKELIHOOD ESTIMATION*/
447 | /*Box 27: Computational implementation of TMLE by hand */
448 |
449 | * Step 1: prediction model for the outcome Q0 (g-computation)
450 | glm $Y $A $W, fam(binomial)
451 | predict double QAW_0, mu
452 | gen aa=$A
453 | replace $A = 0
454 | predict double Q0W_0, mu
455 | replace $A= 1
456 | predict double Q1W_0, mu
457 | replace $A = aa
458 | drop aa
459 |
460 | // Q to logit scale
461 | gen logQAW = log(QAW / (1 - QAW))
462 | gen logQ1W = log(Q1W / (1 - Q1W))
463 | gen logQ0W = log(Q0W / (1 - Q0W))
464 |
465 | * Step 2: prediction model for the treatment g0 (IPTW)
466 | glm $A $W, fam(binomial)
467 | predict gw, mu
468 | gen double H1W = $A / gw
469 | gen double H0W = (1 - $A ) / (1 - gw)
470 |
471 | * Step 3: Computing the clever covariate H(A,W) and estimating the parameter (epsilon) (MLE)
472 | glm $Y H1W H0W, fam(binomial) offset(logQAW) noconstant
473 | mat a = e(b)
474 | gen eps1 = a[1,1]
475 | gen eps2 = a[1,2]
476 |
477 | * Step 4: update from Q0 to Q1
478 | gen double Q1W_1 = exp(eps1 / gw + logQ1W) / (1 + exp(eps1 / gw + logQ1W))
479 | gen double Q0W_1 = exp(eps2 / (1 - gw) + logQ0W) / (1 + exp(eps2 / (1 - gw) + logQ0W))
480 |
481 | * Step 5: Targeted estimate of the ATE
482 | gen ATE = (Q1W_1 - Q0W_1)
483 | summ ATE
484 | global ATE = r(mean)
485 | drop ATE
486 |
487 | * Step 6: Statistical inference (efficient influence curve)
488 | qui sum(Q1W_1)
489 | gen EY1tmle = r(mean)
490 | qui sum(Q0W_1)
491 | gen EY0tmle = r(mean)
492 |
493 | gen d1 = (($A * ($Y - Q1W_1)/gw)) + Q1W_1 - EY1tmle
494 | gen d0 = ((1 - $A ) * ($Y - Q0W_1)/(1 - gw)) + Q0W_1 - EY0tmle
495 |
496 | gen IC = d1 - d0
497 | qui sum IC
498 | gen varIC = r(Var) / r(N)
499 | drop d1 d0 IC
500 |
501 | global LCI = $ATE - 1.96*sqrt(varIC)
502 | global UCI = $ATE + 1.96*sqrt(varIC)
503 | display "ATE:" %05.4f $ATE _col(15) "95%CI: " %05.4f $LCI "," %05.4f $UCI
504 |
505 | /* Box 28: TMLE with data-adaptive estimation using the Stata’s user writen eltmle */
506 | * if not already installed, type:
507 | * ssc install eltmle
508 | preserve
509 | eltmle $Y $A $W, tmle
510 | restore
511 |
512 |
513 | /* 7. Simulation */
514 | /* Box 29: Data generation for the Monte Carlo experiment */
515 |
516 | * Data generation
517 | clear
518 | set obs 1000
519 | set seed 777
520 | gen w1 = round(runiform(1, 5)) //Quintiles of Socioeconomic Deprivation
521 | gen w2 = rbinomial(1, 0.45) //Binary: probability age >65 = 0.45
522 | gen w3 = round(runiform(0, 1) + 0.75*(w2) + 0.8*(w1)) //Stage
523 | recode w3 (5/6=1) //Stage (TNM): categorical 4 levels
524 | gen w4 = round(runiform(0, 1) + 0.75*(w2) + 0.2*(w1)) //Comorbidites: categorical four levels
525 | gen A = (rbinomial(1,invlogit(-1 - 0.15*(w4) + 1.5*(w2) + 0.75*(w3) + 0.25*(w1) + 0.8*(w2)*(w4)))) //Binary treatment
526 | gen Y1 = (invlogit(-3 + 1 + 0.25*(w4) + 0.75*(w3) + 0.8*(w2)*(w4) + 0.05*(w1))) // Potential outcome 1
527 | gen Y0 = (invlogit(-3 + 0 + 0.25*(w4) + 0.75*(w3) + 0.8*(w2)*(w4) + 0.05*(w1))) // Potential outcome 2
528 | gen psi = Y1-Y0 // Simulated ATE
529 | gen Y = A*(Y1) + (1 - A)*Y0 //Binary outcome
530 |
531 |
532 | // Estimate the true simulated ATE
533 | mean psi
534 |
535 | // ATE estimation
536 | * Regression adjustment
537 | teffects ra (Y w1 w2 w3 w4) (A)
538 | estimates store ra
539 |
540 | * IPTW
541 | teffects ipw (Y) (A w1 w2 w3 w4)
542 | estimates store ipw
543 |
544 | * IPTW-RA
545 | teffects ipwra (Y w1 w2 w3 w4) (A w1 w2 w3 w4)
546 | estimates store ipwra
547 |
548 | * AIPTW
549 | teffects aipw (Y w1 w2 w3 w4) (A w1 w2 w3 w4)
550 | estimates store aipw
551 |
552 | * Results
553 | qui reg psi
554 | estimates store psi
555 | estout psi ra ipw ipwra aipw
556 |
557 | // Ensemble learning maximum likelihood estimation
558 | preserve
559 | eltmle Y A w1 w2 w3 w4, tmle
560 | restore
561 |
562 | // Relative bias of each ATE
563 | * Regression adjustment
564 | display abs(0.1787 - 0.203419)/0.1787
565 |
566 | * IPTW
567 | display abs(0.1787 - 0.2776)/0.1787
568 |
569 | * IPTW-RA
570 | display abs(0.1787 - .2052088)/0.1787
571 |
572 | * AIPTW
573 | display abs(0.1787 - 0.2030)/0.1787
574 |
575 | * ELTMLE
576 | display abs(0.1787 - 0.1784)/0.1787
577 |
578 |
--------------------------------------------------------------------------------
/rhc.Rdata:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/migariane/TutorialCausalInferenceEstimators/d809b657b382e227837d3032b1517612c478818d/rhc.Rdata
--------------------------------------------------------------------------------
/rhc.dta:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/migariane/TutorialCausalInferenceEstimators/d809b657b382e227837d3032b1517612c478818d/rhc.dta
--------------------------------------------------------------------------------